2026-07-01 06:53:26 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"iter"
|
2026-07-03 21:22:36 +00:00
|
|
|
"strings"
|
2026-07-01 06:53:26 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/VictorVargas/rony-llm-agent/pkg/llm"
|
|
|
|
|
"github.com/VictorVargas/rony-llm-agent/pkg/persona"
|
|
|
|
|
"github.com/VictorVargas/rony-llm-agent/pkg/tools"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MaxIterations is the default maximum number of agent iterations.
|
|
|
|
|
const DefaultMaxIterations = 50
|
|
|
|
|
|
|
|
|
|
// MaxToolOutputBytes is the default maximum size for tool output in bytes.
|
|
|
|
|
const DefaultMaxToolOutputBytes = 50 * 1024 // 50KB
|
|
|
|
|
|
|
|
|
|
// Sandbox defines the interface for filesystem sandbox operations.
|
|
|
|
|
type Sandbox interface {
|
|
|
|
|
ValidateToolCall(tool tools.Tool, call llm.ToolCall) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Approver is called before executing tools with Ask permission.
|
|
|
|
|
// Return true to allow, false to deny.
|
|
|
|
|
type Approver func(tool tools.Tool, call llm.ToolCall) bool
|
|
|
|
|
|
|
|
|
|
// OnIterationHook is called after each iteration.
|
|
|
|
|
type OnIterationHook func(Iteration)
|
|
|
|
|
|
|
|
|
|
// Config holds the dependencies and settings for the agent loop.
|
|
|
|
|
type Config struct {
|
2026-07-03 21:22:36 +00:00
|
|
|
LLM llm.LLMClient
|
|
|
|
|
Persona persona.Persona
|
|
|
|
|
Tools tools.Registry
|
|
|
|
|
Sandbox Sandbox
|
|
|
|
|
MaxIters int
|
|
|
|
|
Approver Approver
|
|
|
|
|
OnIteration OnIterationHook
|
|
|
|
|
ToolTimeout time.Duration
|
|
|
|
|
ChatTemplateKwargs map[string]any // passed to the LLM provider (e.g. Qwen enable_thinking)
|
2026-07-09 06:27:17 +00:00
|
|
|
AgentsMD string // discovered AGENTS.md content, folded into the system prompt
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iteration represents a single cycle of the agent loop.
|
|
|
|
|
type Iteration struct {
|
2026-07-12 23:14:15 +00:00
|
|
|
Number int
|
|
|
|
|
ToolCalls []llm.ToolCall
|
|
|
|
|
ToolsUsed int
|
|
|
|
|
Duration time.Duration
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Response is the final output of the agent loop.
|
|
|
|
|
type Response struct {
|
2026-07-12 23:14:15 +00:00
|
|
|
Content string
|
|
|
|
|
ToolCalls []llm.ToolCall
|
|
|
|
|
Iterations int
|
|
|
|
|
Duration time.Duration
|
|
|
|
|
TokenUsage llm.TokenUsage
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Loop is the main agent loop that orchestrates LLM calls and tool execution.
|
|
|
|
|
type Loop struct {
|
|
|
|
|
cfg Config
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates a new Loop with the given configuration.
|
|
|
|
|
func New(cfg Config) *Loop {
|
|
|
|
|
if cfg.MaxIters == 0 {
|
|
|
|
|
cfg.MaxIters = DefaultMaxIterations
|
|
|
|
|
}
|
|
|
|
|
return &Loop{cfg: cfg}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run executes the agent loop and returns the final response.
|
2026-07-05 23:17:37 +00:00
|
|
|
// Optional history messages are appended after the system prompt and before
|
|
|
|
|
// the new user input.
|
|
|
|
|
func (l *Loop) Run(ctx context.Context, input string, history ...llm.Message) (Response, error) {
|
2026-07-01 06:53:26 +00:00
|
|
|
start := time.Now()
|
|
|
|
|
|
2026-07-05 23:17:37 +00:00
|
|
|
messages := l.buildInitialMessages(input, history)
|
2026-07-12 23:14:15 +00:00
|
|
|
// Tool schemas don't change between iterations, so build the JSON once
|
|
|
|
|
// per Run instead of re-marshaling every tool on every loop pass.
|
|
|
|
|
toolSchemas := l.getToolSchemas()
|
2026-07-01 06:53:26 +00:00
|
|
|
var finalContent string
|
|
|
|
|
var allToolCalls []llm.ToolCall
|
|
|
|
|
var totalUsage llm.TokenUsage
|
|
|
|
|
iterations := 0
|
2026-07-12 23:14:15 +00:00
|
|
|
completed := false
|
2026-07-01 06:53:26 +00:00
|
|
|
|
|
|
|
|
for iterations < l.cfg.MaxIters {
|
|
|
|
|
iterations++
|
|
|
|
|
|
|
|
|
|
resp, err := l.cfg.LLM.Generate(ctx, llm.CompletionRequest{
|
2026-07-03 21:22:36 +00:00
|
|
|
Messages: messages,
|
2026-07-12 23:14:15 +00:00
|
|
|
Tools: toolSchemas,
|
2026-07-03 21:22:36 +00:00
|
|
|
ChatTemplateKwargs: l.cfg.ChatTemplateKwargs,
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Response{}, fmt.Errorf("LLM generate failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalUsage.InputTokens += resp.Usage.InputTokens
|
|
|
|
|
totalUsage.OutputTokens += resp.Usage.OutputTokens
|
|
|
|
|
totalUsage.TotalTokens += resp.Usage.TotalTokens
|
|
|
|
|
|
|
|
|
|
if len(resp.ToolCalls) == 0 {
|
|
|
|
|
finalContent = resp.Content
|
2026-07-12 23:14:15 +00:00
|
|
|
completed = true
|
2026-07-01 06:53:26 +00:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
// Record the assistant's own turn (including which tools it asked
|
|
|
|
|
// for) before the results, so the next request has a coherent
|
|
|
|
|
// assistant-tool_calls / tool-result pair instead of a dangling
|
|
|
|
|
// tool message the model can't attribute to anything.
|
|
|
|
|
messages = append(messages, llm.Message{
|
|
|
|
|
Role: llm.RoleAssistant,
|
|
|
|
|
Content: resp.Content,
|
|
|
|
|
ToolCalls: resp.ToolCalls,
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-01 06:53:26 +00:00
|
|
|
for _, call := range resp.ToolCalls {
|
|
|
|
|
result, err := l.executeTool(ctx, call)
|
|
|
|
|
if err != nil {
|
|
|
|
|
messages = append(messages, llm.Message{
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
Role: llm.RoleTool,
|
|
|
|
|
ToolCallID: call.ID,
|
|
|
|
|
Content: fmt.Sprintf("Error: %v", err),
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
messages = append(messages, llm.Message{
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
Role: llm.RoleTool,
|
|
|
|
|
ToolCallID: call.ID,
|
|
|
|
|
Content: result.Content,
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
allToolCalls = append(allToolCalls, call)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
duration := time.Since(start)
|
2026-07-12 23:14:15 +00:00
|
|
|
// Only report the max-iterations failure when the loop actually ran out
|
|
|
|
|
// of budget without producing a final answer — an answer that arrives
|
|
|
|
|
// exactly on the last allowed iteration is still a success (the old
|
|
|
|
|
// `iterations >= MaxIters` check threw that valid response away).
|
|
|
|
|
if !completed {
|
2026-07-01 06:53:26 +00:00
|
|
|
return Response{}, fmt.Errorf("max iterations (%d) reached", l.cfg.MaxIters)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Response{
|
2026-07-12 23:14:15 +00:00
|
|
|
Content: finalContent,
|
|
|
|
|
ToolCalls: allToolCalls,
|
|
|
|
|
Iterations: iterations,
|
|
|
|
|
Duration: duration,
|
|
|
|
|
TokenUsage: totalUsage,
|
2026-07-01 06:53:26 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RunStream executes the agent loop with streaming output.
|
2026-07-05 23:17:37 +00:00
|
|
|
// Optional history messages are appended after the system prompt and before
|
|
|
|
|
// the new user input.
|
|
|
|
|
func (l *Loop) RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[llm.StreamChunk, error] {
|
2026-07-01 06:53:26 +00:00
|
|
|
return func(yield func(llm.StreamChunk, error) bool) {
|
2026-07-05 23:17:37 +00:00
|
|
|
messages := l.buildInitialMessages(input, history)
|
2026-07-12 23:14:15 +00:00
|
|
|
// Same as Run: the schemas are identical on every iteration.
|
|
|
|
|
toolSchemas := l.getToolSchemas()
|
2026-07-01 06:53:26 +00:00
|
|
|
iterations := 0
|
|
|
|
|
|
|
|
|
|
for iterations < l.cfg.MaxIters {
|
|
|
|
|
iterations++
|
|
|
|
|
|
|
|
|
|
stream := l.cfg.LLM.Stream(ctx, llm.CompletionRequest{
|
2026-07-03 21:22:36 +00:00
|
|
|
Messages: messages,
|
2026-07-12 23:14:15 +00:00
|
|
|
Tools: toolSchemas,
|
2026-07-03 21:22:36 +00:00
|
|
|
ChatTemplateKwargs: l.cfg.ChatTemplateKwargs,
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
var hasToolCalls bool
|
2026-07-03 21:22:36 +00:00
|
|
|
var responseBuilder strings.Builder
|
2026-07-01 06:53:26 +00:00
|
|
|
for chunk, err := range stream {
|
|
|
|
|
if err != nil {
|
|
|
|
|
yield(llm.StreamChunk{}, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(chunk.ToolCalls) > 0 {
|
|
|
|
|
hasToolCalls = true
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
|
|
|
|
|
// Same reasoning as in Run: without recording the
|
|
|
|
|
// assistant's own tool_calls turn first, the tool
|
|
|
|
|
// results that follow have nothing for the model to
|
|
|
|
|
// attribute them to on the next request.
|
|
|
|
|
messages = append(messages, llm.Message{
|
|
|
|
|
Role: llm.RoleAssistant,
|
|
|
|
|
Content: responseBuilder.String(),
|
|
|
|
|
ToolCalls: chunk.ToolCalls,
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-01 06:53:26 +00:00
|
|
|
for _, tc := range chunk.ToolCalls {
|
|
|
|
|
result, err := l.executeTool(ctx, tc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
messages = append(messages, llm.Message{
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
Role: llm.RoleTool,
|
|
|
|
|
ToolCallID: tc.ID,
|
|
|
|
|
Content: fmt.Sprintf("Error: %v", err),
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
messages = append(messages, llm.Message{
|
Fix tool call tracking and streaming assembly for all providers
- agent/loop.go: Record assistant message with ToolCalls before tool results,
and set ToolCallID on tool-result messages so follow-up requests have complete
context (prevents models from losing track of already-attempted tools).
- llm/providers/llamacpp/client.go: Buffer fragmented tool call deltas during
streaming, assemble them into complete calls when finish_reason arrives.
Add ToolCalls, ToolCallID, Name fields to request building.
- llm/providers/openai/client.go: Send ToolCalls, ToolCallID, Name when
building chat requests so messages are wire-format correct.
- llm/types.go: Add ToolCalls field to Message struct for serialization
back into conversation history.
- agent/integration_test.go: Move integration test skip from TestMain to a
per-test skipUnlessIntegration() so it doesn't hide other package tests.
- sandbox & tools: Add edge-case tests (relative traversal, array paths,
non-path strings, zero-value guards, sentinel errors).
2026-07-08 23:11:57 +00:00
|
|
|
Role: llm.RoleTool,
|
|
|
|
|
ToolCallID: tc.ID,
|
|
|
|
|
Content: result.Content,
|
2026-07-01 06:53:26 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-07-10 07:04:44 +00:00
|
|
|
|
|
|
|
|
// Surface which tools were actually called, and with
|
|
|
|
|
// what arguments, to the caller — a dedicated chunk,
|
|
|
|
|
// separate from the content-streaming gate below, since
|
|
|
|
|
// that gate exists to hide raw provider deltas during a
|
|
|
|
|
// tool-call round, not to hide the fact that a call
|
|
|
|
|
// happened at all. Without this, callers (e.g. a UI
|
|
|
|
|
// wanting to show "used tool X" or track which files a
|
|
|
|
|
// write/edit touched) have no way to observe tool
|
|
|
|
|
// calls unless they also happen to be the Approver.
|
|
|
|
|
if !yield(llm.StreamChunk{ToolCalls: chunk.ToolCalls}, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 07:05:30 +00:00
|
|
|
// A trailing usage-only chunk (no Delta/ReasoningDelta, per
|
|
|
|
|
// providers that report token usage in a separate final
|
|
|
|
|
// event) must still be forwarded, or callers can never see
|
|
|
|
|
// real token counts.
|
|
|
|
|
hasContent := chunk.Delta != "" || chunk.ReasoningDelta != ""
|
|
|
|
|
hasUsage := chunk.Usage.TotalTokens > 0
|
2026-07-12 23:14:15 +00:00
|
|
|
switch {
|
|
|
|
|
case !hasToolCalls && (hasContent || hasUsage):
|
2026-07-03 21:22:36 +00:00
|
|
|
responseBuilder.WriteString(chunk.Delta)
|
|
|
|
|
if !yield(chunk, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-12 23:14:15 +00:00
|
|
|
case hasToolCalls && hasUsage:
|
|
|
|
|
// The content gate above exists to hide raw provider
|
|
|
|
|
// deltas during a tool-call round, but it also swallowed
|
|
|
|
|
// that round's token usage — so callers tracking context
|
|
|
|
|
// occupancy (e.g. a UI's context bar deciding when to
|
|
|
|
|
// compact) only ever saw the usage of the final,
|
|
|
|
|
// tool-free round. Forward the usage on its own,
|
|
|
|
|
// without the content.
|
|
|
|
|
if !yield(llm.StreamChunk{Usage: chunk.Usage}, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasToolCalls {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 21:22:36 +00:00
|
|
|
yield(llm.StreamChunk{}, fmt.Errorf("max iterations (%d) reached", l.cfg.MaxIters))
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 23:17:37 +00:00
|
|
|
func (l *Loop) buildInitialMessages(input string, history []llm.Message) []llm.Message {
|
2026-07-09 06:27:17 +00:00
|
|
|
systemPrompt := persona.AssembleSystemPrompt(l.cfg.Persona, l.cfg.AgentsMD)
|
2026-07-05 23:17:37 +00:00
|
|
|
messages := make([]llm.Message, 0, len(history)+2)
|
|
|
|
|
messages = append(messages, llm.Message{Role: llm.RoleSystem, Content: systemPrompt})
|
|
|
|
|
messages = append(messages, history...)
|
|
|
|
|
messages = append(messages, llm.Message{Role: llm.RoleUser, Content: input})
|
|
|
|
|
return messages
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Loop) getToolSchemas() []json.RawMessage {
|
|
|
|
|
var schemas []json.RawMessage
|
|
|
|
|
for _, tool := range l.cfg.Tools.List() {
|
2026-07-03 21:22:36 +00:00
|
|
|
def := map[string]interface{}{
|
|
|
|
|
"type": "function",
|
|
|
|
|
"function": map[string]interface{}{
|
|
|
|
|
"name": tool.Name,
|
|
|
|
|
"description": tool.Description,
|
|
|
|
|
"parameters": json.RawMessage(tool.InputSchema),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
data, _ := json.Marshal(def)
|
|
|
|
|
schemas = append(schemas, data)
|
2026-07-01 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
return schemas
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Loop) executeTool(ctx context.Context, call llm.ToolCall) (tools.ToolResult, error) {
|
|
|
|
|
tool, found := l.cfg.Tools.Get(call.Name)
|
|
|
|
|
if !found {
|
|
|
|
|
return tools.ToolResult{IsError: true}, fmt.Errorf("tool %q not found", call.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tool.Permission == tools.Ask && l.cfg.Approver != nil && !l.cfg.Approver(tool, call) {
|
|
|
|
|
return tools.ToolResult{Content: "Tool execution denied by user"}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if l.cfg.Sandbox != nil {
|
|
|
|
|
if err := l.cfg.Sandbox.ValidateToolCall(tool, call); err != nil {
|
|
|
|
|
return tools.ToolResult{IsError: true}, fmt.Errorf("sandbox violation: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if l.cfg.OnIteration != nil {
|
|
|
|
|
l.cfg.OnIteration(Iteration{
|
|
|
|
|
Number: 1,
|
|
|
|
|
ToolCalls: []llm.ToolCall{call},
|
|
|
|
|
ToolsUsed: 1,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := tool.Handler(ctx, call.Arguments)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return tools.ToolResult{IsError: true, Content: fmt.Sprintf("Tool error: %v", err)}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|