summaryrefslogtreecommitdiff
path: root/libpanto/src/openai_chat_json.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-01 08:21:00 -0600
committert <t@tjp.lol>2026-06-01 11:20:56 -0600
commit1beefefc69beee214430eb5bd2528a4f5692d2a8 (patch)
treea459dbde5da17babe7d872999341d2006ebfe02e /libpanto/src/openai_chat_json.zig
parent5c016cfdbcb122e2b4f0496ef946642d68953c4b (diff)
Rename system extension layer to base for clarity
Replace all references to the "system" layer with "base" to better reflect its role as the foundational extension/tool layer in panto's hierarchy. The layer hierarchy now consistently uses: project > user > base. Includes: - Update agent README and build.zig documentation - Refactor tool registry and config module to support layered lookups - Add TestHarness abstraction for cleaner test setup - Improve JSON serialization with wire-encoded tool names - Add glob pattern matching for tool/extension discovery
Diffstat (limited to 'libpanto/src/openai_chat_json.zig')
-rw-r--r--libpanto/src/openai_chat_json.zig36
1 files changed, 31 insertions, 5 deletions
diff --git a/libpanto/src/openai_chat_json.zig b/libpanto/src/openai_chat_json.zig
index 37d347b..6991559 100644
--- a/libpanto/src/openai_chat_json.zig
+++ b/libpanto/src/openai_chat_json.zig
@@ -117,7 +117,7 @@ pub fn serializeRequest(
if (tools.count() > 0) {
try s.objectField("tools");
try s.beginArray();
- var it = tools.iterator();
+ var it = tools.toolsForLLM();
while (it.next()) |t| {
try s.beginObject();
try s.objectField("type");
@@ -125,6 +125,7 @@ pub fn serializeRequest(
try s.objectField("function");
try s.beginObject();
try s.objectField("name");
+ // `t.decl.name` is already wire-encoded by `toolsForLLM`.
try s.write(t.decl.name);
try s.objectField("description");
try s.write(t.decl.description);
@@ -261,7 +262,11 @@ fn writeMessage(s: *std.json.Stringify, msg: conversation.Message, allocator: Al
try s.objectField("function");
try s.beginObject();
try s.objectField("name");
- try s.write(tu.name);
+ // Replayed assistant tool_use. The conversation stores the
+ // internal (dotted) name; encode `.` -> `__` so it matches
+ // what we advertise in `tools`.
+ var name_buf: [tool_registry_mod.max_wire_name_len]u8 = undefined;
+ try s.write(tool_registry_mod.encodeName(&name_buf, tu.name));
try s.objectField("arguments");
// `arguments` is a string carrying JSON, per the OpenAI
// wire format — not a nested object.
@@ -664,9 +669,7 @@ test "serializeRequest - emits tools array when registry non-empty" {
var tools = emptyTools();
defer tools.deinit();
- try tools.register(makeStaticTool(
- "echo",
- "Echo a message back.",
+ try tools.register(makeStaticTool("echo", "Echo a message back.",
\\{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}
));
@@ -691,6 +694,29 @@ test "serializeRequest - emits tools array when registry non-empty" {
try testing.expect(params.get("required").? == .array);
}
+test "serializeRequest - dotted tool name is wire-encoded with __" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+ try conv.addUserMessage("go");
+
+ var tools = emptyTools();
+ defer tools.deinit();
+ try tools.register(makeStaticTool("std.read", "Read a file.", "{\"type\":\"object\"}"));
+
+ const cfg = testConfig("gpt-4o");
+ const body = try serializeRequest(allocator, &cfg, &conv, &tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+
+ const f = parsed.value.object.get("tools").?.array.items[0].object.get("function").?.object;
+ // Internal `std.read` crosses the wire as `std__read` (OpenAI forbids dots).
+ try testing.expectEqualStrings("std__read", f.get("name").?.string);
+}
+
test "serializeRequest - assistant ToolUse becomes tool_calls" {
const allocator = testing.allocator;