Repository navigation
code_mode_tools breaks transfer_task (and handoff): runtime-handled tools wrapped into run_tools_with_javascript #4713
Description
Summary
When an agent sets code_mode_tools: true and has sub_agents (or handoffs), the transfer_task (and handoff) tool is wrapped into the code mode toolset. Calling it from JavaScript fails with tool "transfer_task" is not available in code mode. The agent cannot delegate at all.
Tool 'TransferTask' is not available. You can only use the tools provided to you.
Reproduction
version: "16"
agents:
root:
model: my_model
instruction: You are a coordinator. Delegate every task to helper.
code_mode_tools: true
sub_agents:
- helper
helper:
model: my_model
description: Answers questions
instruction: Answer briefly.
models:
my_model:
provider: openai
model: gpt-4oRun docker agent run agent.yaml and ask: "Ask helper to say hello."
Expected vs actual
- Expected:
roottransfers the task tohelper, as it does withoutcode_mode_tools. - Actual: the model sees
declare function TransferTask(...)in therun_tools_with_javascriptdescription, calls it, and receivestool "transfer_task" is not available in code mode. Retrying gives the same error.
Root cause
Pinned to main at 223f940.
-
getToolsForAgentappends the transfer and handoff toolsets, then wraps all toolsets in code mode
(pkg/teamloader/teamloader.go):if len(a.SubAgents) > 0 { toolSets = append(toolSets, transfertask.New()) } if len(a.Handoffs) > 0 { toolSets = append(toolSets, handoff.New()) } ... if a.CodeModeTools || runConfig.GlobalCodeMode { ... toolSets = []tools.ToolSet{loadOpts.codeMode(toolSets...)} }
-
transfer_taskis handled by the runtime and has noHandler
(pkg/tools/builtin/transfertask/transfertask.go):Name: ToolNameTransferTask, RuntimeHandler: ToolNameTransferTask, Category: "transfer",
-
Code mode rejects any tool without a
Handler
(pkg/tools/codemode/exec.go):if tool.Handler == nil { return "", args, fmt.Errorf("tool %q is not available in code mode", tool.Name) }
-
isExcludedToolonly keepstodoandAllowRepeatedCallstools out of the wrapper
(pkg/tools/codemode/codemode.go):return tool.Category == "todo" || tool.AllowRepeatedCalls
So
transfer_taskis listed in the generated TypeScript declarations (TransferTask, PascalCase viatypeName()infunctions.go) even though it can never run there.
Proposed fix
In isExcludedTool, also exempt tools that have a non-empty RuntimeHandler (or category transfer / handoff), so they stay top-level tools next to run_tools_with_javascript:
func isExcludedTool(tool tools.Tool) bool {
return tool.Category == "todo" || tool.AllowRepeatedCalls || tool.RuntimeHandler != ""
}Alternative: in teamloader, append transfertask.New() and handoff.New() after the code mode wrap, so they are never passed to loadOpts.codeMode.
Test: add a case next to codemode_test.go that wraps a toolset containing a tool with RuntimeHandler set and no Handler. Assert it is not in the run_tools_with_javascript description and is still returned by the wrapper's Tools() as a top-level tool.
Side note: permissions and tool_guard
Inner calls made inside run_tools_with_javascript are not seen by the permissions or tool_guard hooks. Only the outer run_tools_with_javascript call is. An agent with a shell tool and code_mode_tools: true can run shell commands that skip per-command permission checks. Either document this or apply the checks to each inner call.
Environment
- docker-agent:
mainat223f940 - Config version: 16
- Config: a multi-agent team config with
sub_agents
Activity
aheritier commentedon Oct 24, 2026
Does it ring any bell on your side @rumpl @dgageot ?