@gajae-code/agent-core 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -2
- package/package.json +4 -4
- package/src/agent-loop.ts +71 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.12.
|
|
5
|
+
## [0.12.12] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
- DeepSeek-family reasoning-content replay 400s are now retryable via a bounded, strip-only circuit breaker. When a proxy strips the encrypted reasoning blob to an empty `encrypted_content`, DeepSeek rejects every follow-up turn with "The `reasoning_content` in the thinking mode must be passed back to the API." Resending the identical history re-triggers this deterministic 400, so the agent loop now strips the unusable `reasoning` items from the Responses history payload in place and resends exactly once (mirroring the `invalid_prompt` poisoned-history breaker). Non-reasoning items are preserved; fail-fast when nothing can be stripped. Budget = one repaired resend.
|
|
10
|
+
|
|
11
|
+
## [0.12.11] - 2026-08-03
|
|
12
|
+
|
|
13
|
+
## [0.12.10] - 2026-08-03
|
|
8
14
|
|
|
9
15
|
### Fixed
|
|
10
16
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.12",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"fmt": "biome format --write ."
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@gajae-code/ai": "0.12.
|
|
36
|
-
"@gajae-code/natives": "0.12.
|
|
37
|
-
"@gajae-code/utils": "0.12.
|
|
35
|
+
"@gajae-code/ai": "0.12.12",
|
|
36
|
+
"@gajae-code/natives": "0.12.12",
|
|
37
|
+
"@gajae-code/utils": "0.12.12",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -24,7 +24,12 @@ import {
|
|
|
24
24
|
COMPOSER_BASH_POLICY_RECOVERY_PROMPT,
|
|
25
25
|
isCurrentComposerBashPolicyBlockedError,
|
|
26
26
|
} from "@gajae-code/ai/providers/composer-discipline";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isInvalidPromptError,
|
|
29
|
+
isReasoningContentReplayError,
|
|
30
|
+
neutralizeReservedControlTokens,
|
|
31
|
+
stripUnusableReasoningItems,
|
|
32
|
+
} from "@gajae-code/ai/utils";
|
|
28
33
|
import { sanitizeText } from "@gajae-code/utils";
|
|
29
34
|
import type { AttemptScope } from "./attempt-scope";
|
|
30
35
|
import {
|
|
@@ -202,6 +207,31 @@ function repairInvalidPromptHistory(messages: AgentMessage[]): boolean {
|
|
|
202
207
|
}
|
|
203
208
|
return changed;
|
|
204
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Strip Responses-API `reasoning` items whose `encrypted_content` a proxy
|
|
212
|
+
* emptied, in-place across the outgoing history's `providerPayload`. DeepSeek in
|
|
213
|
+
* thinking mode rejects replay of reasoning whose encrypted blob was stripped,
|
|
214
|
+
* so dropping those items lets the model re-reason instead of re-triggering a
|
|
215
|
+
* deterministic 400 ("reasoning_content ... must be passed back to the API").
|
|
216
|
+
* Only the opaque Responses history payload is mutated; durable message content
|
|
217
|
+
* and ordering are preserved. Returns whether any item was actually removed —
|
|
218
|
+
* the circuit breaker uses this to decide between a single repaired resend
|
|
219
|
+
* (removed) and immediate fail-fast (unchanged).
|
|
220
|
+
*/
|
|
221
|
+
function repairReasoningContentReplayHistory(messages: AgentMessage[]): boolean {
|
|
222
|
+
let removed = 0;
|
|
223
|
+
for (const message of messages) {
|
|
224
|
+
const payload = (message as { providerPayload?: { type?: string; items?: Array<Record<string, unknown>> } })
|
|
225
|
+
.providerPayload;
|
|
226
|
+
if (payload?.type !== "openaiResponsesHistory" || !Array.isArray(payload.items)) continue;
|
|
227
|
+
const { result, removed: count } = stripUnusableReasoningItems(payload.items);
|
|
228
|
+
if (count > 0) {
|
|
229
|
+
payload.items = result;
|
|
230
|
+
removed += count;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return removed > 0;
|
|
234
|
+
}
|
|
205
235
|
|
|
206
236
|
function managedFailureOutcome(message: AssistantMessage, scope?: AttemptScope): ManagedAttemptOutcome {
|
|
207
237
|
return {
|
|
@@ -1418,6 +1448,9 @@ async function runLoopBody(
|
|
|
1418
1448
|
// Fires at most one repaired resend per run for the poisoned-history
|
|
1419
1449
|
// `invalid_prompt` circuit breaker below.
|
|
1420
1450
|
let invalidPromptRepairAttempted = false;
|
|
1451
|
+
// Fires at most one repaired resend per run for the reasoning-content replay
|
|
1452
|
+
// breaker below (DeepSeek "reasoning_content ... must be passed back").
|
|
1453
|
+
let reasoningContentRepairAttempted = false;
|
|
1421
1454
|
let previousMalformedToolSignatures = new Set<string>();
|
|
1422
1455
|
type SyntheticRecoveryKind = "malformed-tool-call" | "composer-bash-policy" | "provider";
|
|
1423
1456
|
let pendingRecovery:
|
|
@@ -1680,6 +1713,40 @@ async function runLoopBody(
|
|
|
1680
1713
|
continue;
|
|
1681
1714
|
}
|
|
1682
1715
|
}
|
|
1716
|
+
// Session-level reasoning-content replay circuit breaker (bounded,
|
|
1717
|
+
// strip-only). DeepSeek V4 (and reasoning-capable siblings on any
|
|
1718
|
+
// OpenAI-compatible proxy) reject every follow-up turn with
|
|
1719
|
+
// "reasoning_content ... must be passed back to the API" once a prior
|
|
1720
|
+
// assistant turn carried reasoning the proxy stripped to an empty
|
|
1721
|
+
// `encrypted_content`. Resending the identical history re-triggers the
|
|
1722
|
+
// deterministic 400, so naive auto-retry would loop. On the first such
|
|
1723
|
+
// rejection of this run, strip the unusable `reasoning` items from the
|
|
1724
|
+
// Responses history payload IN PLACE (never dropping text, tool-call, or
|
|
1725
|
+
// tool-output items). If that removed anything, resend exactly once so
|
|
1726
|
+
// the model re-reasons; if nothing could be stripped, fall through to
|
|
1727
|
+
// terminal handling and fail fast. Budget = one repaired resend.
|
|
1728
|
+
if (
|
|
1729
|
+
!config.fallbackManaged &&
|
|
1730
|
+
message.stopReason === "error" &&
|
|
1731
|
+
!reasoningContentRepairAttempted &&
|
|
1732
|
+
isReasoningContentReplayError(message)
|
|
1733
|
+
) {
|
|
1734
|
+
reasoningContentRepairAttempted = true;
|
|
1735
|
+
// The rejected turn was already committed to the context by the
|
|
1736
|
+
// streaming path. Repair (and resend) only the history that
|
|
1737
|
+
// preceded it: replaying an errored assistant turn re-triggers the
|
|
1738
|
+
// rejection and leaves a second assistant tail behind.
|
|
1739
|
+
const rejectedIndex = currentContext.messages.length - 1;
|
|
1740
|
+
const rejectedCommitted =
|
|
1741
|
+
rejectedIndex >= 0 && currentContext.messages[rejectedIndex]?.role === "assistant";
|
|
1742
|
+
const retained = rejectedCommitted
|
|
1743
|
+
? currentContext.messages.slice(0, rejectedIndex)
|
|
1744
|
+
: currentContext.messages;
|
|
1745
|
+
if (repairReasoningContentReplayHistory(retained)) {
|
|
1746
|
+
if (rejectedCommitted) currentContext.messages.splice(rejectedIndex, 1);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1683
1750
|
|
|
1684
1751
|
const overflow = managedContextOverflow(message, config);
|
|
1685
1752
|
if (config.fallbackManaged && overflow) {
|
|
@@ -2702,7 +2769,9 @@ async function executeToolCalls(
|
|
|
2702
2769
|
caughtError = e;
|
|
2703
2770
|
result = {
|
|
2704
2771
|
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
2705
|
-
details: {
|
|
2772
|
+
details: {
|
|
2773
|
+
failureKind: record.argumentValidationFailed ? "argument_validation" : "execution",
|
|
2774
|
+
},
|
|
2706
2775
|
};
|
|
2707
2776
|
isError = true;
|
|
2708
2777
|
}
|