summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-07 14:57:58 -0600
committert <t@tjp.lol>2026-06-07 14:57:58 -0600
commit17a985cb41727b8a1bca4d95f334106c09386040 (patch)
tree237a7f1e0d45dafb61ace13f5112ecd31170df4c
parent1eddee33902757f7e06993825a3ad1011e7ec346 (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.
-rw-r--r--docs/libpanto-cleanup.md18
-rw-r--r--libpanto/src/agent.zig22
-rw-r--r--libpanto/src/anthropic_messages_json.zig10
-rw-r--r--libpanto/src/conversation.zig22
-rw-r--r--libpanto/src/file_system_jsonl_store.zig2
-rw-r--r--libpanto/src/openai_chat_json.zig6
-rw-r--r--libpanto/src/provider_anthropic_messages.zig2
-rw-r--r--libpanto/src/provider_openai_chat.zig2
-rw-r--r--libpanto/src/public.zig53
-rw-r--r--src/main.zig2
-rw-r--r--src/system_prompt.zig18
11 files changed, 71 insertions, 86 deletions
diff --git a/docs/libpanto-cleanup.md b/docs/libpanto-cleanup.md
index a495ff5..bc1e1aa 100644
--- a/docs/libpanto-cleanup.md
+++ b/docs/libpanto-cleanup.md
@@ -71,9 +71,25 @@ touching `agent.zig`.
### Three buckets — wrap, handle, or alias
+> **Updated during implementation:** `Conversation` moved out of the
+> behavioral-wrap bucket. Its internal interface was pared down to exactly
+> the intended public surface (constructors + the `add*`/`replace*`
+> builders; `messages`/`allocator` are fine as public data fields for a Zig
+> object), so `public.zig` now **aliases** `conversation.Conversation`
+> straight through instead of pointer-wrapping it. `Agent.conversation()`
+> returns a borrowed `*Conversation` for in-place surgery. Only `Agent` and
+> `Stream` remain true façades — they have genuinely-internal fields
+> (`config`/`registry`/`session`/`open_stream_fn`/... and
+> `queue`/`response`/`pending_error`/...) plus API-shaping renames
+> (`Stream.phase`→`state`, `Phase`→`State`) that an alias can't express.
+> `addAssistantMessageWithUsage` was merged into
+> `addAssistantMessage(blocks, ?usage)` on the real type (the separate
+> method deleted), so the alias already has the intended one-method shape.
+
| bucket | rule | types |
| --- | --- | --- |
-| **behavioral** | wrap a **pointer** to a heap-pinned internal | `Agent`, `Stream`, `Conversation` |
+| **behavioral** | wrap a **pointer** to a heap-pinned internal | `Agent`, `Stream` |
+| **behavioral (pared + aliased)** | trim the real type to the public surface, then alias | `Conversation` |
| **data (read/output)** | **alias** straight through; inspected field-by-field | `Event` (+ nested), `Usage`, `Pricing`, `SessionInfo` |
| **data (constructed)** | **alias** straight through; Zig users build them with `ArrayList` directly | `ContentBlock`, `Message`, the block types, `Config` family, `ResultPart` |
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.
diff --git a/src/main.zig b/src/main.zig
index 61a88bd..2887730 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -358,7 +358,7 @@ pub fn main(init: std.process.Init) !void {
// On resume, reconstruct the conversation from the store. (The
// dangling-prompt recovery feature was dropped in R2.) On a fresh
// session, the agent starts with an empty conversation.
- var adopted_conversation: ?panto.ConversationData = null;
+ var adopted_conversation: ?panto.Conversation = null;
if (is_resume) {
adopted_conversation = try session.load();
const sid = session.info.id;
diff --git a/src/system_prompt.zig b/src/system_prompt.zig
index 39c3ea8..eea0b97 100644
--- a/src/system_prompt.zig
+++ b/src/system_prompt.zig
@@ -193,7 +193,7 @@ pub fn reconcileResumeLayers(
const resolved = try resolveLayers(arena, io, base_dir, user_dir, project_dir);
const config_blocks = try resolved.blocks(arena);
- const window = try effectiveConfigWindow(arena, agent.conversation().messages());
+ const window = try effectiveConfigWindow(arena, agent.conversation().messages.items);
if (blocksEqual(config_blocks, window)) return;
@@ -339,7 +339,7 @@ test "blocksEqual - length and content" {
test "effectiveConfigWindow - no replace anchors to leading system blocks" {
const alloc = testing.allocator;
- var conv = panto.ConversationData.init(alloc);
+ var conv = panto.Conversation.init(alloc);
defer conv.deinit();
try conv.addSystemMessage("seed");
@@ -357,7 +357,7 @@ test "effectiveConfigWindow - no replace anchors to leading system blocks" {
test "effectiveConfigWindow - anchors to last replace block" {
const alloc = testing.allocator;
- var conv = panto.ConversationData.init(alloc);
+ var conv = panto.Conversation.init(alloc);
defer conv.deinit();
try conv.addSystemMessage("old seed");
@@ -483,7 +483,7 @@ const SPAgentHarness = struct {
fn init(
self: *SPAgentHarness,
session: panto.Session,
- adopted: ?panto.ConversationData,
+ adopted: ?panto.Conversation,
) !void {
self.config = .{
.provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } },
@@ -502,9 +502,9 @@ const SPAgentHarness = struct {
/// memory until the first assistant message; a realistic resume only
/// sees what a completed turn persisted.)
fn forceFlush(self: *SPAgentHarness) !void {
- var conv = panto.ConversationData.init(testing.allocator);
+ var conv = panto.Conversation.init(testing.allocator);
defer conv.deinit();
- try conv.addAssistantMessage(&.{});
+ try conv.addAssistantMessage(&.{}, null);
const id: panto.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" };
var batch = [_]panto.PersistentMessage{
.{ .message = conv.messages.items[0], .identity = id },
@@ -557,7 +557,7 @@ test "seedFresh then no-config-change resume is a no-op" {
try h2.init(sess2, conv2);
defer h2.deinit();
try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent);
- const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
+ const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages.items);
try testing.expectEqual(seeded_count, eff_after.items.len);
}
@@ -600,7 +600,7 @@ test "resume after config change appends replace + append sequence" {
try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent);
// The effective prompt now reflects only the new config blocks.
- const eff = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
+ const eff = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages.items);
try testing.expectEqual(@as(usize, 2), eff.items.len);
try testing.expectEqualStrings("new seed", eff.items[0]);
try testing.expectEqualStrings("new append", eff.items[1]);
@@ -608,7 +608,7 @@ test "resume after config change appends replace + append sequence" {
// A second no-op resume must not change the effective prompt (anchors
// to the new `replace` window, not the stale original seed).
try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent);
- const eff2 = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
+ const eff2 = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages.items);
try testing.expectEqual(@as(usize, 2), eff2.items.len);
}