summaryrefslogtreecommitdiff
path: root/src/lua_runtime.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-13 23:45:59 -0600
committert <t@tjp.lol>2026-06-15 15:08:32 -0600
commit02b4c7a35ac0b714bc045d54fb4bb45d1ce4e490 (patch)
tree134196a82dd6fdd55b735be4c0cf38428c85038d /src/lua_runtime.zig
parent71643a5d69ffc40882c9fcde3cc8a3bcf02d7396 (diff)
Add Codex Responses support and session debugging
Teach provider config and auth resolution about the Codex Responses dialect, including user-facing `style = "openai_responses"` with `dialect = "codex"`. Serialize and parse Responses traffic with provider-specific reasoning replay, assistant phase metadata, and robust function-call assembly keyed by `output_index` so streamed tool inputs survive proxy quirks and empty terminal payloads. Also persist thinking origins and message metadata across sessions, add the Anthropic interleaved-thinking header switch, write per-session debug logs, and improve the TUI and scripts for inspecting tool output and session costs.
Diffstat (limited to 'src/lua_runtime.zig')
-rw-r--r--src/lua_runtime.zig140
1 files changed, 140 insertions, 0 deletions
diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig
index 2574fae..35247bb 100644
--- a/src/lua_runtime.zig
+++ b/src/lua_runtime.zig
@@ -87,6 +87,15 @@ pub const LuaRuntime = struct {
/// calls to drive libuv to completion. `0` until
/// `installScheduler` runs.
uv_run_ref: c_int = 0,
+ /// Registry ref to `require("luv").update_time`. libuv caches the
+ /// loop's notion of "now" and only refreshes it while `uv.run`
+ /// executes; the loop is idle between batches (and during a slow
+ /// model's streaming), so its clock goes stale. The scheduler calls
+ /// this at the start of each batch so handlers that arm timers
+ /// (e.g. the shell tool's timeout) compute deadlines against the
+ /// real current time, not a frozen one. `0` until
+ /// `installScheduler` runs.
+ uv_update_time_ref: c_int = 0,
/// Pointer to the in-flight batch, valid only for the duration of
/// one `invoke_batch` call. The `panto._record_result` C function
@@ -220,6 +229,7 @@ pub const LuaRuntime = struct {
try installRecordResult(self);
try installWrapperClosure(self);
try cacheUvRun(self);
+ try cacheUvUpdateTime(self);
}
/// Tear down the runtime: free every owned string, unref every
@@ -244,6 +254,9 @@ pub const LuaRuntime = struct {
if (self.uv_run_ref != 0) {
c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_run_ref);
}
+ if (self.uv_update_time_ref != 0) {
+ c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_update_time_ref);
+ }
// Free the UI event bridge (Lua handler/component refs + caches)
// BEFORE closing the state, since it unrefs registry entries.
@@ -724,6 +737,12 @@ fn runBatch(
return;
}
+ // Refresh libuv's cached loop clock before any handler arms a timer.
+ // The loop sits idle between batches (and while a slow model streams),
+ // so its notion of "now" is stale; a timeout armed against it would
+ // otherwise fire the instant `uv.run` refreshes time below.
+ refreshLoopClock(self);
+
var slots = try allocator.alloc(Slot, calls.len);
defer allocator.free(slots);
for (slots) |*s| s.* = .{};
@@ -1156,6 +1175,45 @@ fn cacheUvRun(self: *LuaRuntime) !void {
self.uv_run_ref = c.luaL_ref(L, lua_bridge.LUA_REGISTRYINDEX);
}
+/// Cache `require("luv").update_time` so the scheduler can refresh the
+/// loop clock at the start of each batch. See `uv_update_time_ref`.
+fn cacheUvUpdateTime(self: *LuaRuntime) !void {
+ const L = self.L;
+ const snippet =
+ \\local uv = require("luv")
+ \\return uv.update_time
+ ;
+ if (c.luaL_loadstring(L, snippet) != 0) {
+ logTopAsError(L, "panto-lua: failed to compile luv.update_time lookup");
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return RuntimeError.LuaInitFailed;
+ }
+ if (c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) {
+ logTopAsError(L, "panto-lua: require('luv') failed (was the bootstrap successful?)");
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return RuntimeError.LuaInitFailed;
+ }
+ if (c.lua_type(L, -1) != lua_bridge.T_FUNCTION) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return RuntimeError.LuaInitFailed;
+ }
+ self.uv_update_time_ref = c.luaL_ref(L, lua_bridge.LUA_REGISTRYINDEX);
+}
+
+/// Refresh libuv's cached loop clock (`uv.update_time()`). Best-effort:
+/// a failure here only means timeout deadlines may be computed against a
+/// slightly stale clock, so we log and continue rather than failing the
+/// batch.
+fn refreshLoopClock(self: *LuaRuntime) void {
+ if (self.uv_update_time_ref == 0) return;
+ const L = self.L;
+ _ = c.lua_rawgeti(L, lua_bridge.LUA_REGISTRYINDEX, @intCast(self.uv_update_time_ref));
+ if (c.lua_pcallk(L, 0, 0, 0, 0, null) != 0) {
+ logTopAsError(L, "panto-lua: uv.update_time failed");
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ }
+}
+
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
@@ -1778,6 +1836,88 @@ test "scheduler: two real std.shell calls in one batch do not deadlock" {
try testing.expect(std.mem.indexOf(u8, okText(results[1]), "second-done") != null);
}
+test "scheduler: shell timeout is measured from dispatch, not a stale loop clock" {
+ // Regression: libuv caches the loop's "now" and only refreshes it while
+ // `uv.run` runs. The loop is idle between batches, so a tool that arms a
+ // timeout timer against the stale clock would have it fire the instant
+ // `uv.run` refreshes time. We force staleness with `uv.sleep` (a blocking
+ // sleep that does NOT run the loop), then dispatch a 1s-timeout shell call
+ // and assert it does not spuriously time out.
+ var rt = try LuaRuntime.create(testing.allocator);
+ defer rt.deinit();
+ const luarocks_rt = bootstrapRealRuntime(rt) catch |e| return e;
+ defer luarocks_rt.deinit();
+
+ const tools_dir = try findAgentToolsDir();
+ defer testing.allocator.free(tools_dir);
+ const shell_path = try std.fs.path.join(testing.allocator, &.{ tools_dir, "shell.lua" });
+ defer testing.allocator.free(shell_path);
+ try rt.loadTool(shell_path, tools_dir);
+
+ var src = rt.toolSource();
+
+ // Prime the loop clock with one run.
+ {
+ const warm = [_]panto.ToolCall{.{ .tool_name = "std.shell", .input = "{\"command\":\"true\"}" }};
+ var wres: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
+ try src.vtable.invoke_batch(src.ctx, &warm, &wres, testing.allocator);
+ freeResults(&wres);
+ }
+
+ // Advance real time ~1.2s WITHOUT running the loop, so its cached clock
+ // goes stale relative to the wall clock by more than the timeout below.
+ {
+ const snippet = "require('luv').sleep(1200)";
+ if (c.luaL_loadstring(rt.L, snippet) != 0) return error.SleepSnippetFailed;
+ if (c.lua_pcallk(rt.L, 0, 0, 0, 0, null) != 0) return error.SleepSnippetFailed;
+ }
+
+ const calls = [_]panto.ToolCall{
+ .{ .tool_name = "std.shell", .input = "{\"command\":\"echo prompt-done\",\"timeout\":1}" },
+ };
+ var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
+ try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
+ defer freeResults(&results);
+
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "prompt-done") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "timed out") == null);
+}
+
+test "scheduler: three real std.shell calls with explicit timeout all succeed" {
+ var rt = try LuaRuntime.create(testing.allocator);
+ defer rt.deinit();
+ const luarocks_rt = bootstrapRealRuntime(rt) catch |e| return e;
+ defer luarocks_rt.deinit();
+
+ const tools_dir = try findAgentToolsDir();
+ defer testing.allocator.free(tools_dir);
+ const shell_path = try std.fs.path.join(testing.allocator, &.{ tools_dir, "shell.lua" });
+ defer testing.allocator.free(shell_path);
+ try rt.loadTool(shell_path, tools_dir);
+
+ var src = rt.toolSource();
+ const calls = [_]panto.ToolCall{
+ .{ .tool_name = "std.shell", .input = "{\"command\":\"echo alpha-done\",\"timeout\":30}" },
+ .{ .tool_name = "std.shell", .input = "{\"command\":\"echo bravo-done\",\"timeout\":30}" },
+ .{ .tool_name = "std.shell", .input = "{\"command\":\"echo charlie-done\",\"timeout\":30}" },
+ };
+ var results: [3]panto.ToolCallResult = .{
+ .{ .err = error.SourceDroppedCall },
+ .{ .err = error.SourceDroppedCall },
+ .{ .err = error.SourceDroppedCall },
+ };
+ try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
+ defer freeResults(&results);
+
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "alpha-done") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[1]), "bravo-done") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[2]), "charlie-done") != null);
+ // None should have hit the timeout path.
+ for (results) |r| {
+ try testing.expect(std.mem.indexOf(u8, okText(r), "timed out") == null);
+ }
+}
+
// Reproduction: two REAL `std.read` calls in one batch. read.lua uses
// only synchronous `uv.fs_*` calls and never yields, so both should
// complete during dispatch without entering the drive loop.