@nexrall/code-core 1.4.12 → 1.4.14
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/dist/agent/loop.d.ts +13 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +204 -16
- package/dist/agent/memory.d.ts +45 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +243 -0
- package/dist/agent/skills.d.ts +35 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +253 -0
- package/dist/api/client.d.ts +33 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +559 -57
- package/dist/commands/loader.d.ts +7 -3
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +13 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +46 -23
- package/dist/types.d.ts +111 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +44 -0
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -102,4 +102,17 @@ export declare function compactMessagesForResume(messages: Message[], opts: {
|
|
|
102
102
|
onNotice?: (text: string) => void;
|
|
103
103
|
}): Promise<boolean>;
|
|
104
104
|
export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Cut a history back to the last point the API will accept as a resumable
|
|
107
|
+
* prefix, WITHOUT mutating the input.
|
|
108
|
+
*
|
|
109
|
+
* Anthropic rejects a conversation whose final assistant turn contains
|
|
110
|
+
* `tool_use` blocks that never got a matching `tool_result`. The main loop
|
|
111
|
+
* always appends in complete pairs so it can't produce that shape, but a crash
|
|
112
|
+
* in the middle of executing a tool round can: the assistant turn was already
|
|
113
|
+
* pushed, the tool_result carrier never was. Dropping that dangling assistant
|
|
114
|
+
* turn costs one model response and makes the rest of the (often very long)
|
|
115
|
+
* history resumable, instead of throwing all of it away.
|
|
116
|
+
*/
|
|
117
|
+
export declare function trimToResumableBoundary(messages: Message[]): Message[];
|
|
105
118
|
//# sourceMappingURL=loop.d.ts.map
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAyKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA+UD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsKD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+sBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAqCtE"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -45,15 +45,19 @@ exports.pruneOldToolResults = pruneOldToolResults;
|
|
|
45
45
|
exports.estimateTokensRough = estimateTokensRough;
|
|
46
46
|
exports.compactMessagesForResume = compactMessagesForResume;
|
|
47
47
|
exports.runAgentLoop = runAgentLoop;
|
|
48
|
+
exports.trimToResumableBoundary = trimToResumableBoundary;
|
|
49
|
+
const types_1 = require("../types");
|
|
48
50
|
const client_1 = require("../api/client");
|
|
49
51
|
const executor_1 = require("../tools/executor");
|
|
50
52
|
const agentTypes_1 = require("./agentTypes");
|
|
53
|
+
const skills_1 = require("./skills");
|
|
51
54
|
const rules_1 = require("../permissions/rules");
|
|
52
55
|
const sandbox_1 = require("../tools/sandbox");
|
|
53
56
|
const index_1 = require("../plugins/index");
|
|
54
57
|
const testIntegrity_1 = require("./testIntegrity");
|
|
55
58
|
const flaky_1 = require("./flaky");
|
|
56
59
|
const claimEvidence_1 = require("./claimEvidence");
|
|
60
|
+
const memory_1 = require("./memory");
|
|
57
61
|
const fs = __importStar(require("fs"));
|
|
58
62
|
const path = __importStar(require("path"));
|
|
59
63
|
const child_process_1 = require("child_process");
|
|
@@ -299,6 +303,8 @@ function humanDescription(name, input) {
|
|
|
299
303
|
const preview = typeof input.prompt === 'string' ? input.prompt.slice(0, 60) : '';
|
|
300
304
|
return `Sub-task: ${desc || preview}${!desc && preview.length === 60 ? '…' : ''}`;
|
|
301
305
|
}
|
|
306
|
+
case 'use_skill':
|
|
307
|
+
return `Use skill: /${input.name ?? '(unknown)'}`;
|
|
302
308
|
case 'get_diagnostics':
|
|
303
309
|
return input.path ? `Get diagnostics: ${input.path}` : 'Get workspace diagnostics';
|
|
304
310
|
case 'go_to_definition':
|
|
@@ -362,6 +368,22 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
362
368
|
nexrallMd: subNexrallMd,
|
|
363
369
|
requestPermission: gatedPermission,
|
|
364
370
|
onText: () => { }, // sub-agent text is returned as the tool result, not streamed live
|
|
371
|
+
// Forwarded DELIBERATELY, and it must be a real handler rather than a no-op.
|
|
372
|
+
//
|
|
373
|
+
// A sub-agent streams no text (onText above is a no-op) but it DOES stream thinking
|
|
374
|
+
// through the parent's UI (see onThinking/onThinkingDelta below), and thinking sets
|
|
375
|
+
// `emittedToCaller`. So a sub-agent stream that dies after reasoning genuinely has
|
|
376
|
+
// rendered output to discard — a no-op here would let the restart proceed and then
|
|
377
|
+
// re-stream that reasoning on top of the copy still on screen, the exact duplication
|
|
378
|
+
// the opt-in exists to prevent.
|
|
379
|
+
//
|
|
380
|
+
// Forwarding is safe because the parent's own output is already closed by this
|
|
381
|
+
// point: dispatching the `task` tool goes through options.onToolUse, which finalizes
|
|
382
|
+
// the parent's bubble (VS Code) / flushes the renderer (CLI) before the sub-agent
|
|
383
|
+
// starts. The only live, discardable element at restart time is the sub-agent's own
|
|
384
|
+
// thinking block. Completed tool rows are left alone — those are real side effects
|
|
385
|
+
// that actually happened.
|
|
386
|
+
onStreamRestart: (reason, chars) => options.onStreamRestart?.(reason, chars),
|
|
365
387
|
// Forward tool events with isSubTask=true so the UI can render a badge
|
|
366
388
|
// instead of prepending "[sub-task]" to the tool name (which caused double-prefix
|
|
367
389
|
// when the name was already labelled, and mixed display concerns into the data layer).
|
|
@@ -798,6 +820,11 @@ async function autoCompactMessages(messages, options, ledger) {
|
|
|
798
820
|
env: options.env,
|
|
799
821
|
clientType: options.clientType,
|
|
800
822
|
abortSignal: options.abortSignal,
|
|
823
|
+
// The summariser renders NOTHING (onEvent below is a no-op) and its result is
|
|
824
|
+
// read only from the returned message, so a restart has nothing to roll back —
|
|
825
|
+
// always safe. Worth enabling: a blip here used to abandon compaction entirely,
|
|
826
|
+
// which then let the very next turn hit the context wall it was meant to prevent.
|
|
827
|
+
allowRestartAfterRender: true,
|
|
801
828
|
}, () => { });
|
|
802
829
|
summary = reply.content
|
|
803
830
|
.filter((b) => b.type === 'text')
|
|
@@ -831,6 +858,45 @@ async function autoCompactMessages(messages, options, ledger) {
|
|
|
831
858
|
void kept;
|
|
832
859
|
return true;
|
|
833
860
|
}
|
|
861
|
+
// ─── Periodic memory compaction ────────────────────────────────────────────
|
|
862
|
+
// A memory file (project or global — see agent/memory.ts) can grow large over
|
|
863
|
+
// many sessions since memory_write only ever appends. writeMemory() already
|
|
864
|
+
// applies an immediate, synchronous byte-cap eviction (oldest entries dropped)
|
|
865
|
+
// as a hard backstop, but that's a blunt instrument — this periodically
|
|
866
|
+
// consolidates the file with a real LLM summarization pass instead, so old
|
|
867
|
+
// facts are condensed into fewer, denser bullets rather than silently lost.
|
|
868
|
+
// Checked opportunistically right after a successful memory_write (see the
|
|
869
|
+
// call site below) rather than on every tool call — cheap to check (a single
|
|
870
|
+
// file stat), and memory_write is the only thing that can push a file over
|
|
871
|
+
// the trigger threshold in the first place.
|
|
872
|
+
async function maybeCompactMemory(scope, options) {
|
|
873
|
+
try {
|
|
874
|
+
await (0, memory_1.compactMemoryIfNeeded)(scope, options.workDir, async (prompt) => {
|
|
875
|
+
const reply = await (0, client_1.streamChat)([{ role: 'user', content: [{ type: 'text', text: prompt }] }], {
|
|
876
|
+
// Same reasoning as autoCompactMessages' summariser: a mechanical
|
|
877
|
+
// consolidation task, always run on the cheapest 1M-context tier
|
|
878
|
+
// regardless of the user's chosen model for the actual conversation.
|
|
879
|
+
model: 'turbo',
|
|
880
|
+
mode: 'ask',
|
|
881
|
+
env: options.env,
|
|
882
|
+
clientType: options.clientType,
|
|
883
|
+
abortSignal: options.abortSignal,
|
|
884
|
+
// Same as the transcript summariser: no rendered output, result read only from
|
|
885
|
+
// the returned message, so restarting on a blip is always safe.
|
|
886
|
+
allowRestartAfterRender: true,
|
|
887
|
+
}, () => { });
|
|
888
|
+
return reply.content
|
|
889
|
+
.filter((b) => b.type === 'text')
|
|
890
|
+
.map((b) => b.text ?? '')
|
|
891
|
+
.join('');
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
// Best-effort — a failed/aborted compaction just means the file stays as-is
|
|
896
|
+
// until the next memory_write call tries again; writeMemory's synchronous
|
|
897
|
+
// byte cap already bounds worst-case growth in the meantime.
|
|
898
|
+
}
|
|
899
|
+
}
|
|
834
900
|
// ─── Resume-time proactive compaction ─────────────────────────────────────────
|
|
835
901
|
//
|
|
836
902
|
// The in-loop auto-compact above only reacts to `lastPromptTokens`, which is
|
|
@@ -940,6 +1006,11 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
940
1006
|
// catalogue (sub-agents can't spawn further), but every level resolves types.
|
|
941
1007
|
const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
|
|
942
1008
|
const agentsCatalogue = depth === 0 ? (0, agentTypes_1.summariseAgents)(agentTypes) : '';
|
|
1009
|
+
// Skills catalogue — unlike agentsCatalogue, available at every depth: a skill is
|
|
1010
|
+
// just a reusable prompt template (via use_skill), not another spawn point, so
|
|
1011
|
+
// sub-agents benefit from the same playbooks without the recursion concerns that
|
|
1012
|
+
// gate agentsCatalogue to the top level.
|
|
1013
|
+
const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
|
|
943
1014
|
// Optional OS-level bash sandbox (opt-in via settings.json "sandbox").
|
|
944
1015
|
const settings = (0, rules_1.loadSettings)(options.workDir);
|
|
945
1016
|
const sandboxCfg = (0, sandbox_1.parseSandboxConfig)(settings.raw.sandbox) ?? undefined;
|
|
@@ -965,6 +1036,11 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
965
1036
|
// iteration backstop, so we can tell the user what happened rather than
|
|
966
1037
|
// silently returning a half-done task.
|
|
967
1038
|
let completedCleanly = false;
|
|
1039
|
+
// Tool rounds actually completed in this call. Counted explicitly rather than derived
|
|
1040
|
+
// from `messages.length` because auto-compaction splices the history SHORTER mid-run:
|
|
1041
|
+
// a long session that compacted and then lost its connection would show a negative
|
|
1042
|
+
// length delta and be judged "no progress", discarding the work it most needs to keep.
|
|
1043
|
+
let completedRounds = 0;
|
|
968
1044
|
let stalledOut = false; // tripped the runaway guard (all-failed rounds)
|
|
969
1045
|
let consecutiveErrorRounds = 0; // rounds where every tool call errored
|
|
970
1046
|
let budget = maxIterations; // extended by auto-continue, capped at hardCap
|
|
@@ -1065,8 +1141,6 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1065
1141
|
if (!autoContinue && iteration === budget - 5 && budget > 5) {
|
|
1066
1142
|
options.onText(`\n⚠️ Approaching the ${budget}-step limit (step ${iteration + 1}). Please wrap up and summarise what has been done.\n`);
|
|
1067
1143
|
}
|
|
1068
|
-
// Collect tool_use blocks emitted during this stream
|
|
1069
|
-
const pendingToolUse = [];
|
|
1070
1144
|
// Build SSE event handler
|
|
1071
1145
|
const onEvent = (event) => {
|
|
1072
1146
|
switch (event.type) {
|
|
@@ -1083,21 +1157,33 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1083
1157
|
options.onThinkingProgress?.(event.tokens);
|
|
1084
1158
|
break;
|
|
1085
1159
|
case 'tool_use':
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
input: event.input,
|
|
1093
|
-
});
|
|
1160
|
+
// Nothing to do here. Streamed tool_use events are a PREVIEW only (their
|
|
1161
|
+
// `input` can still be partial JSON mid-stream); the blocks actually
|
|
1162
|
+
// executed are read from the completed message's content at the end of
|
|
1163
|
+
// this iteration. An accumulator here used to exist but was never read —
|
|
1164
|
+
// keeping it invited the assumption that dispatch happens from the stream,
|
|
1165
|
+
// which would break the restart guarantee below.
|
|
1094
1166
|
break;
|
|
1095
1167
|
case 'usage':
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1168
|
+
// A `partial` report belongs to an attempt that was cut short and restarted.
|
|
1169
|
+
// Its token counts are real spend (already billed upstream) and are forwarded
|
|
1170
|
+
// so a cost display stays honest — but they must NOT drive `lastPromptTokens`,
|
|
1171
|
+
// which feeds the auto-compaction trigger: a truncated attempt understates the
|
|
1172
|
+
// prompt size, and letting it overwrite a correct reading would suppress a
|
|
1173
|
+
// compaction the next turn actually needs.
|
|
1174
|
+
if (!event.partial) {
|
|
1175
|
+
// A `replayed` report DOES update this: it describes the real, complete turn
|
|
1176
|
+
// (served from the backend's idempotency cache after a dropped connection),
|
|
1177
|
+
// so its prompt size is accurate and needed for the compaction decision.
|
|
1178
|
+
// Only `partial` — a truncated attempt — must be excluded.
|
|
1179
|
+
lastPromptTokens =
|
|
1180
|
+
(event.usage.input_tokens ?? 0) +
|
|
1181
|
+
(event.usage.cache_creation_input_tokens ?? 0) +
|
|
1182
|
+
(event.usage.cache_read_input_tokens ?? 0);
|
|
1183
|
+
}
|
|
1184
|
+
// Both flags mean "do not add these tokens to a running cost total": `partial`
|
|
1185
|
+
// was a discarded attempt, `replayed` was already counted when first billed.
|
|
1186
|
+
options.onUsage(event.usage, event.partial === true || event.replayed === true);
|
|
1101
1187
|
break;
|
|
1102
1188
|
case 'message_complete':
|
|
1103
1189
|
// The complete message is returned from streamChat, we handle it below
|
|
@@ -1108,11 +1194,29 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1108
1194
|
case 'retry_resolved':
|
|
1109
1195
|
options.onRetryResolved?.();
|
|
1110
1196
|
break;
|
|
1197
|
+
case 'stream_restart':
|
|
1198
|
+
// The attempt that died had already rendered text/thinking; the replacement
|
|
1199
|
+
// attempt re-sends the turn in full, so the caller must drop that partial
|
|
1200
|
+
// render or it shows up twice.
|
|
1201
|
+
//
|
|
1202
|
+
// Nothing else needs undoing, and that is a property of the code below, not
|
|
1203
|
+
// an assumption: tools are dispatched from `assistantMessage.content` AFTER
|
|
1204
|
+
// streamChat() returns, and a restarted attempt threw instead of returning.
|
|
1205
|
+
// So no tool ran, no file changed, no checkpoint or hook fired.
|
|
1206
|
+
options.onStreamRestart?.(event.reason, event.discardedChars);
|
|
1207
|
+
break;
|
|
1111
1208
|
case 'balance_status':
|
|
1112
1209
|
options.onBalanceStatus?.(event.balance, event.zero);
|
|
1113
1210
|
break;
|
|
1114
1211
|
case 'done':
|
|
1212
|
+
break;
|
|
1115
1213
|
case 'error':
|
|
1214
|
+
// Reaching the loop means the stream did NOT reject on this — i.e. the turn's
|
|
1215
|
+
// message had already been delivered and the error arrived late (typically the
|
|
1216
|
+
// backend's post-turn billing write failing). The answer stands, so this must
|
|
1217
|
+
// not fail the turn, but it must not vanish either: route it to the system-
|
|
1218
|
+
// notice channel so it is visible and logged rather than silently swallowed.
|
|
1219
|
+
options.onNotice?.(`⚠️ ${event.message}`);
|
|
1116
1220
|
break;
|
|
1117
1221
|
}
|
|
1118
1222
|
};
|
|
@@ -1130,6 +1234,12 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1130
1234
|
abortSignal: options.abortSignal,
|
|
1131
1235
|
extraTools: options.mcpManager?.getAnthropicTools(),
|
|
1132
1236
|
agents: agentsCatalogue || undefined,
|
|
1237
|
+
skills: skillsCatalogue || undefined,
|
|
1238
|
+
// Only allow a post-render restart when the caller actually implements the
|
|
1239
|
+
// rollback. Without a handler the partial output can't be un-rendered, so we
|
|
1240
|
+
// keep the old conservative behaviour (fail the turn) rather than duplicate
|
|
1241
|
+
// text on screen.
|
|
1242
|
+
allowRestartAfterRender: !!options.onStreamRestart,
|
|
1133
1243
|
}, onEvent);
|
|
1134
1244
|
}
|
|
1135
1245
|
catch (err) {
|
|
@@ -1146,7 +1256,14 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1146
1256
|
break;
|
|
1147
1257
|
}
|
|
1148
1258
|
runSimpleHooks(hooks.OnError, options.workDir);
|
|
1149
|
-
|
|
1259
|
+
// Carry the work already done in this turn out with the error. The loop
|
|
1260
|
+
// owns a COPY of the caller's history, so a plain throw would strand every
|
|
1261
|
+
// completed tool round inside this function and the caller would fall back
|
|
1262
|
+
// to a history containing only the original user message — the user pays
|
|
1263
|
+
// for N minutes of agent work and then has to start over because
|
|
1264
|
+
// "continue" remembers none of it. `messages` always ends on a complete
|
|
1265
|
+
// assistant→tool_result pair here, so it is valid to resend as-is.
|
|
1266
|
+
throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
|
|
1150
1267
|
}
|
|
1151
1268
|
if (options.abortSignal?.aborted)
|
|
1152
1269
|
break;
|
|
@@ -1393,6 +1510,14 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1393
1510
|
catch (err) {
|
|
1394
1511
|
result = { error: `Tool execution failed: ${err.message}` };
|
|
1395
1512
|
}
|
|
1513
|
+
// Opportunistic memory compaction — memory_write is the only tool that can
|
|
1514
|
+
// grow a memory file, so this is the cheapest point to check whether it just
|
|
1515
|
+
// crossed the compaction threshold. Fire-and-forget: never blocks the turn,
|
|
1516
|
+
// never surfaces its own errors to the model (see maybeCompactMemory).
|
|
1517
|
+
if (name === 'memory_write' && result.error === undefined) {
|
|
1518
|
+
const scope = input.scope === 'global' ? 'global' : 'project';
|
|
1519
|
+
void maybeCompactMemory(scope, options);
|
|
1520
|
+
}
|
|
1396
1521
|
// PostToolUse can inject context for the model or flag a problem.
|
|
1397
1522
|
const post = runToolHooks(hooks.PostToolUse, 'PostToolUse', name, input, options.workDir, result);
|
|
1398
1523
|
const injected = [pre.context, post.context].filter(Boolean).join('\n');
|
|
@@ -1470,6 +1595,9 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1470
1595
|
content: toolResultContent,
|
|
1471
1596
|
};
|
|
1472
1597
|
messages.push(toolResultMessage);
|
|
1598
|
+
// A complete assistant→tool_result pair just landed: the history is valid to
|
|
1599
|
+
// resend from here, so this is exactly one unit of salvageable progress.
|
|
1600
|
+
completedRounds++;
|
|
1473
1601
|
// Incremental persistence hook: history is valid here (ends on a
|
|
1474
1602
|
// tool_result user turn). Let the caller checkpoint progress so a crash
|
|
1475
1603
|
// mid-run loses only the in-flight step, not the whole session.
|
|
@@ -1510,9 +1638,69 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1510
1638
|
}
|
|
1511
1639
|
}
|
|
1512
1640
|
}
|
|
1641
|
+
catch (err) {
|
|
1642
|
+
// Every other failure path (a tool executor blowing up, a hook throwing, an
|
|
1643
|
+
// out-of-memory in a large read) gets the same treatment as a dead stream:
|
|
1644
|
+
// the turn's completed work leaves with the error instead of dying inside
|
|
1645
|
+
// this function's private copy of the history. Already-tagged errors pass
|
|
1646
|
+
// straight through so we don't double-wrap and lose the original message.
|
|
1647
|
+
if (err instanceof types_1.AgentTurnError)
|
|
1648
|
+
throw err;
|
|
1649
|
+
if (options.abortSignal?.aborted || err.name === 'AbortError')
|
|
1650
|
+
throw err;
|
|
1651
|
+
throw new types_1.AgentTurnError(err?.message || 'The turn ended unexpectedly', trimToResumableBoundary(messages), completedRounds, err);
|
|
1652
|
+
}
|
|
1513
1653
|
finally {
|
|
1514
1654
|
runSimpleHooks(hooks.OnStop, options.workDir);
|
|
1515
1655
|
}
|
|
1516
1656
|
return messages;
|
|
1517
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Cut a history back to the last point the API will accept as a resumable
|
|
1660
|
+
* prefix, WITHOUT mutating the input.
|
|
1661
|
+
*
|
|
1662
|
+
* Anthropic rejects a conversation whose final assistant turn contains
|
|
1663
|
+
* `tool_use` blocks that never got a matching `tool_result`. The main loop
|
|
1664
|
+
* always appends in complete pairs so it can't produce that shape, but a crash
|
|
1665
|
+
* in the middle of executing a tool round can: the assistant turn was already
|
|
1666
|
+
* pushed, the tool_result carrier never was. Dropping that dangling assistant
|
|
1667
|
+
* turn costs one model response and makes the rest of the (often very long)
|
|
1668
|
+
* history resumable, instead of throwing all of it away.
|
|
1669
|
+
*/
|
|
1670
|
+
function trimToResumableBoundary(messages) {
|
|
1671
|
+
// A trailing assistant turn is unresumable if it invoked ANY tool whose result is
|
|
1672
|
+
// missing. That includes server-side invocations (`server_tool_use`, produced by
|
|
1673
|
+
// web search / tool search), which carry a DIFFERENT block type than a client tool
|
|
1674
|
+
// call: matching only 'tool_use' would let a dangling web-search turn through, and
|
|
1675
|
+
// it would 400 on resend exactly like a dangling client call. `chooseFinalContent`
|
|
1676
|
+
// in api/client.ts deliberately preserves these blocks verbatim, so they really do
|
|
1677
|
+
// reach this function.
|
|
1678
|
+
const isUnansweredInvocation = (m) => {
|
|
1679
|
+
if (m?.role !== 'assistant' || !Array.isArray(m.content))
|
|
1680
|
+
return false;
|
|
1681
|
+
const blocks = m.content;
|
|
1682
|
+
// A client tool_use is answered by a tool_result in the NEXT (user) message, which
|
|
1683
|
+
// by definition doesn't exist if this is the last turn — so any of them is dangling.
|
|
1684
|
+
if (blocks.some((b) => b?.type === 'tool_use'))
|
|
1685
|
+
return true;
|
|
1686
|
+
// Server-side invocations are different: Anthropic pairs each `server_tool_use` with
|
|
1687
|
+
// its `*_tool_result` inside this SAME message, so a completed search turn is
|
|
1688
|
+
// perfectly resumable and must not be discarded.
|
|
1689
|
+
//
|
|
1690
|
+
// Pair by id rather than asking "is there any result block at all". A turn cut off
|
|
1691
|
+
// after two searches where only the first returned would otherwise look complete and
|
|
1692
|
+
// 400 on resend — exactly the mid-turn-death case this helper exists for. Matching on
|
|
1693
|
+
// the `_tool_result` suffix also covers result types beyond web/tool search
|
|
1694
|
+
// (code_execution, mcp) without needing to enumerate them.
|
|
1695
|
+
const answeredIds = new Set(blocks.filter((b) => /_tool_result$/.test(b?.type ?? '')).map((b) => b?.tool_use_id));
|
|
1696
|
+
return blocks.some((b) => b?.type === 'server_tool_use' && !answeredIds.has(b?.id));
|
|
1697
|
+
};
|
|
1698
|
+
// Loop rather than a single check: this is an exported helper that accepts any
|
|
1699
|
+
// history, not only the strictly-paired ones the agent loop builds, so consecutive
|
|
1700
|
+
// dangling assistant turns must all be removed for the result to be resendable.
|
|
1701
|
+
let end = messages.length;
|
|
1702
|
+
while (end > 0 && isUnansweredInvocation(messages[end - 1]))
|
|
1703
|
+
end--;
|
|
1704
|
+
return end === messages.length ? messages : messages.slice(0, end);
|
|
1705
|
+
}
|
|
1518
1706
|
//# sourceMappingURL=loop.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type MemoryScope = 'project' | 'global';
|
|
2
|
+
/** Per-workDir file, keyed the same way CheckpointManager keys its store
|
|
3
|
+
* (sha1 of the resolved absolute path) so two differently-named checkouts of
|
|
4
|
+
* the same repo don't collide, and the same repo always maps to the same file
|
|
5
|
+
* regardless of cwd casing/trailing-slash quirks. */
|
|
6
|
+
export declare function memoryFilePath(scope: MemoryScope, workDir?: string): string;
|
|
7
|
+
export declare const MEMORY_ENTRY_MAX_CHARS = 400;
|
|
8
|
+
export declare const MEMORY_MAX_BYTES = 12000;
|
|
9
|
+
export declare const MEMORY_COMPACT_TRIGGER_BYTES = 16000;
|
|
10
|
+
export declare const MEMORY_HARD_CAP_BYTES: number;
|
|
11
|
+
export interface MemoryWriteResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
already: boolean;
|
|
14
|
+
scope: MemoryScope;
|
|
15
|
+
file: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function writeMemory(content: string, scope: MemoryScope, workDir?: string): Promise<MemoryWriteResult>;
|
|
18
|
+
export declare function readMemory(scope: MemoryScope, workDir?: string): string;
|
|
19
|
+
/** Merge BOTH scopes for prompt injection — project-specific first (most
|
|
20
|
+
* relevant to what the agent is doing right now), global preferences after. */
|
|
21
|
+
export declare function readAllMemory(workDir?: string): string;
|
|
22
|
+
export declare function clearMemory(scope: MemoryScope, workDir?: string): void;
|
|
23
|
+
export declare function memoryStats(scope: MemoryScope, workDir?: string): {
|
|
24
|
+
file: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
entries: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Summarize the OLDEST half of a memory file into a few consolidated bullets via
|
|
30
|
+
* an LLM call, keeping the most recent entries verbatim — same "compact, don't
|
|
31
|
+
* silently drop" instinct as the conversation-history auto-compactor, applied to
|
|
32
|
+
* memory instead of a transcript. This is the EXPECTED bounding mechanism for
|
|
33
|
+
* memory file size — called opportunistically (fire-and-forget) right after a
|
|
34
|
+
* successful memory_write (see maybeCompactMemory in loop.ts) once a file crosses
|
|
35
|
+
* MEMORY_COMPACT_TRIGGER_BYTES. writeMemory's own emergency synchronous eviction
|
|
36
|
+
* (MEMORY_HARD_CAP_BYTES) sits well above this threshold specifically so this
|
|
37
|
+
* summarizing pass gets the chance to run first in the common case; the blunt
|
|
38
|
+
* eviction is a last resort for if compaction has been failing/unavailable.
|
|
39
|
+
*
|
|
40
|
+
* `summarize` is injected (rather than importing streamChat directly) to avoid
|
|
41
|
+
* pulling api/client.ts's network/auth machinery into every consumer of this
|
|
42
|
+
* module and to keep this file trivially unit-testable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function compactMemoryIfNeeded(scope: MemoryScope, workDir: string | undefined, summarize: (prompt: string) => Promise<string>): Promise<boolean>;
|
|
45
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/agent/memory.ts"],"names":[],"mappings":"AAiCA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE/C;;;sDAGsD;AACtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3E;AAED,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAMnD,eAAO,MAAM,qBAAqB,QAAmC,CAAC;AA4CtE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuBnH;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;gFACgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAGtE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAKlH;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAC7C,OAAO,CAAC,OAAO,CAAC,CA+BlB"}
|