-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(devin): carry reasoning signatures across turns #6116
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
Closed
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c483502
fix(devin): carry reasoning signatures across turns
wtfsayo e61c7c3
fix(devin): never replay a Claude signature, and pin the signature-on…
wtfsayo e0b81e5
docs(structure): note the withheld Anthropic signature in Devin replay
wtfsayo ecd9eaa
fix(devin): replay Claude signatures and retry a refused turn without…
wtfsayo bfc56f5
fix(devin): retry a refused Claude turn even after it streamed reasoning
wtfsayo 073ef93
docs(structure): the Claude signature retry ignores reasoning-only ou…
wtfsayo 796b07f
fix(devin): hold the signed attempt's reasoning until its outcome is …
wtfsayo 37848f4
fix(devin): heartbeat while signed reasoning is held, and keep the re…
wtfsayo 78d62ed
fix(devin): report the refused attempt's usage before the retry starts
wtfsayo 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /** | ||
| * Devin reasoning signatures across turns. | ||
| * | ||
| * GetChatMessage returns the turn's reasoning attestation as `delta_signature` | ||
| * (#10) together with `delta_signature_type` (#21) in the same frame, and the | ||
| * native client replays both on the assistant prompt as #12 and #18. Measured | ||
| * live on swe-2-high the pair arrives AFTER the visible answer (reasoning, text, | ||
| * then signature), so the Responses layer stores it as its own signature-only | ||
| * reasoning item behind the thinking-text item. GPT and Gemini rows stream no | ||
| * thinking text at all, only the signature. | ||
| * | ||
| * The type is carried inside the stored signature because the reasoning | ||
| * envelope that round-trips through the client keeps a single signature string. | ||
| * A signature stored before this prefix existed replays without a type. | ||
| */ | ||
| import type { OcxAssistantMessage } from "../../types"; | ||
| import { isProviderIssuedThinkingSignature } from "../../responses/reasoning-envelope"; | ||
|
|
||
| const TYPED_SIGNATURE_PREFIX = "devin-sig1:"; | ||
|
|
||
| export function encodeDevinSignature(signature: string, signatureType: string | undefined): string { | ||
| return signatureType && !signatureType.includes(":") | ||
| ? `${TYPED_SIGNATURE_PREFIX}${signatureType}:${signature}` | ||
| : signature; | ||
| } | ||
|
|
||
| export function decodeDevinSignature(stored: string): { signature: string; signatureType?: string } { | ||
| if (!stored.startsWith(TYPED_SIGNATURE_PREFIX)) return { signature: stored }; | ||
| const rest = stored.slice(TYPED_SIGNATURE_PREFIX.length); | ||
| const colon = rest.indexOf(":"); | ||
| if (colon <= 0) return { signature: stored }; | ||
| return { signature: rest.slice(colon + 1), signatureType: rest.slice(0, colon) }; | ||
| } | ||
|
|
||
| /** | ||
| * The assistant turn's reasoning for ChatMessagePrompt #11/#12/#18. | ||
| * | ||
| * All of the turn's thinking text is replayed. A signature rides along only | ||
| * when it covers exactly that text: | ||
| * - one thinking block carrying its own issued signature (a stray | ||
| * signature-only block does not displace it); | ||
| * - at most one unsigned thinking block plus exactly one signature-only block, | ||
| * which is how a single Devin turn arrives once its late #10 frame has been | ||
| * split into its own reasoning item. With no thinking block at all this is a | ||
| * GPT or Gemini row, where the signature is the only reasoning there is. | ||
| * Any other mix (two signed blocks, a signed block beside unsigned text) has no | ||
| * single attestation for the joined text, so the turn is replayed unsigned. | ||
| * | ||
| * `withholdAnthropic` drops an Anthropic signature and keeps the text: the | ||
| * fallback for a Claude turn Cognition refused (see hasAnthropicSignature). | ||
| */ | ||
| export function devinAssistantReasoning( | ||
| message: OcxAssistantMessage, | ||
| modelId = "", | ||
| withholdAnthropic = false, | ||
| ): { thinking?: string; signature?: string; signature_type?: string } { | ||
| const blocks = message.content.filter( | ||
| (part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking", | ||
| ); | ||
| const textBlocks = blocks.filter(part => Boolean(part.thinking)); | ||
| const signatureOnly = blocks.filter(part => !part.thinking && isProviderIssuedThinkingSignature(part.signature)); | ||
| const text = textBlocks.map(part => part.thinking).join("\n"); | ||
| let stored: string | undefined; | ||
| if (textBlocks.length === 1 && isProviderIssuedThinkingSignature(textBlocks[0]!.signature)) { | ||
| stored = textBlocks[0]!.signature; | ||
| } else if (textBlocks.length <= 1 && signatureOnly.length === 1) { | ||
| stored = signatureOnly[0]!.signature; | ||
| } | ||
| let decoded = stored ? decodeDevinSignature(stored) : undefined; | ||
| if (decoded && withholdAnthropic && signatureTypeFor(decoded, modelId) === "anthropic") decoded = undefined; | ||
| return { | ||
| ...(text ? { thinking: text } : {}), | ||
| ...(decoded ? { signature: decoded.signature } : {}), | ||
| ...(decoded?.signatureType ? { signature_type: decoded.signatureType } : {}), | ||
| }; | ||
| } | ||
|
|
||
| /** A stored signature from before its type was recorded falls back to the model being called. */ | ||
| function signatureTypeFor(decoded: { signatureType?: string }, modelId: string): string | undefined { | ||
| return decoded.signatureType ?? (/claude/i.test(modelId) ? "anthropic" : undefined); | ||
| } | ||
|
|
||
| /** | ||
| * True when the mapped history replays a Claude signature. Cognition streams | ||
| * Claude's thinking as a summary while the signature covers the original, so | ||
| * the pair can fail validation: live on claude-opus-5-5 a signed replay of a | ||
| * visible-thinking turn was refused with `invalid_argument` in 5 of 6 tries and | ||
| * a text-only one in none, while a signed replay that is accepted is what lets | ||
| * the model recall its earlier reasoning. The adapter therefore sends the | ||
| * signature and retries a refusal once without it. | ||
| */ | ||
| export function hasAnthropicSignature(items: ReadonlyArray<{ signature?: string; signature_type?: string }>, modelId: string): boolean { | ||
| return items.some(item => Boolean(item.signature) && signatureTypeFor({ signatureType: item.signature_type }, modelId) === "anthropic"); | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.