summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/libpanto-cleanup.md50
-rw-r--r--libpanto/src/agent.zig23
-rw-r--r--libpanto/src/config.zig8
-rw-r--r--libpanto/src/public.zig93
-rw-r--r--src/command.zig8
-rw-r--r--src/compaction.zig2
-rw-r--r--src/main.zig20
-rw-r--r--src/system_prompt.zig107
8 files changed, 191 insertions, 120 deletions
diff --git a/docs/libpanto-cleanup.md b/docs/libpanto-cleanup.md
index 5fb8db3..a495ff5 100644
--- a/docs/libpanto-cleanup.md
+++ b/docs/libpanto-cleanup.md
@@ -116,18 +116,26 @@ natural there as well.)
> (`ResultParts.fromText`/`fromTextOwned`/`deinit`, replacing the freestanding
> `textResult`/`ownedTextResult`/`freeResultParts`).
>
-> **FLAGGED:** the CLI still reaches two internal namespaces — `panto.agent`
-> and `panto.conversation` — because it is a *deep* embedder that drives the
-> loop below the curated `Agent`/`Conversation` façades: it needs
-> system-prompt seeding *through* the agent (`agent.addSystemMessage`),
-> `compaction_system_prompt`, the injectable `open_stream_fn` test seam,
-> direct `agent.conversation` field access, raw standalone `Conversation`
-> values (the façade `Conversation` only wraps a pointer to an agent-owned
-> one), and `compactAndPersist`. These two namespaces remain re-exported from
-> `public.zig` as a documented deep-embedder escape hatch (not part of the
-> stable surface a C-ABI/binding should use). Closing this gap — growing the
-> façade with standalone `Conversation` construction and agent
-> system-prompt/compaction plumbing — is the natural follow-up.
+> **Holes plugged (escape hatch removed).** The deep-embedder gap is closed;
+> the CLI now uses **only** the curated `public.zig` surface (zero
+> `panto.agent`/`panto.conversation` reaches), and the transitional
+> internal-namespace re-exports are deleted. The additions that closed it:
+> - **`Agent.init` / `Agent.deinit`** on the public façade (heap-pin the
+> inner; the handle is a cheap copyable value passed by-value everywhere).
+> - **`Agent.addSystemMessage(text)`** (`.append`) and
+> **`Agent.setSystemPrompt(text)`** (`.replace`) — the two `SystemMode`s.
+> - **`CompactionConfig.compaction_prompt`** owns the compaction system
+> prompt (auto-compaction reads it; the old `Agent.compaction_system_prompt`
+> field is deleted). **`Agent.compact(override_system_prompt: ?[]const u8,
+> extra)`** falls back to it when the override is null.
+> - **`ConversationData`** = the owned conversation value type (what
+> `Session.load` returns and `Agent.init` adopts), distinct from the
+> borrowed `Conversation` handle returned by `Agent.conversation()`.
+> - **`Agent.sessionId()`** accessor.
+>
+> `compactAndPersist` was an internal detail — the public `Agent.compact()`
+> *is* it (renamed, persists by default); the CLI `/compact` command now
+> calls `agent.compact(...)`.
## The two jobs the API must serve
@@ -165,7 +173,7 @@ pub const OpenAIChatConfig = config.OpenAIChatConfig;
pub const AnthropicMessagesConfig = config.AnthropicMessagesConfig;
pub const APIStyle = config.APIStyle;
pub const ReasoningEffort = config.ReasoningEffort;
-pub const CompactionConfig = config.CompactionConfig;
+pub const CompactionConfig = config.CompactionConfig; // now owns `compaction_prompt`
pub const RetryConfig = config.RetryConfig;
```
@@ -185,9 +193,14 @@ pub const Agent = struct {
pub fn setConfig(self: Agent, config: *const Config) void; // swap provider/model between turns
pub fn run(self: Agent, message: UserMessage) !Stream;
- pub fn compact(self: Agent, system_prompt: []const u8, extra: ?[]const u8) !CompactionResult; // persists
+ // override_system_prompt falls back to config.compaction.compaction_prompt:
+ pub fn compact(self: Agent, override_system_prompt: ?[]const u8, extra: ?[]const u8) !CompactionResult; // persists
+
+ pub fn addSystemMessage(self: Agent, text: []const u8) !void; // .append, persists
+ pub fn setSystemPrompt(self: Agent, text: []const u8) !void; // .replace, persists
pub fn conversation(self: Agent) Conversation; // borrowed handle
+ pub fn sessionId(self: Agent) []const u8;
};
pub const UserMessage = struct { text: []const u8 }; // top-level, NOT nested in Agent
@@ -201,8 +214,13 @@ Changes from today:
longer means rebuilding the whole tool list.
- **`UserMessage` is top-level**, defined in `public.zig` (translated to the
internal `agent.Agent.UserMessage` inside `run`).
-- **`Agent.addSystemMessage` is dropped** — system messages are a
- `Conversation` concern (see below).
+- **`Agent.addSystemMessage` / `setSystemPrompt` are kept on `Agent`** (they
+ persist through the turn-path, which a bare `Conversation` mutation does
+ not). *Updated during implementation:* the plan originally proposed
+ dropping `Agent.addSystemMessage` in favor of `Conversation`-only system
+ messages, but the CLI's system-prompt seeding/reconciliation needs the
+ agent to *persist* the system entry with its `SystemMode`, so the two
+ agent methods stay (façade `addSystemMessage`/`setSystemPrompt`).
- **`compactAndPersist` → `compact`** (persists by default). The pure
no-persist transform stays a private internal helper.
- **`registry()` / `httpClient`-style accessors not exposed.**
diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig
index 5f9bee5..6d62b8a 100644
--- a/libpanto/src/agent.zig
+++ b/libpanto/src/agent.zig
@@ -206,11 +206,6 @@ pub const Agent = struct {
/// Injectable streaming seam. Defaults to the real provider dispatch
/// (`provider_mod.openStream`); tests override it with a stub.
open_stream_fn: provider_mod.OpenStreamFn = provider_mod.openStream,
- /// Compaction system prompt used for automatic compaction on context
- /// overflow. Borrowed; set by the embedder (resolved from its
- /// `COMPACTION.md` layers). When null, auto-compaction is disabled and
- /// a context-overflow error propagates unchanged.
- compaction_system_prompt: ?[]const u8 = null,
/// Set by the embedder after `runStep` returns to learn whether an
/// automatic compaction occurred this turn (so it can persist the
/// rewritten conversation). Reset at the top of each `runStep`.
@@ -454,7 +449,7 @@ pub const Agent = struct {
err: anyerror,
) !provider_mod.ProviderStream {
if (self.auto_compacted) return err; // already retried once this turn
- const sys = self.compaction_system_prompt orelse return err;
+ const sys = self.config.compaction.compaction_prompt orelse return err;
const res = try self.compact(sys, null);
if (!res.compacted) return err; // nothing to shed; give up
self.auto_compacted = true;
@@ -595,11 +590,19 @@ pub const Agent = struct {
/// if anything was compacted, appends the new compaction window
/// (summary + restated kept suffix) to the store. Returns the same
/// `CompactionResult` for the embedder to report.
+ ///
+ /// `override_system_prompt`, when non-null, is the compaction system
+ /// prompt for this run; when null it falls back to
+ /// `config.compaction.compaction_prompt`. With neither set, this errors
+ /// (`error.NoCompactionPrompt`).
pub fn compactAndPersist(
self: *Agent,
- system_prompt: []const u8,
+ override_system_prompt: ?[]const u8,
extra_instructions: ?[]const u8,
) !CompactionResult {
+ const system_prompt = override_system_prompt orelse
+ self.config.compaction.compaction_prompt orelse
+ return error.NoCompactionPrompt;
const res = try self.compact(system_prompt, extra_instructions);
if (res.compacted) {
try turn_persist.persistCompaction(
@@ -2852,13 +2855,12 @@ test "runStep: auto-compacts on context overflow and retries once" {
var h = TestHarness.init(allocator);
defer h.deinit();
h.activate();
- h.config.compaction = .{ .keep_verbatim = 10 };
+ h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." };
var ns = null_store_mod.NullStore.init(allocator);
var agent = Agent.init(allocator, io, &h.config, ns.store().create(), null);
defer agent.deinit();
h.seedInto(&agent);
agent.open_stream_fn = stub.install();
- agent.compaction_system_prompt = "Summarize the conversation.";
const conv = &agent.conversation;
try conv.addSystemMessage("you are helpful");
@@ -3233,13 +3235,12 @@ test "runStep: context-overflow compaction fires a compaction retry notification
var h = TestHarness.init(allocator);
defer h.deinit();
h.activate();
- h.config.compaction = .{ .keep_verbatim = 10 };
+ h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." };
var ns = null_store_mod.NullStore.init(allocator);
var agent = Agent.init(allocator, io, &h.config, ns.store().create(), null);
defer agent.deinit();
h.seedInto(&agent);
agent.open_stream_fn = stub.install();
- agent.compaction_system_prompt = "Summarize the conversation.";
const conv = &agent.conversation;
try conv.addSystemMessage("you are helpful");
diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig
index 203a091..5e01bb1 100644
--- a/libpanto/src/config.zig
+++ b/libpanto/src/config.zig
@@ -116,6 +116,14 @@ pub const WireIdentity = struct {
pub const CompactionConfig = struct {
keep_verbatim: u32 = 20_000,
model: ?ProviderConfig = null,
+ /// The compaction system prompt. Used both for automatic compaction on
+ /// context overflow and as the default for an explicit `Agent.compact`
+ /// call (which may override it per-call). Borrowed; set by the embedder
+ /// (e.g. resolved from its `COMPACTION.md` layers, or a built-in
+ /// default). When null, auto-compaction is disabled and a
+ /// context-overflow error propagates unchanged, and an explicit
+ /// `compact` with no override is a no-op error.
+ compaction_prompt: ?[]const u8 = null,
};
/// Policy for retrying transient provider/API failures. Conservative
diff --git a/libpanto/src/public.zig b/libpanto/src/public.zig
index 951bd3a..56a0676 100644
--- a/libpanto/src/public.zig
+++ b/libpanto/src/public.zig
@@ -64,7 +64,7 @@ pub const RetryConfig = config_mod.RetryConfig;
pub const WireIdentity = config_mod.WireIdentity;
// ===========================================================================
-// Conversation construction (data, aliased — Job 2)
+// Conversation construction (data, aliased)
// ===========================================================================
pub const ContentBlock = conversation_mod.ContentBlock;
@@ -82,9 +82,17 @@ pub const CompactionSummaryBlock = conversation_mod.CompactionSummaryBlock;
pub const Usage = conversation_mod.Usage;
pub const effectiveSystemBlocks = conversation_mod.effectiveSystemBlocks;
-/// A borrowed handle onto a conversation owned by an `Agent`, or a
-/// standalone-owned conversation built by hand. Pointer-wrapping makes the
-/// "don't move the conversation" invariant structural.
+/// The owned conversation value type. This is what `Session.load` returns
+/// and what `Agent.init` adopts. 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,
@@ -113,7 +121,7 @@ pub const Conversation = struct {
};
// ===========================================================================
-// Tools (data + small façade — Job 1)
+// Tools (data + small façade)
// ===========================================================================
pub const Tool = tool_mod.Tool;
@@ -179,6 +187,31 @@ pub const CompactionResult = agent_mod.Agent.CompactionResult;
pub const Agent = struct {
inner: *agent_mod.Agent,
+ /// Construct an agent. The inner is heap-pinned at `init`, so the
+ /// returned `Agent` is a cheap copyable handle ("don't move the Agent"
+ /// stops being a rule anyone can violate). `session` is minted via
+ /// `store.create()` (fresh) or `resolve`/`latest` (resume). When
+ /// `maybe_conversation` is non-null it is adopted (ownership
+ /// transferred) — the resume path hands the value returned by
+ /// `Session.load`.
+ pub fn init(
+ allocator: std.mem.Allocator,
+ io: std.Io,
+ config: *const Config,
+ session: Session,
+ maybe_conversation: ?ConversationData,
+ ) !Agent {
+ const inner = try allocator.create(agent_mod.Agent);
+ inner.* = agent_mod.Agent.init(allocator, io, config, session, maybe_conversation);
+ return .{ .inner = inner };
+ }
+
+ pub fn deinit(self: Agent) void {
+ const allocator = self.inner.allocator;
+ self.inner.deinit();
+ allocator.destroy(self.inner);
+ }
+
pub fn registerTool(self: Agent, t: Tool) !void {
return self.inner.registerTool(t);
}
@@ -191,18 +224,41 @@ pub const Agent = struct {
pub fn run(self: Agent, message: UserMessage) !Stream {
return .{ .inner = try self.inner.run(.{ .text = message.text }) };
}
+
+ /// Append a system message to the conversation and persist it (the
+ /// `.append` `SystemMode`: it adds to the effective prompt).
+ pub fn addSystemMessage(self: Agent, text: []const u8) !void {
+ return self.inner.addSystemMessage(text, .append);
+ }
+ /// Replace the effective system prompt and persist it (the `.replace`
+ /// `SystemMode`: it discards all prior system text). On replay the log
+ /// reconstructs the same effective prompt.
+ pub fn setSystemPrompt(self: Agent, text: []const u8) !void {
+ return self.inner.addSystemMessage(text, .replace);
+ }
+
/// Compact and persist (the explicit `/compact` entry point).
- pub fn compact(self: Agent, system_prompt: []const u8, extra: ?[]const u8) !CompactionResult {
- return self.inner.compactAndPersist(system_prompt, extra);
+ /// `override_system_prompt` falls back to
+ /// `config.compaction.compaction_prompt` when null. `extra` is appended
+ /// to the compaction prompt for this run (the `/compact $ARGUMENTS`
+ /// path).
+ pub fn compact(self: Agent, override_system_prompt: ?[]const u8, extra: ?[]const u8) !CompactionResult {
+ 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 };
}
+
+ /// The id of the session this agent appends to.
+ pub fn sessionId(self: Agent) []const u8 {
+ return self.inner.session.info.id;
+ }
};
// ===========================================================================
-// Pricing (data, aliased — Job 1)
+// Pricing (data, aliased)
// ===========================================================================
pub const Pricing = pricing_mod.Pricing;
@@ -220,27 +276,6 @@ pub const NullStore = null_store_mod.NullStore;
pub const FileSystemJSONLStore = fs_store_mod.FileSystemJSONLStore;
// ===========================================================================
-// Deep-embedder escape hatches.
-//
-// The `panto` CLI is a *deep* embedder: it drives the agent loop at a level
-// the curated `Agent`/`Conversation` façades deliberately do not expose
-// (system-prompt seeding through the agent, `compaction_system_prompt`, the
-// injectable `open_stream_fn` test seam, direct `conversation` field access,
-// raw standalone `Conversation` values, `compactAndPersist`, etc.). Rather
-// than inflate the curated surface to cover one in-tree embedder, these two
-// internal namespaces stay reachable for embedders who opt into the
-// unstable internal API. A C-ABI or language binding should use the curated
-// surface above, not these.
-//
-// FLAGGED (see docs/libpanto-cleanup.md): exposing `agent`/`conversation`
-// internals is a known gap between the curated façade and what a deep
-// embedder needs. Revisit if/when the façade grows the missing operations
-// (e.g. `Conversation` standalone construction, agent system-prompt and
-// compaction-prompt plumbing).
-pub const agent = agent_mod;
-pub const conversation = conversation_mod;
-
-// ===========================================================================
// Tests
// ===========================================================================
diff --git a/src/command.zig b/src/command.zig
index 49ad5b8..0f6fb01 100644
--- a/src/command.zig
+++ b/src/command.zig
@@ -32,10 +32,10 @@ const panto = @import("panto");
pub const Context = struct {
allocator: std.mem.Allocator,
- /// The active agent. Owns the conversation (`agent.conversation`) and
- /// the session store (`agent.session_store`); commands reach both
- /// through it rather than holding separate pointers.
- agent: *panto.agent.Agent,
+ /// The active agent (a copyable public handle). Commands reach the
+ /// conversation via `agent.conversation()` and compact via
+ /// `agent.compact(...)`.
+ agent: panto.Agent,
/// REPL output writer and its backing file writer (for flushing).
stdout: *std.Io.Writer,
diff --git a/src/compaction.zig b/src/compaction.zig
index 4ec5732..5d64f98 100644
--- a/src/compaction.zig
+++ b/src/compaction.zig
@@ -22,7 +22,7 @@ pub fn register(registry: *command.Registry) !void {
fn run(args: []const u8, ctx: *command.Context) anyerror!void {
const extra: ?[]const u8 = if (args.len > 0) args else null;
- const res = ctx.agent.compactAndPersist(ctx.compaction_prompt, extra) catch |err| {
+ const res = ctx.agent.compact(ctx.compaction_prompt, extra) catch |err| {
try ctx.stdout.print("\n[compaction failed: {s}]\n> ", .{@errorName(err)});
try ctx.stdout_file.flush();
return;
diff --git a/src/main.zig b/src/main.zig
index 995735f..61a88bd 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -144,7 +144,7 @@ const CLIRenderer = struct {
/// it terminates (`turn_complete` → `next()` returns null). A failure
/// surfaces as the error from `next()`; the caller renders it. The stream is
/// always `deinit`ed (persisting the turn tail) on every exit path.
-fn driveTurn(agent: *panto.agent.Agent, message: panto.agent.Agent.UserMessage, renderer: *CLIRenderer) !void {
+fn driveTurn(agent: panto.Agent, message: panto.UserMessage, renderer: *CLIRenderer) !void {
var stream = try agent.run(message);
defer stream.deinit();
while (try stream.next()) |ev| try renderer.render(ev);
@@ -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.conversation.Conversation = null;
+ var adopted_conversation: ?panto.ConversationData = null;
if (is_resume) {
adopted_conversation = try session.load();
const sid = session.info.id;
@@ -376,11 +376,11 @@ pub fn main(init: std.process.Init) !void {
// built but before the first turn, so in-place registration is visible.
// System-prompt seeding/reconciliation below runs *through* the agent
// so those entries persist.
- const active_config: panto.Config = .{
+ var active_config: panto.Config = .{
.provider = provider_config,
.compaction = compaction_cfg,
};
- var agent = panto.agent.Agent.init(
+ const agent = try panto.Agent.init(
alloc,
io,
&active_config,
@@ -424,7 +424,7 @@ pub fn main(init: std.process.Init) !void {
io,
init.environ_map,
luarocks_rt.layout.agent_dir,
- &agent,
+ agent,
);
} else {
// Fresh session — source and install the system prompt.
@@ -433,7 +433,7 @@ pub fn main(init: std.process.Init) !void {
io,
init.environ_map,
luarocks_rt.layout.agent_dir,
- &agent,
+ agent,
);
}
@@ -476,7 +476,9 @@ pub fn main(init: std.process.Init) !void {
init.environ_map,
luarocks_rt.layout.agent_dir,
);
- agent.compaction_system_prompt = compaction_prompt;
+ // The compaction prompt is resolved after extension load; set it on the
+ // config the agent re-reads each turn (visible before the first turn).
+ active_config.compaction.compaction_prompt = compaction_prompt;
const banner_base: []const u8 = switch (provider_config) {
inline else => |c| c.base_url,
@@ -521,7 +523,7 @@ pub fn main(init: std.process.Init) !void {
var cmd_ctx: command.Context = .{
.allocator = alloc,
- .agent = &agent,
+ .agent = agent,
.stdout = stdout,
.stdout_file = &stdout_file,
.compaction_prompt = compaction_prompt,
@@ -570,7 +572,7 @@ pub fn main(init: std.process.Init) !void {
// auto-compaction) through its session store — the CLI no longer
// touches persistence.
cli_renderer.beginTurn();
- driveTurn(&agent, .{ .text = line }, &cli_renderer) catch |err| {
+ driveTurn(agent, .{ .text = line }, &cli_renderer) catch |err| {
cli_renderer.renderError(err);
try stdout.print("\n[error: {s}]\n", .{@errorName(err)});
};
diff --git a/src/system_prompt.zig b/src/system_prompt.zig
index 500b135..39c3ea8 100644
--- a/src/system_prompt.zig
+++ b/src/system_prompt.zig
@@ -137,7 +137,7 @@ pub fn seedFresh(
io: Io,
environ_map: *const std.process.Environ.Map,
base_dir: []const u8,
- agent: *panto.agent.Agent,
+ agent: panto.Agent,
) !void {
const user_dir = try userLayerDir(arena, environ_map);
const project_dir = try projectLayerDir(arena, io);
@@ -151,13 +151,13 @@ pub fn seedFreshLayers(
base_dir: ?[]const u8,
user_dir: ?[]const u8,
project_dir: ?[]const u8,
- agent: *panto.agent.Agent,
+ agent: panto.Agent,
) !void {
const resolved = try resolveLayers(arena, io, base_dir, user_dir, project_dir);
const blocks = try resolved.blocks(arena);
for (blocks) |text| {
// The agent adds to its conversation and persists the entry.
- try agent.addSystemMessage(text, .append);
+ try agent.addSystemMessage(text);
}
}
@@ -174,7 +174,7 @@ pub fn reconcileResume(
io: Io,
environ_map: *const std.process.Environ.Map,
base_dir: []const u8,
- agent: *panto.agent.Agent,
+ agent: panto.Agent,
) !void {
const user_dir = try userLayerDir(arena, environ_map);
const project_dir = try projectLayerDir(arena, io);
@@ -188,21 +188,21 @@ pub fn reconcileResumeLayers(
base_dir: ?[]const u8,
user_dir: ?[]const u8,
project_dir: ?[]const u8,
- agent: *panto.agent.Agent,
+ agent: panto.Agent,
) !void {
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.items);
+ const window = try effectiveConfigWindow(arena, agent.conversation().messages());
if (blocksEqual(config_blocks, window)) return;
- // Re-seed: one `replace` for the seed, then one `append` per append.
- // The agent adds to its conversation and persists each entry (with the
- // correct system mode).
- try agent.addSystemMessage(config_blocks[0], .replace);
+ // Re-seed: one `setSystemPrompt` (replace) for the seed, then one
+ // `addSystemMessage` (append) per append. The agent adds to its
+ // conversation and persists each entry (with the correct system mode).
+ try agent.setSystemPrompt(config_blocks[0]);
for (config_blocks[1..]) |text| {
- try agent.addSystemMessage(text, .append);
+ try agent.addSystemMessage(text);
}
}
@@ -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.conversation.Conversation.init(alloc);
+ var conv = panto.ConversationData.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.conversation.Conversation.init(alloc);
+ var conv = panto.ConversationData.init(alloc);
defer conv.deinit();
try conv.addSystemMessage("old seed");
@@ -473,38 +473,45 @@ fn openTmpStore(arena: Allocator, root: []const u8) !panto.FileSystemJSONLStore
/// harness only holds the config to keep the agent's borrowed pointer valid.
const SPAgentHarness = struct {
config: panto.Config,
- agent: panto.agent.Agent,
+ agent: panto.Agent,
+ /// A second handle onto the same session (a copyable value proxying to
+ /// the same store + id) so the test can `forceFlush` without reaching
+ /// agent internals. `session_id` mirrors the agent's session id.
+ session: panto.Session,
+ session_id: []const u8,
fn init(
self: *SPAgentHarness,
session: panto.Session,
- adopted: ?panto.conversation.Conversation,
- ) void {
+ adopted: ?panto.ConversationData,
+ ) !void {
self.config = .{
.provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } },
};
- self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, session, adopted);
+ self.session = session;
+ self.session_id = session.info.id;
+ self.agent = try panto.Agent.init(testing.allocator, testing.io, &self.config, session, adopted);
}
fn deinit(self: *SPAgentHarness) void {
self.agent.deinit();
}
-};
-/// Force the session to flush to disk by appending an assistant message
-/// through the agent's session. (System/user messages buffer in memory
-/// until the first assistant message; a realistic resume only sees what a
-/// completed turn persisted.)
-fn forceFlush(agent: *panto.agent.Agent) !void {
- var conv = panto.conversation.Conversation.init(testing.allocator);
- defer conv.deinit();
- try conv.addAssistantMessage(&.{});
- const id: panto.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" };
- var batch = [_]panto.PersistentMessage{
- .{ .message = conv.messages.items[0], .identity = id },
- };
- try agent.session.append(&batch);
-}
+ /// Force the session to flush to disk by appending an assistant message
+ /// through the (shared) session handle. (System/user messages buffer in
+ /// 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);
+ defer conv.deinit();
+ try conv.addAssistantMessage(&.{});
+ const id: panto.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" };
+ var batch = [_]panto.PersistentMessage{
+ .{ .message = conv.messages.items[0], .identity = id },
+ };
+ try self.session.append(&batch);
+ }
+};
test "seedFresh then no-config-change resume is a no-op" {
const alloc = testing.allocator;
@@ -526,12 +533,12 @@ test "seedFresh then no-config-change resume is a no-op" {
var session_id_len: usize = 0;
{
var h: SPAgentHarness = undefined;
- h.init(store.create(), null);
+ try h.init(store.create(), null);
defer h.deinit();
- try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent);
- try forceFlush(&h.agent); // persist seed (+ assistant) to disk
- @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id);
- session_id_len = h.agent.session.info.id.len;
+ try seedFreshLayers(arena, testing.io, null, null, project_dir, h.agent);
+ try h.forceFlush(); // persist seed (+ assistant) to disk
+ @memcpy(session_id_buf[0..h.session_id.len], h.session_id);
+ session_id_len = h.session_id.len;
}
const session_id = session_id_buf[0..session_id_len];
@@ -547,10 +554,10 @@ test "seedFresh then no-config-change resume is a no-op" {
var sess2 = (try store.resolve(session_id)).?;
const conv2 = try sess2.load();
var h2: SPAgentHarness = undefined;
- h2.init(sess2, conv2);
+ 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.items);
+ try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent);
+ const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
try testing.expectEqual(seeded_count, eff_after.items.len);
}
@@ -572,12 +579,12 @@ test "resume after config change appends replace + append sequence" {
var session_id_len: usize = 0;
{
var h: SPAgentHarness = undefined;
- h.init(store.create(), null);
+ try h.init(store.create(), null);
defer h.deinit();
- try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent);
- try forceFlush(&h.agent); // persist old seed (+ assistant) to disk
- @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id);
- session_id_len = h.agent.session.info.id.len;
+ try seedFreshLayers(arena, testing.io, null, null, project_dir, h.agent);
+ try h.forceFlush(); // persist old seed (+ assistant) to disk
+ @memcpy(session_id_buf[0..h.session_id.len], h.session_id);
+ session_id_len = h.session_id.len;
}
const session_id = session_id_buf[0..session_id_len];
@@ -588,20 +595,20 @@ test "resume after config change appends replace + append sequence" {
var sess2 = (try store.resolve(session_id)).?;
const conv2 = try sess2.load();
var h2: SPAgentHarness = undefined;
- h2.init(sess2, conv2);
+ try h2.init(sess2, conv2);
defer h2.deinit();
- try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent);
+ 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.items);
+ const eff = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
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]);
// 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.items);
+ try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent);
+ const eff2 = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages());
try testing.expectEqual(@as(usize, 2), eff2.items.len);
}