diff options
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 147 |
1 files changed, 51 insertions, 96 deletions
diff --git a/src/main.zig b/src/main.zig index 61e78d5..9059a6f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,7 +14,6 @@ const glob = @import("glob.zig"); const system_prompt = @import("system_prompt.zig"); const command = @import("command.zig"); const command_compaction = @import("compaction.zig"); -const session_persist = @import("session_persist.zig"); // Shorthand alias for the Lua C API. The bridge module owns the actual // `@cImport`; we re-use it here so the smoke check uses identical types. @@ -44,7 +43,6 @@ test { _ = system_prompt; _ = command; _ = command_compaction; - _ = session_persist; } const Receiver = panto.provider.Receiver; @@ -55,34 +53,24 @@ const MessageRole = panto.conversation.MessageRole; /// Receiver that prints streaming deltas to stdout. Thinking blocks are /// dimmed with ANSI escape codes; text blocks render plain. /// -/// Also captures one `?Usage` per assistant response during a turn. In -/// a tool-using turn the agent loop drives multiple streamStep calls; -/// each one ends with `onMessageComplete(msg, usage)`. We append the -/// usage — including `null` when the wire didn't deliver any — to -/// `per_message_usage` in order so `persistTurn` can pair each captured -/// usage with its corresponding assistant message. +/// Persistence is owned by the agent now; the receiver is display-only. const CLIReceiver = struct { stdout: *std.Io.Writer, file: *std.Io.File.Writer, allocator: std.mem.Allocator, - /// One slot per assistant message completed during the current - /// turn, in completion order. `null` means the provider did not - /// report usage on the wire for that message (typical of - /// OpenAI-compatible proxies that ignore `stream_options.include_usage`). - per_message_usage: std.ArrayList(?panto.session_manager.Usage) = .empty, - pub fn receiver(self: *CLIReceiver) Receiver { return .{ .ptr = self, .vtable = &vtable }; } - /// Reset usage state at the start of each turn. + /// Per-turn reset hook. Currently a no-op (no per-turn receiver state), + /// retained as the REPL's turn-boundary signal. pub fn beginTurn(self: *CLIReceiver) void { - self.per_message_usage.clearRetainingCapacity(); + _ = self; } pub fn deinit(self: *CLIReceiver) void { - self.per_message_usage.deinit(self.allocator); + _ = self; } const vtable: ReceiverVTable = .{ @@ -184,13 +172,8 @@ const CLIReceiver = struct { usage: ?panto.session_manager.Usage, ) anyerror!void { const self: *CLIReceiver = @ptrCast(@alignCast(ptr)); - // Only assistant messages come through streaming. The receiver - // contract says onMessageComplete fires exactly once per - // streamStep, with role=.assistant; record the usage slot in - // turn order regardless of whether the wire actually had usage. - if (message.role == .assistant) { - try self.per_message_usage.append(self.allocator, usage); - } + _ = message; + _ = usage; try self.stdout.writeAll("\n"); try self.file.flush(); } @@ -403,17 +386,19 @@ pub fn main(init: std.process.Init) !void { }; defer session_mgr.deinit(); - var conv = panto.conversation.Conversation.init(alloc); - defer conv.deinit(); - const is_resume = session_mgr.getEntries().len > 0; + + // On resume, reconstruct the conversation from the store. The dangling + // user prompt (a trailing user entry with no assistant reply) is split + // out and currently ignored (a future TUI will prefill it for editing). + // On a fresh session, the agent starts with an empty conversation. + var adopted_conversation: ?panto.conversation.Conversation = null; if (is_resume) { - // Resumed an existing session — rebuild the conversation from the - // log. The system prompt is part of the log. (Reconciliation - // against the on-disk SYSTEM.md happens after the agent tree is - // staged, below.) - conv.deinit(); - conv = try session_mgr.rebuildConversation(); + var loaded = try session_mgr.loadConversation(alloc); + // We don't use the dangling prompt yet; free it. + if (loaded.dangling_user) |d| alloc.free(d); + loaded.dangling_user = null; + adopted_conversation = loaded.conversation; try stdout.print( "resumed session {s} ({d} entries)\n", .{ session_mgr.getSessionId()[0..@min(8, session_mgr.getSessionId().len)], session_mgr.getEntries().len }, @@ -421,11 +406,33 @@ pub fn main(init: std.process.Init) !void { } // The tool registry is owned here and referenced by the active config - // snapshot. Extensions register their tool source into it *before* the - // snapshot is built, so the agent's first turn sees them. + // snapshot. Extensions register their tool source into it *after* the + // agent is built; the agent holds a `*const Config` pointing at this + // registry, so in-place registration before the first turn is visible. var registry = panto.ToolRegistry.init(alloc); defer registry.deinit(); + // Assemble the active configuration snapshot and build the agent now, + // before luarocks bootstrap and extension loading. The agent adopts the + // store (for free persistence) and the resumed conversation (or starts + // fresh). System-prompt seeding/reconciliation below runs *through* the + // agent so those entries persist. + const active_config: panto.config.Config = .{ + .provider = provider_config, + .registry = ®istry, + .compaction = compaction_cfg, + }; + var agent = panto.agent.Agent.init( + alloc, + io, + &active_config, + session_mgr.store(), + adopted_conversation, + ); + defer agent.deinit(); + agent.persist_provider = banner_provider_initial; + agent.persist_model = banner_model_initial; + // Spin up the long-lived Lua runtime. All Lua extensions load into // one `lua_State`; module-global state survives across calls. The // runtime registers with the agent as a single `ToolSource` named @@ -461,8 +468,7 @@ pub fn main(init: std.process.Init) !void { io, init.environ_map, luarocks_rt.layout.agent_dir, - &conv, - &session_mgr, + &agent, ); } else { // Fresh session — source and install the system prompt. @@ -471,8 +477,7 @@ pub fn main(init: std.process.Init) !void { io, init.environ_map, luarocks_rt.layout.agent_dir, - &conv, - &session_mgr, + &agent, ); } @@ -506,17 +511,6 @@ pub fn main(init: std.process.Init) !void { try registry.registerSource(rt.toolSource()); } - // Assemble the active configuration snapshot (provider + tool registry) - // and create the agent holding a pointer to it. Swapping this pointer - // later (model/provider/tool changes) takes effect at the next turn. - const active_config: panto.config.Config = .{ - .provider = provider_config, - .registry = ®istry, - .compaction = compaction_cfg, - }; - var agent = panto.agent.Agent.init(alloc, io, &active_config); - defer agent.deinit(); - // Resolve the compaction system prompt (COMPACTION.md across layers, // last wins; built-in default otherwise) and arm automatic compaction. // Owned by `sp_arena`, which lives for the whole REPL. @@ -572,9 +566,7 @@ pub fn main(init: std.process.Init) !void { var cmd_ctx: command.Context = .{ .allocator = alloc, - .conv = &conv, .agent = &agent, - .session_mgr = &session_mgr, .stdout = stdout, .stdout_file = &stdout_file, .compaction_prompt = compaction_prompt, @@ -617,54 +609,18 @@ pub fn main(init: std.process.Init) !void { continue; } - try conv.addUserMessage(line); - try session_persist.appendUserPromptToSession( - alloc, - &session_mgr, - line, - banner_provider_initial, - banner_model_initial, - ); - - const entries_before_step = conv.messages.items.len; + // Submit + persist the user prompt, then drive the turn. The agent + // owns the conversation and persists everything it generates + // (assistant/tool-result messages, and the compaction window on + // auto-compaction) through its session store — the CLI no longer + // touches persistence. + try agent.submitUserMessage(line); cli_recv.beginTurn(); - agent.runStep(&conv, &recv) catch |err| { + agent.runStep(&recv) catch |err| { try stdout.print("\n[error: {s}]\n", .{@errorName(err)}); }; - if (agent.auto_compacted) { - // Automatic compaction rewrote the in-memory conversation, so - // message indices no longer align with `entries_before_step`. - // Persist the whole post-compaction window (summary + duplicated - // kept suffix + the retried turn) as fresh entries; the - // superseded prefix stays on disk and is ignored on replay. - try session_persist.persistCompaction( - alloc, - &session_mgr, - &conv, - banner_provider_initial, - banner_model_initial, - ); - } else { - // Persist whatever new entries the agent produced this turn. The - // agent loop may have appended: - // - assistant message(s) (one per provider response) - // - user messages containing ToolResult blocks (one per tool round) - // Each assistant message gets paired with the Usage that the - // receiver captured at its onMessageComplete time (or null if - // the provider didn't emit usage that round). - try session_persist.persistTurn( - alloc, - &session_mgr, - &conv, - entries_before_step, - banner_provider_initial, - banner_model_initial, - cli_recv.per_message_usage.items, - ); - } - try stdout.writeAll("\n> "); try stdout_file.flush(); } @@ -784,4 +740,3 @@ fn openSession( }, } } - |
