-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement Agent execution loop and type-safe compile-time Tool binding system #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
c73223d
feat: scaffold agent and tool modules with build system integration
mrazza 1f23015
feat: implement type-safe tool execution with dynamic argument mappin…
mrazza 7deedb3
refactor: update root test structure and include standard library exp…
mrazza 1d7e204
refactor: enforce string return types for tools and clarify memory ow…
mrazza 7bcbd35
refactor: support named, optional, and unordered tool arguments with …
mrazza 338c324
refactor: remove stale comments regarding TurnResult lifecycle manage…
mrazza 8fde026
refactor: unify LLM step execution output into a single StepOutcome s…
mrazza b177145
fix: prevent memory leak in Agent loop by deinitializing tool outcome…
mrazza 8e383af
refactor: simplify Provider interface by importing StepOutcome and St…
mrazza c117c54
refactor: rename last_step to prev_continuation and update Agent turn…
mrazza ac2e736
refactor: add initTakingResultOwnership to ToolResult to remove an un…
mrazza 23332cd
refactor: alias std.mem.Allocator to Allocator in Agent.zig
mrazza c40e215
feat: implement streaming support for agent turns and update CLI to s…
mrazza 9d05cbc
feat: unify agent streaming and tool result updates via StreamingChun…
mrazza 2cebef9
feat: display tool call arguments and clean up TypeScript execution logs
mrazza b0f089b
docs: add comprehensive doc comments to agent types and structures
mrazza f3f0b0b
refactor: update Agent.executeTurn signature and align callback type …
mrazza b03c256
refactor: inline tool definitions within the tool registration array
mrazza ac07a79
refactor: inject allocator and io dependencies into Agent to simplify…
mrazza a004b87
refactor: implement init function for Agent and update usage in tests
mrazza d4afe53
feat: implement concurrent tool execution using async I/O and remove …
mrazza 81ae2c6
feat: include tool call and result IDs in console output formatting
mrazza a9928cd
refactor: remove global IO state by passing Io dependency explicitly …
mrazza 8bd1733
refactor: replace generic anyerror with specific ToolError in tool ex…
mrazza d26e1bd
fix: update tool error return type to ToolError.ToolNotFound in Agent…
mrazza c1d42aa
refactor: update Agent execution methods to return explicit AgentErro…
mrazza b43b9fd
refactor: implement tool execution using Io.Select for improved futur…
mrazza bf183dc
refactor: bundle tool call id and name into ToolCallDelta for improve…
mrazza 2c88724
refactor: simplify delta conversion and introduce TransientDelta to m…
mrazza 03a97d3
refactor: move StepAccumulator and TransientDelta to a separate accum…
mrazza ed8a187
docs: add doc comments to Agent public methods for improved API clarity
mrazza 3d33390
refactor: consolidate test suite creation into a centralized helper f…
mrazza 295eb32
test: add comprehensive unit tests for tool execution, streaming prov…
mrazza bb11e3c
refactor: clean up whitespace and formatting in Agent.zig structures
mrazza 7a70cfd
refactor: replace tool argument hash map with linear search and impro…
mrazza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| const std = @import("std"); | ||
| const llm = @import("llm"); | ||
| const Tool = @import("./Tool.zig"); | ||
| const types = @import("./types.zig"); | ||
|
|
||
| const Agent = @This(); | ||
|
|
||
| provider: llm.Provider, | ||
| tools: []const Tool, | ||
| session_config: llm.types.SessionConfig, | ||
| prev_continuation: ?llm.types.StepContinuation, | ||
|
|
||
| pub fn deinit(self: *Agent) void { | ||
| if (self.prev_continuation) |*ls| { | ||
| ls.deinit(); | ||
| self.prev_continuation = null; | ||
| } | ||
| } | ||
|
|
||
| pub fn executeTurn(self: *Agent, allocator: std.mem.Allocator, turn: types.Turn) !types.TurnResult { | ||
| var next_steps: std.ArrayList(llm.types.Step) = .empty; | ||
| defer next_steps.deinit(allocator); | ||
| try next_steps.append(allocator, .{ .prompt = turn.prompt }); | ||
|
|
||
| var intermediate_results: std.ArrayList(types.IntermediateStepResult) = .empty; | ||
| defer { | ||
| // `intermediate_results` will own the memory for all non-final steps; that is, all steps | ||
| // that are model or tool outputs but not the initial user input or final result. | ||
| // These values will, in the event of a non-error, have their ownership transfered to | ||
| // the returned `TurnResult`. | ||
| for (intermediate_results.items) |*ir| { | ||
| ir.deinit(); | ||
| } | ||
| intermediate_results.deinit(allocator); | ||
| } | ||
|
|
||
| while (true) { | ||
| const step_outcome = try self.provider.executeStep( | ||
| allocator, | ||
| self.session_config, | ||
| next_steps.items, | ||
| self.prev_continuation, | ||
| ); | ||
| next_steps.clearRetainingCapacity(); | ||
|
|
||
| var step_result = step_outcome.result; | ||
| const step_continuation = step_outcome.continuation; | ||
|
|
||
| if (self.prev_continuation) |*old_continuation| old_continuation.deinit(); | ||
| self.prev_continuation = step_continuation; | ||
|
|
||
| if (step_result.tool_calls.len > 0) { | ||
| intermediate_results.append(allocator, .{ .step_result = step_result }) catch |err| { | ||
| step_result.deinit(); | ||
| return err; | ||
| }; | ||
| for (step_result.tool_calls) |tool_call| { | ||
| const tool = for (self.tools) |t| { | ||
| if (std.mem.eql(u8, t.descriptor.name, tool_call.name)) { | ||
| break t; | ||
| } | ||
| } else { | ||
| return error.ToolNotFound; | ||
| }; | ||
|
|
||
| var tool_result = try tool.execute(allocator, tool_call.id, tool_call.arguments); | ||
| intermediate_results.append(allocator, .{ .tool_result = tool_result }) catch |err| { | ||
| tool_result.deinit(); | ||
| return err; | ||
| }; | ||
| try next_steps.append(allocator, .{ .tool_result = tool_result }); | ||
| } | ||
| } else { | ||
| return .{ | ||
| .allocator = allocator, | ||
| .final_step = step_result, | ||
| .intermediate_steps = try intermediate_results.toOwnedSlice(allocator), | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const testing_pkg = @import("testing"); | ||
| threadlocal var test_mock_provider: ?*testing_pkg.MockProvider = null; | ||
|
|
||
| test "Agent.executeTurn - no tool calls" { | ||
| const allocator = std.testing.allocator; | ||
| var mock_provider = testing_pkg.MockProvider{}; | ||
| const prov = mock_provider.provider(); | ||
|
|
||
| const mock_model = llm.types.Model{ | ||
| .id = "mock-model", | ||
| .display_name = "Mock Model", | ||
| }; | ||
|
|
||
| var agent = Agent{ | ||
| .provider = prov, | ||
| .tools = &.{}, | ||
| .session_config = .{ | ||
| .model = mock_model, | ||
| .tools = &.{}, | ||
| }, | ||
| .prev_continuation = null, | ||
| }; | ||
| defer agent.deinit(); | ||
|
|
||
| mock_provider.execute_step_result = llm.types.StepResult{ | ||
| .model_output = &.{.{ .text = "Hello user!" }}, | ||
| .thoughts = &.{}, | ||
| .tool_calls = &.{}, | ||
| .ptr = &mock_provider, | ||
| .vtable = &testing_pkg.MockProvider.mock_step_vtable, | ||
| }; | ||
| var mock_continuation: testing_pkg.MockProvider.MockStepContinuation = .{}; | ||
| mock_provider.execute_step_continuation = mock_continuation.stepContinuation(); | ||
|
|
||
| const turn = types.Turn{ .prompt = "Hi agent" }; | ||
|
|
||
| var result = try agent.executeTurn(allocator, turn); | ||
| defer result.deinit(); | ||
|
|
||
| try std.testing.expectEqual(@as(usize, 1), mock_provider.execute_step_calls); | ||
| try std.testing.expect(agent.prev_continuation != null); | ||
| try std.testing.expectEqualStrings("Hello user!", result.final_step.model_output[0].text); | ||
| } | ||
|
|
||
| const MockToolImpl = struct { | ||
| pub fn execute(allocator: std.mem.Allocator, val: i64) ![]const u8 { | ||
| if (test_mock_provider) |mp| { | ||
| mp.execute_step_result = llm.types.StepResult{ | ||
| .model_output = &.{.{ .text = "Final output after tool" }}, | ||
| .thoughts = &.{}, | ||
| .tool_calls = &.{}, | ||
| .ptr = mp, | ||
| .vtable = &testing_pkg.MockProvider.mock_step_vtable, | ||
| }; | ||
| } | ||
| return try std.fmt.allocPrint(allocator, "Tool result for {d}", .{val}); | ||
| } | ||
| }; | ||
|
|
||
| test "Agent.executeTurn - executes tool call and runs again" { | ||
| const allocator = std.testing.allocator; | ||
| var mock_provider = testing_pkg.MockProvider{}; | ||
| const prov = mock_provider.provider(); | ||
| test_mock_provider = &mock_provider; | ||
| defer test_mock_provider = null; | ||
|
|
||
| const tool_desc = llm.types.Tool{ | ||
| .name = "mock_tool", | ||
| .description = "A mock tool for testing", | ||
| .parameters = &.{ | ||
| .{ | ||
| .name = "val", | ||
| .description = "integer value", | ||
| .type = .integer, | ||
| .required = true, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const tool = Tool.init(tool_desc, MockToolImpl.execute); | ||
| const tools = &[_]Tool{tool}; | ||
|
|
||
| const mock_model = llm.types.Model{ | ||
| .id = "mock-model", | ||
| .display_name = "Mock Model", | ||
| }; | ||
|
|
||
| var agent = Agent{ | ||
| .provider = prov, | ||
| .tools = tools, | ||
| .session_config = .{ | ||
| .model = mock_model, | ||
| .tools = &.{tool.descriptor}, | ||
| }, | ||
| .prev_continuation = null, | ||
| }; | ||
| defer agent.deinit(); | ||
|
|
||
| const args = [_]llm.types.Argument{ | ||
| .{ .name = "val", .value = .{ .integer = 42 } }, | ||
| }; | ||
| const tool_calls = [_]llm.types.ToolCall{ | ||
| .{ | ||
| .id = "call-id-123", | ||
| .name = "mock_tool", | ||
| .arguments = @constCast(&args), | ||
| }, | ||
| }; | ||
|
|
||
| mock_provider.execute_step_result = llm.types.StepResult{ | ||
| .model_output = &.{}, | ||
| .thoughts = &.{}, | ||
| .tool_calls = &tool_calls, | ||
| .ptr = &mock_provider, | ||
| .vtable = &testing_pkg.MockProvider.mock_step_vtable, | ||
| }; | ||
| mock_provider.execute_step_continuation = llm.types.StepContinuation{ | ||
| .ptr = &mock_provider, | ||
| .vtable = &testing_pkg.MockProvider.mock_continuation_vtable, | ||
| }; | ||
|
|
||
| const turn = types.Turn{ .prompt = "Hi agent, run mock_tool" }; | ||
|
|
||
| var result = try agent.executeTurn(allocator, turn); | ||
| defer result.deinit(); | ||
|
|
||
| try std.testing.expectEqual(@as(usize, 2), mock_provider.execute_step_calls); | ||
| try std.testing.expect(agent.prev_continuation != null); | ||
| try std.testing.expectEqualStrings("Final output after tool", result.final_step.model_output[0].text); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.