diff options
Diffstat (limited to 'libpanto/src/agent.zig')
| -rw-r--r-- | libpanto/src/agent.zig | 61 |
1 files changed, 60 insertions, 1 deletions
diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig index 0ff231a..8a8a2e2 100644 --- a/libpanto/src/agent.zig +++ b/libpanto/src/agent.zig @@ -1150,6 +1150,10 @@ pub const Stream = struct { streaming, /// A provider response completed; decide tools-vs-done. after_response, + /// Tool dispatch has been announced to callers; run the blocking + /// dispatch on the next pull so `tool_dispatch_start` is observable + /// before long-running tools complete. + dispatching_tools, /// The turn reached its terminal `turn_complete`. done, /// A failure already propagated; `next()` is poisoned. @@ -1328,12 +1332,25 @@ pub const Stream = struct { return .turn_complete; } - // Dispatch the tool calls, bracketed by boundary events. + // Announce tool dispatch and return immediately. The + // dispatch itself can block for a long time (especially + // with parallel tools); yielding this boundary event first + // lets UIs/renderers show all tool calls as running instead + // of appearing frozen until the slowest tool completes. const count = toolUseCount(last); self._queue.push(.{ .tool_dispatch_start = .{ .count = count } }) catch |e| { self.state = .failed; return e; }; + self.state = .dispatching_tools; + if (self._queue.pop()) |ev| return ev; + }, + .dispatching_tools => { + const conv = &self._agent.conversation; + const last = conv.messages.items[conv.messages.items.len - 1]; + std.debug.assert(last.role == .assistant); + std.debug.assert(Agent.hasToolUseBlock(last)); + self._agent.dispatchToolCalls(last) catch |err| { self.state = .failed; return err; @@ -2394,6 +2411,48 @@ test "runStep dispatches a tool call and loops to a final text turn" { try testing.expectEqualStrings("ok", conv.messages.items[3].content.items[0].Text.items); } +test "Stream emits tool_dispatch_start before running tools" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{.{ .Text = "ok" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var s = try agent.run(.{ .text = "call a tool" }); + defer s.deinit(); + + const first = (try s.next()).?; + try testing.expect(first == .message_complete); + try testing.expectEqual(@as(usize, 2), agent.conversation.messages.items.len); + + const start = (try s.next()).?; + try testing.expect(start == .tool_dispatch_start); + try testing.expectEqual(@as(usize, 1), start.tool_dispatch_start.count); + // The ToolResult user message must not exist yet; otherwise callers do + // not get a chance to render the tool as running before dispatch blocks. + try testing.expectEqual(@as(usize, 2), agent.conversation.messages.items.len); + + const complete = (try s.next()).?; + try testing.expect(complete == .tool_dispatch_complete); + try testing.expectEqual(@as(usize, 3), agent.conversation.messages.items.len); +} + test "runStep dispatches multiple tool calls in parallel" { const allocator = testing.allocator; |
