diff options
| author | t <t@tjp.lol> | 2026-07-03 13:01:25 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-03 13:14:44 -0600 |
| commit | 51bbb62ce5d82c460c75adfc03813e84ce5183d3 (patch) | |
| tree | 9d3ce5b25c72b87e6b1febeb47882b40cdbf6e5e /libpanto-lua/src/module.zig | |
| parent | c4d7f70ff831cfcdad0f2a6224f9a14f86905de6 (diff) | |
support pending submissions on an Agent object
Diffstat (limited to 'libpanto-lua/src/module.zig')
| -rw-r--r-- | libpanto-lua/src/module.zig | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/libpanto-lua/src/module.zig b/libpanto-lua/src/module.zig index 17d0fb2..910334f 100644 --- a/libpanto-lua/src/module.zig +++ b/libpanto-lua/src/module.zig @@ -48,6 +48,10 @@ //! if ev.type == "content_delta" then io.write(ev.delta) end //! end //! +//! -- Borrowed host agents only (`panto.ext.agent` in the CLI): queue the +//! -- next user message; the HOST opens and pumps the turn, not the caller. +//! panto.ext.agent:submit("prompt") -- or a { <block>, ... } array +//! //! ## Tools, coroutines, and luv //! //! Tool handlers run as Lua coroutines driven by libuv (via the `luv` @@ -354,6 +358,7 @@ fn registerMetatables(L: *c.lua_State) void { if (c.luaL_newmetatable(L, AGENT_MT) != 0) { c.lua_createtable(L, 0, 6); // methods setMethod(L, "run", agentRun); + setMethod(L, "submit", agentSubmit); setMethod(L, "register_tool", agentRegisterTool); setMethod(L, "set_config", agentSetConfig); setMethod(L, "add_system_message", agentAddSystemMessage); @@ -670,6 +675,50 @@ fn agentRun(L_opt: ?*c.lua_State) callconv(.c) c_int { return 1; } +/// `agent:submit(text | { <block>, ... })` — queue user-message blocks for +/// the HOST front end to open the next turn with, instead of opening (and +/// pumping) a turn here like `run` does. Only available on the borrowed +/// host session agent (`panto.ext.agent`): the panto CLI drains the queue +/// once slash-command handling completes and drives the turn through its +/// native renderer (streaming paint, Esc interrupt, auth fallbacks). +/// Accepts a plain string or the same block-array shape as +/// `conv:add_user_blocks`. Multiple calls append to one pending message. +fn agentSubmit(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: submit is only available on the host session agent (panto.ext.agent); use run"); + if (box.closed) return luaErr(L, "panto: agent is closed"); + + const alloc = box.agent.conversation.allocator; + var blocks: std.ArrayList(panto.ContentBlock) = .empty; + defer blocks.deinit(c_allocator); + + if (c.lua_type(L, 2) == T_STRING) { + var len: usize = 0; + const ptr = c.lua_tolstring(L, 2, &len); + const tb = panto.textualBlockFromSlice(alloc, ptr[0..len]) catch + return luaErr(L, "panto: out of memory"); + blocks.append(c_allocator, .{ .Text = tb }) catch { + var b: panto.ContentBlock = .{ .Text = tb }; + b.deinit(alloc); + return luaErr(L, "panto: out of memory"); + }; + } else { + c.luaL_checktype(L, 2, T_TABLE); + collectBlocks(L, alloc, 2, &blocks) catch |e| { + for (blocks.items) |*b| b.deinit(alloc); + return luaErr(L, blockErrorMessage(e)); + }; + } + + box.agent.queueSubmission(blocks.items) catch { + for (blocks.items) |*b| b.deinit(alloc); + return luaErr(L, "panto: out of memory"); + }; + return 0; +} + fn agentGc(L_opt: ?*c.lua_State) callconv(.c) c_int { const L = L_opt.?; const box = checkAgent(L, 1); @@ -2568,6 +2617,43 @@ test "_wrap_agent: borrowed agent shares the host's, and __gc leaves it alive" { ); } +test "submit: queues on borrowed agents, refused on owned ones" { + 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" } + \\local ok, err = pcall(function() a:submit("nope") end) + \\assert(not ok and err:find("panto.ext.agent"), tostring(err)) + ); + // Wrap borrowed, as the CLI's `setExtAgent` does. + _ = 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, + \\b:submit("hello") + \\b:submit({ { type = "text", text = "world" } }) + \\local ok, err = pcall(function() b:submit({ { type = "bogus" } }) end) + \\assert(not ok, "bad block shape must error") + ); + // Both submits accumulated into ONE pending message on the shared agent. + const taken = (try owner.agent.takeSubmission()) orelse return error.NothingQueued; + defer { + for (taken) |*b| b.deinit(owner.agent.conversation.allocator); + owner.agent.conversation.allocator.free(taken); + } + try std.testing.expectEqual(@as(usize, 2), taken.len); + try std.testing.expectEqualStrings("hello", taken[0].Text.items); + try std.testing.expectEqualStrings("world", taken[1].Text.items); + try std.testing.expectEqual(@as(?[]panto.ContentBlock, null), try owner.agent.takeSubmission()); +} + 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. |
