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_bridge.zig | 151 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 140 insertions(+), 11 deletions(-) (limited to 'src/lua_bridge.zig') diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index cb57ade..245b0ec 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -36,6 +36,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const panto = @import("panto"); pub const c = @cImport({ @cInclude("lua.h"); @@ -260,20 +261,118 @@ pub fn pushJsonAsLua( pushJsonValue(L, parsed.value) catch return BridgeError.OutOfMemory; } -/// Read a Lua value at `idx` and serialize it to a JSON-compatible owned -/// byte string. Used to convert handler return values into ToolResult -/// content. For now we only accept string returns; extending to richer -/// types is straightforward but unnecessary for slice 2. +/// Read a tool handler's return value at `idx` and convert it into an +/// owned `[]panto.ResultPart`. Two accepted shapes: +/// +/// * a plain string -> one `.text` part; +/// * a table `{ text = "...", attachments = { { media_type = "...", +/// data = "..." }, ... } }` -> an optional `.text` part followed by +/// one `.media` part per attachment (`data` is base64-encoded bytes). +/// +/// Every returned slice/part owns its bytes (allocated with `allocator`); +/// the caller frees via `panto.freeResultParts`. pub fn readHandlerResult( L: *c.lua_State, idx: c_int, allocator: Allocator, -) BridgeError![]u8 { - if (c.lua_type(L, idx) != T_STRING) return BridgeError.BadHandlerReturn; +) BridgeError![]panto.ResultPart { + const ty = c.lua_type(L, idx); + if (ty == T_STRING) { + var len: usize = 0; + const ptr = c.lua_tolstring(L, idx, &len); + if (ptr == null) return BridgeError.BadHandlerReturn; + return (panto.textResult(allocator, ptr[0..len]) catch + BridgeError.OutOfMemory); + } + if (ty != T_TABLE) return BridgeError.BadHandlerReturn; + return readHandlerResultTable(L, idx, allocator); +} + +/// Read a string field `name` from the table at `tbl_idx`. Returns null +/// if absent/nil, an error if present-but-not-a-string. The returned +/// slice borrows from Lua's internal buffer — copy before popping. +fn optStringField(L: *c.lua_State, tbl_idx: c_int, name: [*:0]const u8) BridgeError!?[]const u8 { + const t = c.lua_getfield(L, tbl_idx, name); + if (t == T_NIL) { + c.lua_settop(L, c.lua_gettop(L) - 1); + return null; + } + if (t != T_STRING) { + c.lua_settop(L, c.lua_gettop(L) - 1); + return BridgeError.BadHandlerReturn; + } var len: usize = 0; - const ptr = c.lua_tolstring(L, idx, &len); - if (ptr == null) return BridgeError.BadHandlerReturn; - return allocator.dupe(u8, ptr[0..len]) catch BridgeError.OutOfMemory; + const ptr = c.lua_tolstring(L, -1, &len); + if (ptr == null) { + c.lua_settop(L, c.lua_gettop(L) - 1); + return BridgeError.BadHandlerReturn; + } + // Note: value still on stack; caller pops after copying. + return ptr[0..len]; +} + +fn readHandlerResultTable( + L: *c.lua_State, + idx: c_int, + allocator: Allocator, +) BridgeError![]panto.ResultPart { + var parts: std.ArrayList(panto.ResultPart) = .empty; + errdefer { + for (parts.items) |p| p.deinit(allocator); + parts.deinit(allocator); + } + + // Optional `text` field. + { + const maybe = try optStringField(L, idx, "text"); + if (maybe) |s| { + const owned = allocator.dupe(u8, s) catch return BridgeError.OutOfMemory; + c.lua_settop(L, c.lua_gettop(L) - 1); // pop field value + parts.append(allocator, .{ .text = owned }) catch { + allocator.free(owned); + return BridgeError.OutOfMemory; + }; + } + } + + // Optional `attachments` array. + { + const t = c.lua_getfield(L, idx, "attachments"); + defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachments + if (t == T_TABLE) { + const n: usize = @intCast(c.lua_rawlen(L, -1)); + var i: usize = 1; + while (i <= n) : (i += 1) { + _ = c.lua_rawgeti(L, -1, @intCast(i)); // push attachment table + defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachment + if (c.lua_type(L, -1) != T_TABLE) return BridgeError.BadHandlerReturn; + + // `media_type` is an optional hint; libpanto detects the + // type from the raw bytes when it's absent. + const media_type: ?[]const u8 = if (try optStringField(L, -1, "media_type")) |mt_slice| blk: { + const owned = allocator.dupe(u8, mt_slice) catch return BridgeError.OutOfMemory; + c.lua_settop(L, c.lua_gettop(L) - 1); // pop media_type value + break :blk owned; + } else null; + errdefer if (media_type) |mt| allocator.free(mt); + + const data_slice = (try optStringField(L, -1, "data")) orelse + return BridgeError.BadHandlerReturn; + const data = allocator.dupe(u8, data_slice) catch return BridgeError.OutOfMemory; + c.lua_settop(L, c.lua_gettop(L) - 1); // pop data value + errdefer allocator.free(data); + + parts.append(allocator, .{ .media = .{ + .media_type = media_type, + .data = data, + } }) catch return BridgeError.OutOfMemory; + } + } else if (t != T_NIL) { + return BridgeError.BadHandlerReturn; + } + } + + return parts.toOwnedSlice(allocator) catch BridgeError.OutOfMemory; } // --------------------------------------------------------------------------- @@ -697,8 +796,38 @@ test "handler invocation: input parsed, result captured" { } const result = try readHandlerResult(L, -1, std.testing.allocator); - defer std.testing.allocator.free(result); - try std.testing.expectEqualStrings("got: hello", result); + defer panto.freeResultParts(std.testing.allocator, result); + try std.testing.expectEqual(@as(usize, 1), result.len); + try std.testing.expectEqualStrings("got: hello", result[0].text); +} + +test "readHandlerResult: table with text and attachments" { + const L = c.luaL_newstate() orelse return error.LuaInitFailed; + defer c.lua_close(L); + c.luaL_openlibs(L); + install(L); + + const script = + \\return { + \\ text = "see image", + \\ attachments = { + \\ { media_type = "image/png", data = "AAA=" }, + \\ { media_type = "application/pdf", data = "BBB=" }, + \\ }, + \\} + ; + if (c.luaL_loadstring(L, script) != 0 or c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) { + return error.LuaScriptFailed; + } + + const result = try readHandlerResult(L, -1, std.testing.allocator); + defer panto.freeResultParts(std.testing.allocator, result); + try std.testing.expectEqual(@as(usize, 3), result.len); + try std.testing.expectEqualStrings("see image", result[0].text); + try std.testing.expectEqualStrings("image/png", result[1].media.media_type.?); + try std.testing.expectEqualStrings("AAA=", result[1].media.data); + try std.testing.expectEqualStrings("application/pdf", result[2].media.media_type.?); + try std.testing.expectEqualStrings("BBB=", result[2].media.data); } test "handler crash: error message surfaces via xpcall traceback hook" { -- cgit v1.3