diff options
| author | t <t@tjp.lol> | 2026-06-10 10:06:07 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-10 12:17:27 -0600 |
| commit | bb85e867bc938138f29fb45230169af1ba60761c (patch) | |
| tree | 5ad1cf50274c61ccfffab7048b4a4e28003d82b5 /src | |
| parent | d26890bbc52e9f0050db40f208763a7f2b714ddd (diff) | |
Add addUserText helper; update all call sites
Conversation.addUserMessage now takes a []ContentBlock (symmetric with
addAssistantMessage). Introduce a thin addUserText wrapper in agent.zig
for the plain-text case and update every call site in agent.zig and
anthropic_messages_json.zig accordingly.
Diffstat (limited to 'src')
| -rw-r--r-- | src/lua_bridge.zig | 163 | ||||
| -rw-r--r-- | src/lua_event_bridge.zig | 20 | ||||
| -rw-r--r-- | src/lua_runtime.zig | 83 | ||||
| -rw-r--r-- | src/luarocks_runtime.zig | 40 | ||||
| -rw-r--r-- | src/main.zig | 7 | ||||
| -rw-r--r-- | src/system_prompt.zig | 15 |
6 files changed, 263 insertions, 65 deletions
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index 9ee5089..6395293 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -102,12 +102,24 @@ pub var command_registrations_key: u8 = 0; pub var on_registrations_key: u8 = 0; /// The key under which we stash the canonical `panto` module table in -/// `LUA_REGISTRYINDEX`. The `package.preload['panto']` loader returns this -/// table, and internal Zig setup (`installEmit`, the runtime's +/// `LUA_REGISTRYINDEX`. Internal Zig setup (`installEmit`, the runtime's /// `_record_result` / wrapper / register_tool paths) fetches it via /// `pushPantoTable` instead of a global lookup — `panto` is not a global. +/// +/// This slot is populated by the runtime's `installPantoModule`, which +/// obtains the **native** `panto` table via `require('panto')` (resolving +/// the staged `panto.so` on `cpath`) and attaches `ext` to it. Until then +/// the slot is empty and `pushPantoTable` pushes `nil`; nothing fetches it +/// before module load. pub var panto_table_key: u8 = 0; +/// The key under which we stash the `panto.ext` extension subtable in +/// `LUA_REGISTRYINDEX`. Built by `install` (before any native module +/// exists) so the host can attach it to the native `panto` table once +/// `require('panto')` resolves. Distinct from `panto_table_key` because +/// `ext` is constructed first and outlives the module-load step. +pub var ext_table_key: u8 = 0; + /// A single declared tool, as harvested from a script's top-level call to /// `panto.ext.register_tool`. All slices reference Lua-owned strings on the /// state's stack/registry; copy them before closing the state. @@ -137,41 +149,68 @@ pub const OnRegistration = struct { // Public bridge API // --------------------------------------------------------------------------- -/// Push the canonical `panto` table (built by `install`) onto the stack -/// from its registry slot. Internal Zig setup uses this instead of a -/// global lookup, since `panto` is not a global — it is delivered to Lua -/// only through `require('panto')`. +/// Push the canonical `panto` module table onto the stack from its +/// registry slot. The slot is populated by the runtime's +/// `installPantoModule` (which `require`s the native module and attaches +/// `ext`); before that runs the slot is empty and this pushes `nil`. +/// Internal Zig setup uses this instead of a global lookup, since `panto` +/// is not a global — it is delivered to Lua only through `require('panto')`. pub fn pushPantoTable(L: *c.lua_State) void { _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &panto_table_key); } -/// The `package.preload['panto']` loader. Returns the canonical `panto` -/// table so `require('panto')` yields it (and `panto.ext`) without a -/// global. Standard preload-loader contract: one return value. +/// Push the `panto.ext` extension subtable from its registry slot. Built +/// by `install`, so it is available before the native module loads (the +/// host attaches it to the native table in `installPantoModule`). +pub fn pushExtTable(L: *c.lua_State) void { + _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &ext_table_key); +} + +/// Stash a `panto` module table (on the stack top) into `panto_table_key`. +/// Called by `installPantoModule` after it builds the native+`ext` table. +/// Does NOT pop — leaves the table on the stack for the caller. +pub fn setPantoTable(L: *c.lua_State) void { + c.lua_pushvalue(L, -1); // dup for the registry slot + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &panto_table_key); +} + +/// Register a `package.preload['panto']` loader returning the `panto` +/// module table currently on the stack top. Does NOT pop the table — +/// leaves it in place for the caller. The loader returns the table stashed +/// under `panto_table_key`, so it must be called together with +/// `setPantoTable` (the runtime does both in `installPantoModule`). +pub fn installPantoPreload(L: *c.lua_State) void { + _ = c.lua_getglobal(L, "package"); + _ = c.lua_getfield(L, -1, "preload"); + c.lua_pushcclosure(L, pantoPreloadThunk, 0); + c.lua_setfield(L, -2, "panto"); // preload.panto = thunk + c.lua_settop(L, c.lua_gettop(L) - 2); // pop preload + package +} + +/// The `package.preload['panto']` loader: returns the augmented `panto` +/// table from `panto_table_key`. Standard preload-loader contract: one +/// return value. Any `require('panto')` after `installPantoModule` — from +/// an extension or otherwise — routes here (preload is searcher slot 1). fn pantoPreloadThunk(L_opt: ?*c.lua_State) callconv(.c) c_int { const L = L_opt.?; pushPantoTable(L); return 1; } -/// Install the `panto` table (with its `panto.ext` extension subtable) -/// into the given state, reachable from Lua via `require('panto')`. +/// Build the `panto.ext` extension subtable and the registry tables that +/// hold harvested tool/command/event-handler registrations. /// -/// There is no `panto` global. The table is stashed in the registry -/// (`panto_table_key`) and a loader is registered into -/// `package.preload['panto']`, so any `require('panto')` — from an -/// extension or otherwise — returns it. Internal Zig code reaches the -/// same table via `pushPantoTable`. +/// This does **not** build the `panto` module table and does **not** touch +/// `package.preload` — the `panto` table comes from the native +/// `libpanto-lua` module via `require('panto')`, wired by the runtime's +/// `installPantoModule` once the staged `panto.so` is on `cpath`. Here we +/// only construct `ext` (stashed under `ext_table_key`) so the host can +/// attach it to that native table later. /// /// All extension-authoring APIs (`register_tool`, `register_command`, -/// `on`, `emit`) live under `panto.ext`; the bare `panto` table is -/// reserved for the future libpanto API. `emit` is installed here as a +/// `on`, `emit`) live under `panto.ext`. `emit` is installed here as a /// safe no-op default; the long-lived runtime overrides it with a closure -/// carrying its context via `installEmit` so a Lua `emit` can drive the -/// native `EventBus`. -/// -/// Also creates the registry tables that hold harvested tool, command, and -/// event-handler registrations. +/// carrying its context via `installEmit` once the module table exists. pub fn install(L: *c.lua_State) void { // Create the registrations table: an array of records, each shaped // { name=, description=, schema_json=, handler= }. @@ -187,8 +226,7 @@ pub fn install(L: *c.lua_State) void { c.lua_createtable(L, 0, 0); c.lua_rawsetp(L, LUA_REGISTRYINDEX, &on_registrations_key); - // Build the `panto` global as { ext = { ... } }. - c.lua_createtable(L, 0, 1); // panto + // Build the `panto.ext` subtable and stash it for later attachment. c.lua_createtable(L, 0, 4); // panto.ext c.lua_pushcclosure(L, registerToolThunk, 0); c.lua_setfield(L, -2, "register_tool"); @@ -199,27 +237,8 @@ pub fn install(L: *c.lua_State) void { // Default `emit`: a no-op until the runtime installs the real one. c.lua_pushcclosure(L, emitNoopThunk, 0); c.lua_setfield(L, -2, "emit"); - // panto.ext = <ext table> - c.lua_setfield(L, -2, "ext"); - - // Stash the `panto` table in the registry (keeps internal Zig access - // working without a global), then register the preload loader so - // `require('panto')` returns it. The table is still on the stack top. - c.lua_pushvalue(L, -1); // dup `panto` for the registry slot - c.lua_rawsetp(L, LUA_REGISTRYINDEX, &panto_table_key); - // Stack: ..., panto. Register the preload loader, consuming `panto`. - installPreloadLoader(L); -} - -/// Register `pantoPreloadThunk` into `package.preload['panto']`. Consumes -/// the `panto` table currently on the stack top (pops it). -fn installPreloadLoader(L: *c.lua_State) void { - c.lua_settop(L, c.lua_gettop(L) - 1); // pop the leftover `panto` - _ = c.lua_getglobal(L, "package"); - _ = c.lua_getfield(L, -1, "preload"); - c.lua_pushcclosure(L, pantoPreloadThunk, 0); - c.lua_setfield(L, -2, "panto"); - c.lua_settop(L, c.lua_gettop(L) - 2); // pop preload + package + // Stash `ext` under its registry key, consuming it. + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &ext_table_key); } /// Override `panto.ext.emit` with a closure that carries `ctx` as a @@ -235,12 +254,11 @@ pub fn installEmit( ctx: *anyopaque, emit_fn: *const fn (L_opt: ?*c.lua_State) callconv(.c) c_int, ) void { - pushPantoTable(L); - _ = c.lua_getfield(L, -1, "ext"); + pushExtTable(L); c.lua_pushlightuserdata(L, ctx); c.lua_pushcclosure(L, emit_fn, 1); c.lua_setfield(L, -2, "emit"); - c.lua_settop(L, c.lua_gettop(L) - 2); // pop ext + panto + c.lua_settop(L, c.lua_gettop(L) - 1); // pop ext } /// The default `panto.ext.emit`: a no-op. Replaced by `installEmit` once @@ -562,6 +580,24 @@ fn readHandlerResultTable( return .{ .items = items }; } +/// Test-only: fabricate a minimal `panto` module table `{ ext = <ext> }` +/// and wire `require('panto')` to it, mirroring what the runtime's +/// `installPantoModule` does in production (minus the native agent/stream +/// surface, which needs a staged `panto.so` unavailable in unit tests). +/// +/// Production code never calls this — it `require`s the real native +/// module. Unit tests that load extension scripts (which `require('panto')` +/// for `panto.ext.*`) call `install(L)` then this, so those scripts +/// resolve without a `.so` on disk. +pub fn installTestPantoTable(L: *c.lua_State) void { + c.lua_createtable(L, 0, 1); // panto + pushExtTable(L); + c.lua_setfield(L, -2, "ext"); // panto.ext = ext + installPantoPreload(L); // preload.panto -> panto_table_key + setPantoTable(L); // stash; leaves panto on stack + c.lua_settop(L, c.lua_gettop(L) - 1); // pop panto +} + // --------------------------------------------------------------------------- // Lua-callable C functions // --------------------------------------------------------------------------- @@ -885,24 +921,38 @@ fn readStringField( // Tests // --------------------------------------------------------------------------- -test "install creates panto.ext table with register_tool/register_command/on/emit" { +test "install creates the ext table with register_tool/register_command/on/emit" { const L = c.luaL_newstate() orelse return error.LuaInitFailed; defer c.lua_close(L); c.luaL_openlibs(L); install(L); - // No `panto` global: it is delivered via `require('panto')`. + // No `panto` global, and `install` alone does NOT build the module + // table — the native module supplies it via `require('panto')`, and + // the host attaches `ext` in `installPantoModule`. So before that + // wiring the module slot is empty. try std.testing.expectEqual(@as(c_int, T_NIL), c.lua_getglobal(L, "panto")); c.lua_settop(L, c.lua_gettop(L) - 1); pushPantoTable(L); - try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); - _ = c.lua_getfield(L, -1, "ext"); + try std.testing.expectEqual(@as(c_int, T_NIL), c.lua_type(L, -1)); + c.lua_settop(L, c.lua_gettop(L) - 1); + + // The `ext` table exists and carries the four authoring functions. + pushExtTable(L); try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); inline for (.{ "register_tool", "register_command", "on", "emit" }) |field| { _ = c.lua_getfield(L, -1, field); try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1)); c.lua_settop(L, c.lua_gettop(L) - 1); } + + // After the test wiring, `require('panto')` resolves to a `{ ext }` + // module table. + installTestPantoTable(L); + pushPantoTable(L); + try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); + _ = c.lua_getfield(L, -1, "ext"); + try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); } test "panto.ext.on records event registrations in order" { @@ -910,6 +960,7 @@ test "panto.ext.on records event registrations in order" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -959,6 +1010,7 @@ test "register_command records name and description" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -989,6 +1041,7 @@ test "register_command rejects a non-function handler" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1010,6 +1063,7 @@ test "register_tool records name, description, schema_json" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1044,6 +1098,7 @@ test "handler invocation: input parsed, result captured" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1080,6 +1135,7 @@ test "readHandlerResult: table with text and attachments" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\return { @@ -1109,6 +1165,7 @@ test "handler crash: error message surfaces via xpcall traceback hook" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") diff --git a/src/lua_event_bridge.zig b/src/lua_event_bridge.zig index 1fbc82a..f6d67e0 100644 --- a/src/lua_event_bridge.zig +++ b/src/lua_event_bridge.zig @@ -738,6 +738,7 @@ test "Lua on-handler fires through the native bus into Lua" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -770,6 +771,7 @@ test "Lua-defined component renders lines through the bridged vtable" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -807,6 +809,7 @@ test "bridged component render truncates to the width contract" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -835,6 +838,7 @@ test "bridged component render error yields a safe fallback line, not a crash" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -862,6 +866,7 @@ test "wrap pattern: Lua reads the native default, wraps it, and renders through defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -933,6 +938,7 @@ test "skills-style claim-by-name (§7.5) works end-to-end through the Lua bridge defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1008,6 +1014,7 @@ test "native-passthrough get/set round-trip: setComponent(getComponent()) keeps defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1056,6 +1063,7 @@ test "Lua emit drives the native bus (custom event reaches a native handler)" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1093,6 +1101,7 @@ test "bridged render: a long error on a NARROW width still satisfies the width c defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1125,6 +1134,7 @@ test "bridged render: non-array / nil / non-string returns each yield a safe fal defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1162,6 +1172,7 @@ test "bridged render: empty array renders zero lines (no fallback)" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1189,6 +1200,7 @@ test "bridged render: UTF-8 line truncates on codepoint boundaries to the width" defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1222,6 +1234,7 @@ test "bridged firstLineChanged is cache-derived: append stays near the tail" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1270,6 +1283,7 @@ test "bridged firstLineChanged: a mid-line replace reports the changed line, shr defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1317,6 +1331,7 @@ test "bridged handleInput round-trips: a Lua method mutates state the next rende defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1363,6 +1378,7 @@ test "bridged component: invalidate frees the ref+cache eagerly; teardown is lea defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); // frees all tracked BridgedComponents @@ -1396,6 +1412,7 @@ test "lifecycle payload fields marshal to Lua: tool id/delta/input/output, think defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1453,6 +1470,7 @@ test "lifecycle handlers fire for every new event name" { defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1506,6 +1524,7 @@ test "claim-by-name at tool_details swaps over the start default; releasing the defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); @@ -1613,6 +1632,7 @@ test "intra-emit clobber: two handlers each mint a Lua component in ONE emit; bo defer c.lua_close(L); c.luaL_openlibs(L); lua_bridge.install(L); + lua_bridge.installTestPantoTable(L); var bridge = EventBridge.init(testing.allocator, L); defer bridge.deinit(); diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index e26d017..2574fae 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -130,6 +130,15 @@ pub const LuaRuntime = struct { // Install the real `panto.ext.emit` now that the bridge exists, so // a Lua `emit(name, data)` reaches the native bus once attached. lua_bridge.installEmit(L, @ptrCast(eb), lua_event_bridge.emitThunk); + + // In production, `require('panto')` is wired to the native module + // by `installPantoModule` (after bootstrap stages `panto.so`). + // Unit tests have no staged `.so`, and load extension scripts that + // `require('panto')` for `panto.ext.*`; give them a fabricated + // `{ ext = ... }` module table so those `require`s resolve. + if (@import("builtin").is_test) { + lua_bridge.installTestPantoTable(L); + } return self; } @@ -139,6 +148,62 @@ pub const LuaRuntime = struct { return self.event_bridge; } + /// Wire `require('panto')` to the native `libpanto-lua` module plus the + /// CLI's `ext` subtable. Run **after** luarocks bootstrap has staged + /// `panto.so` and configured `package.cpath`, and **before** + /// `installScheduler` (whose `_record_result` / wrapper closure live on + /// the module table). + /// + /// The sequence (all in Zig, via the C API, so the CLI never calls + /// `luaopen_panto` itself — it only speaks `require`): + /// + /// 1. `require('panto')` resolves the staged `panto.so` on `cpath`, + /// running its `luaopen_panto`, which returns a **fresh** table + /// (the agent/stream surface). + /// 2. Attach the `ext` subtable (built by `lua_bridge.install`) onto + /// that fresh native table — mutating the host's own copy, which + /// is safe precisely because the table is fresh, not shared. + /// 3. Install a `package.preload['panto']` loader returning that same + /// augmented table, so any *later* `require('panto')` (e.g. from + /// an extension) gets `ext` too. `require` already cached the + /// table in `package.loaded` from step 1; the preload entry wins + /// over `cpath` for any cache miss. + /// 4. Stash the augmented table under `panto_table_key` so internal + /// Zig (`pushPantoTable`, the scheduler) reaches it. + pub fn installPantoModule(self: *LuaRuntime) !void { + const L = self.L; + + // 1. require('panto') -> native agent/stream table on the stack. + _ = c.lua_getglobal(L, "require"); + _ = c.lua_pushlstring(L, "panto", 5); + if (c.lua_pcallk(L, 1, 1, 0, 0, null) != 0) { + logTopAsError(L, "panto-lua: require('panto') failed (was panto.so staged on cpath?)"); + c.lua_settop(L, c.lua_gettop(L) - 1); + return RuntimeError.LuaInitFailed; + } + if (c.lua_type(L, -1) != lua_bridge.T_TABLE) { + c.lua_settop(L, c.lua_gettop(L) - 1); + return RuntimeError.LuaInitFailed; + } + // Stack: ..., panto (native, fresh) + + // 2. panto.ext = <ext table>. + lua_bridge.pushExtTable(L); + if (c.lua_type(L, -1) != lua_bridge.T_TABLE) { + c.lua_settop(L, c.lua_gettop(L) - 2); + return RuntimeError.LuaInitFailed; + } + c.lua_setfield(L, -2, "ext"); // panto.ext = ext; pops ext + // Stack: ..., panto (augmented) + + // 3. Set package.preload['panto'] to return this augmented table. + lua_bridge.installPantoPreload(L); // leaves `panto` on the stack + + // 4. Stash in the registry slot for internal Zig access. + lua_bridge.setPantoTable(L); // dups + stores; leaves `panto` + c.lua_settop(L, c.lua_gettop(L) - 1); // pop the leftover `panto` + } + /// Install the libuv-driven coroutine scheduler: /// - Register `panto._record_result` (C function with `self` as /// light-userdata upvalue) so the wrapper closure can hand @@ -148,7 +213,9 @@ pub const LuaRuntime = struct { /// - Cache `require("luv").run` for fast per-tick access. /// /// Must be called after luarocks bootstrap has installed luv, - /// otherwise the `require("luv")` step will fail. + /// otherwise the `require("luv")` step will fail. Also after + /// `installPantoModule`, since the scheduler writes onto the module + /// table. pub fn installScheduler(self: *LuaRuntime) !void { try installRecordResult(self); try installWrapperClosure(self); @@ -261,15 +328,15 @@ pub const LuaRuntime = struct { // // Push `panto.ext.register_tool` *first*, then load+run the chunk // so its return value naturally lands above it; calling pcall then - // consumes both in the right order. + // consumes both in the right order. We reach `register_tool` + // through the `ext` table directly (it exists from `install`, + // independent of whether the native module table has been wired). const L = self.L; - lua_bridge.pushPantoTable(L); - _ = c.lua_getfield(L, -1, "ext"); + lua_bridge.pushExtTable(L); _ = c.lua_getfield(L, -1, "register_tool"); - // Stack: ..., panto, ext, register_tool. Collapse to just - // register_tool by overwriting the lowest of the three. - c.lua_copy(L, -1, -3); // overwrite `panto` with `register_tool` - c.lua_settop(L, c.lua_gettop(L) - 2); // pop ext + duplicate + // Stack: ..., ext, register_tool. Collapse to just register_tool. + c.lua_copy(L, -1, -2); // overwrite `ext` with `register_tool` + c.lua_settop(L, c.lua_gettop(L) - 1); // pop duplicate // Stack: ..., register_tool if (c.luaL_loadfilex(L, path_z.ptr, null) != 0) { diff --git a/src/luarocks_runtime.zig b/src/luarocks_runtime.zig index e652096..7f01c19 100644 --- a/src/luarocks_runtime.zig +++ b/src/luarocks_runtime.zig @@ -38,6 +38,7 @@ const panto_home = @import("panto_home.zig"); const embedded_luarocks = @import("embedded_luarocks"); const embedded_lua_headers = @import("embedded_lua_headers"); const embedded_agent = @import("embedded_agent"); +const embedded_panto_so = @import("embedded_panto_so"); const lua_bridge = @import("lua_bridge.zig"); const c = lua_bridge.c; @@ -137,6 +138,7 @@ pub fn bootstrap( try panto_home.ensureDirsExist(layout, io); try stageLuaHeaders(allocator, io, layout); try stageAgentTree(allocator, io, layout); + try stagePantoModule(allocator, io, layout); try stageDefaultConfig(allocator, io, layout); const lua_wrapper_path = try writeLuaWrapper(allocator, io, layout, panto_executable_path); defer allocator.free(lua_wrapper_path); @@ -203,6 +205,33 @@ fn stageLuaHeaders( } // --------------------------------------------------------------------------- +// Stage the embedded native `panto.so` +// --------------------------------------------------------------------------- + +/// Write the binary-embedded `libpanto-lua` shared object to +/// `<tree>/lib/lua/<short>/panto.so`, which is already on `package.cpath` +/// (see `configurePackagePaths`). This guarantees the embedded VM's +/// `require('panto')` resolves to the native agent/stream module on a +/// cold, network-less machine — the module is *ours* and version-locked +/// to this binary's Lua, so unlike luv it cannot be fetched from luarocks. +/// +/// The `.so` leaves `lua_*` undefined and resolves them against the host +/// binary at `dlopen` time, identical to how a luarocks-installed C rock +/// (e.g. luv) works — `exe.rdynamic = true` keeps those symbols findable. +/// +/// Uses the same `writeIfDifferent` discipline as `stageLuaHeaders` so an +/// unchanged `.so` keeps its on-disk mtime across reruns. +fn stagePantoModule( + allocator: Allocator, + io: Io, + layout: panto_home.Layout, +) !void { + var dir = try Io.Dir.cwd().openDir(io, layout.lib_lua_dir, .{}); + defer dir.close(io); + try writeIfDifferent(allocator, io, dir, "panto.so", embedded_panto_so.bytes); +} + +// --------------------------------------------------------------------------- // Stage the embedded `agent/` tree // --------------------------------------------------------------------------- @@ -311,11 +340,20 @@ fn writeIfDifferent( name: []const u8, contents: []const u8, ) !void { - if (dir.readFileAlloc(io, name, allocator, .limited(1 << 22))) |existing| { + // Size the read limit to the new content (plus a small margin) so it + // never truncates the comparison — staged files range from tiny + // headers to a multi-megabyte `panto.so`. A larger on-disk file can't + // match `contents` anyway, so capping at `contents.len + 1` is enough + // to detect inequality without reading unbounded bytes. + const limit = contents.len + 1; + if (dir.readFileAlloc(io, name, allocator, .limited(limit))) |existing| { defer allocator.free(existing); if (std.mem.eql(u8, existing, contents)) return; } else |err| switch (err) { error.FileNotFound => {}, + // A file larger than our limit definitionally differs from + // `contents`; fall through and rewrite. + error.StreamTooLong => {}, else => return err, } try dir.writeFile(io, .{ .sub_path = name, .data = contents }); diff --git a/src/main.zig b/src/main.zig index 34a687a..06fd29e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -355,6 +355,13 @@ pub fn main(init: std.process.Init) !void { ); } + // Bootstrap staged `panto.so` onto the embedded VM's cpath and + // configured `package.path`/`cpath`; now wire `require('panto')` to + // the native module + the CLI's `ext` subtable. Must run before + // `installScheduler` (which writes onto the module table) and before + // any extension loads (which `require('panto')`). + try rt.installPantoModule(); + // luv is installed (or already present) at this point; wire the // libuv-driven coroutine scheduler before any extensions get a // chance to register tools that might want to yield. diff --git a/src/system_prompt.zig b/src/system_prompt.zig index 6676442..9dce3e5 100644 --- a/src/system_prompt.zig +++ b/src/system_prompt.zig @@ -330,6 +330,15 @@ fn projectLayerDir(arena: Allocator, io: Io) ![]u8 { const testing = std.testing; +/// Test helper: append a single-text user message (the `addUserMessage` +/// signature now takes a block slice). +fn addUserText(conv: *panto.Conversation, text: []const u8) !void { + const tb = try panto.textualBlockFromSlice(conv.allocator, text); + var block: panto.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + test "blocksEqual - length and content" { try testing.expect(blocksEqual(&.{ "a", "b" }, &.{ "a", "b" })); try testing.expect(!blocksEqual(&.{ "a", "b" }, &.{ "a", "c" })); @@ -344,7 +353,7 @@ test "effectiveConfigWindow - no replace anchors to leading system blocks" { try conv.addSystemMessage("seed"); try conv.addSystemMessage("append-1"); - try conv.addUserMessage("hi"); + try addUserText(&conv, "hi"); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); @@ -361,10 +370,10 @@ test "effectiveConfigWindow - anchors to last replace block" { defer conv.deinit(); try conv.addSystemMessage("old seed"); - try conv.addUserMessage("hi"); + try addUserText(&conv, "hi"); try conv.replaceSystemMessage("new seed"); try conv.addSystemMessage("new append"); - try conv.addUserMessage("again"); + try addUserText(&conv, "again"); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); |
