diff options
| author | t <t@tjp.lol> | 2026-06-07 14:57:58 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-07 14:57:58 -0600 |
| commit | 17a985cb41727b8a1bca4d95f334106c09386040 (patch) | |
| tree | 237a7f1e0d45dafb61ace13f5112ecd31170df4c /libpanto/src | |
| parent | 1eddee33902757f7e06993825a3ad1011e7ec346 (diff) | |
Alias Conversation instead of facading it; merge addAssistantMessage
A facade that forwards every method 1:1 is worse than paring the real type
down to the intended surface and aliasing it. conversation.Conversation's
interface is already the public surface we want (init/deinit, the
add*/replace* builders, and messages/allocator as plain data fields), so
public.zig now aliases it straight through and Agent.conversation() returns
a borrowed *Conversation for in-place surgery. Drops the ConversationData
alias and the pointer-wrapper.
Merged addAssistantMessageWithUsage into addAssistantMessage(blocks, ?usage)
on the real type (separate method deleted), so the alias has the intended
one-method shape; all call sites updated.
Only Agent and Stream remain true facades -- they have genuinely-internal
fields plus API-shaping renames (Stream.phase->state, Phase->State) an alias
can't express.
Diffstat (limited to 'libpanto/src')
| -rw-r--r-- | libpanto/src/agent.zig | 22 | ||||
| -rw-r--r-- | libpanto/src/anthropic_messages_json.zig | 10 | ||||
| -rw-r--r-- | libpanto/src/conversation.zig | 22 | ||||
| -rw-r--r-- | libpanto/src/file_system_jsonl_store.zig | 2 | ||||
| -rw-r--r-- | libpanto/src/openai_chat_json.zig | 6 | ||||
| -rw-r--r-- | libpanto/src/provider_anthropic_messages.zig | 2 | ||||
| -rw-r--r-- | libpanto/src/provider_openai_chat.zig | 2 | ||||
| -rw-r--r-- | libpanto/src/public.zig | 53 |
8 files changed, 44 insertions, 75 deletions
diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig index 6d62b8a..45347ea 100644 --- a/libpanto/src/agent.zig +++ b/libpanto/src/agent.zig @@ -1552,7 +1552,7 @@ const StubResponse = struct { } const moved = try blocks.toOwnedSlice(self.allocator); defer self.allocator.free(moved); - try self.conv.addAssistantMessageWithUsage(moved, self.turn.usage); + try self.conv.addAssistantMessage(moved, self.turn.usage); const msg = self.conv.messages.items[self.conv.messages.items.len - 1]; try out.push(.{ .message_complete = .{ .message = msg, .usage = self.turn.usage } }); @@ -2651,11 +2651,11 @@ test "compact: summarizes prefix, keeps suffix, system survives" { try conv.addUserMessage("first question here with several words"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, - }); + }, null); try conv.addUserMessage("second recent question"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "second recent answer") }, - }); + }, null); const res = try agent.compact("Summarize the conversation.", null); try testing.expect(res.compacted); @@ -2714,21 +2714,21 @@ test "compact: restated suffix usage reconstructs a fresh cumulative chain" { // = 600. Its real usage (with cache buckets) is irrelevant // post-compaction. try conv.addUserMessage("first question here with several words"); - try conv.addAssistantMessageWithUsage( + try conv.addAssistantMessage( &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with words") }}, .{ .input = 500, .output = 50, .cache_read = 40, .cache_write = 10 }, ); // Kept turn 1: cumulative 608 (delta 8 over prefix). Reasoning is a // subset of output. try conv.addUserMessage("kept question"); // 2 words => ceil(2*1.3)=3 tokens - try conv.addAssistantMessageWithUsage( + try conv.addAssistantMessage( &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "kept answer") }}, .{ .input = 600, .output = 8, .reasoning = 5 }, ); // Kept turn 2: cumulative 614 (delta 6). Cache buckets present to prove // they collapse into `input` on restatement. try conv.addUserMessage("two more words here"); // 4 words => ceil(4*1.3)=6 - try conv.addAssistantMessageWithUsage( + try conv.addAssistantMessage( &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "final answer") }}, .{ .input = 600, .output = 4, .cache_read = 8, .cache_write = 2 }, ); @@ -2788,7 +2788,7 @@ test "compact: no-op when conversation already fits the budget" { try conv.addUserMessage("hi"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "hello") }, - }); + }, null); const res = try agent.compact("Summarize.", null); try testing.expect(!res.compacted); @@ -2825,11 +2825,11 @@ test "compact: extra instructions are appended to the system prompt" { try conv.addUserMessage("question one two three"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer one two three") }, - }); + }, null); try conv.addUserMessage("question two"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer two") }, - }); + }, null); const res = try agent.compact("Base prompt.", "keep bug #3 details"); try testing.expect(res.compacted); @@ -2867,7 +2867,7 @@ test "runStep: auto-compacts on context overflow and retries once" { try conv.addUserMessage("first question with several words here"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, - }); + }, null); try drainTurn(&agent, "second recent question"); @@ -3247,7 +3247,7 @@ test "runStep: context-overflow compaction fires a compaction retry notification try conv.addUserMessage("first question with several words here"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, - }); + }, null); var rr = RetryRecorder{ .allocator = allocator }; defer rr.deinit(); diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index 940d55e..c2b133e 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -712,7 +712,7 @@ test "serializeRequest - signed assistant Thinking blocks round-trip" { .signature = sig, } }, .{ .Text = try conversation.textualBlockFromSlice(allocator, "the answer is 42") }, - }); + }, null); const cfg = testConfig("claude-x"); var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); @@ -755,7 +755,7 @@ test "serializeRequest - unsigned Thinking blocks are dropped" { .signature = null, } }, .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, - }); + }, null); const cfg = testConfig("claude-x"); var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); @@ -968,7 +968,7 @@ test "serializeRequest - dotted names are wire-encoded in tools and history" { try input.appendSlice(allocator, "{}"); try conv.addAssistantMessage(&.{ .{ .ToolUse = .{ .id = id, .name = name, .input = input } }, - }); + }, null); var tools = tool_registry_mod.ToolRegistry.init(allocator); defer tools.deinit(); @@ -1006,7 +1006,7 @@ test "serializeRequest - assistant ToolUse becomes tool_use content block" { try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "calling tool") }, .{ .ToolUse = .{ .id = id, .name = name, .input = input } }, - }); + }, null); var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); defer empty_tools.deinit(); @@ -1144,7 +1144,7 @@ test "serializeRequest - empty ToolUse input becomes {} not invalid JSON" { const name = try allocator.dupe(u8, "noargs"); try conv.addAssistantMessage(&.{ .{ .ToolUse = .{ .id = id, .name = name, .input = .empty } }, - }); + }, null); var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); defer empty_tools.deinit(); diff --git a/libpanto/src/conversation.zig b/libpanto/src/conversation.zig index 1fc5016..a436378 100644 --- a/libpanto/src/conversation.zig +++ b/libpanto/src/conversation.zig @@ -292,15 +292,11 @@ pub const Conversation = struct { }); } - /// Append an assistant message. Ownership of the blocks is transferred - /// to the conversation; the caller must not deinit them after this call. - pub fn addAssistantMessage(self: *Conversation, blocks: []const ContentBlock) !void { - return self.addAssistantMessageWithUsage(blocks, null); - } - - /// Append an assistant message tagged with its provider-reported - /// `usage`. Ownership of the blocks is transferred to the conversation. - pub fn addAssistantMessageWithUsage( + /// Append an assistant message, optionally tagged with its + /// provider-reported `usage` (pass `null` for none). Ownership of the + /// blocks is transferred to the conversation; the caller must not + /// deinit them after this call. + pub fn addAssistantMessage( self: *Conversation, blocks: []const ContentBlock, usage: ?Usage, @@ -415,7 +411,7 @@ test "Conversation - add messages and verify content" { try conv.addUserMessage("Hello!"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "Hi there!") }, - }); + }, null); try std.testing.expectEqual(@as(usize, 3), conv.messages.items.len); @@ -455,7 +451,7 @@ test "Conversation - deinit frees without leaks" { try conv.addUserMessage("user message"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "response") }, - }); + }, null); conv.deinit(); } @@ -468,7 +464,7 @@ test "ContentBlock - Thinking variant" { try conv.addAssistantMessage(&.{ .{ .Thinking = .{ .text = try textualBlockFromSlice(allocator, "hmm...") } }, .{ .Text = try textualBlockFromSlice(allocator, "answer") }, - }); + }, null); try std.testing.expectEqual(@as(usize, 2), conv.messages.items[0].content.items.len); try std.testing.expectEqualStrings("hmm...", conv.messages.items[0].content.items[0].Thinking.text.items); @@ -574,7 +570,7 @@ test "latestCompactionIndex - null when never compacted" { try conv.addUserMessage("hi"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "hello") }, - }); + }, null); try std.testing.expect(latestCompactionIndex(conv.messages.items) == null); } diff --git a/libpanto/src/file_system_jsonl_store.zig b/libpanto/src/file_system_jsonl_store.zig index 10b65b3..bbf8ea5 100644 --- a/libpanto/src/file_system_jsonl_store.zig +++ b/libpanto/src/file_system_jsonl_store.zig @@ -2055,7 +2055,7 @@ test "FileSystemJSONLStore catalog: create → append → load round-trips" { var conv = conversation_mod.Conversation.init(testing.allocator); defer conv.deinit(); try conv.addUserMessage("ping"); - try conv.addAssistantMessage(&.{}); + try conv.addAssistantMessage(&.{}, null); const id: session_store_mod.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" }; var batch = [_]session_store_mod.PersistentMessage{ diff --git a/libpanto/src/openai_chat_json.zig b/libpanto/src/openai_chat_json.zig index 2307ce8..53d4b0c 100644 --- a/libpanto/src/openai_chat_json.zig +++ b/libpanto/src/openai_chat_json.zig @@ -640,7 +640,7 @@ test "serializeRequest - assistant Thinking blocks are stripped from outbound hi try conv.addAssistantMessage(&.{ .{ .Thinking = .{ .text = try conversation.textualBlockFromSlice(allocator, "thinking step") } }, .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer here") }, - }); + }, null); const cfg = testConfig("gpt-4o"); var tools = emptyTools(); @@ -865,7 +865,7 @@ test "serializeRequest - assistant ToolUse becomes tool_calls" { try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "calling tool") }, .{ .ToolUse = .{ .id = id, .name = name, .input = args } }, - }); + }, null); var tools = emptyTools(); defer tools.deinit(); @@ -1053,7 +1053,7 @@ test "serializeRequest - compaction summary trims superseded prefix" { try conv.addUserMessage("ancient question"); try conv.addAssistantMessage(&.{ .{ .Text = try conversation.textualBlockFromSlice(allocator, "ancient answer") }, - }); + }, null); // Compaction summary resets conversation context. try conv.addCompactionSummary("summary of the ancient exchange"); // Kept-verbatim suffix replayed after the summary. diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig index 4bd146a..d76793b 100644 --- a/libpanto/src/provider_anthropic_messages.zig +++ b/libpanto/src/provider_anthropic_messages.zig @@ -502,7 +502,7 @@ const StreamState = struct { defer self.allocator.free(moved_blocks); const usage: ?provider_mod.Usage = if (self.usage_seen) self.usage else null; - try conv.addAssistantMessageWithUsage(moved_blocks, usage); + try conv.addAssistantMessage(moved_blocks, usage); const msg = conv.messages.items[conv.messages.items.len - 1]; try out.push(.{ .message_complete = .{ .message = msg, .usage = usage } }); diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig index 30bccb6..ed646e8 100644 --- a/libpanto/src/provider_openai_chat.zig +++ b/libpanto/src/provider_openai_chat.zig @@ -635,7 +635,7 @@ const StreamState = struct { const moved_blocks = try self.blocks.toOwnedSlice(self.allocator); defer self.allocator.free(moved_blocks); - try conv.addAssistantMessageWithUsage(moved_blocks, self.usage); + try conv.addAssistantMessage(moved_blocks, self.usage); const msg = conv.messages.items[conv.messages.items.len - 1]; try out.push(.{ .message_complete = .{ .message = msg, .usage = self.usage } }); diff --git a/libpanto/src/public.zig b/libpanto/src/public.zig index 56a0676..31d5f7b 100644 --- a/libpanto/src/public.zig +++ b/libpanto/src/public.zig @@ -82,43 +82,14 @@ pub const CompactionSummaryBlock = conversation_mod.CompactionSummaryBlock; pub const Usage = conversation_mod.Usage; pub const effectiveSystemBlocks = conversation_mod.effectiveSystemBlocks; -/// The owned conversation value type. This is what `Session.load` returns -/// and what `Agent.init` adopts. Build one standalone with `init` for +/// The conversation type. Aliased straight through: its interface has been +/// pared down to exactly the public surface (constructors `init`/`deinit`, +/// the `add*`/`replace*` builders, and the `messages`/`allocator` data +/// fields), so no façade is needed. This is what `Session.load` returns, +/// what `Agent.init` adopts, and what `Agent.conversation()` borrows a +/// pointer to (for in-place surgery). Build one standalone with `init` for /// by-hand construction; once handed to an `Agent` it is owned by the agent. -/// For *operating on* an agent's live conversation, use the `Conversation` -/// handle (below) via `Agent.conversation()`. -pub const ConversationData = conversation_mod.Conversation; - -/// A borrowed handle onto a conversation owned by an `Agent`. Pointer- -/// wrapping makes the "don't move the conversation" invariant structural: -/// the handle is a cheap copyable view, minted per-call by -/// `Agent.conversation()`, borrowing state owned by its `Agent`. -pub const Conversation = struct { - inner: *conversation_mod.Conversation, - - /// Read the in-memory messages directly, for context-management surgery. - pub fn messages(self: Conversation) []conversation_mod.Message { - return self.inner.messages.items; - } - pub fn addUserMessage(self: Conversation, text: []const u8) !void { - return self.inner.addUserMessage(text); - } - pub fn addAssistantMessage(self: Conversation, blocks: []const ContentBlock, usage: ?Usage) !void { - if (usage) |u| { - return self.inner.addAssistantMessageWithUsage(blocks, u); - } - return self.inner.addAssistantMessage(blocks); - } - pub fn addSystemMessage(self: Conversation, text: []const u8) !void { - return self.inner.addSystemMessage(text); - } - pub fn replaceSystemMessage(self: Conversation, text: []const u8) !void { - return self.inner.replaceSystemMessage(text); - } - pub fn addCompactionSummary(self: Conversation, text: []const u8) !void { - return self.inner.addCompactionSummary(text); - } -}; +pub const Conversation = conversation_mod.Conversation; // =========================================================================== // Tools (data + small façade) @@ -199,7 +170,7 @@ pub const Agent = struct { io: std.Io, config: *const Config, session: Session, - maybe_conversation: ?ConversationData, + maybe_conversation: ?Conversation, ) !Agent { const inner = try allocator.create(agent_mod.Agent); inner.* = agent_mod.Agent.init(allocator, io, config, session, maybe_conversation); @@ -246,9 +217,11 @@ pub const Agent = struct { return self.inner.compactAndPersist(override_system_prompt, extra); } - /// A borrowed handle onto the agent's live conversation. - pub fn conversation(self: Agent) Conversation { - return .{ .inner = &self.inner.conversation }; + /// A borrowed pointer to the agent's live conversation, for in-place + /// context-management surgery. Valid for the agent's lifetime; do not + /// retain past it. + pub fn conversation(self: Agent) *Conversation { + return &self.inner.conversation; } /// The id of the session this agent appends to. |
