From 996b15c9b91d96bf624ae2e7947044401260473c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 23 Apr 2026 13:13:28 +0300 Subject: [PATCH] fix(extensions): return nil error for blocked/disabled tools so LLM sees the reason Tool blocking via OnToolCall and SetActiveTools returned both a ToolResponse (IsError=true) and a Go error. Fantasy treats a non-nil Go error from tool.Run() as a critical failure, aborting the agent loop without delivering the tool result to the LLM. The model never saw the block reason and would retry or hallucinate. - Return nil error for blocked tools (OnToolCall Block=true) - Return nil error for disabled tools (SetActiveTools) - Return nil error for extension tool execution failures - Update tests to assert nil error (IsError response conveys the error) Fixes #20 --- internal/extensions/wrapper.go | 8 +++----- internal/extensions/wrapper_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/extensions/wrapper.go b/internal/extensions/wrapper.go index 466ac554..6af25692 100644 --- a/internal/extensions/wrapper.go +++ b/internal/extensions/wrapper.go @@ -90,8 +90,7 @@ func (w *wrappedTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.T // 0. Check if tool is disabled via SetActiveTools. if w.runner.IsToolDisabled(toolName) { return fantasy.NewTextErrorResponse( - fmt.Sprintf("Error: tool %q is currently disabled", toolName)), - fmt.Errorf("tool %q disabled by extension", toolName) + fmt.Sprintf("Error: tool %q is currently disabled", toolName)), nil } kind := toolKindFor(toolName) @@ -111,8 +110,7 @@ func (w *wrappedTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.T if reason == "" { reason = "blocked by extension" } - return fantasy.NewTextErrorResponse(fmt.Sprintf("Error: %s", reason)), - fmt.Errorf("tool blocked by extension: %s", reason) + return fantasy.NewTextErrorResponse(fmt.Sprintf("Error: %s", reason)), nil } } @@ -238,7 +236,7 @@ func (t *extensionTool) Run(ctx context.Context, call fantasy.ToolCall) (fantasy } if err != nil { - return fantasy.NewTextErrorResponse(err.Error()), err + return fantasy.NewTextErrorResponse(err.Error()), nil } return fantasy.NewTextResponse(result), nil } diff --git a/internal/extensions/wrapper_test.go b/internal/extensions/wrapper_test.go index 9573cb17..ead5f7ba 100644 --- a/internal/extensions/wrapper_test.go +++ b/internal/extensions/wrapper_test.go @@ -142,8 +142,8 @@ func TestWrappedTool_BlockExecution(t *testing.T) { if toolRan { t.Error("tool should not have run after block") } - if err == nil { - t.Error("expected error from blocked tool") + if err != nil { + t.Error("expected nil error for blocked tool (error is conveyed via IsError response)") } if resp.IsError != true { t.Error("expected IsError=true from blocked response") @@ -234,8 +234,8 @@ func TestExtensionTool_Error(t *testing.T) { tools := ExtensionToolsAsLLMTools(defs, nil) resp, err := tools[0].Run(context.Background(), fantasy.ToolCall{Input: "x"}) - if err == nil { - t.Error("expected error") + if err != nil { + t.Error("expected nil error (error is conveyed via IsError response)") } if !resp.IsError { t.Error("expected IsError=true")