From 16ea45b6854232541fb45f7d2e5b767118cccf43 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 2 Jun 2026 10:50:05 -0600 Subject: tool call resiliency --- src/config_file.zig | 4 ++- src/main.zig | 70 ++++++++++++++++++++++++++++++++++++----------------- src/models_toml.zig | 4 +-- 3 files changed, 53 insertions(+), 25 deletions(-) (limited to 'src') 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 @@ //! [.] //! model = # wire model id; defaults to if omitted //! reasoning = # default | off | minimal | low | medium | high -//! max_tokens = # anthropic_messages only +//! max_tokens = # per-request output token cap //! api_version = # anthropic_messages only //! # pricing (all optional; USD per million tokens): //! input = @@ -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, -- cgit v1.3