@cryer/star-cli 0.3.13 → 0.3.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/README.md +17 -5
- package/README.zh-CN.md +15 -5
- package/dist/{chunk-6PLZ3FLG.js → chunk-2ISBRSBA.js} +375 -235
- package/dist/chunk-2ISBRSBA.js.map +1 -0
- package/dist/{chunk-QELCMXQX.js → chunk-4HANB3DJ.js} +381 -212
- package/dist/chunk-4HANB3DJ.js.map +1 -0
- package/dist/{chunk-OF42OVM4.js → chunk-54KH4JSO.js} +204 -125
- package/dist/chunk-54KH4JSO.js.map +1 -0
- package/dist/{chunk-RD4S5LKM.js → chunk-CECMCDZG.js} +43 -30
- package/dist/chunk-CECMCDZG.js.map +1 -0
- package/dist/{chunk-MVAMPVTA.js → chunk-KILMI3S5.js} +13 -5
- package/dist/{chunk-MVAMPVTA.js.map → chunk-KILMI3S5.js.map} +1 -1
- package/dist/{chunk-NMB3TNQU.js → chunk-NRLGGA7L.js} +507 -175
- package/dist/chunk-NRLGGA7L.js.map +1 -0
- package/dist/loop-M64D5YII.js +17 -0
- package/dist/main.js +177 -131
- package/dist/main.js.map +1 -1
- package/dist/{provider-EWMUPBLH.js → provider-3Z4B7RMF.js} +2 -2
- package/dist/{repl-ZABBT33D.js → repl-N2H7JDSH.js} +617 -327
- package/dist/repl-N2H7JDSH.js.map +1 -0
- package/dist/{tools-4OLADVDR.js → tools-5RKM7ZI3.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-6PLZ3FLG.js.map +0 -1
- package/dist/chunk-NMB3TNQU.js.map +0 -1
- package/dist/chunk-OF42OVM4.js.map +0 -1
- package/dist/chunk-QELCMXQX.js.map +0 -1
- package/dist/chunk-RD4S5LKM.js.map +0 -1
- package/dist/loop-DWQ5WTPD.js +0 -17
- package/dist/repl-ZABBT33D.js.map +0 -1
- /package/dist/{loop-DWQ5WTPD.js.map → loop-M64D5YII.js.map} +0 -0
- /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
- /package/dist/{tools-4OLADVDR.js.map → tools-5RKM7ZI3.js.map} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
INTERRUPTED_TOOL_RESULT_TEXT,
|
|
4
4
|
UNFINISHED_TOOL_RESULT_TEXT,
|
|
5
|
+
computeCostUsd,
|
|
5
6
|
coreMessageText,
|
|
6
7
|
diffTreeNames,
|
|
7
8
|
isSyntheticUserMessage,
|
|
@@ -12,11 +13,11 @@ import {
|
|
|
12
13
|
toCoreMessages,
|
|
13
14
|
toStarMessage,
|
|
14
15
|
trackTree
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-CECMCDZG.js";
|
|
16
17
|
import {
|
|
17
18
|
createModel,
|
|
18
19
|
streamActivity
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-KILMI3S5.js";
|
|
20
21
|
import {
|
|
21
22
|
resolveModelConfig
|
|
22
23
|
} from "./chunk-4IRY44TG.js";
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
ToolRegistry,
|
|
25
26
|
createDefaultRegistry,
|
|
26
27
|
debugStreamLog
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-54KH4JSO.js";
|
|
28
29
|
import {
|
|
29
30
|
TodoStore,
|
|
30
31
|
beginTurn,
|
|
@@ -44,7 +45,7 @@ import {
|
|
|
44
45
|
stripOversizedImages,
|
|
45
46
|
undoTurnSnapshots,
|
|
46
47
|
userMemoryPath
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-2ISBRSBA.js";
|
|
48
49
|
|
|
49
50
|
// src/agent/loop.ts
|
|
50
51
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -147,6 +148,157 @@ function estimateToolSchemaTokens(tools) {
|
|
|
147
148
|
return tokens;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// src/context/elision.ts
|
|
152
|
+
var DEFAULT_ELISION_ACTIVATION_FRACTION = 0.5;
|
|
153
|
+
var DEFAULT_ELISION_KEEP_RECENT_MESSAGES = 24;
|
|
154
|
+
var DEFAULT_ELISION_MIN_TOKENS = 250;
|
|
155
|
+
var ELIDED_TOOL_RESULT_TEXT = "[stale tool output elided to free context \u2014 it was shown in full earlier; re-run the tool if you need it again]";
|
|
156
|
+
var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
|
|
157
|
+
var ELIDED_TOOL_CALL_ARGS_TEXT = "arguments elided to free context \u2014 the call already ran; this placeholder is not valid tool input, re-read the file if you need the old content";
|
|
158
|
+
function elideStaleContent(messages, thresholdTokens, overheadTokens = 0, options = {}) {
|
|
159
|
+
const activationFraction = options.activationFraction ?? DEFAULT_ELISION_ACTIVATION_FRACTION;
|
|
160
|
+
const keepRecentMessages = options.keepRecentMessages ?? DEFAULT_ELISION_KEEP_RECENT_MESSAGES;
|
|
161
|
+
const minTokens = options.minTokens ?? DEFAULT_ELISION_MIN_TOKENS;
|
|
162
|
+
let total = overheadTokens;
|
|
163
|
+
for (const star of messages) total += estimateMessageTokens(star.message);
|
|
164
|
+
if (total < thresholdTokens * activationFraction) return null;
|
|
165
|
+
const cutoff = messages.length - keepRecentMessages;
|
|
166
|
+
if (cutoff <= 0) return null;
|
|
167
|
+
let elidedCount = 0;
|
|
168
|
+
const out = messages.slice();
|
|
169
|
+
for (let i = 0; i < cutoff; i++) {
|
|
170
|
+
const star = out[i];
|
|
171
|
+
if (!star || star.meta?.elided) continue;
|
|
172
|
+
const message = star.message;
|
|
173
|
+
if (message.role === "tool") {
|
|
174
|
+
if (estimateMessageTokens(message) < minTokens) continue;
|
|
175
|
+
out[i] = {
|
|
176
|
+
message: {
|
|
177
|
+
...message,
|
|
178
|
+
content: message.content.map((part) => ({
|
|
179
|
+
...part,
|
|
180
|
+
result: ELIDED_TOOL_RESULT_TEXT
|
|
181
|
+
}))
|
|
182
|
+
},
|
|
183
|
+
meta: { ...star.meta, elided: true }
|
|
184
|
+
};
|
|
185
|
+
elidedCount++;
|
|
186
|
+
} else if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
187
|
+
if (!message.content.some((part) => part.type === "tool-call")) continue;
|
|
188
|
+
if (estimateMessageTokens(message) < minTokens) continue;
|
|
189
|
+
out[i] = {
|
|
190
|
+
message: {
|
|
191
|
+
...message,
|
|
192
|
+
content: message.content.map(
|
|
193
|
+
(part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
|
|
194
|
+
)
|
|
195
|
+
},
|
|
196
|
+
meta: { ...star.meta, elided: true }
|
|
197
|
+
};
|
|
198
|
+
elidedCount++;
|
|
199
|
+
} else if (message.role === "user" && Array.isArray(message.content)) {
|
|
200
|
+
if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
|
|
201
|
+
out[i] = {
|
|
202
|
+
message: {
|
|
203
|
+
...message,
|
|
204
|
+
content: message.content.map(
|
|
205
|
+
(part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
|
|
206
|
+
)
|
|
207
|
+
},
|
|
208
|
+
meta: { ...star.meta, elided: true }
|
|
209
|
+
};
|
|
210
|
+
elidedCount++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (elidedCount === 0) return null;
|
|
214
|
+
return { messages: out, elidedCount };
|
|
215
|
+
}
|
|
216
|
+
var ELIDED_ASSISTANT_TEXT = "[assistant content elided by emergency context reduction]";
|
|
217
|
+
function isElidedToolCallArgs(args) {
|
|
218
|
+
return args !== null && typeof args === "object" && args.elided === ELIDED_TOOL_CALL_ARGS_TEXT;
|
|
219
|
+
}
|
|
220
|
+
function emergencyElideMessage(star) {
|
|
221
|
+
const message = star.message;
|
|
222
|
+
if (message.role === "system") return null;
|
|
223
|
+
if (message.role === "tool") {
|
|
224
|
+
if (message.content.every((part) => part.result === ELIDED_TOOL_RESULT_TEXT)) return null;
|
|
225
|
+
return {
|
|
226
|
+
message: {
|
|
227
|
+
...message,
|
|
228
|
+
content: message.content.map((part) => ({
|
|
229
|
+
...part,
|
|
230
|
+
result: ELIDED_TOOL_RESULT_TEXT
|
|
231
|
+
}))
|
|
232
|
+
},
|
|
233
|
+
meta: { ...star.meta, elided: true }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (message.role === "user") {
|
|
237
|
+
if (!Array.isArray(message.content)) return null;
|
|
238
|
+
if (!message.content.some((part) => part.type === "image" || part.type === "file")) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
message: {
|
|
243
|
+
...message,
|
|
244
|
+
content: message.content.map(
|
|
245
|
+
(part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
|
|
246
|
+
)
|
|
247
|
+
},
|
|
248
|
+
meta: { ...star.meta, elided: true }
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (typeof message.content === "string") {
|
|
252
|
+
if (message.content === ELIDED_ASSISTANT_TEXT) return null;
|
|
253
|
+
return {
|
|
254
|
+
message: { ...message, content: ELIDED_ASSISTANT_TEXT },
|
|
255
|
+
meta: { ...star.meta, elided: true }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
let changed = false;
|
|
259
|
+
const content = message.content.map((part) => {
|
|
260
|
+
if (part.type === "tool-call" && !isElidedToolCallArgs(part.args)) {
|
|
261
|
+
changed = true;
|
|
262
|
+
return { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } };
|
|
263
|
+
}
|
|
264
|
+
if ((part.type === "text" || part.type === "reasoning") && part.text !== ELIDED_ASSISTANT_TEXT) {
|
|
265
|
+
changed = true;
|
|
266
|
+
return { ...part, text: ELIDED_ASSISTANT_TEXT };
|
|
267
|
+
}
|
|
268
|
+
return part;
|
|
269
|
+
});
|
|
270
|
+
if (!changed) return null;
|
|
271
|
+
return { message: { ...message, content }, meta: { ...star.meta, elided: true } };
|
|
272
|
+
}
|
|
273
|
+
function emergencyElideContent(messages, maxTokens, overheadTokens = 0, options = {}) {
|
|
274
|
+
const minTokens = options.minTokens ?? 1;
|
|
275
|
+
const out = messages.slice();
|
|
276
|
+
const tokens = messages.map((star) => estimateMessageTokens(star.message));
|
|
277
|
+
let estimatedTokens = overheadTokens;
|
|
278
|
+
for (const count of tokens) estimatedTokens += count;
|
|
279
|
+
let elidedCount = 0;
|
|
280
|
+
const floors = minTokens < DEFAULT_ELISION_MIN_TOKENS ? [DEFAULT_ELISION_MIN_TOKENS, minTokens] : [minTokens];
|
|
281
|
+
for (const floor of floors) {
|
|
282
|
+
for (let i = 0; i < out.length && estimatedTokens > maxTokens; i++) {
|
|
283
|
+
const star = out[i];
|
|
284
|
+
if (!star || (tokens[i] ?? 0) < floor) continue;
|
|
285
|
+
const replacement = emergencyElideMessage(star);
|
|
286
|
+
if (!replacement) continue;
|
|
287
|
+
const replacementTokens = estimateMessageTokens(replacement.message);
|
|
288
|
+
if (replacementTokens >= (tokens[i] ?? 0)) continue;
|
|
289
|
+
out[i] = replacement;
|
|
290
|
+
estimatedTokens += replacementTokens - (tokens[i] ?? 0);
|
|
291
|
+
elidedCount++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
messages: out,
|
|
296
|
+
elidedCount,
|
|
297
|
+
estimatedTokens,
|
|
298
|
+
withinBudget: estimatedTokens <= maxTokens
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
150
302
|
// src/context/compaction.ts
|
|
151
303
|
var MIN_KEPT_MESSAGES = 4;
|
|
152
304
|
function placeholderMessage(droppedCount) {
|
|
@@ -227,20 +379,22 @@ function serializeSummaryInput(messages) {
|
|
|
227
379
|
[... ${total - MAX_SUMMARY_INPUT_CHARS} characters omitted ...]
|
|
228
380
|
${tail}`;
|
|
229
381
|
}
|
|
230
|
-
async function summarizeMessages(messages, model, signal, temperature) {
|
|
382
|
+
async function summarizeMessages(messages, model, signal, temperature, onUsage) {
|
|
231
383
|
const transcript = serializeSummaryInput(messages);
|
|
232
|
-
const { text } = await generateText({
|
|
384
|
+
const { text, usage } = await generateText({
|
|
233
385
|
model,
|
|
234
386
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
235
387
|
prompt: `Summarize this conversation so far:
|
|
236
388
|
|
|
237
389
|
${transcript}`,
|
|
238
390
|
temperature,
|
|
391
|
+
maxRetries: 0,
|
|
239
392
|
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
240
393
|
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
241
394
|
// falls back to the truncation placeholder on any failure.
|
|
242
395
|
abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
|
|
243
396
|
});
|
|
397
|
+
onUsage?.(usage);
|
|
244
398
|
return text.trim();
|
|
245
399
|
}
|
|
246
400
|
function compactMessages(messages, maxTokens, opts = {}) {
|
|
@@ -298,69 +452,6 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
298
452
|
};
|
|
299
453
|
}
|
|
300
454
|
|
|
301
|
-
// src/context/elision.ts
|
|
302
|
-
var ACTIVATION_FRACTION = 0.5;
|
|
303
|
-
var KEEP_RECENT_MESSAGES = 24;
|
|
304
|
-
var MIN_ELISION_TOKENS = 250;
|
|
305
|
-
var ELIDED_TOOL_RESULT_TEXT = "[stale tool output elided to free context \u2014 it was shown in full earlier; re-run the tool if you need it again]";
|
|
306
|
-
var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
|
|
307
|
-
var ELIDED_TOOL_CALL_ARGS_TEXT = "arguments elided to free context \u2014 the call already ran; this placeholder is not valid tool input, re-read the file if you need the old content";
|
|
308
|
-
function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
|
|
309
|
-
let total = overheadTokens;
|
|
310
|
-
for (const star of messages) total += estimateMessageTokens(star.message);
|
|
311
|
-
if (total < thresholdTokens * ACTIVATION_FRACTION) return null;
|
|
312
|
-
const cutoff = messages.length - KEEP_RECENT_MESSAGES;
|
|
313
|
-
if (cutoff <= 0) return null;
|
|
314
|
-
let elidedCount = 0;
|
|
315
|
-
const out = messages.slice();
|
|
316
|
-
for (let i = 0; i < cutoff; i++) {
|
|
317
|
-
const star = out[i];
|
|
318
|
-
if (!star || star.meta?.elided) continue;
|
|
319
|
-
const message = star.message;
|
|
320
|
-
if (message.role === "tool") {
|
|
321
|
-
if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
|
|
322
|
-
out[i] = {
|
|
323
|
-
message: {
|
|
324
|
-
...message,
|
|
325
|
-
content: message.content.map((part) => ({
|
|
326
|
-
...part,
|
|
327
|
-
result: ELIDED_TOOL_RESULT_TEXT
|
|
328
|
-
}))
|
|
329
|
-
},
|
|
330
|
-
meta: { ...star.meta, elided: true }
|
|
331
|
-
};
|
|
332
|
-
elidedCount++;
|
|
333
|
-
} else if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
334
|
-
if (!message.content.some((part) => part.type === "tool-call")) continue;
|
|
335
|
-
if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
|
|
336
|
-
out[i] = {
|
|
337
|
-
message: {
|
|
338
|
-
...message,
|
|
339
|
-
content: message.content.map(
|
|
340
|
-
(part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
|
|
341
|
-
)
|
|
342
|
-
},
|
|
343
|
-
meta: { ...star.meta, elided: true }
|
|
344
|
-
};
|
|
345
|
-
elidedCount++;
|
|
346
|
-
} else if (message.role === "user" && Array.isArray(message.content)) {
|
|
347
|
-
if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
|
|
348
|
-
out[i] = {
|
|
349
|
-
message: {
|
|
350
|
-
...message,
|
|
351
|
-
content: message.content.map(
|
|
352
|
-
(part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
|
|
353
|
-
)
|
|
354
|
-
},
|
|
355
|
-
meta: { ...star.meta, elided: true }
|
|
356
|
-
};
|
|
357
|
-
elidedCount++;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (elidedCount === 0) return null;
|
|
361
|
-
return { messages: out, elidedCount };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
455
|
// src/core/events.ts
|
|
365
456
|
function usageTotalTokens(usage) {
|
|
366
457
|
const summed = usage.promptTokens + usage.completionTokens;
|
|
@@ -375,6 +466,19 @@ function toStreamErrorInfo(error) {
|
|
|
375
466
|
if (typeof raw.statusCode === "number") info2.statusCode = raw.statusCode;
|
|
376
467
|
if (typeof raw.status === "number") info2.statusCode = raw.status;
|
|
377
468
|
if (typeof raw.code === "string") info2.code = raw.code;
|
|
469
|
+
if (raw.isRetryable === true) info2.isRetryable = true;
|
|
470
|
+
const causeCode2 = raw.cause && typeof raw.cause === "object" ? raw.cause.code : void 0;
|
|
471
|
+
if (typeof causeCode2 === "string") info2.causeCode = causeCode2;
|
|
472
|
+
if (raw.responseHeaders && typeof raw.responseHeaders === "object") {
|
|
473
|
+
const strings = {};
|
|
474
|
+
for (const [key, value] of Object.entries(raw.responseHeaders)) {
|
|
475
|
+
if (typeof value === "string") strings[key] = value;
|
|
476
|
+
}
|
|
477
|
+
if (Object.keys(strings).length > 0) info2.responseHeaders = strings;
|
|
478
|
+
}
|
|
479
|
+
if (typeof raw.responseBody === "string" && raw.responseBody.length > 0) {
|
|
480
|
+
info2.responseBody = raw.responseBody;
|
|
481
|
+
}
|
|
378
482
|
return info2;
|
|
379
483
|
}
|
|
380
484
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -401,7 +505,7 @@ function toStreamErrorInfo(error) {
|
|
|
401
505
|
}
|
|
402
506
|
|
|
403
507
|
// src/core/git.ts
|
|
404
|
-
import { execFileSync } from "child_process";
|
|
508
|
+
import { execFile, execFileSync } from "child_process";
|
|
405
509
|
var GIT_TIMEOUT_MS = 2e3;
|
|
406
510
|
var GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
407
511
|
var COMMIT_DIFF_MAX_LINES = 2e3;
|
|
@@ -441,6 +545,47 @@ function getGitSummaryCached(cwd) {
|
|
|
441
545
|
gitSummaryCache.set(cwd, { at: now, summary });
|
|
442
546
|
return summary;
|
|
443
547
|
}
|
|
548
|
+
function runGitAsync(args, cwd) {
|
|
549
|
+
return new Promise((resolve) => {
|
|
550
|
+
execFile(
|
|
551
|
+
"git",
|
|
552
|
+
args,
|
|
553
|
+
{
|
|
554
|
+
cwd,
|
|
555
|
+
timeout: GIT_TIMEOUT_MS,
|
|
556
|
+
encoding: "utf8",
|
|
557
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
558
|
+
},
|
|
559
|
+
(error, stdout) => resolve(error ? null : stdout.replace(/\n$/, ""))
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
var gitSummaryPending = /* @__PURE__ */ new Map();
|
|
564
|
+
function getGitSummaryCachedAsync(cwd) {
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
const hit = gitSummaryCache.get(cwd);
|
|
567
|
+
if (hit && now - hit.at < GIT_SUMMARY_CACHE_TTL_MS) return Promise.resolve(hit.summary);
|
|
568
|
+
const pending = gitSummaryPending.get(cwd);
|
|
569
|
+
if (pending) return pending;
|
|
570
|
+
const task = (async () => {
|
|
571
|
+
const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
572
|
+
if (branch === null) {
|
|
573
|
+
gitSummaryCache.set(cwd, { at: Date.now(), summary: null });
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const status = await runGitAsync(["status", "--porcelain"], cwd) ?? "";
|
|
577
|
+
const log = await runGitAsync(["log", "--oneline", "-3"], cwd) ?? "";
|
|
578
|
+
const summary = {
|
|
579
|
+
branch,
|
|
580
|
+
dirtyCount: status === "" ? 0 : status.split("\n").length,
|
|
581
|
+
recentCommits: log === "" ? [] : log.split("\n")
|
|
582
|
+
};
|
|
583
|
+
gitSummaryCache.set(cwd, { at: Date.now(), summary });
|
|
584
|
+
return summary;
|
|
585
|
+
})().finally(() => gitSummaryPending.delete(cwd));
|
|
586
|
+
gitSummaryPending.set(cwd, task);
|
|
587
|
+
return task;
|
|
588
|
+
}
|
|
444
589
|
function formatGitSummary(summary) {
|
|
445
590
|
const lines = [
|
|
446
591
|
"Git context for the working directory (refreshed each turn):",
|
|
@@ -575,6 +720,10 @@ function runCommand(hook, event, ctx, timeoutSec) {
|
|
|
575
720
|
child.stdin.on("error", () => {
|
|
576
721
|
});
|
|
577
722
|
child.stdin.end(inputJson);
|
|
723
|
+
} else {
|
|
724
|
+
child.stdin?.on("error", () => {
|
|
725
|
+
});
|
|
726
|
+
child.stdin?.end();
|
|
578
727
|
}
|
|
579
728
|
let stderr = "";
|
|
580
729
|
const stderrDecoder = new StringDecoder("utf8");
|
|
@@ -675,7 +824,9 @@ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
675
824
|
"EPIPE",
|
|
676
825
|
"EAI_AGAIN",
|
|
677
826
|
"ENOTFOUND",
|
|
678
|
-
"ECONNABORTED"
|
|
827
|
+
"ECONNABORTED",
|
|
828
|
+
"EHOSTUNREACH",
|
|
829
|
+
"ENETUNREACH"
|
|
679
830
|
]);
|
|
680
831
|
function isNetworkFailure(error) {
|
|
681
832
|
for (const code of [error.code, error.causeCode]) {
|
|
@@ -1224,6 +1375,7 @@ function splitShellSegments(tokens) {
|
|
|
1224
1375
|
}
|
|
1225
1376
|
return segments;
|
|
1226
1377
|
}
|
|
1378
|
+
var DYNAMIC_SHELL_WORD = "\0star-dynamic-word\0";
|
|
1227
1379
|
function segmentWords(segment) {
|
|
1228
1380
|
const words = [];
|
|
1229
1381
|
const redirectTargets = [];
|
|
@@ -1234,11 +1386,16 @@ function segmentWords(segment) {
|
|
|
1234
1386
|
if (target?.kind === "word") {
|
|
1235
1387
|
redirectTargets.push(target.text);
|
|
1236
1388
|
k += 1;
|
|
1389
|
+
} else if (target?.kind === "subst") {
|
|
1390
|
+
redirectTargets.push(DYNAMIC_SHELL_WORD);
|
|
1391
|
+
k += 1;
|
|
1237
1392
|
}
|
|
1238
1393
|
continue;
|
|
1239
1394
|
}
|
|
1240
1395
|
if (token.kind === "word") {
|
|
1241
1396
|
words.push(token.text);
|
|
1397
|
+
} else if (token.kind === "subst") {
|
|
1398
|
+
words.push(DYNAMIC_SHELL_WORD);
|
|
1242
1399
|
}
|
|
1243
1400
|
}
|
|
1244
1401
|
return { words, redirectTargets };
|
|
@@ -1389,7 +1546,11 @@ function baseName(word) {
|
|
|
1389
1546
|
const base = word.replace(/\\/g, "/").split("/").pop() ?? word;
|
|
1390
1547
|
return base.toLowerCase().replace(/\.exe$/, "");
|
|
1391
1548
|
}
|
|
1549
|
+
function isDynamicTarget(target) {
|
|
1550
|
+
return target.includes(DYNAMIC_SHELL_WORD) || /\$[A-Za-z_]/.test(target) || /%[A-Za-z_][A-Za-z0-9_]*%/.test(target);
|
|
1551
|
+
}
|
|
1392
1552
|
function isDangerousDeleteTarget(target) {
|
|
1553
|
+
if (isDynamicTarget(target)) return true;
|
|
1393
1554
|
if (/^\/+$/.test(target)) return true;
|
|
1394
1555
|
const norm = target.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1395
1556
|
if (norm === "/*" || norm === "*") return true;
|
|
@@ -1663,7 +1824,7 @@ function decodePowerShellEncoded(encoded) {
|
|
|
1663
1824
|
}
|
|
1664
1825
|
function segmentIsDangerous(segment, depth, lex) {
|
|
1665
1826
|
const { words, redirectTargets } = segmentWords(segment);
|
|
1666
|
-
if (redirectTargets.some((target) => BLOCK_DEVICE.test(target))) {
|
|
1827
|
+
if (redirectTargets.some((target) => isDynamicTarget(target) || BLOCK_DEVICE.test(target))) {
|
|
1667
1828
|
return true;
|
|
1668
1829
|
}
|
|
1669
1830
|
const invocation = commandInvocation(words);
|
|
@@ -1705,10 +1866,10 @@ function segmentIsDangerous(segment, depth, lex) {
|
|
|
1705
1866
|
return recursive && targets.some(isDangerousDeleteTarget);
|
|
1706
1867
|
}
|
|
1707
1868
|
case "format":
|
|
1708
|
-
return rest.some((arg) => /^[a-z]:$/i.test(arg));
|
|
1869
|
+
return rest.some((arg) => isDynamicTarget(arg) || /^[a-z]:$/i.test(arg));
|
|
1709
1870
|
case "dd":
|
|
1710
1871
|
return rest.some(
|
|
1711
|
-
(arg) => arg.startsWith("if=") || arg.startsWith("of=") && BLOCK_DEVICE.test(arg.slice(3))
|
|
1872
|
+
(arg) => arg.startsWith("if=") || arg.startsWith("of=") && (isDynamicTarget(arg.slice(3)) || BLOCK_DEVICE.test(arg.slice(3)))
|
|
1712
1873
|
);
|
|
1713
1874
|
case "find":
|
|
1714
1875
|
return findIsDangerous(rest, depth, lex);
|
|
@@ -2007,21 +2168,23 @@ function sanitizeTitle(raw) {
|
|
|
2007
2168
|
} while (title !== previous);
|
|
2008
2169
|
return title.slice(0, TITLE_MAX_CHARS).trim();
|
|
2009
2170
|
}
|
|
2010
|
-
async function generateSessionTitle(userText, model) {
|
|
2011
|
-
const { text } = await generateText2({
|
|
2171
|
+
async function generateSessionTitle(userText, model, onUsage) {
|
|
2172
|
+
const { text, usage } = await generateText2({
|
|
2012
2173
|
model,
|
|
2013
2174
|
system: TITLE_SYSTEM_PROMPT,
|
|
2014
2175
|
prompt: userText.slice(0, TITLE_INPUT_MAX_CHARS),
|
|
2015
2176
|
maxTokens: TITLE_MAX_TOKENS,
|
|
2177
|
+
maxRetries: 0,
|
|
2016
2178
|
abortSignal: AbortSignal.timeout(TITLE_TIMEOUT_MS)
|
|
2017
2179
|
});
|
|
2180
|
+
onUsage?.(usage);
|
|
2018
2181
|
return sanitizeTitle(text);
|
|
2019
2182
|
}
|
|
2020
|
-
function scheduleSessionTitle(store, userText, model) {
|
|
2183
|
+
function scheduleSessionTitle(store, userText, model, onUsage) {
|
|
2021
2184
|
void (async () => {
|
|
2022
2185
|
const meta = await store.meta();
|
|
2023
2186
|
if (meta.title) return;
|
|
2024
|
-
const title = await generateSessionTitle(userText, model);
|
|
2187
|
+
const title = await generateSessionTitle(userText, model, onUsage);
|
|
2025
2188
|
if (!title) return;
|
|
2026
2189
|
const current = await store.meta();
|
|
2027
2190
|
if (current.title) return;
|
|
@@ -2190,44 +2353,8 @@ var MAX_SUBAGENT_DEPTH = 1;
|
|
|
2190
2353
|
var MAX_REPORT_CHARS = 2e4;
|
|
2191
2354
|
var SUBAGENT_PROMPT = "You are a subagent spawned to handle a focused subtask on behalf of the main agent. Work autonomously with the tools available to you. Your final text is returned to the main agent as the tool result \u2014 make it a self-contained report: state what you found or did, cite exact file paths with line numbers for anything the main agent may need to open, and note explicitly anything left undone or unverified. Report outcomes, not process.";
|
|
2192
2355
|
var READ_ONLY_PROMPT = "This subtask is read-only: your tools cannot modify files, run shell commands, or kill tasks. Investigate and report; do not attempt changes.";
|
|
2193
|
-
function convertUsagePricing(usage, from, to, config, names) {
|
|
2194
|
-
if (from === to) return usage;
|
|
2195
|
-
const fromCfg = findModelPricing(config, names?.fromName, from.modelId);
|
|
2196
|
-
const toCfg = findModelPricing(config, names?.toName, to.modelId);
|
|
2197
|
-
if (fromCfg?.promptPrice === void 0 || fromCfg.completionPrice === void 0 || toCfg?.promptPrice === void 0 || toCfg.completionPrice === void 0 || toCfg.promptPrice <= 0 || toCfg.completionPrice <= 0) {
|
|
2198
|
-
return usage;
|
|
2199
|
-
}
|
|
2200
|
-
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
2201
|
-
const promptUsd = (usage.promptTokens - cached) * fromCfg.promptPrice;
|
|
2202
|
-
const cachedUsd = cached * (fromCfg.cacheReadPrice ?? fromCfg.promptPrice);
|
|
2203
|
-
const cacheReadUsd = (usage.cacheReadInputTokens ?? 0) * (fromCfg.cacheReadPrice ?? 0);
|
|
2204
|
-
const completionUsd = usage.completionTokens * fromCfg.completionPrice;
|
|
2205
|
-
const scaledCached = cachedUsd / (toCfg.cacheReadPrice ?? toCfg.promptPrice);
|
|
2206
|
-
const promptTokens = Math.round(promptUsd / toCfg.promptPrice + scaledCached);
|
|
2207
|
-
const cachedPromptTokens = Math.round(scaledCached);
|
|
2208
|
-
const toCacheReadPrice = toCfg.cacheReadPrice !== void 0 && toCfg.cacheReadPrice > 0 ? toCfg.cacheReadPrice : void 0;
|
|
2209
|
-
const completionTokens = Math.round(
|
|
2210
|
-
completionUsd / toCfg.completionPrice + (toCacheReadPrice === void 0 ? cacheReadUsd / toCfg.completionPrice : 0)
|
|
2211
|
-
);
|
|
2212
|
-
const cacheReadInputTokens = toCacheReadPrice === void 0 ? 0 : Math.round(cacheReadUsd / toCacheReadPrice);
|
|
2213
|
-
const result = {
|
|
2214
|
-
promptTokens,
|
|
2215
|
-
completionTokens,
|
|
2216
|
-
totalTokens: promptTokens + completionTokens
|
|
2217
|
-
};
|
|
2218
|
-
if (cachedPromptTokens > 0) result.cachedPromptTokens = cachedPromptTokens;
|
|
2219
|
-
if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
|
|
2220
|
-
return result;
|
|
2221
|
-
}
|
|
2222
|
-
function findModelPricing(config, name, modelId) {
|
|
2223
|
-
if (name !== void 0) {
|
|
2224
|
-
const byName = config.models.find((m) => m.name === name);
|
|
2225
|
-
if (byName) return byName;
|
|
2226
|
-
}
|
|
2227
|
-
return config.models.find((m) => m.model === modelId);
|
|
2228
|
-
}
|
|
2229
2356
|
async function runSubagent(deps, args, cwd, signal, agentIdRef) {
|
|
2230
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2357
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-M64D5YII.js");
|
|
2231
2358
|
const child = new AgentLoop2({
|
|
2232
2359
|
model: deps.model,
|
|
2233
2360
|
registry: createDefaultRegistry(),
|
|
@@ -2242,6 +2369,7 @@ ${READ_ONLY_PROMPT}` : ""}` : `${SUBAGENT_PROMPT}${args.read_only ? `
|
|
|
2242
2369
|
${READ_ONLY_PROMPT}` : ""}`,
|
|
2243
2370
|
providerMetadata: deps.providerMetadata,
|
|
2244
2371
|
temperature: deps.temperature,
|
|
2372
|
+
maxTokens: deps.maxTokens,
|
|
2245
2373
|
streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
|
|
2246
2374
|
streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
|
|
2247
2375
|
vision: deps.vision,
|
|
@@ -2443,15 +2571,16 @@ ${context}
|
|
|
2443
2571
|
}
|
|
2444
2572
|
return messages;
|
|
2445
2573
|
}
|
|
2446
|
-
async function checkTaskComplete(model, request, finalReply, signal, temperature) {
|
|
2574
|
+
async function checkTaskComplete(model, request, finalReply, toolSummary, signal, temperature, onUsage) {
|
|
2447
2575
|
const controller = new AbortController();
|
|
2448
2576
|
const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
|
|
2449
2577
|
const onAbort = () => controller.abort();
|
|
2450
2578
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2451
2579
|
try {
|
|
2452
|
-
const { text } = await generateText3({
|
|
2580
|
+
const { text, usage } = await generateText3({
|
|
2453
2581
|
model,
|
|
2454
2582
|
maxTokens: 8,
|
|
2583
|
+
maxRetries: 0,
|
|
2455
2584
|
temperature,
|
|
2456
2585
|
abortSignal: controller.signal,
|
|
2457
2586
|
prompt: [
|
|
@@ -2463,9 +2592,14 @@ ${request.slice(0, 2e3)}
|
|
|
2463
2592
|
`<reply>
|
|
2464
2593
|
${finalReply.slice(0, 2e3)}
|
|
2465
2594
|
</reply>`,
|
|
2466
|
-
"
|
|
2595
|
+
"These are the actual tool outcomes, including failures:",
|
|
2596
|
+
`<tool-outcomes>
|
|
2597
|
+
${toolSummary.slice(0, 6e3)}
|
|
2598
|
+
</tool-outcomes>`,
|
|
2599
|
+
"Has the agent fully completed the task \u2014 every requested action actually performed, and the result verified by running a check (tests, build, or inspecting the produced output) rather than assumed? Failed tools and missing verification mean the work is not complete even when the reply claims success. For a task that changed code, completion also requires tests covering the change when the project has a test setup, and a test run that actually passed. Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
|
|
2467
2600
|
].join("\n")
|
|
2468
2601
|
});
|
|
2602
|
+
onUsage(usage);
|
|
2469
2603
|
const verdict = text.trim().toUpperCase();
|
|
2470
2604
|
if (verdict.startsWith("NOT_DONE")) return false;
|
|
2471
2605
|
if (verdict.startsWith("DONE")) return true;
|
|
@@ -2529,15 +2663,6 @@ var AgentLoop = class {
|
|
|
2529
2663
|
// each new user turn restarts the streak.
|
|
2530
2664
|
lastToolSignature = null;
|
|
2531
2665
|
repeatedToolCalls = 0;
|
|
2532
|
-
// Token usage subagents consumed, waiting to fold into this loop's next
|
|
2533
|
-
// finish event with usage: sync children report when their tool call
|
|
2534
|
-
// settles, background ones whenever they finish (drained or not).
|
|
2535
|
-
// Consumers meter only root-loop finish events, so folding here bills
|
|
2536
|
-
// subagent spend with zero consumer changes — /cost, /usage and
|
|
2537
|
-
// sessionBudgetUsd all cover it. A background child still running when the
|
|
2538
|
-
// session's last finish has passed never reports (best effort, noted in
|
|
2539
|
-
// agent-tasks cleanup paths).
|
|
2540
|
-
subagentUsage = null;
|
|
2541
2666
|
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
2542
2667
|
// execution so file snapshots attribute root changes to a turn and mark
|
|
2543
2668
|
// subagent changes as excluded from turn-level retraction.
|
|
@@ -2556,6 +2681,7 @@ var AgentLoop = class {
|
|
|
2556
2681
|
// turn context string. Saves a full toCoreMessages + attachTurnContext copy
|
|
2557
2682
|
// per stream attempt, which retried steps used to pay every time.
|
|
2558
2683
|
requestMessagesCache = null;
|
|
2684
|
+
historyTokenSnapshot = null;
|
|
2559
2685
|
// Memoized per (registry, plan flag): tools register on the registry only
|
|
2560
2686
|
// at loop construction, and the plan filter re-reads the live permission
|
|
2561
2687
|
// mode on every call — so a built map stays valid until either input
|
|
@@ -2598,11 +2724,12 @@ var AgentLoop = class {
|
|
|
2598
2724
|
// their stderr surfaces through this callback (REPL system message / stderr
|
|
2599
2725
|
// in print mode).
|
|
2600
2726
|
onHookWarning;
|
|
2727
|
+
onUsage;
|
|
2601
2728
|
constructor(opts) {
|
|
2602
2729
|
this.opts = opts;
|
|
2603
2730
|
const depth = opts.subagentDepth ?? 0;
|
|
2604
2731
|
if (depth > 0) {
|
|
2605
|
-
opts.registry = new ToolRegistry(new TodoStore());
|
|
2732
|
+
opts.registry = new ToolRegistry(new TodoStore({ persistence: "memory" }));
|
|
2606
2733
|
} else {
|
|
2607
2734
|
setTodoPersistGuard(() => {
|
|
2608
2735
|
const mode = this.opts.config.permissionMode;
|
|
@@ -2618,6 +2745,7 @@ var AgentLoop = class {
|
|
|
2618
2745
|
opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
|
|
2619
2746
|
opts.registry?.register(createRememberTool());
|
|
2620
2747
|
if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
|
|
2748
|
+
const subagentSessionStore = opts.sessionStore;
|
|
2621
2749
|
opts.registry.register(
|
|
2622
2750
|
createSubagentTool({
|
|
2623
2751
|
model: opts.model,
|
|
@@ -2626,13 +2754,14 @@ var AgentLoop = class {
|
|
|
2626
2754
|
system: opts.system,
|
|
2627
2755
|
providerMetadata: opts.providerMetadata,
|
|
2628
2756
|
temperature: opts.temperature,
|
|
2757
|
+
maxTokens: opts.maxTokens,
|
|
2629
2758
|
streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
|
|
2630
2759
|
streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
|
|
2631
2760
|
vision: opts.vision,
|
|
2632
2761
|
depth,
|
|
2633
2762
|
modelName: opts.modelName,
|
|
2634
2763
|
getConfirmHandler: () => this.confirmHandler,
|
|
2635
|
-
onUsage: (usage,
|
|
2764
|
+
onUsage: (usage, _childModel, childModelName) => this.recordUsage(usage, childModelName, subagentSessionStore)
|
|
2636
2765
|
})
|
|
2637
2766
|
);
|
|
2638
2767
|
}
|
|
@@ -2698,8 +2827,25 @@ var AgentLoop = class {
|
|
|
2698
2827
|
// budgets deliberately use only the first three (history + schemas +
|
|
2699
2828
|
// calibration), since in-flight reasoning is never resent and must not
|
|
2700
2829
|
// trigger a compaction the next request won't need.
|
|
2830
|
+
historyTokens() {
|
|
2831
|
+
const cached = this.historyTokenSnapshot;
|
|
2832
|
+
if (cached?.source === this.messages && cached.length === this.messages.length) {
|
|
2833
|
+
return cached.total;
|
|
2834
|
+
}
|
|
2835
|
+
let total = 0;
|
|
2836
|
+
let start = 0;
|
|
2837
|
+
if (cached?.source === this.messages && cached.length < this.messages.length) {
|
|
2838
|
+
total = cached.total;
|
|
2839
|
+
start = cached.length;
|
|
2840
|
+
}
|
|
2841
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
2842
|
+
total += estimateMessageTokens(this.messages[i]?.message ?? { role: "user", content: "" });
|
|
2843
|
+
}
|
|
2844
|
+
this.historyTokenSnapshot = { source: this.messages, length: this.messages.length, total };
|
|
2845
|
+
return total;
|
|
2846
|
+
}
|
|
2701
2847
|
estimateContextTokens() {
|
|
2702
|
-
return
|
|
2848
|
+
return this.historyTokens() + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
|
|
2703
2849
|
}
|
|
2704
2850
|
// Auxiliary model for cheap side calls: config.smallModel, resolved once.
|
|
2705
2851
|
// An unresolvable name/key degrades to the main model instead of breaking
|
|
@@ -2724,24 +2870,30 @@ var AgentLoop = class {
|
|
|
2724
2870
|
this.resolveAuxModel();
|
|
2725
2871
|
return this.auxModelValue ? this.auxTemperatureValue : this.opts.temperature;
|
|
2726
2872
|
}
|
|
2873
|
+
getAuxModelName() {
|
|
2874
|
+
this.resolveAuxModel();
|
|
2875
|
+
return this.auxModelValue && this.opts.config.smallModel ? this.opts.config.smallModel : this.opts.modelName ?? this.opts.model.modelId;
|
|
2876
|
+
}
|
|
2877
|
+
recordUsage(usage, modelName = this.opts.modelName ?? this.opts.model.modelId, store = this.opts.sessionStore) {
|
|
2878
|
+
const normalized = { ...usage, totalTokens: usageTotalTokens(usage) };
|
|
2879
|
+
const model = this.opts.config.models.find((entry) => entry.name === modelName);
|
|
2880
|
+
const costUsd = computeCostUsd(normalized, model);
|
|
2881
|
+
this.onUsage?.(normalized, modelName, costUsd);
|
|
2882
|
+
void store?.addUsage(normalized, { modelName, costUsd }).catch(
|
|
2883
|
+
(error) => this.onHookWarning?.(`failed to record token usage: ${String(error)}`)
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2727
2886
|
// Rebinds the per-call provider metadata (e.g. after the reasoning effort
|
|
2728
2887
|
// for the current model changed via /model) without recreating the loop.
|
|
2729
2888
|
setProviderMetadata(metadata) {
|
|
2730
2889
|
this.opts.providerMetadata = metadata;
|
|
2731
2890
|
}
|
|
2732
2891
|
// Swaps the persistence target (/new starts a fresh session mid-REPL).
|
|
2733
|
-
// Title generation is re-armed so the new session gets one after its first
|
|
2734
|
-
// turn, and snapshot checkpoints now flow to the new store. The redo stack
|
|
2735
|
-
// dies with the old session context: its trees describe working-tree states
|
|
2736
|
-
// that no longer line up with the new session's conversation. Subagent
|
|
2737
|
-
// usage still waiting to fold belongs to the old conversation too — it
|
|
2738
|
-
// must not bill the new session's first finish.
|
|
2739
2892
|
setSessionStore(store) {
|
|
2740
2893
|
this.opts.sessionStore = store;
|
|
2741
2894
|
this.titleScheduled = false;
|
|
2742
2895
|
this.titleInput = void 0;
|
|
2743
2896
|
this.redoStack = [];
|
|
2744
|
-
this.subagentUsage = null;
|
|
2745
2897
|
if (store) this.bindSnapshotHooks(store);
|
|
2746
2898
|
}
|
|
2747
2899
|
bindSnapshotHooks(store) {
|
|
@@ -2763,13 +2915,17 @@ var AgentLoop = class {
|
|
|
2763
2915
|
onRemove: (ids) => store.removeCheckpoints(ids)
|
|
2764
2916
|
});
|
|
2765
2917
|
}
|
|
2918
|
+
resetWholesaleHistoryState() {
|
|
2919
|
+
this.serverTokenBaseline = null;
|
|
2920
|
+
this.requestMessagesCache = null;
|
|
2921
|
+
this.historyTokenSnapshot = null;
|
|
2922
|
+
this.opts.registry?.resetVolatileState();
|
|
2923
|
+
}
|
|
2766
2924
|
async loadMessages(messages) {
|
|
2767
2925
|
this.messages = reconcileStarMessages(messages.map(toStarMessage));
|
|
2768
2926
|
this.turnMarkers = [];
|
|
2769
2927
|
this.redoStack = [];
|
|
2770
|
-
this.
|
|
2771
|
-
this.serverTokenBaseline = null;
|
|
2772
|
-
this.opts.registry?.resetVolatileState();
|
|
2928
|
+
this.resetWholesaleHistoryState();
|
|
2773
2929
|
}
|
|
2774
2930
|
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2775
2931
|
// drop, which snapshot turn's file changes it would revert (undefined =
|
|
@@ -2803,8 +2959,8 @@ var AgentLoop = class {
|
|
|
2803
2959
|
REDO_LABEL_MAX
|
|
2804
2960
|
);
|
|
2805
2961
|
this.messages = result.messages;
|
|
2806
|
-
await this.
|
|
2807
|
-
this.
|
|
2962
|
+
await this.replacePersisted([...this.messages]);
|
|
2963
|
+
this.resetWholesaleHistoryState();
|
|
2808
2964
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2809
2965
|
let turn;
|
|
2810
2966
|
let tree;
|
|
@@ -2887,8 +3043,8 @@ var AgentLoop = class {
|
|
|
2887
3043
|
const removed = this.messages.length - cut;
|
|
2888
3044
|
if (removed === 0) return 0;
|
|
2889
3045
|
this.messages = this.messages.slice(0, cut);
|
|
2890
|
-
await this.
|
|
2891
|
-
this.
|
|
3046
|
+
await this.replacePersisted([...this.messages]);
|
|
3047
|
+
this.resetWholesaleHistoryState();
|
|
2892
3048
|
this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
|
|
2893
3049
|
this.redoStack = [];
|
|
2894
3050
|
return removed;
|
|
@@ -2899,7 +3055,18 @@ var AgentLoop = class {
|
|
|
2899
3055
|
await this.persist(message);
|
|
2900
3056
|
}
|
|
2901
3057
|
async persist(message, meta) {
|
|
2902
|
-
|
|
3058
|
+
const store = this.opts.sessionStore;
|
|
3059
|
+
if (!store) return;
|
|
3060
|
+
if (!await store.append(message, meta) && !await store.recoverMessages()) {
|
|
3061
|
+
throw new Error(`Session ${store.id} could not persist the message history`);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
async replacePersisted(messages) {
|
|
3065
|
+
const store = this.opts.sessionStore;
|
|
3066
|
+
if (!store) return;
|
|
3067
|
+
if (!await store.replaceMessages(messages) && !await store.recoverMessages()) {
|
|
3068
|
+
throw new Error(`Session ${store.id} could not persist the rewritten history`);
|
|
3069
|
+
}
|
|
2903
3070
|
}
|
|
2904
3071
|
// Replaces oversized image parts in the in-memory history with text
|
|
2905
3072
|
// placeholders and rewrites the persisted session, so the failed request
|
|
@@ -2911,7 +3078,8 @@ var AgentLoop = class {
|
|
|
2911
3078
|
message: messages[i] ?? star.message,
|
|
2912
3079
|
meta: star.meta
|
|
2913
3080
|
}));
|
|
2914
|
-
await this.
|
|
3081
|
+
await this.replacePersisted(this.messages);
|
|
3082
|
+
this.resetWholesaleHistoryState();
|
|
2915
3083
|
return removed;
|
|
2916
3084
|
}
|
|
2917
3085
|
// Same rewrite as above, but for text-only endpoints that reject any image
|
|
@@ -2924,7 +3092,8 @@ var AgentLoop = class {
|
|
|
2924
3092
|
message: messages[i] ?? star.message,
|
|
2925
3093
|
meta: star.meta
|
|
2926
3094
|
}));
|
|
2927
|
-
await this.
|
|
3095
|
+
await this.replacePersisted(this.messages);
|
|
3096
|
+
this.resetWholesaleHistoryState();
|
|
2928
3097
|
return removed;
|
|
2929
3098
|
}
|
|
2930
3099
|
// Background title generation runs once per loop, fed by the first real
|
|
@@ -2940,7 +3109,12 @@ var AgentLoop = class {
|
|
|
2940
3109
|
const input = this.titleInput;
|
|
2941
3110
|
if (!input) return;
|
|
2942
3111
|
this.titleScheduled = true;
|
|
2943
|
-
scheduleSessionTitle(
|
|
3112
|
+
scheduleSessionTitle(
|
|
3113
|
+
store,
|
|
3114
|
+
input,
|
|
3115
|
+
this.getAuxModel(),
|
|
3116
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName(), store)
|
|
3117
|
+
);
|
|
2944
3118
|
}
|
|
2945
3119
|
// A user interrupt (Esc) ends the turn mid-stream, before the normal
|
|
2946
3120
|
// assistant-message persistence runs. Whatever text and tool calls already
|
|
@@ -2978,8 +3152,7 @@ var AgentLoop = class {
|
|
|
2978
3152
|
]
|
|
2979
3153
|
};
|
|
2980
3154
|
this.messages.push({ message: synthetic });
|
|
2981
|
-
await this.persist(synthetic)
|
|
2982
|
-
});
|
|
3155
|
+
await this.persist(synthetic);
|
|
2983
3156
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
2984
3157
|
}
|
|
2985
3158
|
}
|
|
@@ -3039,6 +3212,7 @@ var AgentLoop = class {
|
|
|
3039
3212
|
let autoContinues = 0;
|
|
3040
3213
|
let lastWasNudge = false;
|
|
3041
3214
|
let usedToolsThisTurn = false;
|
|
3215
|
+
const toolOutcomes = [];
|
|
3042
3216
|
let streamCutContinues = 0;
|
|
3043
3217
|
let openTodos = [];
|
|
3044
3218
|
let todoRevision = this.opts.registry.todos.revision;
|
|
@@ -3111,24 +3285,57 @@ ${finished.result}`
|
|
|
3111
3285
|
}
|
|
3112
3286
|
}
|
|
3113
3287
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
3114
|
-
const
|
|
3288
|
+
const outputReserve = Math.min(this.opts.maxTokens ?? 4096, Math.floor(maxTokens / 20));
|
|
3289
|
+
const requestBudget = Math.max(1, maxTokens - outputReserve);
|
|
3290
|
+
const threshold = Math.min(resolveCompactThreshold(config, maxTokens), requestBudget);
|
|
3115
3291
|
const calibration = this.calibrationDelta();
|
|
3116
|
-
const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration
|
|
3292
|
+
const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration, {
|
|
3293
|
+
activationFraction: config.elisionThresholdFraction,
|
|
3294
|
+
keepRecentMessages: config.elisionKeepRecentMessages,
|
|
3295
|
+
minTokens: config.elisionMinTokens
|
|
3296
|
+
});
|
|
3117
3297
|
if (elided) {
|
|
3118
3298
|
this.messages = elided.messages;
|
|
3119
|
-
await this.
|
|
3120
|
-
this.
|
|
3299
|
+
await this.replacePersisted([...this.messages]);
|
|
3300
|
+
this.resetWholesaleHistoryState();
|
|
3121
3301
|
}
|
|
3122
3302
|
const compacted = compactMessages(this.messages, threshold, {
|
|
3123
3303
|
overheadTokens: toolSchemaTokens + calibration
|
|
3124
3304
|
});
|
|
3125
3305
|
if (compacted.compacted) {
|
|
3126
3306
|
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
3127
|
-
await this.
|
|
3307
|
+
await this.replacePersisted([...this.messages]);
|
|
3128
3308
|
this.turnMarkers = [];
|
|
3129
|
-
this.
|
|
3309
|
+
this.resetWholesaleHistoryState();
|
|
3310
|
+
}
|
|
3311
|
+
if (this.historyTokens() + toolSchemaTokens + calibration > requestBudget) {
|
|
3312
|
+
const emergency = emergencyElideContent(
|
|
3313
|
+
this.messages,
|
|
3314
|
+
requestBudget,
|
|
3315
|
+
toolSchemaTokens + calibration
|
|
3316
|
+
);
|
|
3317
|
+
if (emergency.elidedCount > 0) {
|
|
3318
|
+
this.messages = emergency.messages;
|
|
3319
|
+
await this.replacePersisted([...this.messages]);
|
|
3320
|
+
this.resetWholesaleHistoryState();
|
|
3321
|
+
yield {
|
|
3322
|
+
type: "notice",
|
|
3323
|
+
message: `Emergency context reduction elided ${emergency.elidedCount} oversized message(s).`
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
if (!emergency.withinBudget) {
|
|
3327
|
+
yield {
|
|
3328
|
+
type: "error",
|
|
3329
|
+
error: {
|
|
3330
|
+
name: "ContextLimitError",
|
|
3331
|
+
message: `The request still exceeds the context window after compaction and emergency elision (${emergency.estimatedTokens}/${requestBudget} estimated tokens). Start a new session or use a model with a larger context window.`
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
await this.runStopHooks("error");
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3130
3337
|
}
|
|
3131
|
-
const requestLocalTokens =
|
|
3338
|
+
const requestLocalTokens = this.historyTokens() + toolSchemaTokens;
|
|
3132
3339
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
3133
3340
|
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
3134
3341
|
let text = "";
|
|
@@ -3180,7 +3387,8 @@ ${finished.result}`
|
|
|
3180
3387
|
serverTokens: event.usage.promptTokens
|
|
3181
3388
|
};
|
|
3182
3389
|
}
|
|
3183
|
-
|
|
3390
|
+
if (event.usage) this.recordUsage(event.usage);
|
|
3391
|
+
yield event;
|
|
3184
3392
|
} else if (event.type === "error") {
|
|
3185
3393
|
failure = event.error;
|
|
3186
3394
|
} else {
|
|
@@ -3240,6 +3448,7 @@ ${finished.result}`
|
|
|
3240
3448
|
}
|
|
3241
3449
|
}
|
|
3242
3450
|
if (failure) {
|
|
3451
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
3243
3452
|
yield { type: "error", error: failure };
|
|
3244
3453
|
await this.runStopHooks("error");
|
|
3245
3454
|
return;
|
|
@@ -3338,8 +3547,10 @@ ${finished.result}`
|
|
|
3338
3547
|
this.getAuxModel(),
|
|
3339
3548
|
inputText,
|
|
3340
3549
|
text,
|
|
3550
|
+
toolOutcomes.join("\n"),
|
|
3341
3551
|
signal,
|
|
3342
|
-
this.getAuxTemperature()
|
|
3552
|
+
this.getAuxTemperature(),
|
|
3553
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName())
|
|
3343
3554
|
);
|
|
3344
3555
|
if (complete === false) {
|
|
3345
3556
|
reason = "completion check reports the task unfinished";
|
|
@@ -3434,6 +3645,10 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3434
3645
|
if (call.name === "todo_write" && !result.isError) {
|
|
3435
3646
|
openTodos = pendingTodoTitles(call.args);
|
|
3436
3647
|
}
|
|
3648
|
+
toolOutcomes.push(
|
|
3649
|
+
`${call.name} (${result.isError ? "failed" : "succeeded"}): ${result.content.slice(0, 500)}`
|
|
3650
|
+
);
|
|
3651
|
+
if (toolOutcomes.length > 20) toolOutcomes.shift();
|
|
3437
3652
|
yield {
|
|
3438
3653
|
type: "tool-result",
|
|
3439
3654
|
id: call.id,
|
|
@@ -3458,8 +3673,7 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3458
3673
|
]
|
|
3459
3674
|
};
|
|
3460
3675
|
this.messages.push({ message: synthetic });
|
|
3461
|
-
await this.persist(synthetic)
|
|
3462
|
-
});
|
|
3676
|
+
await this.persist(synthetic);
|
|
3463
3677
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
3464
3678
|
}
|
|
3465
3679
|
for (const { message, meta } of pendingImages) {
|
|
@@ -3521,7 +3735,8 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3521
3735
|
dropped,
|
|
3522
3736
|
this.getAuxModel(),
|
|
3523
3737
|
signal,
|
|
3524
|
-
this.getAuxTemperature()
|
|
3738
|
+
this.getAuxTemperature(),
|
|
3739
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName())
|
|
3525
3740
|
);
|
|
3526
3741
|
if (summary.trim().length === 0) return compacted.messages;
|
|
3527
3742
|
const messages = compacted.messages.slice();
|
|
@@ -3546,7 +3761,8 @@ ${summary}`
|
|
|
3546
3761
|
providerMetadata: this.opts.providerMetadata,
|
|
3547
3762
|
idleTimeoutMs: (this.opts.streamIdleTimeoutSec ?? this.opts.config.streamIdleTimeoutSec) * 1e3 * timeoutScale,
|
|
3548
3763
|
firstPartTimeoutMs: (this.opts.streamFirstChunkTimeoutSec ?? this.opts.config.streamFirstChunkTimeoutSec) * 1e3 * timeoutScale,
|
|
3549
|
-
temperature: this.opts.temperature
|
|
3764
|
+
temperature: this.opts.temperature,
|
|
3765
|
+
maxTokens: this.opts.maxTokens
|
|
3550
3766
|
})) {
|
|
3551
3767
|
if (event.type === "finish" && event.truncated) {
|
|
3552
3768
|
yield {
|
|
@@ -3632,53 +3848,6 @@ ${summary}`
|
|
|
3632
3848
|
return empty;
|
|
3633
3849
|
}
|
|
3634
3850
|
}
|
|
3635
|
-
// Accumulates a settled subagent's usage. When the child provably ran on a
|
|
3636
|
-
// different, fully priced model, the counts are first converted into
|
|
3637
|
-
// this-model-priced equivalents (convertUsagePricing): session cost is
|
|
3638
|
-
// token totals × the active model's price, so the conversion keeps the $
|
|
3639
|
-
// estimate accurate where raw sums would misprice. Price lookup goes by
|
|
3640
|
-
// the [[models]] entry names both loops were resolved from, not model id.
|
|
3641
|
-
addSubagentUsage(usage, childModel, childModelName) {
|
|
3642
|
-
const converted = convertUsagePricing(usage, childModel, this.opts.model, this.opts.config, {
|
|
3643
|
-
fromName: childModelName,
|
|
3644
|
-
toName: this.opts.modelName
|
|
3645
|
-
});
|
|
3646
|
-
const pending = this.subagentUsage ?? {
|
|
3647
|
-
promptTokens: 0,
|
|
3648
|
-
completionTokens: 0,
|
|
3649
|
-
totalTokens: 0
|
|
3650
|
-
};
|
|
3651
|
-
pending.promptTokens += converted.promptTokens;
|
|
3652
|
-
pending.completionTokens += converted.completionTokens;
|
|
3653
|
-
pending.totalTokens += usageTotalTokens(converted);
|
|
3654
|
-
if (converted.cachedPromptTokens) {
|
|
3655
|
-
pending.cachedPromptTokens = (pending.cachedPromptTokens ?? 0) + converted.cachedPromptTokens;
|
|
3656
|
-
}
|
|
3657
|
-
if (converted.cacheReadInputTokens) {
|
|
3658
|
-
pending.cacheReadInputTokens = (pending.cacheReadInputTokens ?? 0) + converted.cacheReadInputTokens;
|
|
3659
|
-
}
|
|
3660
|
-
this.subagentUsage = pending;
|
|
3661
|
-
}
|
|
3662
|
-
// Folds pending subagent usage into an outgoing finish event (and clears
|
|
3663
|
-
// it). A finish without usage reports nothing to consumers, so the pending
|
|
3664
|
-
// amount is kept for the next finish that carries usage instead. Totals
|
|
3665
|
-
// sum the billed classes (usageTotalTokens), not whatever extra classes a
|
|
3666
|
-
// provider folded into its reported totalTokens.
|
|
3667
|
-
foldSubagentUsage(event) {
|
|
3668
|
-
const pending = this.subagentUsage;
|
|
3669
|
-
if (!pending || !event.usage) return event;
|
|
3670
|
-
this.subagentUsage = null;
|
|
3671
|
-
const usage = {
|
|
3672
|
-
promptTokens: event.usage.promptTokens + pending.promptTokens,
|
|
3673
|
-
completionTokens: event.usage.completionTokens + pending.completionTokens,
|
|
3674
|
-
totalTokens: usageTotalTokens(event.usage) + usageTotalTokens(pending)
|
|
3675
|
-
};
|
|
3676
|
-
const cachedPromptTokens = (event.usage.cachedPromptTokens ?? 0) + (pending.cachedPromptTokens ?? 0);
|
|
3677
|
-
const cacheReadInputTokens = (event.usage.cacheReadInputTokens ?? 0) + (pending.cacheReadInputTokens ?? 0);
|
|
3678
|
-
if (cachedPromptTokens > 0) usage.cachedPromptTokens = cachedPromptTokens;
|
|
3679
|
-
if (cacheReadInputTokens > 0) usage.cacheReadInputTokens = cacheReadInputTokens;
|
|
3680
|
-
return { ...event, usage };
|
|
3681
|
-
}
|
|
3682
3851
|
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3683
3852
|
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3684
3853
|
// times in a row; 0 disables. Any different signature resets the streak.
|
|
@@ -3774,7 +3943,7 @@ export {
|
|
|
3774
3943
|
compactMessages,
|
|
3775
3944
|
WORKING_DIFF_MAX_LINES,
|
|
3776
3945
|
isGitRepo,
|
|
3777
|
-
|
|
3946
|
+
getGitSummaryCachedAsync,
|
|
3778
3947
|
collectCommitContext,
|
|
3779
3948
|
buildCommitPrompt,
|
|
3780
3949
|
collectWorkingDiff,
|
|
@@ -3787,4 +3956,4 @@ export {
|
|
|
3787
3956
|
attachTurnContext,
|
|
3788
3957
|
AgentLoop
|
|
3789
3958
|
};
|
|
3790
|
-
//# sourceMappingURL=chunk-
|
|
3959
|
+
//# sourceMappingURL=chunk-4HANB3DJ.js.map
|