From ac5c4898dfa0a9e57424336774893dfc72b132e9 Mon Sep 17 00:00:00 2001 From: t Date: Wed, 3 Jun 2026 09:11:36 -0600 Subject: image uploads --- src/lua_runtime.zig | 134 ++++++++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) (limited to 'src/lua_runtime.zig') diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index 7d18fe9..73078c0 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -548,9 +548,9 @@ const Slot = struct { /// `true` if the handler returned cleanly, `false` if it raised /// via the `pcall` wrapping. ok: bool = false, - /// Result payload as owned bytes. Allocated from `allocator`. - /// Caller frees. - value: ?[]u8 = null, + /// Result payload as owned parts. Allocated from `allocator`. + /// Caller frees via `panto.freeResultParts`. + value: ?[]panto.ResultPart = null, /// On `ok = false`, an owned copy of the error message. err_msg: ?[]u8 = null, }; @@ -703,11 +703,11 @@ fn runBatch( continue; } if (slot.ok) { - results[i] = .{ .ok = slot.value orelse try allocator.dupe(u8, "") }; + results[i] = .{ .ok = slot.value orelse try panto.textResult(allocator, "") }; // Free the err_msg if both ended up set somehow. if (slot.err_msg) |m| allocator.free(m); } else { - if (slot.value) |v| allocator.free(v); + if (slot.value) |v| panto.freeResultParts(allocator, v); std.log.warn( "panto-lua: tool '{s}' failed: {s}", .{ @@ -734,12 +734,13 @@ fn formatToolError( allocator: Allocator, tool_name: []const u8, message: []const u8, -) ![]u8 { - return std.fmt.allocPrint( +) ![]panto.ResultPart { + const text = try std.fmt.allocPrint( allocator, "panto-lua: tool '{s}' failed: {s}", .{ tool_name, message }, ); + return panto.ownedTextResult(allocator, text); } /// Start one coroutine: create a thread under the runtime's lua_State, @@ -815,7 +816,7 @@ fn runLegacySync( }; var err_msg: ?[]u8 = null; defer if (err_msg) |m| allocator.free(m); - const out_bytes = invokeCoroutineSync( + const out_parts = invokeCoroutineSync( self.L, handler_ref, call.input, @@ -834,7 +835,7 @@ fn runLegacySync( ) catch return .{ .err = error.OutOfMemory }; return .{ .ok = bytes }; }; - return .{ .ok = out_bytes }; + return .{ .ok = out_parts }; } /// Run a handler synchronously, with no event loop. On Lua-level @@ -847,7 +848,7 @@ fn invokeCoroutineSync( input: []const u8, allocator: Allocator, err_msg_out: *?[]u8, -) ![]u8 { +) ![]panto.ResultPart { const co = c.lua_newthread(L) orelse return RuntimeError.LuaInitFailed; defer c.lua_settop(L, c.lua_gettop(L) - 1); @@ -1058,6 +1059,29 @@ fn logTopAsError(L: *c.lua_State, prefix: []const u8) void { const testing = std.testing; +/// Test helper: concatenated text across a result's text parts. +/// Returns the first text part's items (sufficient for current tests, +/// which return a single text part). +fn okText(result: panto.ToolCallResult) []const u8 { + switch (result) { + .ok => |parts| { + for (parts) |p| { + if (p == .text) return p.text; + } + return ""; + }, + .err => return "", + } +} + +/// Test helper: free a results slice (parts on `.ok`). +fn freeResults(results: []panto.ToolCallResult) void { + for (results) |r| switch (r) { + .ok => |b| panto.freeResultParts(testing.allocator, b), + .err => {}, + }; +} + fn writeTempScript(dir: Io.Dir, name: []const u8, source: []const u8) ![]const u8 { try dir.writeFile(testing.io, .{ .sub_path = name, .data = source }); var buf: [std.fs.max_path_bytes]u8 = undefined; @@ -1204,14 +1228,11 @@ test "invokeBatch runs each call through a coroutine and returns the result" { }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expectEqualStrings("got: hello", results[0].ok); - try testing.expectEqualStrings("HI!", results[1].ok); - try testing.expectEqualStrings("got: again", results[2].ok); + try testing.expectEqualStrings("got: hello", okText(results[0])); + try testing.expectEqualStrings("HI!", okText(results[1])); + try testing.expectEqualStrings("got: again", okText(results[2])); } test "module-global state survives across calls in the same runtime" { @@ -1250,14 +1271,11 @@ test "module-global state survives across calls in the same runtime" { }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expectEqualStrings("1", results[0].ok); - try testing.expectEqualStrings("2", results[1].ok); - try testing.expectEqualStrings("3", results[2].ok); + try testing.expectEqualStrings("1", okText(results[0])); + try testing.expectEqualStrings("2", okText(results[1])); + try testing.expectEqualStrings("3", okText(results[2])); // And a second batch keeps the counter going. var more: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; @@ -1267,8 +1285,8 @@ test "module-global state survives across calls in the same runtime" { &more, testing.allocator, ); - defer testing.allocator.free(more[0].ok); - try testing.expectEqualStrings("4", more[0].ok); + defer freeResults(&more); + try testing.expectEqualStrings("4", okText(more[0])); } test "handler crash: per-call error surfaces, sibling calls succeed" { @@ -1306,12 +1324,9 @@ test "handler crash: per-call error surfaces, sibling calls succeed" { .{ .err = error.SourceDroppedCall }, }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expectEqualStrings("fine", results[0].ok); + try testing.expectEqualStrings("fine", okText(results[0])); // A handler crash is surfaced as an *ok* result whose payload is // the formatted error message — not as `.err` — because libpanto // would otherwise abort the entire turn on per-call `.err`. The @@ -1319,11 +1334,11 @@ test "handler crash: per-call error surfaces, sibling calls succeed" { // prefix and includes the Lua-side error message. try testing.expect(std.mem.startsWith( u8, - results[1].ok, + okText(results[1]), "panto-lua: tool 'boom' failed:", )); - try testing.expect(std.mem.indexOf(u8, results[1].ok, "kaboom") != null); - try testing.expectEqualStrings("fine", results[2].ok); + try testing.expect(std.mem.indexOf(u8, okText(results[1]), "kaboom") != null); + try testing.expectEqualStrings("fine", okText(results[2])); } test "directory-style extension can require sibling modules" { @@ -1369,8 +1384,8 @@ test "directory-style extension can require sibling modules" { const calls = [_]panto.ToolCall{.{ .tool_name = "shout", .input = "{\"text\":\"hi\"}" }}; var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer testing.allocator.free(results[0].ok); - try testing.expectEqualStrings("HI!", results[0].ok); + defer freeResults(&results); + try testing.expectEqualStrings("HI!", okText(results[0])); } test "yielding handler with no event loop surfaces LuaHandlerYielded" { @@ -1395,10 +1410,7 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" { const calls = [_]panto.ToolCall{.{ .tool_name = "sleeper", .input = "{}" }}; var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); // Same policy as the crash test: the failure is surfaced as `.ok` // text so libpanto doesn't abort the turn. The error type name @@ -1406,10 +1418,10 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" { // sync path's formatToolError call. try testing.expect(std.mem.startsWith( u8, - results[0].ok, + okText(results[0]), "panto-lua: tool 'sleeper' failed:", )); - try testing.expect(std.mem.indexOf(u8, results[0].ok, "LuaHandlerYielded") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "LuaHandlerYielded") != null); } // Integration test: requires a `$PANTO_HOME` with luv already @@ -1488,13 +1500,10 @@ test "scheduler: yielding handler is resumed by libuv" { .{ .err = error.SourceDroppedCall }, }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expectEqualStrings("awake", results[0].ok); - try testing.expectEqualStrings("awake", results[1].ok); + try testing.expectEqualStrings("awake", okText(results[0])); + try testing.expectEqualStrings("awake", okText(results[1])); } // Contract-violation guard: a handler that yields WITHOUT arming any @@ -1537,14 +1546,11 @@ test "scheduler: handler that yields without arming libuv work is surfaced as an .{ .err = error.SourceDroppedCall }, }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expect(std.mem.startsWith(u8, results[0].ok, "panto-lua: tool 'abandon' failed:")); - try testing.expect(std.mem.indexOf(u8, results[0].ok, "still suspended") != null); - try testing.expectEqualStrings("ok", results[1].ok); + try testing.expect(std.mem.startsWith(u8, okText(results[0]), "panto-lua: tool 'abandon' failed:")); + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "still suspended") != null); + try testing.expectEqualStrings("ok", okText(results[1])); } /// Absolute path to the repo's `agent/tools` directory, derived from @@ -1618,13 +1624,10 @@ test "scheduler: two real std.shell calls in one batch do not deadlock" { .{ .err = error.SourceDroppedCall }, }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expect(std.mem.indexOf(u8, results[0].ok, "first-done") != null); - try testing.expect(std.mem.indexOf(u8, results[1].ok, "second-done") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "first-done") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[1]), "second-done") != null); } // Reproduction: two REAL `std.read` calls in one batch. read.lua uses @@ -1658,13 +1661,10 @@ test "scheduler: two real std.read calls in one batch do not deadlock" { }; try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); - defer for (results) |r| switch (r) { - .ok => |b| testing.allocator.free(b), - .err => {}, - }; + defer freeResults(&results); - try testing.expect(results[0].ok.len > 0); - try testing.expect(results[1].ok.len > 0); + try testing.expect(okText(results[0]).len > 0); + try testing.expect(okText(results[1]).len > 0); } test "loadExtension: duplicate tool name from a second extension errors" { -- cgit v1.3