@@ -6,6 +6,7 @@ package iteragent
66import (
77 "context"
88 "encoding/json"
9+ "errors"
910 "fmt"
1011 "log/slog"
1112 "strings"
@@ -15,6 +16,11 @@ import (
1516 "github.com/GrayCodeAI/iteragent/openapi"
1617)
1718
19+ // ErrUnknownTool is returned when a tool call references a tool that is not registered.
20+ var ErrUnknownTool = errors .New ("unknown tool" )
21+
22+ const maxAgentIterations = 20
23+
1824// ToolCall represents a tool invocation.
1925type ToolCall struct {
2026 Tool string `json:"tool"`
@@ -57,6 +63,7 @@ type AgentHooks struct {
5763type Agent struct {
5864 provider Provider
5965 tools map [string ]Tool
66+ toolsMu sync.RWMutex
6067 logger * slog.Logger
6168 Events chan Event
6269 SystemPrompt string
@@ -185,7 +192,7 @@ func (a *Agent) executeToolsSequential(ctx context.Context, calls []ToolCall, it
185192 var toolResults strings.Builder
186193 for _ , call := range calls {
187194 result , isError := a .executeSingleTool (ctx , call , iteration , emitFn )
188- if isError && result == fmt . Sprintf ( "unknown tool: %s" , call . Tool ) {
195+ if isError && errors . Is ( isErrorToErr ( isError , result ), ErrUnknownTool ) {
189196 toolResults .WriteString (fmt .Sprintf ("Tool %s: %s\n " , call .Tool , result ))
190197 } else {
191198 toolResults .WriteString (fmt .Sprintf ("Tool %s result:\n %s\n \n " , call .Tool , result ))
@@ -194,6 +201,17 @@ func (a *Agent) executeToolsSequential(ctx context.Context, calls []ToolCall, it
194201 return toolResults .String ()
195202}
196203
204+ // isErrorToErr converts the (result, isErrorBool) pair back to an error for checking.
205+ func isErrorToErr (isError bool , result string ) error {
206+ if ! isError {
207+ return nil
208+ }
209+ if strings .HasPrefix (result , "unknown tool:" ) {
210+ return ErrUnknownTool
211+ }
212+ return errors .New (result )
213+ }
214+
197215func (a * Agent ) executeToolsParallel (ctx context.Context , calls []ToolCall , iteration int , emitFn func (Event )) string {
198216 type indexedResult struct {
199217 call ToolCall
@@ -213,11 +231,13 @@ func (a *Agent) executeToolsParallel(ctx context.Context, calls []ToolCall, iter
213231 ToolName : c .Tool ,
214232 ToolCallID : fmt .Sprintf ("%s-%d-%d" , c .Tool , iteration , i ),
215233 })
234+ a .toolsMu .RLock ()
216235 tool , ok := a .tools [c .Tool ]
236+ a .toolsMu .RUnlock ()
217237 if ! ok {
218238 res := fmt .Sprintf ("unknown tool: %s" , c .Tool )
219239 if a .hooks .OnToolEnd != nil {
220- a .hooks .OnToolEnd (c .Tool , res , fmt . Errorf ( "unknown tool: %s" , c . Tool ) )
240+ a .hooks .OnToolEnd (c .Tool , res , ErrUnknownTool )
221241 }
222242 results [i ] = indexedResult {call : c , result : res , isError : true , unknown : true }
223243 emitFn (Event {Type : string (EventToolExecutionEnd ), ToolName : c .Tool , Result : results [i ].result , IsError : true })
@@ -272,7 +292,9 @@ func (a *Agent) executeToolsBatched(ctx context.Context, calls []ToolCall, itera
272292// executeSingleTool runs one tool call and emits start/end events.
273293// Returns (result string, isError bool).
274294func (a * Agent ) executeSingleTool (ctx context.Context , call ToolCall , iteration int , emitFn func (Event )) (string , bool ) {
295+ a .toolsMu .RLock ()
275296 tool , ok := a .tools [call .Tool ]
297+ a .toolsMu .RUnlock ()
276298 if ! ok {
277299 result := fmt .Sprintf ("unknown tool: %s" , call .Tool )
278300 emitFn (Event {Type : string (EventToolExecutionEnd ), ToolName : call .Tool , Result : result , IsError : true })
@@ -330,10 +352,7 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string, emitF
330352 }
331353 userMessage = filtered
332354
333- allTools := make ([]Tool , 0 , len (a .tools ))
334- for _ , t := range a .tools {
335- allTools = append (allTools , t )
336- }
355+ allTools := a .GetTools ()
337356
338357 messages := []Message {
339358 {Role : "system" , Content : systemPrompt + "\n \n " + ToolDescriptions (allTools )},
@@ -342,8 +361,7 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string, emitF
342361
343362 opts := a .completionOpts ()
344363
345- const maxIterations = 20
346- for i := 0 ; i < maxIterations ; i ++ {
364+ for i := 0 ; i < maxAgentIterations ; i ++ {
347365 a .logger .Info ("agent iteration" , "step" , i + 1 )
348366 emit (Event {Type : string (EventTurnStart ), Content : fmt .Sprintf ("turn %d" , i + 1 )})
349367
@@ -401,7 +419,7 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string, emitF
401419 emit (Event {Type : string (EventTurnEnd ), Content : "" })
402420 }
403421
404- return "" , fmt .Errorf ("agent exceeded max iterations (%d)" , maxIterations )
422+ return "" , fmt .Errorf ("agent exceeded max iterations (%d)" , maxAgentIterations )
405423}
406424
407425func (a * Agent ) emit (e Event ) {
@@ -517,11 +535,15 @@ func (a *Agent) WithTools(tools []Tool) *Agent {
517535 for _ , t := range tools {
518536 toolMap [t .Name ] = t
519537 }
538+ a .toolsMu .Lock ()
520539 a .tools = toolMap
540+ a .toolsMu .Unlock ()
521541 return a
522542}
523543
524544func (a * Agent ) GetTools () []Tool {
545+ a .toolsMu .RLock ()
546+ defer a .toolsMu .RUnlock ()
525547 tools := make ([]Tool , 0 , len (a .tools ))
526548 for _ , t := range a .tools {
527549 tools = append (tools , t )
@@ -530,7 +552,9 @@ func (a *Agent) GetTools() []Tool {
530552}
531553
532554func (a * Agent ) AddTool (tool Tool ) * Agent {
555+ a .toolsMu .Lock ()
533556 a .tools [tool .Name ] = tool
557+ a .toolsMu .Unlock ()
534558 return a
535559}
536560
@@ -601,6 +625,7 @@ func (a *Agent) registerMcpTools(ctx context.Context, adapter *mcp.ToolAdapter)
601625 if err != nil {
602626 return nil , fmt .Errorf ("list mcp tools: %w" , err )
603627 }
628+ a .toolsMu .Lock ()
604629 for _ , t := range tools {
605630 execute := t .Execute
606631 a .tools [t .Name ] = Tool {
@@ -609,6 +634,7 @@ func (a *Agent) registerMcpTools(ctx context.Context, adapter *mcp.ToolAdapter)
609634 Execute : execute ,
610635 }
611636 }
637+ a .toolsMu .Unlock ()
612638 // Track the client so Close() can shut it down.
613639 if client := adapter .Client (); client != nil {
614640 a .mu .Lock ()
@@ -668,6 +694,7 @@ func (a *Agent) registerOpenApiTools(adapter *openapi.Adapter) (*Agent, error) {
668694 if err != nil {
669695 return nil , fmt .Errorf ("list openapi tools: %w" , err )
670696 }
697+ a .toolsMu .Lock ()
671698 for _ , t := range tools {
672699 execute := t .Execute
673700 a .tools [t .Name ] = Tool {
@@ -676,6 +703,7 @@ func (a *Agent) registerOpenApiTools(adapter *openapi.Adapter) (*Agent, error) {
676703 Execute : execute ,
677704 }
678705 }
706+ a .toolsMu .Unlock ()
679707 return a , nil
680708}
681709
@@ -731,6 +759,9 @@ func (a *Agent) Prompt(ctx context.Context, text string) chan Event {
731759 a .pendingWg .Add (1 )
732760 go func () {
733761 defer func () {
762+ if r := recover (); r != nil {
763+ emitFn (Event {Type : string (EventError ), Content : fmt .Sprintf ("panic: %v" , r ), IsError : true })
764+ }
734765 a .pendingWg .Done ()
735766 a .mu .Lock ()
736767 a .isStreaming = false
@@ -776,6 +807,9 @@ func (a *Agent) PromptMessages(ctx context.Context, messages []Message) chan Eve
776807 a .pendingWg .Add (1 )
777808 go func () {
778809 defer func () {
810+ if r := recover (); r != nil {
811+ emitFn (Event {Type : string (EventError ), Content : fmt .Sprintf ("panic: %v" , r ), IsError : true })
812+ }
779813 a .pendingWg .Done ()
780814 a .mu .Lock ()
781815 a .isStreaming = false
@@ -812,7 +846,7 @@ func (a *Agent) PromptMessages(ctx context.Context, messages []Message) chan Eve
812846
813847 opts := a .completionOpts ()
814848
815- for i := 0 ; i < 20 ; i ++ {
849+ for i := 0 ; i < maxAgentIterations ; i ++ {
816850 // Check for cancellation before each turn.
817851 select {
818852 case <- loopCtx .Done ():
0 commit comments