summaryrefslogtreecommitdiff
path: root/libpanto-c
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-19 14:53:03 -0600
committert <t@tjp.lol>2026-06-19 14:55:35 -0600
commit81ddb61dcfcc9bc85585f262bf303e1a9da507b0 (patch)
treeb38b39a937de047d35246a3e64df6fda5737850e /libpanto-c
parent270cd00129551647e7c764a13836e03e0b2dc4e2 (diff)
libpanto fixes
- ensuring stream.next() sequences are interruptible - Agent.run() takes any user message, not just chat text
Diffstat (limited to 'libpanto-c')
-rw-r--r--libpanto-c/include/panto.h13
-rw-r--r--libpanto-c/src/lib.zig28
2 files changed, 35 insertions, 6 deletions
diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h
index 3b4a22a..c79e77c 100644
--- a/libpanto-c/include/panto.h
+++ b/libpanto-c/include/panto.h
@@ -50,7 +50,11 @@ typedef struct { PantoContentBlockTag block_type; size_t index; } PantoBlockStar
typedef struct { size_t index; PantoSlice id, name; } PantoToolDetails;
typedef struct { size_t index; PantoSlice delta; } PantoContentDelta;
typedef struct { size_t index; PantoContentBlockTag block_type; } PantoBlockComplete;
-typedef struct { bool has_usage; PantoUsage usage; } PantoMessageComplete;
+/* One assistant message finished streaming. `message_index` indexes the
+ * agent's live conversation (`panto_agent_conversation`), whose tail is this
+ * message when the event is delivered; fetch its `block_count` blocks from
+ * there for full content. `usage` is the wire-reported token usage. */
+typedef struct { PantoMessageRole role; size_t message_index; size_t block_count; bool has_usage; PantoUsage usage; } PantoMessageComplete;
typedef struct { bool compacted; size_t kept_turns; size_t summarized_messages; } PantoCompactionResult;
typedef struct { size_t attempt, max_attempts; uint64_t delay_ms; PantoSlice error_name; bool has_status_code; uint16_t status_code; bool has_retry_after_ms; uint64_t retry_after_ms; PantoSlice message; bool compaction; } PantoProviderRetry;
typedef struct { size_t count; } PantoToolDispatchStart;
@@ -181,7 +185,12 @@ PANTO_EXPORT PantoStatus panto_agent_set_config(PantoAgent *agent, const PantoCo
PANTO_EXPORT PantoStatus panto_agent_add_system_message(PantoAgent *agent, const char *text);
PANTO_EXPORT PantoStatus panto_agent_set_system_prompt(PantoAgent *agent, const char *text);
PANTO_EXPORT PantoStatus panto_agent_compact(PantoAgent *agent, const char *override_system_prompt, const char *extra_instructions, PantoCompactionResult *out);
-PANTO_EXPORT PantoStatus panto_agent_run(PantoAgent *agent, const char *user_text, PantoStream **out);
+/* Open a turn from a user-role message built with the PantoMessageBuilder API
+ * (the same builder used to rebuild persisted messages): one `add_text` block
+ * for a plain chat turn, or `add_tool_result` blocks to resume after the
+ * embedder handled tool calls itself. Consumes `user_message` (the builder is
+ * destroyed); do not reuse or destroy it afterwards. */
+PANTO_EXPORT PantoStatus panto_agent_run(PantoAgent *agent, PantoMessageBuilder *user_message, PantoStream **out);
/* Reset a failed stream back to its turn-open boundary so the caller can
* resume panto_stream_next() after changing the agent config (e.g. catch a
* terminal bad-request, swap the provider config, and retry the SAME turn
diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig
index 9d584a0..7f61ae3 100644
--- a/libpanto-c/src/lib.zig
+++ b/libpanto-c/src/lib.zig
@@ -50,7 +50,7 @@ pub const PantoBlockStart = extern struct { block_type: PantoContentBlockTag, in
pub const PantoToolDetails = extern struct { index: usize, id: PantoSlice, name: PantoSlice };
pub const PantoContentDelta = extern struct { index: usize, delta: PantoSlice };
pub const PantoBlockComplete = extern struct { index: usize, block_type: PantoContentBlockTag };
-pub const PantoMessageComplete = extern struct { has_usage: bool, usage: PantoUsage };
+pub const PantoMessageComplete = extern struct { role: PantoMessageRole, message_index: usize, block_count: usize, has_usage: bool, usage: PantoUsage };
pub const PantoCompactionResult = extern struct { compacted: bool, kept_turns: usize, summarized_messages: usize };
pub const PantoProviderRetry = extern struct { attempt: usize, max_attempts: usize, delay_ms: u64, error_name: PantoSlice, has_status_code: bool, status_code: u16, has_retry_after_ms: bool, retry_after_ms: u64, message: PantoSlice, compaction: bool };
pub const PantoToolDispatchStart = extern struct { count: usize };
@@ -803,9 +803,21 @@ export fn panto_agent_compact(a: *PantoAgent, override_system_prompt: ?[*:0]cons
out.* = .{ .compacted = r.compacted, .kept_turns = r.kept_turns, .summarized_messages = r.summarized_messages };
return .ok;
}
-export fn panto_agent_run(a: *PantoAgent, text: ?[*:0]const u8, out: **PantoStream) PantoStatus {
+/// Open a turn from a user-role message built with the `PantoMessageBuilder`
+/// API — the same builder used to rebuild persisted messages. A plain chat
+/// turn is one `add_text` block; a turn that resumes after the embedder handled
+/// tool calls is one or more `add_tool_result` blocks. Consumes the builder
+/// (success or failure): `Agent.run` adopts the block bytes, so only the
+/// builder's list backing + shell are freed here, never the per-block bytes.
+export fn panto_agent_run(a: *PantoAgent, user_message: *PantoMessageBuilder, out: **PantoStream) PantoStatus {
const h: *AgentHandle = @ptrCast(@alignCast(a));
- const st = h.agent.run(.{ .text = cstr(text) }) catch |e| return setErr("{t}", .{e});
+ const b = unwrapBuilder(user_message);
+ defer {
+ b.blocks.deinit(allocator);
+ if (b.identity) |id| panto.freeWireIdentity(allocator, id);
+ allocator.destroy(b);
+ }
+ const st = h.agent.run(.{ .blocks = b.blocks.items }) catch |e| return setErr("{t}", .{e});
out.* = @ptrCast(st);
return .ok;
}
@@ -825,6 +837,14 @@ export fn panto_stream_next(s: *PantoStream, out: *PantoEvent) PantoNextStatus {
const ev = st.next() catch |e| return setErrNext("{t}", .{e});
if (ev) |e| {
out.* = marshalEvent(e) catch |err| return setErrNext("{t}", .{err});
+ // The just-completed assistant message is the conversation's current
+ // tail (no further message is committed within this `next()` call), so
+ // its index is `len - 1`. Stamp it so the embedder can fetch the full
+ // message via `panto_agent_conversation` rather than marshalling its
+ // blocks onto every event.
+ if (out.tag == .message_complete) {
+ out.data.message_complete.message_index = st._agent.conversation.messages.items.len - 1;
+ }
return .event;
}
return .done;
@@ -855,7 +875,7 @@ fn marshalEvent(ev: panto.Event) !PantoEvent {
.tool_details => |t| .{ .tag = .tool_details, .data = .{ .tool_details = .{ .index = t.index, .id = try dupOut(t.id), .name = try dupOut(t.name) } } },
.content_delta => |d| .{ .tag = .content_delta, .data = .{ .content_delta = .{ .index = d.index, .delta = try dupOut(d.delta) } } },
.block_complete => |b| .{ .tag = .block_complete, .data = .{ .block_complete = .{ .index = b.index, .block_type = @enumFromInt(@intFromEnum(b.block)) } } },
- .message_complete => |m| .{ .tag = .message_complete, .data = .{ .message_complete = .{ .has_usage = m.usage != null, .usage = if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 } } } },
+ .message_complete => |m| .{ .tag = .message_complete, .data = .{ .message_complete = .{ .role = @enumFromInt(@intFromEnum(m.message.role)), .message_index = 0, .block_count = m.message.content.items.len, .has_usage = m.usage != null, .usage = if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 } } } },
.provider_retry => |r| .{ .tag = .provider_retry, .data = .{ .provider_retry = .{ .attempt = r.attempt, .max_attempts = r.max_attempts, .delay_ms = r.delay_ms, .error_name = try dupOut(@errorName(r.err)), .has_status_code = r.status_code != null, .status_code = r.status_code orelse 0, .has_retry_after_ms = r.retry_after_ms != null, .retry_after_ms = r.retry_after_ms orelse 0, .message = try dupOut(r.message orelse ""), .compaction = r.compaction } } },
.tool_dispatch_start => |t| .{ .tag = .tool_dispatch_start, .data = .{ .tool_dispatch_start = .{ .count = t.count } } },
.tool_dispatch_result => .{ .tag = .tool_dispatch_result, .data = .{ .tool_dispatch_result = .{ .message_index = 0 } } },