Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/adapters/devin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,9 @@ function assistantText(message: OcxAssistantMessage): string {
* clients of the same service write #11 thinking with #12 signature and #18
* signature_type on the assistant prompt.
*
* The signature attests the thinking it was produced with, so a block without
* one contributes its text and nothing else rather than borrowing a neighbour's.
* The wire has room for only one thinking/signature pair. Preserve that
* association by replaying the last non-empty block as a unit rather than
* combining independently signed blocks.
*/
function assistantThinking(
message: OcxAssistantMessage,
Expand All @@ -276,13 +277,11 @@ function assistantThinking(
(part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
);
if (blocks.length === 0) return {};
const thinking = blocks.map(b => b.thinking).filter(Boolean).join("\n");
// Only one signature can ride the prompt, so take the last block that has
// one: that is the block the turn actually ended on.
const signature = blocks.filter(b => b.signature).at(-1)?.signature;
const block = blocks.findLast(b => Boolean(b.thinking || b.signature));

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 Select the last block with reasoning text

When a valid thinking/signature pair is followed by a signature-only block, this predicate selects the latter even though it has no replayable text. Such blocks are valid internal input: src/responses/parser.ts creates them for an envelope containing sig but no plaintext. For a reasoning-only assistant turn, mapOneMessage then drops the entire turn because reasoning.thinking is absent; otherwise it emits an unusable detached signature and loses the earlier valid pair. Search for the last block with non-empty thinking, then carry that same block's signature, as the newly documented contract states.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

if (!block) return {};
return {
...(thinking ? { thinking } : {}),
...(signature ? { signature } : {}),
...(block.thinking ? { thinking: block.thinking } : {}),
...(block.signature ? { signature: block.signature } : {}),
};
}

Expand Down
3 changes: 3 additions & 0 deletions structure/adapters/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Some adapters share another adapter's routed-tool semantics while retaining inde
adapter accepts them on return. One tool's canonical identity can be another tool's advertised
local name, and resolving that name to either owner would dispatch the call to a tool the caller
may not have named, so it is treated as ambiguous and fails before dispatch too.
Assistant reasoning replay likewise follows the Cognition wire shape: because one history prompt
carries only one thinking/signature pair, the adapter selects the final non-empty thinking block
as a unit and never combines text from one block with another block's signature.

There is no second Devin transport. An Agent Client Protocol adapter that spawned a local
`devin acp` child once existed under the `devin-cli` adapter id and was removed: the CLI's
Expand Down
1 change: 1 addition & 0 deletions structure/data-planes/inbound-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,4 @@ Modern `tool` images continue through the existing following-user carrier. These
an OpenCodex conversion limit, not a provider capability claim. Final Responses-to-adapter
admission follows the [registry contract](../adapters/registry.md#untranslated-input-media).
Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged.
Once normalized input reaches Devin, history reasoning follows the [single-pair replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance).
1 change: 1 addition & 0 deletions structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,4 @@ prose the model reads beside them. Vendor tool execution stays disabled on both
adapters, and Qoder's explicit refusal of original images is unchanged.

Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged.
Direct Devin history reasoning follows the [single-pair replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance), outside the translated Chat replay contract.
1 change: 1 addition & 0 deletions structure/providers/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ Translated Chat request construction uses the [inline-image budget](../transport

Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate.
Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged.
The separate Devin adapter's history reasoning follows its [single-pair replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance); Cursor replay is unchanged.
2 changes: 2 additions & 0 deletions structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,5 @@ Translated audio/file admission follows the [final-adapter input contract](adapt
The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Other invalid requests remain terminal.

Regression coverage: `tests/responses/responses-forward-prompt-envelope.test.ts`, `tests/routing/router-combo-failover-classification.test.ts`, and `tests/server/server-combo-failover-e2e.test.ts`.

Devin history reasoning preserves the [single-pair replay contract](adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance).
1 change: 1 addition & 0 deletions structure/transports/byte-accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ These optimizations do not add request queues, retry policies, or RSS-based admi

Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate.
Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged.
Devin history reasoning preserves the [single-pair replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance); this does not change request byte accounting.
1 change: 1 addition & 0 deletions structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ Renamed fixed-key providers receive [missing reasoning metadata](../catalog.md#r

Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate.
Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged.
The Devin transport preserves the [single-pair reasoning replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance) when constructing its Cognition prompt.
2 changes: 1 addition & 1 deletion structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate.
Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. Devin history reasoning preserves the [single-pair replay contract](../adapters/registry.md#semantic-inheritance-is-not-constructor-inheritance) after Responses parsing keeps independently signed blocks separate.
26 changes: 26 additions & 0 deletions tests/providers/devin-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,32 @@ describe("devin reasoning replay", () => {
expect(history.find(m => m.role === "assistant")?.thinking).toBe("only thought");
});

test("multiple thinking blocks keep the final block's text and signature together", () => {
const history = mapOcxMessagesToDevin(parsedWith([
{
role: "assistant",
content: [
{ type: "thinking", thinking: "first thought", signature: "sig-first" },
{ type: "thinking", thinking: "final thought", signature: "sig-final" },
],
},
]));
expect(history[0]?.thinking).toBe("final thought");
expect(history[0]?.signature).toBe("sig-final");

const unsignedLast = mapOcxMessagesToDevin(parsedWith([
{
role: "assistant",
content: [
{ type: "thinking", thinking: "signed thought", signature: "sig-signed" },
{ type: "thinking", thinking: "unsigned thought" },
],
},
]));
expect(unsignedLast[0]?.thinking).toBe("unsigned thought");
expect(unsignedLast[0]?.signature).toBeUndefined();
});

test("the encoded prompt carries thinking at #11 and its signature at #12", () => {
const req = buildGetChatMessageRequestForTests({
apiKey: "devin-session-token$x",
Expand Down
Loading