diff options
Diffstat (limited to 'libpanto-lua/src')
| -rw-r--r-- | libpanto-lua/src/module.zig | 87 |
1 files changed, 86 insertions, 1 deletions
diff --git a/libpanto-lua/src/module.zig b/libpanto-lua/src/module.zig index ecf89af..17d0fb2 100644 --- a/libpanto-lua/src/module.zig +++ b/libpanto-lua/src/module.zig @@ -85,6 +85,7 @@ pub const c = @cImport({ // canonical 5.4 values as clean `c_int`s. const T_NIL: c_int = 0; const T_BOOLEAN: c_int = 1; +const T_LIGHTUSERDATA: c_int = 2; const T_NUMBER: c_int = 3; const T_STRING: c_int = 4; const T_TABLE: c_int = 5; @@ -179,10 +180,19 @@ const AgentBox = struct { /// The lazily-created Lua tool source (null until first register_tool). tools: ?*LuaToolSource, closed: bool = false, + /// A borrowed box wraps an agent owned by the host embedder (see + /// `_wrap_agent`): teardown is the host's job, `config` is undefined, + /// and ownership-dependent methods (`set_config`, `register_tool`) + /// are refused. + borrowed: bool = false, fn deinit(self: *AgentBox, L: *c.lua_State) void { if (self.closed) return; self.closed = true; + // Borrowed: the host owns the agent and everything it borrows; + // this box holds nothing else (empty arena, no store ref, no + // tools, no Io retain). + if (self.borrowed) return; // Tear down the agent first (it borrows config + store + source). self.agent.deinit(); if (self.tools) |ts| ts.deinit(L); @@ -321,7 +331,7 @@ pub export fn luaopen_panto(L_opt: ?*c.lua_State) callconv(.c) c_int { registerMetatables(L); - c.lua_createtable(L, 0, 5); + c.lua_createtable(L, 0, 6); c.lua_pushcclosure(L, agentNew, 0); c.lua_setfield(L, -2, "agent"); c.lua_pushcclosure(L, conversationNew, 0); @@ -330,6 +340,11 @@ pub export fn luaopen_panto(L_opt: ?*c.lua_State) callconv(.c) c_int { c.lua_setfield(L, -2, "null_store"); c.lua_pushcclosure(L, fsJSONLStoreNew, 0); c.lua_setfield(L, -2, "file_system_jsonl_store"); + // Internal/unstable: wrap a host-owned `*panto.Agent` (lightuserdata) + // as a *borrowed* Agent userdata. Used by the panto CLI to expose its + // session agent to extensions (`panto.ext.agent`). + c.lua_pushcclosure(L, wrapAgent, 0); + c.lua_setfield(L, -2, "_wrap_agent"); _ = c.lua_pushstring(L, "libpanto-lua 0.0.0 (Lua 5.4)"); c.lua_setfield(L, -2, "_VERSION"); return 1; @@ -479,6 +494,35 @@ fn buildAgent(L: *c.lua_State, box: *AgentBox) ConfigError!void { } } +/// `panto._wrap_agent(ptr)` — wrap a host-owned `*panto.Agent`, passed as +/// a lightuserdata, into a *borrowed* Agent userdata sharing the AGENT_MT +/// method surface. The host retains ownership: `__gc` tears nothing down, +/// and methods that would entangle box-owned state with the host's agent +/// (`set_config`, `register_tool`) are refused. +fn wrapAgent(L_opt: ?*c.lua_State) callconv(.c) c_int { + const L = L_opt.?; + if (c.lua_type(L, 1) != T_LIGHTUSERDATA) + return luaErr(L, "panto._wrap_agent: expected an agent pointer (lightuserdata)"); + const ptr = c.lua_touserdata(L, 1) orelse + return luaErr(L, "panto._wrap_agent: null agent pointer"); + + const ud = c.lua_newuserdatauv(L, @sizeOf(AgentBox), 0) orelse + return luaErr(L, "panto._wrap_agent: out of memory"); + const box: *AgentBox = @ptrCast(@alignCast(ud)); + box.* = .{ + .agent = @ptrCast(@alignCast(ptr)), + .config = undefined, // never read: set_config is refused on borrowed + .old_configs = .empty, + .store_ref = c.LUA_NOREF, + .string_arena = std.heap.ArenaAllocator.init(c_allocator), + .tools = null, + .closed = false, + .borrowed = true, + }; + _ = c.luaL_setmetatable(L, AGENT_MT); + return 1; +} + /// `agent:set_config{...}` — swap the active provider/model/policy. Takes /// effect at the next turn boundary (libpanto re-reads the snapshot each /// turn). The new config is parsed with the *same* parser as construction, @@ -488,6 +532,7 @@ fn buildAgent(L: *c.lua_State, box: *AgentBox) ConfigError!void { fn agentSetConfig(L_opt: ?*c.lua_State) callconv(.c) c_int { const L = L_opt.?; const box = checkAgent(L, 1); + if (box.borrowed) return luaErr(L, "panto: set_config is not supported on a borrowed agent"); if (box.closed) return luaErr(L, "panto: agent is closed"); c.luaL_checktype(L, 2, T_TABLE); @@ -1421,6 +1466,7 @@ fn agentRegisterTool(L_opt: ?*c.lua_State) callconv(.c) c_int { const L = L_opt.?; const box = checkAgent(L, 1); if (box.closed) return luaErr(L, "panto: agent is closed"); + if (box.borrowed) return luaErr(L, "panto: register_tool is not supported on a borrowed agent (use panto.ext.register_tool)"); c.luaL_checktype(L, 2, T_TABLE); if (box.tools == null) { @@ -2483,6 +2529,45 @@ test "agent: set_config swaps without error; system prompt setters work" { ); } +test "_wrap_agent: borrowed agent shares the host's, and __gc leaves it alive" { + const L = try newTestState(); + defer c.lua_close(L); + try runScript(L, + \\a = panto.agent { api_style="openai_chat", api_key="k", base_url="http://127.0.0.1:1", model="m" } + ); + // Recover the underlying `*panto.Agent`, as a host embedder would + // hold it, and wrap it borrowed. + _ = c.lua_getglobal(L, "a"); + const owner = checkAgent(L, -1); + c.lua_settop(L, c.lua_gettop(L) - 1); + _ = c.lua_getglobal(L, "panto"); + _ = c.lua_getfield(L, -1, "_wrap_agent"); + c.lua_pushlightuserdata(L, owner.agent); + if (c.lua_pcallk(L, 1, 1, 0, 0, null) != 0) return error.WrapFailed; + c.lua_setglobal(L, "b"); + c.lua_settop(L, c.lua_gettop(L) - 1); // pop panto + + try runScript(L, + \\-- Writes through the borrowed wrapper land on the shared agent. + \\b:add_system_message("from borrowed") + \\assert(a:conversation():len() == 1) + \\-- Ownership-dependent methods are refused. + \\local ok, err = pcall(function() + \\ b:set_config { api_style="openai_chat", api_key="k", base_url="http://127.0.0.1:1", model="m" } + \\end) + \\assert(not ok and err:find("borrowed"), tostring(err)) + \\ok, err = pcall(function() + \\ b:register_tool { name="t", description="d", schema={type="object"}, handler=function() end } + \\end) + \\assert(not ok and err:find("borrowed"), tostring(err)) + \\-- Collecting the borrowed wrapper must not tear the agent down. + \\b = nil + \\collectgarbage("collect") + \\a:add_system_message("still alive") + \\assert(a:conversation():len() == 2) + ); +} + test "register_tool without luv fails loud at registration" { // The test binary links Lua but NOT luv, so require('luv') fails. We // assert that surfaces as a clear error at register time, not later. |
