@gajae-code/agent-core 0.12.8 → 0.12.10
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 -0
- package/dist/types/types.d.ts +7 -1
- package/package.json +4 -4
- package/src/agent-loop.ts +105 -36
- package/src/agent.ts +50 -0
- package/src/types.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.12.10] - 2026-08-03
|
|
6
|
+
|
|
7
|
+
## [0.12.9] - 2026-08-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Composer repository-file shell policy rejections now receive one bounded, tool-enabled recovery turn without persisting the synthetic instruction. Generic loops retain their repository tools with `toolChoice: auto`; Cursor remote turns continue only when native tools did not already recover, queued user follow-ups take priority, and a second policy block terminates instead of looping. Existing malformed-tool recovery remains tool-free and does not consume dynamic tool-choice state.
|
|
12
|
+
|
|
5
13
|
## [0.12.8] - 2026-08-02
|
|
6
14
|
|
|
7
15
|
## [0.12.7] - 2026-07-31
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Effort, ImageContent, Message, Model, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TransportFailureFacts, TSchema } from "@gajae-code/ai";
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Effort, ImageContent, Message, Model, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TransportFailureFacts, TSchema, UserMessage } from "@gajae-code/ai";
|
|
2
2
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
3
3
|
import type { AttemptMinter, AttemptRunHandle, AttemptScope } from "./attempt-scope";
|
|
4
4
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
@@ -292,6 +292,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
292
292
|
* continues with another turn.
|
|
293
293
|
*/
|
|
294
294
|
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
295
|
+
/**
|
|
296
|
+
* Supplies one bounded synthetic recovery instruction before the loop would
|
|
297
|
+
* otherwise yield. Unlike a follow-up, it is sent only to the provider and
|
|
298
|
+
* is not committed to durable agent message history.
|
|
299
|
+
*/
|
|
300
|
+
getSyntheticRecoveryMessage?: () => Promise<UserMessage | undefined>;
|
|
295
301
|
/**
|
|
296
302
|
* Cooperative pause checkpoint evaluated at safe loop boundaries.
|
|
297
303
|
*
|
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.10",
|
|
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.10",
|
|
36
|
+
"@gajae-code/natives": "0.12.10",
|
|
37
|
+
"@gajae-code/utils": "0.12.10",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
validateToolArguments,
|
|
21
21
|
zodToWireSchema,
|
|
22
22
|
} from "@gajae-code/ai";
|
|
23
|
+
import {
|
|
24
|
+
COMPOSER_BASH_POLICY_RECOVERY_PROMPT,
|
|
25
|
+
isCurrentComposerBashPolicyBlockedError,
|
|
26
|
+
} from "@gajae-code/ai/providers/composer-discipline";
|
|
23
27
|
import { isInvalidPromptError, neutralizeReservedControlTokens } from "@gajae-code/ai/utils";
|
|
24
28
|
import { sanitizeText } from "@gajae-code/utils";
|
|
25
29
|
import type { AttemptScope } from "./attempt-scope";
|
|
@@ -123,6 +127,15 @@ const standaloneOwnershipStates = new WeakMap<StandaloneRunOwnership, Standalone
|
|
|
123
127
|
* bounded too.
|
|
124
128
|
*/
|
|
125
129
|
const MAX_CONSECUTIVE_MALFORMED_TURNS = 5;
|
|
130
|
+
|
|
131
|
+
function isComposerBashPolicyBlockedToolResult(result: ToolResultMessage): boolean {
|
|
132
|
+
return (
|
|
133
|
+
result.isError &&
|
|
134
|
+
result.toolName === "bash" &&
|
|
135
|
+
result.content.some(content => content.type === "text" && isCurrentComposerBashPolicyBlockedError(content.text))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
function managedContextOverflow(message: AssistantMessage, config: AgentLoopConfig): boolean {
|
|
127
140
|
const transportFailure = managedTransportFailure(message);
|
|
128
141
|
// Managed empty-stop responses may be repaired by the managed shell below; only
|
|
@@ -150,12 +163,11 @@ function managedRetryableFailure(failure: unknown): boolean {
|
|
|
150
163
|
const facts = managedTransportFailure(failure);
|
|
151
164
|
if (!facts) return false;
|
|
152
165
|
const trigger = classifyFallbackTrigger(facts);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
166
|
+
// A plain `forbidden` is terminal: retrying it just re-sends a request the
|
|
167
|
+
// caller is not authorized to make, and the credential-mutating consumers
|
|
168
|
+
// downstream would block healthy credentials on the way.
|
|
169
|
+
if (trigger.class === "auth") return trigger.authDisposition !== "forbidden";
|
|
170
|
+
return trigger.class === "rate_limit" || trigger.class === "quota" || trigger.class === "server";
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
/**
|
|
@@ -1407,12 +1419,16 @@ async function runLoopBody(
|
|
|
1407
1419
|
// `invalid_prompt` circuit breaker below.
|
|
1408
1420
|
let invalidPromptRepairAttempted = false;
|
|
1409
1421
|
let previousMalformedToolSignatures = new Set<string>();
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1422
|
+
type SyntheticRecoveryKind = "malformed-tool-call" | "composer-bash-policy" | "provider";
|
|
1423
|
+
let pendingRecovery:
|
|
1424
|
+
| {
|
|
1425
|
+
kind: SyntheticRecoveryKind;
|
|
1426
|
+
inserted: boolean;
|
|
1427
|
+
syntheticMessage?: UserMessage;
|
|
1428
|
+
}
|
|
1429
|
+
| undefined;
|
|
1415
1430
|
let malformedToolRecoveryAttempted = false;
|
|
1431
|
+
let composerBashPolicyRecoveryAttempted = false;
|
|
1416
1432
|
// Deterministic terminal circuit breaker for argument-validation loops.
|
|
1417
1433
|
//
|
|
1418
1434
|
// Counts CONSECUTIVE turns whose tool calls were all malformed, regardless of
|
|
@@ -1507,6 +1523,8 @@ async function runLoopBody(
|
|
|
1507
1523
|
let recovered: HarmonyRecoveredToolCall | undefined;
|
|
1508
1524
|
let message: AssistantMessage;
|
|
1509
1525
|
const attemptTransaction = transaction;
|
|
1526
|
+
const recoveryAttempt = pendingRecovery;
|
|
1527
|
+
const wasMalformedToolRecoveryAttempt = recoveryAttempt?.kind === "malformed-tool-call";
|
|
1510
1528
|
try {
|
|
1511
1529
|
const attemptConfig = attemptTransaction
|
|
1512
1530
|
? {
|
|
@@ -1515,14 +1533,22 @@ async function runLoopBody(
|
|
|
1515
1533
|
attemptTransaction.stageAssistantMessageEvent(partial, event),
|
|
1516
1534
|
}
|
|
1517
1535
|
: config;
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1536
|
+
if (recoveryAttempt && !recoveryAttempt.inserted) {
|
|
1537
|
+
const recoveryContent =
|
|
1538
|
+
recoveryAttempt.kind === "composer-bash-policy"
|
|
1539
|
+
? COMPOSER_BASH_POLICY_RECOVERY_PROMPT
|
|
1540
|
+
: recoveryAttempt.kind === "malformed-tool-call"
|
|
1541
|
+
? repeatedToolFailureRecoveryPrompt
|
|
1542
|
+
: undefined;
|
|
1543
|
+
if (recoveryContent) {
|
|
1544
|
+
recoveryAttempt.syntheticMessage = {
|
|
1545
|
+
role: "user",
|
|
1546
|
+
content: recoveryContent,
|
|
1547
|
+
synthetic: true,
|
|
1548
|
+
timestamp: Date.now(),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
recoveryAttempt.inserted = true;
|
|
1526
1552
|
}
|
|
1527
1553
|
message = await streamAssistantResponse(
|
|
1528
1554
|
currentContext,
|
|
@@ -1535,8 +1561,12 @@ async function runLoopBody(
|
|
|
1535
1561
|
attemptScope,
|
|
1536
1562
|
streamFn,
|
|
1537
1563
|
harmonyRetryAttempt,
|
|
1538
|
-
|
|
1539
|
-
? {
|
|
1564
|
+
recoveryAttempt?.syntheticMessage
|
|
1565
|
+
? {
|
|
1566
|
+
syntheticMessage: recoveryAttempt.syntheticMessage,
|
|
1567
|
+
disableTools: wasMalformedToolRecoveryAttempt,
|
|
1568
|
+
forceAutoToolChoice: !wasMalformedToolRecoveryAttempt,
|
|
1569
|
+
}
|
|
1540
1570
|
: undefined,
|
|
1541
1571
|
);
|
|
1542
1572
|
const detection = detectHarmonyLeakInAssistantMessage(message);
|
|
@@ -1708,8 +1738,6 @@ async function runLoopBody(
|
|
|
1708
1738
|
if (config.fallbackManaged && message.stopReason !== "error" && message.stopReason !== "aborted") {
|
|
1709
1739
|
await config.onManagedAttemptAccepted?.();
|
|
1710
1740
|
}
|
|
1711
|
-
const wasRecoveryAttempt = recoveryState.pending;
|
|
1712
|
-
|
|
1713
1741
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
1714
1742
|
// Create placeholder tool results for any tool calls in the aborted message
|
|
1715
1743
|
// This maintains the tool_use/tool_result pairing that the API requires
|
|
@@ -1749,8 +1777,9 @@ async function runLoopBody(
|
|
|
1749
1777
|
|
|
1750
1778
|
const toolResults: ToolResultMessage[] = [];
|
|
1751
1779
|
let repeatedMalformedToolCall = false;
|
|
1780
|
+
let sawComposerBashPolicyBlock = false;
|
|
1752
1781
|
if (hasMoreToolCalls) {
|
|
1753
|
-
if (
|
|
1782
|
+
if (wasMalformedToolRecoveryAttempt) {
|
|
1754
1783
|
for (const toolCall of toolCalls) {
|
|
1755
1784
|
const result = createAbortedToolResult(
|
|
1756
1785
|
toolCall,
|
|
@@ -1781,6 +1810,7 @@ async function runLoopBody(
|
|
|
1781
1810
|
|
|
1782
1811
|
toolResults.push(...executionResult.toolResults);
|
|
1783
1812
|
steeringMessagesFromExecution = executionResult.steeringMessages;
|
|
1813
|
+
sawComposerBashPolicyBlock = executionResult.toolResults.some(isComposerBashPolicyBlockedToolResult);
|
|
1784
1814
|
|
|
1785
1815
|
const malformedSignatures = executionResult.malformedToolCallSignatures;
|
|
1786
1816
|
const allToolCallsMalformed =
|
|
@@ -1804,10 +1834,8 @@ async function runLoopBody(
|
|
|
1804
1834
|
}
|
|
1805
1835
|
}
|
|
1806
1836
|
|
|
1807
|
-
if (
|
|
1808
|
-
|
|
1809
|
-
recoveryState.inserted = false;
|
|
1810
|
-
recoveryState.syntheticMessage = undefined;
|
|
1837
|
+
if (recoveryAttempt) {
|
|
1838
|
+
pendingRecovery = undefined;
|
|
1811
1839
|
}
|
|
1812
1840
|
|
|
1813
1841
|
stream.push({ type: "turn_end", message, toolResults, scope: attemptScope });
|
|
@@ -1828,10 +1856,26 @@ async function runLoopBody(
|
|
|
1828
1856
|
stream.end(newMessages);
|
|
1829
1857
|
return;
|
|
1830
1858
|
}
|
|
1831
|
-
if (
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1859
|
+
if (sawComposerBashPolicyBlock && !composerBashPolicyRecoveryAttempted) {
|
|
1860
|
+
pendingRecovery = { kind: "composer-bash-policy", inserted: false };
|
|
1861
|
+
composerBashPolicyRecoveryAttempted = true;
|
|
1862
|
+
} else if (sawComposerBashPolicyBlock) {
|
|
1863
|
+
message.stopReason = "error";
|
|
1864
|
+
const recoveryLimitMessage =
|
|
1865
|
+
"Composer bash policy blocked repository file I/O again after its one automatic recovery turn. Continue with dedicated repository tools.";
|
|
1866
|
+
message.errorMessage = message.errorMessage
|
|
1867
|
+
? `${message.errorMessage} | ${recoveryLimitMessage}`
|
|
1868
|
+
: recoveryLimitMessage;
|
|
1869
|
+
publishAgentEnd(
|
|
1870
|
+
stream,
|
|
1871
|
+
config,
|
|
1872
|
+
buildAgentEndEvent(newMessages, telemetry, stepCounter.count, "completed", attemptScope),
|
|
1873
|
+
attemptScope,
|
|
1874
|
+
);
|
|
1875
|
+
stream.end(newMessages);
|
|
1876
|
+
return;
|
|
1877
|
+
} else if (repeatedMalformedToolCall && !malformedToolRecoveryAttempted) {
|
|
1878
|
+
pendingRecovery = { kind: "malformed-tool-call", inserted: false };
|
|
1835
1879
|
malformedToolRecoveryAttempted = true;
|
|
1836
1880
|
} else if (consecutiveMalformedTurns >= MAX_CONSECUTIVE_MALFORMED_TURNS) {
|
|
1837
1881
|
// Deterministic terminal circuit breaker. The one-shot recovery turn
|
|
@@ -1868,12 +1912,23 @@ async function runLoopBody(
|
|
|
1868
1912
|
stream.end(newMessages);
|
|
1869
1913
|
return;
|
|
1870
1914
|
}
|
|
1915
|
+
// Poll the consume-on-read recovery candidate before follow-ups so a real
|
|
1916
|
+
// user message can supersede it without letting the stale recovery resurface
|
|
1917
|
+
// after that follow-up turn.
|
|
1918
|
+
const syntheticRecoveryMessage = await config.getSyntheticRecoveryMessage?.();
|
|
1871
1919
|
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
1872
1920
|
if (followUpMessages.length > 0) {
|
|
1873
1921
|
// Set as pending so inner loop processes them
|
|
1874
1922
|
pendingMessages = followUpMessages;
|
|
1875
1923
|
continue;
|
|
1876
1924
|
}
|
|
1925
|
+
if (syntheticRecoveryMessage) {
|
|
1926
|
+
// Provider-side tool protocols (such as Cursor) can finish their remote
|
|
1927
|
+
// turn after a local policy rejection. Continue once without committing
|
|
1928
|
+
// the recovery instruction to durable history.
|
|
1929
|
+
pendingRecovery = { kind: "provider", inserted: true, syntheticMessage: syntheticRecoveryMessage };
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1877
1932
|
|
|
1878
1933
|
// No more messages, exit
|
|
1879
1934
|
break;
|
|
@@ -1920,7 +1975,11 @@ async function streamAssistantResponse(
|
|
|
1920
1975
|
scope?: AttemptScope,
|
|
1921
1976
|
streamFn?: StreamFn,
|
|
1922
1977
|
harmonyRetryAttempt = 0,
|
|
1923
|
-
recoveryMode?: {
|
|
1978
|
+
recoveryMode?: {
|
|
1979
|
+
syntheticMessage: UserMessage;
|
|
1980
|
+
disableTools?: boolean;
|
|
1981
|
+
forceAutoToolChoice?: boolean;
|
|
1982
|
+
},
|
|
1924
1983
|
): Promise<AssistantMessage> {
|
|
1925
1984
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
1926
1985
|
let messages = context.messages;
|
|
@@ -1951,7 +2010,11 @@ async function streamAssistantResponse(
|
|
|
1951
2010
|
await config.convertToLlm([recoveryMode.syntheticMessage]),
|
|
1952
2011
|
config.model,
|
|
1953
2012
|
);
|
|
1954
|
-
llmContext = {
|
|
2013
|
+
llmContext = {
|
|
2014
|
+
...llmContext,
|
|
2015
|
+
messages: [...llmContext.messages, ...syntheticMessages],
|
|
2016
|
+
tools: recoveryMode.disableTools ? [] : llmContext.tools,
|
|
2017
|
+
};
|
|
1955
2018
|
} else {
|
|
1956
2019
|
llmContext = {
|
|
1957
2020
|
...llmContext,
|
|
@@ -1959,7 +2022,7 @@ async function streamAssistantResponse(
|
|
|
1959
2022
|
await config.convertToLlm([...messages, recoveryMode.syntheticMessage]),
|
|
1960
2023
|
config.model,
|
|
1961
2024
|
),
|
|
1962
|
-
tools: [],
|
|
2025
|
+
tools: recoveryMode.disableTools ? [] : llmContext.tools,
|
|
1963
2026
|
};
|
|
1964
2027
|
}
|
|
1965
2028
|
}
|
|
@@ -1977,6 +2040,8 @@ async function streamAssistantResponse(
|
|
|
1977
2040
|
|
|
1978
2041
|
const resolvedMetadata = config.metadataResolver ? config.metadataResolver(config.model.provider) : config.metadata;
|
|
1979
2042
|
|
|
2043
|
+
// Synthetic recovery requests choose their tool mode explicitly below and
|
|
2044
|
+
// must never consume a queued dynamic choice intended for an ordinary turn.
|
|
1980
2045
|
const dynamicToolChoice = recoveryMode ? undefined : config.getToolChoice?.();
|
|
1981
2046
|
const dynamicReasoning = config.getReasoning?.();
|
|
1982
2047
|
const harmonyMitigationEnabled = isHarmonyLeakMitigationTarget(config.model);
|
|
@@ -1994,7 +2059,11 @@ async function streamAssistantResponse(
|
|
|
1994
2059
|
: AbortSignal.any(requestSignals);
|
|
1995
2060
|
const effectiveTemperature =
|
|
1996
2061
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
1997
|
-
const effectiveToolChoice = recoveryMode
|
|
2062
|
+
const effectiveToolChoice = recoveryMode?.disableTools
|
|
2063
|
+
? "none"
|
|
2064
|
+
: recoveryMode?.forceAutoToolChoice
|
|
2065
|
+
? "auto"
|
|
2066
|
+
: (dynamicToolChoice ?? config.toolChoice);
|
|
1998
2067
|
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
|
|
1999
2068
|
|
|
2000
2069
|
const chatStepNumber = stepCounter.count;
|
package/src/agent.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
type ToolChoice,
|
|
21
21
|
type ToolResultMessage,
|
|
22
22
|
} from "@gajae-code/ai";
|
|
23
|
+
import {
|
|
24
|
+
CURSOR_COMPOSER_BASH_POLICY_RECOVERY_PROMPT,
|
|
25
|
+
isCurrentComposerBashPolicyBlockedError,
|
|
26
|
+
} from "@gajae-code/ai/providers/composer-discipline";
|
|
23
27
|
import { extractHttpStatusFromError } from "@gajae-code/utils";
|
|
24
28
|
import { agentLoop, agentLoopContinue } from "./agent-loop";
|
|
25
29
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
@@ -67,6 +71,20 @@ function assertUserImagePlaceholdersHavePayload(messages: readonly AgentMessage[
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
const CURSOR_NATIVE_REPOSITORY_RECOVERY_TOOL_NAMES = new Set(["read", "grep", "search", "find", "write", "delete"]);
|
|
75
|
+
|
|
76
|
+
function isCursorComposerBashPolicyBlockedResult(message: ToolResultMessage): boolean {
|
|
77
|
+
return (
|
|
78
|
+
message.isError &&
|
|
79
|
+
message.toolName === "bash" &&
|
|
80
|
+
message.content.some(content => content.type === "text" && isCurrentComposerBashPolicyBlockedError(content.text))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSuccessfulCursorNativeRepositoryToolResult(message: ToolResultMessage): boolean {
|
|
85
|
+
return message.isError !== true && CURSOR_NATIVE_REPOSITORY_RECOVERY_TOOL_NAMES.has(message.toolName);
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
/**
|
|
71
89
|
* Whether persisted history ends at a point where a new model turn can resume.
|
|
72
90
|
* Assistant-ended histories require an in-memory queued message and are handled
|
|
@@ -1488,6 +1506,11 @@ export class Agent {
|
|
|
1488
1506
|
messages: this.#state.messages.slice(),
|
|
1489
1507
|
tools: this.#state.tools,
|
|
1490
1508
|
};
|
|
1509
|
+
// Cursor can execute native tools inside one remote turn, then return
|
|
1510
|
+
// `turnEnded` without another model request. Remember a Composer policy
|
|
1511
|
+
// rejection until the loop reaches that safe continuation boundary.
|
|
1512
|
+
let cursorComposerBashRecoveryPending = false;
|
|
1513
|
+
let cursorComposerBashRecoveryAttempted = false;
|
|
1491
1514
|
|
|
1492
1515
|
const cursorOnToolResult =
|
|
1493
1516
|
!fallbackManaged && (this.#cursorExecHandlers || this.#cursorOnToolResult)
|
|
@@ -1507,6 +1530,16 @@ export class Agent {
|
|
|
1507
1530
|
}
|
|
1508
1531
|
} catch {}
|
|
1509
1532
|
}
|
|
1533
|
+
if (isCursorComposerBashPolicyBlockedResult(finalMessage)) {
|
|
1534
|
+
cursorComposerBashRecoveryPending = true;
|
|
1535
|
+
} else if (
|
|
1536
|
+
cursorComposerBashRecoveryPending &&
|
|
1537
|
+
isSuccessfulCursorNativeRepositoryToolResult(finalMessage)
|
|
1538
|
+
) {
|
|
1539
|
+
// The same remote turn already replanned through a native tool,
|
|
1540
|
+
// so do not create a redundant local continuation afterward.
|
|
1541
|
+
cursorComposerBashRecoveryPending = false;
|
|
1542
|
+
}
|
|
1510
1543
|
// Cursor executes tools server-side during streaming, so the assistant message
|
|
1511
1544
|
// already incorporates results. We buffer here and emit in correct order
|
|
1512
1545
|
// when the assistant message ends.
|
|
@@ -1652,6 +1685,23 @@ export class Agent {
|
|
|
1652
1685
|
}
|
|
1653
1686
|
return queued;
|
|
1654
1687
|
},
|
|
1688
|
+
getSyntheticRecoveryMessage: async () => {
|
|
1689
|
+
if (
|
|
1690
|
+
this.#activeRunId !== runId ||
|
|
1691
|
+
!cursorComposerBashRecoveryPending ||
|
|
1692
|
+
cursorComposerBashRecoveryAttempted
|
|
1693
|
+
) {
|
|
1694
|
+
return undefined;
|
|
1695
|
+
}
|
|
1696
|
+
cursorComposerBashRecoveryPending = false;
|
|
1697
|
+
cursorComposerBashRecoveryAttempted = true;
|
|
1698
|
+
return {
|
|
1699
|
+
role: "user",
|
|
1700
|
+
content: CURSOR_COMPOSER_BASH_POLICY_RECOVERY_PROMPT,
|
|
1701
|
+
synthetic: true,
|
|
1702
|
+
timestamp: Date.now(),
|
|
1703
|
+
};
|
|
1704
|
+
},
|
|
1655
1705
|
onBeforeYield: async () => {
|
|
1656
1706
|
if (this.#activeRunId !== runId) return;
|
|
1657
1707
|
await this.#onBeforeYield?.();
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ToolResultMessage,
|
|
16
16
|
TransportFailureFacts,
|
|
17
17
|
TSchema,
|
|
18
|
+
UserMessage,
|
|
18
19
|
} from "@gajae-code/ai";
|
|
19
20
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
20
21
|
import type { AttemptMinter, AttemptRunHandle, AttemptScope } from "./attempt-scope";
|
|
@@ -319,6 +320,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
319
320
|
* continues with another turn.
|
|
320
321
|
*/
|
|
321
322
|
getFollowUpMessages?: () => Promise<AgentMessage[]>;
|
|
323
|
+
/**
|
|
324
|
+
* Supplies one bounded synthetic recovery instruction before the loop would
|
|
325
|
+
* otherwise yield. Unlike a follow-up, it is sent only to the provider and
|
|
326
|
+
* is not committed to durable agent message history.
|
|
327
|
+
*/
|
|
328
|
+
getSyntheticRecoveryMessage?: () => Promise<UserMessage | undefined>;
|
|
322
329
|
/**
|
|
323
330
|
* Cooperative pause checkpoint evaluated at safe loop boundaries.
|
|
324
331
|
*
|