Skip to content

🧭 fix: Handle Mistyped Handoff Tools Locally#161

Merged
danny-avila merged 4 commits into
devfrom
danny-avila/pr-61-handoff-edgecase
May 8, 2026
Merged

🧭 fix: Handle Mistyped Handoff Tools Locally#161
danny-avila merged 4 commits into
devfrom
danny-avila/pr-61-handoff-edgecase

Conversation

@danny-avila
Copy link
Copy Markdown
Owner

@danny-avila danny-avila commented May 8, 2026

Summary

I fixed mistyped graph-managed handoff tool calls so they fail closed inside ToolNode with exact-name guidance instead of being silently executed as a guessed handoff or dispatched to the host tool executor.

Fixes #60.

  • Added exact handoff-name detection helpers that only suggest a registered handoff when the model emitted a strict longer prefix match.
  • Routed unknown handoff-shaped calls through the local/direct runTool error path so the model receives a normal ToolMessage error tied to its original tool call id.
  • Preserved host event dispatch for real event-driven tools, while preventing typo-shaped handoffs from reaching ON_TOOL_EXECUTE.
  • Added regression coverage for plain graph handoffs, mixed event-driven host tools, local-execution direct-tool overlays, and event-driven ToolNodes without direct tool names.
  • Added an opt-in live Anthropic handoff integration test using real Run.processStream execution and a reusable test:live:handoffs script.

This is empirically better than the contributor patch because the tests now measure the behavioral contract directly: a mistyped handoff is not executed, does not reach the host executor, and still returns actionable exact-name feedback. The live integration test also proves a real Anthropic handoff routes through the graph-managed transfer tool and preserves instructions into the receiving agent. The prior approach added synthetic correction output after resolving the typo, but it introduced several extra passes over the same call list and did not prove the host-dispatch leakage case. This patch keeps the error in the same existing unknown-tool path and verifies the real edge cases with executable assertions.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • Ran NODE_OPTIONS='--experimental-vm-modules' npx jest src/specs/agent-handoffs.test.ts --runInBand.
  • Ran NODE_OPTIONS='--experimental-vm-modules' npx jest src/specs/agent-handoffs.live.test.ts --runInBand to verify the live test is skipped by default.
  • Ran NODE_OPTIONS='--experimental-vm-modules' npx jest src/specs/agent-handoffs.live.test.ts src/specs/agent-handoffs.test.ts --runInBand.
  • Ran npm run test:live:handoffs with .env credentials; the live Anthropic handoff passed with claude-sonnet-4-6.
  • Ran npx tsc --noEmit.
  • Ran npx eslint src/tools/ToolNode.ts src/specs/agent-handoffs.test.ts.
  • Ran npx eslint src/specs/agent-handoffs.live.test.ts.
  • Ran git diff --check.
  • Ran live OpenAI handoff steering script successfully across three real handoff scenarios.
  • Ran a compact live Anthropic handoff smoke with claude-sonnet-4-6; it returned handoff: true, toolNames: ["lc_transfer_to_specialist"], and specialistReplied: true.

Test Configuration:

  • Base branch: origin/dev at da06b06ada4b6fe3c12757fa2084b445e84b7197
  • Node: v20.19.5
  • npm: 10.8.2
  • Anthropic model used for live smoke and live Jest handoff: claude-sonnet-4-6

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

@danny-avila
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 932eac10de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tools/ToolNode.ts Outdated
Comment on lines +588 to +591
private getRegisteredHandoffNames(): Iterable<string> {
return this.directToolNames != null && this.directToolNames.size > 0
? this.directToolNames
: this.toolMap.keys();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include toolMap handoffs when computing typo suggestions

getRegisteredHandoffNames() returns only directToolNames whenever that set is non-empty, which drops graph handoff tools that still live in toolMap. In runs with local execution enabled, applyToolExecutionOverrides() commonly populates directToolNames with code tools, so a mistyped lc_transfer_to_* handoff in the normal (non-event-driven) path no longer gets the new exact-name guidance and falls back to a generic not-found error. This regresses the handoff-typo behavior for local-execution configurations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1c99eae: getRegisteredHandoffNames() now treats directToolNames as an overlay and includes toolMap handoff names as well. I added coverage for a handoff in toolMap with an unrelated direct local tool name present, and verified with NODE_OPTIONS='--experimental-vm-modules' npx jest src/specs/agent-handoffs.test.ts --runInBand, npx tsc --noEmit, npx eslint src/tools/ToolNode.ts src/specs/agent-handoffs.test.ts, and git diff --check.

@danny-avila
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c99eaeea5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tools/ToolNode.ts Outdated
Comment on lines 2924 to 2927
if (directToolNames == null || directToolNames.size === 0) {
return this.executeViaEvent(filteredCalls, config, input, {
batchIndices: filteredIndices,
turn,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route unknown handoff typos through local path consistently

When eventDrivenMode is enabled and directToolNames is empty, this early return dispatches every call to executeViaEvent, so handoff-shaped unknown calls never reach shouldHandleUnknownHandoffLocally. That creates a behavior gap with the Send input branch (which now does local fail-closed handling), and it still lets typoed lc_transfer_to_* calls leak to the host executor in configurations where handoff tools exist only in toolMap (for example schema-only/event-defined handoff names or manually constructed ToolNodes).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7c074e9: the event-driven batch path now classifies calls before using the all-host fast path, so unknown handoff-shaped calls stay local even when directToolNames is empty. I added a regression for an event-driven ToolNode with no direct tool names, a real host tool, and a mistyped handoff that exists only in toolMap. Verified with NODE_OPTIONS='--experimental-vm-modules' npx jest src/specs/agent-handoffs.test.ts --runInBand, npx tsc --noEmit, npx eslint src/tools/ToolNode.ts src/specs/agent-handoffs.test.ts, and git diff --check.

@danny-avila
Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila merged commit bb75c67 into dev May 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant