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 ?
rumpl commentedon Oct 25, 2026
Handoff and transfer task are kinda soecial in our code but IMO we should make transfer task work with code mode. Doesn't make sense for handoff
rumpl commentedon Oct 25, 2026
Huh. Why are todo tools excluded, shouldn't be