From 9c64a7d4462a11674e2dea481b037b5f5d9c62fc Mon Sep 17 00:00:00 2001 From: t Date: Tue, 2 Jun 2026 09:02:49 -0600 Subject: system prompt building and logging --- libpanto/src/anthropic_messages_json.zig | 81 ++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 10 deletions(-) (limited to 'libpanto/src/anthropic_messages_json.zig') diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig index 5b65c2b..f8bfc2e 100644 --- a/libpanto/src/anthropic_messages_json.zig +++ b/libpanto/src/anthropic_messages_json.zig @@ -111,20 +111,26 @@ fn writeRawJson(s: *std.json.Stringify, raw: []const u8) !void { try s.write(parsed.value); } +/// Build the top-level Anthropic `system` string. Applies the shared +/// append/replace derivation (see `conversation.effectiveSystemBlocks`), +/// strips trailing newlines from each surviving block, and joins them with +/// a horizontal rule (`\n\n---\n\n`). Anthropic's wire format requires a +/// single string, so this rule is its concession to that constraint. fn collectSystemPrompt( conv: *const conversation.Conversation, out: *std.ArrayList(u8), allocator: Allocator, ) !void { + var blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items); + defer blocks.deinit(allocator); + + const sep = "\n\n---\n\n"; var first = true; - for (conv.messages.items) |msg| { - if (msg.role != .system) continue; - for (msg.content.items) |block| { - if (block != .Text) continue; - if (!first) try out.append(allocator, '\n'); - try out.appendSlice(allocator, block.Text.items); - first = false; - } + for (blocks.items) |text| { + const trimmed = std.mem.trimEnd(u8, text, "\n"); + if (!first) try out.appendSlice(allocator, sep); + try out.appendSlice(allocator, trimmed); + first = false; } } @@ -208,6 +214,11 @@ fn writeBlock(s: *std.json.Stringify, block: conversation.ContentBlock) !void { try s.write(tr.content.items); try s.endObject(); }, + // System blocks never reach here: `serializeRequest` filters out + // `.system` messages before emitting the `messages` array, and the + // system text is hoisted into the top-level `system` string by + // `collectSystemPrompt`. Present only to keep the switch exhaustive. + .System => {}, } } @@ -526,7 +537,7 @@ test "serializeRequest - system extracted into top-level field" { try testing.expectEqualStrings("Hello!", content[0].object.get("text").?.string); } -test "serializeRequest - multiple system messages concatenated with newlines" { +test "serializeRequest - multiple system messages joined with horizontal rule" { const allocator = testing.allocator; var conv = conversation.Conversation.init(allocator); @@ -545,7 +556,57 @@ test "serializeRequest - multiple system messages concatenated with newlines" { var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); defer parsed.deinit(); try testing.expectEqualStrings( - "Be terse.\nBe accurate.", + "Be terse.\n\n---\n\nBe accurate.", + parsed.value.object.get("system").?.string, + ); +} + +test "serializeRequest - replace-mode system block wipes prior system text" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("original seed"); + try conv.addSystemMessage("original append"); + try conv.replaceSystemMessage("fresh seed"); + try conv.addSystemMessage("fresh append"); + try conv.addUserMessage("Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expectEqualStrings( + "fresh seed\n\n---\n\nfresh append", + parsed.value.object.get("system").?.string, + ); +} + +test "serializeRequest - trailing newlines stripped before the rule join" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("line one\n\n"); + try conv.addSystemMessage("line two\n"); + try conv.addUserMessage("Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expectEqualStrings( + "line one\n\n---\n\nline two", parsed.value.object.get("system").?.string, ); } -- cgit v1.3