fix(graph): prefer node tools before synthetic handoff routing - #90
Merged
Merged
Conversation
Handoff tools were crowding out real node tools because routing copy led with "select a transfer tool", descriptions reused the target agent's instructions, and the tool result implied the turn was already over. Co-authored-by: Cursor <cursoragent@cursor.com>
3 tasks
apucacao
approved these changes
Sep 16, 2026
andrewklatzke
added a commit
to launchdarkly/js-ai-sdk
that referenced
this pull request
Sep 16, 2026
## Summary When a graph node has multiple outgoing edges, `route()` augments the node's config with synthetic `__handoff_*` tools. In practice this crowded out the node's own tools — a node that reliably called its real tool standalone would call only a handoff tool inside a graph, so the response carried routing information instead of the tool's data. Three things in the routing augmentation pushed the model that way, all fixed here: - The appended routing directive led with "Select exactly one transfer tool to route to the next agent", making the transfer read as the task and the node's real work optional. It now asks the model to complete its task with its available tools first, and only then call exactly one transfer tool. - Handoff tool descriptions fell back to the *target* node's instructions verbatim, so `__handoff_<target>` advertised itself as the tool that does the target's work. Descriptions now always lead with `Transfer control to <key>.`, with any `handoff.description` or target-instructions text appended as trailing detail. - The handoff handler returned `Transferring to <key>`, which reads as though control had already left, so the model wrapped up instead of continuing. Selecting an edge only records the choice — execution continues until the model produces its final text — so the result now says the handoff is recorded and asks the model to finish its own work. Behavior is unchanged for nodes with zero or one outgoing edge, which never enter this path. Framework-native runners build their own `transfer_to_*` tools and are untouched. ## Test plan - [x] Existing client suite passes (30 tests, includes `src/__tests__/graph.test.ts`) - [x] Verified against a real multi-node graph whose root node was skipping its tool: the node's own tool now fires and the handoff still routes correctly - [ ] Reviewer sanity check on the routing copy — this is model-steering, so wording matters Note: the matching change for the Python SDK is launchdarkly/python-ai-sdk#90 Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a graph node has multiple outgoing edges,
route()augments the node's config with synthetic__handoff_*tools. In practice this crowded out the node's own tools — a node that reliably called its real tool standalone would call only a handoff tool inside a graph, so the response carried routing information instead of the tool's data.Three things in the routing augmentation pushed the model that way, all fixed here:
__handoff_<target>advertised itself as the tool that does the target's work. Descriptions now always lead withTransfer control to <key>., with anyhandoff.descriptionor target-instructions text appended as trailing detail.Transferring to <key>, which reads as though control had already left, so the model wrapped up instead of continuing. Selecting an edge only records the choice — execution continues until the model produces its final text — so the result now says the handoff is recorded and asks the model to finish its own work.Behavior is unchanged for nodes with zero or one outgoing edge, which never enter this path. Framework-native runners build their own
transfer_to_*tools and are untouched.Test plan
packages/client/tests/test_graph.py)Note: the matching change for the JS SDK is launchdarkly/js-ai-sdk#59