summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-02 10:50:05 -0600
committert <t@tjp.lol>2026-06-02 11:11:29 -0600
commit16ea45b6854232541fb45f7d2e5b767118cccf43 (patch)
tree1bf37d766e4b9de2b646c1475497ae7e9d5699c7 /src
parent9c64a7d4462a11674e2dea481b037b5f5d9c62fc (diff)
tool call resiliency
Diffstat (limited to 'src')
-rw-r--r--src/config_file.zig4
-rw-r--r--src/main.zig70
-rw-r--r--src/models_toml.zig4
3 files changed, 53 insertions, 25 deletions
diff --git a/src/config_file.zig b/src/config_file.zig
index 533eb48..f7589fe 100644
--- a/src/config_file.zig
+++ b/src/config_file.zig
@@ -130,6 +130,7 @@ pub fn buildProviderConfig(
const def_opt = defs.get(ref.provider, ref.model);
const wire_model: []const u8 = if (def_opt) |d| d.model else ref.model;
const reasoning: panto.config.ReasoningEffort = if (def_opt) |d| d.reasoning else .default;
+ const max_tokens: u32 = if (def_opt) |d| (d.max_tokens orelse 64_000) else 64_000;
switch (prov.style) {
.openai_chat => return .{ .openai_chat = .{
@@ -137,13 +138,14 @@ pub fn buildProviderConfig(
.base_url = prov.base_url,
.model = wire_model,
.reasoning = reasoning,
+ .max_tokens = max_tokens,
} },
.anthropic_messages => return .{ .anthropic_messages = .{
.api_key = prov.api_key,
.base_url = prov.base_url,
.model = wire_model,
.api_version = if (def_opt) |d| (d.api_version orelse "2023-06-01") else "2023-06-01",
- .max_tokens = if (def_opt) |d| (d.max_tokens orelse 4096) else 4096,
+ .max_tokens = max_tokens,
} },
}
}
diff --git a/src/main.zig b/src/main.zig
index 20686eb..28f1144 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -704,6 +704,13 @@ fn persistTurn(
model: []const u8,
per_message_usage: []const ?panto.session_manager.Usage,
) !void {
+ var batch_messages: std.ArrayList(panto.session_manager.DiskMessage) = .empty;
+ defer batch_messages.deinit(alloc);
+ var batch_providers: std.ArrayList(?[]const u8) = .empty;
+ defer batch_providers.deinit(alloc);
+ var batch_models: std.ArrayList(?[]const u8) = .empty;
+ defer batch_models.deinit(alloc);
+
var i = start_index;
var assistant_seen: usize = 0;
while (i < conv.messages.items.len) : (i += 1) {
@@ -718,22 +725,23 @@ fn persistTurn(
blocks[allocated] = try panto.session.contentBlockToDisk(alloc, block);
allocated += 1;
}
+ if (msg.role == .assistant and hasToolUseWithoutFollowingResults(conv, i)) {
+ for (blocks[0..allocated]) |b| b.deinit(alloc);
+ alloc.free(blocks);
+ continue;
+ }
switch (msg.role) {
.system => {
// Mid-turn system messages aren't a thing in pantograph today.
// Treat as harmless and persist verbatim, sans stamps.
- _ = try mgr.appendMessage(
- .{ .role = .system, .content = blocks },
- null,
- null,
- );
+ try batch_messages.append(alloc, .{ .role = .system, .content = blocks });
+ try batch_providers.append(alloc, null);
+ try batch_models.append(alloc, null);
},
.user => {
- _ = try mgr.appendMessage(
- .{ .role = .user, .content = blocks },
- provider,
- model,
- );
+ try batch_messages.append(alloc, .{ .role = .user, .content = blocks });
+ try batch_providers.append(alloc, provider);
+ try batch_models.append(alloc, model);
},
.assistant => {
// Pair this assistant message with its usage, if the
@@ -745,19 +753,37 @@ fn persistTurn(
else
null;
assistant_seen += 1;
- _ = try mgr.appendMessage(
- .{
- .role = .assistant,
- .content = blocks,
- .provider = try alloc.dupe(u8, provider),
- .model = try alloc.dupe(u8, model),
- .stop_reason = try alloc.dupe(u8, "stop"),
- .usage = usage,
- },
- null,
- null,
- );
+ try batch_messages.append(alloc, .{
+ .role = .assistant,
+ .content = blocks,
+ .provider = try alloc.dupe(u8, provider),
+ .model = try alloc.dupe(u8, model),
+ .stop_reason = try alloc.dupe(u8, "stop"),
+ .usage = usage,
+ });
+ try batch_providers.append(alloc, null);
+ try batch_models.append(alloc, null);
},
}
}
+ try mgr.appendMessagesAtomic(batch_messages.items, batch_providers.items, batch_models.items);
+}
+
+fn hasToolUseWithoutFollowingResults(conv: *const panto.conversation.Conversation, index: usize) bool {
+ const msg = conv.messages.items[index];
+ for (msg.content.items) |block| {
+ if (block != .ToolUse) continue;
+ if (index + 1 >= conv.messages.items.len) return true;
+ const next = conv.messages.items[index + 1];
+ if (next.role != .user) return true;
+ var found = false;
+ for (next.content.items) |next_block| {
+ if (next_block == .ToolResult and std.mem.eql(u8, next_block.ToolResult.tool_use_id, block.ToolUse.id)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) return true;
+ }
+ return false;
}
diff --git a/src/models_toml.zig b/src/models_toml.zig
index 36d8c65..790b61f 100644
--- a/src/models_toml.zig
+++ b/src/models_toml.zig
@@ -10,7 +10,7 @@
//! [<provider>.<alias>]
//! model = <string> # wire model id; defaults to <alias> if omitted
//! reasoning = <string> # default | off | minimal | low | medium | high
-//! max_tokens = <int> # anthropic_messages only
+//! max_tokens = <int> # per-request output token cap
//! api_version = <string> # anthropic_messages only
//! # pricing (all optional; USD per million tokens):
//! input = <float>
@@ -64,7 +64,7 @@ pub const ModelDef = struct {
/// TOML omitted `model`.
model: []const u8,
reasoning: ReasoningEffort,
- /// anthropic_messages only; null = use the provider/library default.
+ /// Per-request output token cap; null = use the provider/library default.
max_tokens: ?u32,
/// anthropic_messages only; null = use the library default.
api_version: ?[]const u8,