Repository navigation
fix(combos): keep tool loops on one target and make failover history safe (#7298) #7354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
c0d12c2
fix(combos): keep tool loops on one target and make failover history …
lidge-jun 94a770a
test(deepseek): expect the bare-turn reasoning placeholder on the str…
lidge-jun 4389ef4
Merge remote-tracking branch 'origin/dev' into fix/combo-continuation…
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * Combo target pinning for an in-flight tool loop (#7298). | ||
| * | ||
| * A combo picks a target per request. A tool loop is several requests that together form one | ||
| * assistant turn: the model emits a call, the client runs it, and the next request carries the | ||
| * result back. Re-picking between those requests hands the second half of a turn to a model that | ||
| * did not produce the first half. That is wasteful at best (a cold prompt cache, a different | ||
| * plan) and a hard failure at worst: DeepSeek's thinking mode refuses a tool continuation whose | ||
| * call carries no reasoning it can read, and a GPT turn never leaves one. | ||
| * | ||
| * So a continuation whose input ends in a tool output keeps the target that completed the lane's | ||
| * previous turn, as long as that target is still selectable right now: enabled, still in the | ||
| * combo, out of cooldown and passing every per-request eligibility filter the caller supplies. | ||
| * When it is not, normal selection (and normal failover) runs, and the strict serializers make | ||
| * the foreign history acceptable instead: see `backfillBareToolCallTurnReasoning` and the Chat | ||
| * adapter's reasoning placeholder. | ||
| * | ||
| * A new user message is never pinned; re-deciding per user turn is what a combo is for. | ||
| */ | ||
| import { pickComboTarget, targetKey, type ComboPick } from "../../combos"; | ||
| import type { NormalizedComboTarget } from "../../combos/types"; | ||
| import type { OcxConfig } from "../../types"; | ||
| import { recallComboTargetForLane } from "./combo-session-recall"; | ||
|
|
||
| export const COMBO_TOOL_LOOP_PIN_REASON = "combo-tool-loop-pin"; | ||
|
|
||
| function isToolOutputItem(item: unknown): boolean { | ||
| if (!item || typeof item !== "object" || Array.isArray(item)) return false; | ||
| const type = (item as { type?: unknown }).type; | ||
| return typeof type === "string" && type.endsWith("_output"); | ||
| } | ||
|
|
||
| /** | ||
| * Whether this request resumes an assistant turn by returning tool results: its Responses | ||
| * `input` (or Chat `messages`) ends with a tool output. | ||
| */ | ||
| export function isToolLoopContinuation(body: unknown): boolean { | ||
| if (!body || typeof body !== "object" || Array.isArray(body)) return false; | ||
| const { input, messages } = body as { input?: unknown; messages?: unknown }; | ||
| if (Array.isArray(input)) return input.length > 0 && isToolOutputItem(input[input.length - 1]); | ||
| if (Array.isArray(messages) && messages.length > 0) { | ||
| const last = messages[messages.length - 1]; | ||
| return !!last && typeof last === "object" && (last as { role?: unknown }).role === "tool"; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * The pick that keeps a tool loop on its previous target, or undefined to select normally. | ||
| * Uses a non-mutating preview so a pinned request never disturbs round-robin rotation state. | ||
| */ | ||
| export function toolLoopPinnedPick(args: { | ||
| config: OcxConfig; | ||
| comboId: string; | ||
| lane: string | undefined; | ||
| body: unknown; | ||
| eligible: (target: NormalizedComboTarget) => boolean; | ||
| now?: number; | ||
| }): ComboPick | undefined { | ||
| if (!isToolLoopContinuation(args.body)) return undefined; | ||
| const previous = recallComboTargetForLane(args.config, args.lane, args.comboId); | ||
| if (!previous) return undefined; | ||
| const previousKey = targetKey(previous); | ||
| const pick = pickComboTarget(args.config, args.comboId, { | ||
| eligible: target => targetKey(target) === previousKey && args.eligible(target), | ||
| now: args.now, | ||
| preview: true, | ||
| }); | ||
| return pick ? { ...pick, attempted: [previousKey] } : undefined; | ||
| } |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When reconstructed, resumed, or synthetic history orders a reasoning item after its
function_call, this marks the whole run as having reasoning and suppresses the placeholder, while leaving the call before its requiredreasoning_text. DeepSeek can therefore still reject the continuation with HTTP 400. This ordering is already documented as a supported input intests/providers/deepseek-reasoning-replay-gaps.test.tslines 157-161; track whether usable reasoning precedes the first client tool call, and move/backfill it at the turn head when it does not.Useful? React with 👍 / 👎.