summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig147
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 = &registry,
+ .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 = &registry,
- .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(
},
}
}
-