@cryer/star-cli 0.3.12 → 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-LSYGHLRU.js → chunk-2ISBRSBA.js} +479 -296
- package/dist/chunk-2ISBRSBA.js.map +1 -0
- package/dist/{chunk-F5RWZMQY.js → chunk-4HANB3DJ.js} +458 -221
- package/dist/chunk-4HANB3DJ.js.map +1 -0
- package/dist/chunk-4IRY44TG.js +0 -0
- package/dist/{chunk-Q5FJN4LN.js → chunk-54KH4JSO.js} +213 -125
- package/dist/chunk-54KH4JSO.js.map +1 -0
- package/dist/{chunk-MYHD3SZB.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-2GAUAYCU.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-SSCURTAH.js → repl-N2H7JDSH.js} +617 -327
- package/dist/repl-N2H7JDSH.js.map +1 -0
- package/dist/{tools-BH55BVIS.js → tools-5RKM7ZI3.js} +3 -3
- package/package.json +16 -25
- package/dist/chunk-2GAUAYCU.js.map +0 -1
- package/dist/chunk-F5RWZMQY.js.map +0 -1
- package/dist/chunk-LSYGHLRU.js.map +0 -1
- package/dist/chunk-MYHD3SZB.js.map +0 -1
- package/dist/chunk-Q5FJN4LN.js.map +0 -1
- package/dist/loop-BYXBNFFY.js +0 -17
- package/dist/repl-SSCURTAH.js.map +0 -1
- /package/dist/{loop-BYXBNFFY.js.map → loop-M64D5YII.js.map} +0 -0
- /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
- /package/dist/{tools-BH55BVIS.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,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
defaultAgentTasks,
|
|
33
34
|
dropTurnSnapshots,
|
|
34
35
|
isOversizedImageError,
|
|
36
|
+
isSecretEnvName,
|
|
35
37
|
isSensitivePath,
|
|
36
38
|
isVisionUnsupportedError,
|
|
37
39
|
pendingTodoTitles,
|
|
@@ -43,7 +45,7 @@ import {
|
|
|
43
45
|
stripOversizedImages,
|
|
44
46
|
undoTurnSnapshots,
|
|
45
47
|
userMemoryPath
|
|
46
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-2ISBRSBA.js";
|
|
47
49
|
|
|
48
50
|
// src/agent/loop.ts
|
|
49
51
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -146,6 +148,157 @@ function estimateToolSchemaTokens(tools) {
|
|
|
146
148
|
return tokens;
|
|
147
149
|
}
|
|
148
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
|
+
|
|
149
302
|
// src/context/compaction.ts
|
|
150
303
|
var MIN_KEPT_MESSAGES = 4;
|
|
151
304
|
function placeholderMessage(droppedCount) {
|
|
@@ -226,20 +379,22 @@ function serializeSummaryInput(messages) {
|
|
|
226
379
|
[... ${total - MAX_SUMMARY_INPUT_CHARS} characters omitted ...]
|
|
227
380
|
${tail}`;
|
|
228
381
|
}
|
|
229
|
-
async function summarizeMessages(messages, model, signal, temperature) {
|
|
382
|
+
async function summarizeMessages(messages, model, signal, temperature, onUsage) {
|
|
230
383
|
const transcript = serializeSummaryInput(messages);
|
|
231
|
-
const { text } = await generateText({
|
|
384
|
+
const { text, usage } = await generateText({
|
|
232
385
|
model,
|
|
233
386
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
234
387
|
prompt: `Summarize this conversation so far:
|
|
235
388
|
|
|
236
389
|
${transcript}`,
|
|
237
390
|
temperature,
|
|
391
|
+
maxRetries: 0,
|
|
238
392
|
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
239
393
|
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
240
394
|
// falls back to the truncation placeholder on any failure.
|
|
241
395
|
abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
|
|
242
396
|
});
|
|
397
|
+
onUsage?.(usage);
|
|
243
398
|
return text.trim();
|
|
244
399
|
}
|
|
245
400
|
function compactMessages(messages, maxTokens, opts = {}) {
|
|
@@ -297,69 +452,6 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
297
452
|
};
|
|
298
453
|
}
|
|
299
454
|
|
|
300
|
-
// src/context/elision.ts
|
|
301
|
-
var ACTIVATION_FRACTION = 0.5;
|
|
302
|
-
var KEEP_RECENT_MESSAGES = 24;
|
|
303
|
-
var MIN_ELISION_TOKENS = 250;
|
|
304
|
-
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]";
|
|
305
|
-
var ELIDED_IMAGE_TEXT = "[image elided to free context \u2014 re-attach it if it is still needed]";
|
|
306
|
-
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";
|
|
307
|
-
function elideStaleContent(messages, thresholdTokens, overheadTokens = 0) {
|
|
308
|
-
let total = overheadTokens;
|
|
309
|
-
for (const star of messages) total += estimateMessageTokens(star.message);
|
|
310
|
-
if (total < thresholdTokens * ACTIVATION_FRACTION) return null;
|
|
311
|
-
const cutoff = messages.length - KEEP_RECENT_MESSAGES;
|
|
312
|
-
if (cutoff <= 0) return null;
|
|
313
|
-
let elidedCount = 0;
|
|
314
|
-
const out = messages.slice();
|
|
315
|
-
for (let i = 0; i < cutoff; i++) {
|
|
316
|
-
const star = out[i];
|
|
317
|
-
if (!star || star.meta?.elided) continue;
|
|
318
|
-
const message = star.message;
|
|
319
|
-
if (message.role === "tool") {
|
|
320
|
-
if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
|
|
321
|
-
out[i] = {
|
|
322
|
-
message: {
|
|
323
|
-
...message,
|
|
324
|
-
content: message.content.map((part) => ({
|
|
325
|
-
...part,
|
|
326
|
-
result: ELIDED_TOOL_RESULT_TEXT
|
|
327
|
-
}))
|
|
328
|
-
},
|
|
329
|
-
meta: { ...star.meta, elided: true }
|
|
330
|
-
};
|
|
331
|
-
elidedCount++;
|
|
332
|
-
} else if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
333
|
-
if (!message.content.some((part) => part.type === "tool-call")) continue;
|
|
334
|
-
if (estimateMessageTokens(message) < MIN_ELISION_TOKENS) continue;
|
|
335
|
-
out[i] = {
|
|
336
|
-
message: {
|
|
337
|
-
...message,
|
|
338
|
-
content: message.content.map(
|
|
339
|
-
(part) => part.type === "tool-call" ? { ...part, args: { elided: ELIDED_TOOL_CALL_ARGS_TEXT } } : part
|
|
340
|
-
)
|
|
341
|
-
},
|
|
342
|
-
meta: { ...star.meta, elided: true }
|
|
343
|
-
};
|
|
344
|
-
elidedCount++;
|
|
345
|
-
} else if (message.role === "user" && Array.isArray(message.content)) {
|
|
346
|
-
if (!message.content.some((part) => part.type === "image" || part.type === "file")) continue;
|
|
347
|
-
out[i] = {
|
|
348
|
-
message: {
|
|
349
|
-
...message,
|
|
350
|
-
content: message.content.map(
|
|
351
|
-
(part) => part.type === "image" || part.type === "file" ? { type: "text", text: ELIDED_IMAGE_TEXT } : part
|
|
352
|
-
)
|
|
353
|
-
},
|
|
354
|
-
meta: { ...star.meta, elided: true }
|
|
355
|
-
};
|
|
356
|
-
elidedCount++;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (elidedCount === 0) return null;
|
|
360
|
-
return { messages: out, elidedCount };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
455
|
// src/core/events.ts
|
|
364
456
|
function usageTotalTokens(usage) {
|
|
365
457
|
const summed = usage.promptTokens + usage.completionTokens;
|
|
@@ -374,6 +466,19 @@ function toStreamErrorInfo(error) {
|
|
|
374
466
|
if (typeof raw.statusCode === "number") info2.statusCode = raw.statusCode;
|
|
375
467
|
if (typeof raw.status === "number") info2.statusCode = raw.status;
|
|
376
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
|
+
}
|
|
377
482
|
return info2;
|
|
378
483
|
}
|
|
379
484
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -400,7 +505,7 @@ function toStreamErrorInfo(error) {
|
|
|
400
505
|
}
|
|
401
506
|
|
|
402
507
|
// src/core/git.ts
|
|
403
|
-
import { execFileSync } from "child_process";
|
|
508
|
+
import { execFile, execFileSync } from "child_process";
|
|
404
509
|
var GIT_TIMEOUT_MS = 2e3;
|
|
405
510
|
var GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
406
511
|
var COMMIT_DIFF_MAX_LINES = 2e3;
|
|
@@ -440,6 +545,47 @@ function getGitSummaryCached(cwd) {
|
|
|
440
545
|
gitSummaryCache.set(cwd, { at: now, summary });
|
|
441
546
|
return summary;
|
|
442
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
|
+
}
|
|
443
589
|
function formatGitSummary(summary) {
|
|
444
590
|
const lines = [
|
|
445
591
|
"Git context for the working directory (refreshed each turn):",
|
|
@@ -574,6 +720,10 @@ function runCommand(hook, event, ctx, timeoutSec) {
|
|
|
574
720
|
child.stdin.on("error", () => {
|
|
575
721
|
});
|
|
576
722
|
child.stdin.end(inputJson);
|
|
723
|
+
} else {
|
|
724
|
+
child.stdin?.on("error", () => {
|
|
725
|
+
});
|
|
726
|
+
child.stdin?.end();
|
|
577
727
|
}
|
|
578
728
|
let stderr = "";
|
|
579
729
|
const stderrDecoder = new StringDecoder("utf8");
|
|
@@ -674,7 +824,9 @@ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
674
824
|
"EPIPE",
|
|
675
825
|
"EAI_AGAIN",
|
|
676
826
|
"ENOTFOUND",
|
|
677
|
-
"ECONNABORTED"
|
|
827
|
+
"ECONNABORTED",
|
|
828
|
+
"EHOSTUNREACH",
|
|
829
|
+
"ENETUNREACH"
|
|
678
830
|
]);
|
|
679
831
|
function isNetworkFailure(error) {
|
|
680
832
|
for (const code of [error.code, error.causeCode]) {
|
|
@@ -1088,7 +1240,16 @@ function lexShell(command) {
|
|
|
1088
1240
|
while (end < command.length && /[A-Za-z0-9_]/.test(command[end] ?? "")) {
|
|
1089
1241
|
end += 1;
|
|
1090
1242
|
}
|
|
1091
|
-
|
|
1243
|
+
let name = command.slice(pos + 1, end);
|
|
1244
|
+
if (name.toLowerCase() === "env" && command[end] === ":" && /[A-Za-z_]/.test(command[end + 1] ?? "")) {
|
|
1245
|
+
let varEnd = end + 1;
|
|
1246
|
+
while (varEnd < command.length && /[A-Za-z0-9_]/.test(command[varEnd] ?? "")) {
|
|
1247
|
+
varEnd += 1;
|
|
1248
|
+
}
|
|
1249
|
+
name = command.slice(end + 1, varEnd);
|
|
1250
|
+
end = varEnd;
|
|
1251
|
+
}
|
|
1252
|
+
word += `$${name}`;
|
|
1092
1253
|
return end;
|
|
1093
1254
|
}
|
|
1094
1255
|
word += "$";
|
|
@@ -1101,8 +1262,9 @@ function lexShell(command) {
|
|
|
1101
1262
|
doubleQuoted = false;
|
|
1102
1263
|
i += 1;
|
|
1103
1264
|
} else if (c === "\\") {
|
|
1104
|
-
|
|
1105
|
-
|
|
1265
|
+
const nextCh = command[i + 1];
|
|
1266
|
+
if (nextCh !== void 0 && (nextCh === "$" || nextCh === "`" || nextCh === '"' || nextCh === "\\")) {
|
|
1267
|
+
word += nextCh;
|
|
1106
1268
|
i += 2;
|
|
1107
1269
|
} else {
|
|
1108
1270
|
word += "\\";
|
|
@@ -1213,6 +1375,7 @@ function splitShellSegments(tokens) {
|
|
|
1213
1375
|
}
|
|
1214
1376
|
return segments;
|
|
1215
1377
|
}
|
|
1378
|
+
var DYNAMIC_SHELL_WORD = "\0star-dynamic-word\0";
|
|
1216
1379
|
function segmentWords(segment) {
|
|
1217
1380
|
const words = [];
|
|
1218
1381
|
const redirectTargets = [];
|
|
@@ -1223,11 +1386,16 @@ function segmentWords(segment) {
|
|
|
1223
1386
|
if (target?.kind === "word") {
|
|
1224
1387
|
redirectTargets.push(target.text);
|
|
1225
1388
|
k += 1;
|
|
1389
|
+
} else if (target?.kind === "subst") {
|
|
1390
|
+
redirectTargets.push(DYNAMIC_SHELL_WORD);
|
|
1391
|
+
k += 1;
|
|
1226
1392
|
}
|
|
1227
1393
|
continue;
|
|
1228
1394
|
}
|
|
1229
1395
|
if (token.kind === "word") {
|
|
1230
1396
|
words.push(token.text);
|
|
1397
|
+
} else if (token.kind === "subst") {
|
|
1398
|
+
words.push(DYNAMIC_SHELL_WORD);
|
|
1231
1399
|
}
|
|
1232
1400
|
}
|
|
1233
1401
|
return { words, redirectTargets };
|
|
@@ -1362,25 +1530,67 @@ var EGRESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
1362
1530
|
"scp",
|
|
1363
1531
|
"sftp",
|
|
1364
1532
|
"ftp",
|
|
1365
|
-
"telnet"
|
|
1533
|
+
"telnet",
|
|
1534
|
+
"dig",
|
|
1535
|
+
"nslookup",
|
|
1536
|
+
"host",
|
|
1537
|
+
"certutil",
|
|
1538
|
+
"bitsadmin",
|
|
1539
|
+
"iwr",
|
|
1540
|
+
"irm",
|
|
1541
|
+
"invoke-webrequest",
|
|
1542
|
+
"invoke-restmethod"
|
|
1366
1543
|
]);
|
|
1367
1544
|
var BLOCK_DEVICE = /^\/dev\/(?:sd[a-z]+\d*|xvd[a-z]+\d*|vd[a-z]+\d*|hd[a-z]+\d*|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?)$/i;
|
|
1368
1545
|
function baseName(word) {
|
|
1369
1546
|
const base = word.replace(/\\/g, "/").split("/").pop() ?? word;
|
|
1370
1547
|
return base.toLowerCase().replace(/\.exe$/, "");
|
|
1371
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
|
+
}
|
|
1372
1552
|
function isDangerousDeleteTarget(target) {
|
|
1553
|
+
if (isDynamicTarget(target)) return true;
|
|
1373
1554
|
if (/^\/+$/.test(target)) return true;
|
|
1374
1555
|
const norm = target.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1375
1556
|
if (norm === "/*" || norm === "*") return true;
|
|
1376
1557
|
if (norm === "~" || norm.startsWith("~/") || /^~[^/]/.test(norm)) return true;
|
|
1377
1558
|
if (norm === "." || norm === "./*") return true;
|
|
1378
1559
|
if (/^[a-zA-Z]:$/.test(norm)) return true;
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1560
|
+
if (/^\/[a-zA-Z]$/.test(norm)) return true;
|
|
1561
|
+
const upper = norm.toUpperCase();
|
|
1562
|
+
const homeVars = [
|
|
1563
|
+
"$HOME",
|
|
1564
|
+
"$USERPROFILE",
|
|
1565
|
+
"$HOMEPATH",
|
|
1566
|
+
"$APPDATA",
|
|
1567
|
+
"$LOCALAPPDATA",
|
|
1568
|
+
"$PROGRAMDATA",
|
|
1569
|
+
"$PROGRAMFILES",
|
|
1570
|
+
"$SYSTEMROOT"
|
|
1571
|
+
];
|
|
1572
|
+
if (homeVars.includes(upper)) return true;
|
|
1573
|
+
if (homeVars.some((v) => upper.startsWith(`${v}/`))) return true;
|
|
1574
|
+
if (/^%(USERPROFILE|HOMEDRIVE|HOMEPATH|APPDATA|LOCALAPPDATA|SYSTEMROOT|PROGRAMDATA|PROGRAMFILES|PROGRAMFILES\(X86\))%$/i.test(
|
|
1575
|
+
norm
|
|
1576
|
+
))
|
|
1577
|
+
return true;
|
|
1578
|
+
if (/^%(USERPROFILE|HOMEPATH|APPDATA|LOCALAPPDATA|SYSTEMROOT|PROGRAMDATA|PROGRAMFILES|PROGRAMFILES\(X86\))%\//i.test(
|
|
1579
|
+
norm
|
|
1580
|
+
))
|
|
1581
|
+
return true;
|
|
1582
|
+
const win = /^(?:[a-zA-Z]:|\/[a-zA-Z])\/(.+)$/.exec(norm);
|
|
1583
|
+
if (win) {
|
|
1584
|
+
const rest = (win[1] ?? "").toLowerCase();
|
|
1585
|
+
if (/^(?:windows|program files(?: \(x86\))?|programdata|boot|recovery|system volume information)(?:\/|$)/.test(
|
|
1586
|
+
rest
|
|
1587
|
+
)) {
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
if (/^users(?:\/[^/]+)?$/.test(rest)) return true;
|
|
1591
|
+
}
|
|
1592
|
+
if (/^\/(?:etc|usr|bin|sbin|lib|lib64|boot|opt|var|srv|sys|proc|dev)$/.test(norm)) return true;
|
|
1593
|
+
if (/^\/(?:home|root)(?:\/[^/]+)?$/.test(norm)) return true;
|
|
1384
1594
|
return false;
|
|
1385
1595
|
}
|
|
1386
1596
|
function rmIsDangerous(args) {
|
|
@@ -1614,7 +1824,7 @@ function decodePowerShellEncoded(encoded) {
|
|
|
1614
1824
|
}
|
|
1615
1825
|
function segmentIsDangerous(segment, depth, lex) {
|
|
1616
1826
|
const { words, redirectTargets } = segmentWords(segment);
|
|
1617
|
-
if (redirectTargets.some((target) => BLOCK_DEVICE.test(target))) {
|
|
1827
|
+
if (redirectTargets.some((target) => isDynamicTarget(target) || BLOCK_DEVICE.test(target))) {
|
|
1618
1828
|
return true;
|
|
1619
1829
|
}
|
|
1620
1830
|
const invocation = commandInvocation(words);
|
|
@@ -1656,10 +1866,10 @@ function segmentIsDangerous(segment, depth, lex) {
|
|
|
1656
1866
|
return recursive && targets.some(isDangerousDeleteTarget);
|
|
1657
1867
|
}
|
|
1658
1868
|
case "format":
|
|
1659
|
-
return rest.some((arg) => /^[a-z]:$/i.test(arg));
|
|
1869
|
+
return rest.some((arg) => isDynamicTarget(arg) || /^[a-z]:$/i.test(arg));
|
|
1660
1870
|
case "dd":
|
|
1661
1871
|
return rest.some(
|
|
1662
|
-
(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)))
|
|
1663
1873
|
);
|
|
1664
1874
|
case "find":
|
|
1665
1875
|
return findIsDangerous(rest, depth, lex);
|
|
@@ -1740,6 +1950,10 @@ function tokenTouchesSensitivePath(word) {
|
|
|
1740
1950
|
}
|
|
1741
1951
|
return isSensitivePath(word);
|
|
1742
1952
|
}
|
|
1953
|
+
function tokenReferencesSecretEnv(word) {
|
|
1954
|
+
const m = /\$([A-Za-z_][A-Za-z0-9_]*)/.exec(word);
|
|
1955
|
+
return m !== null && isSecretEnvName(m[1]);
|
|
1956
|
+
}
|
|
1743
1957
|
function bashHygiene(command, lex) {
|
|
1744
1958
|
let sensitive = false;
|
|
1745
1959
|
let egress = false;
|
|
@@ -1751,7 +1965,7 @@ function bashHygiene(command, lex) {
|
|
|
1751
1965
|
for (const segment of splitShellSegments(tokens)) {
|
|
1752
1966
|
const { words, redirectTargets } = segmentWords(segment);
|
|
1753
1967
|
for (const word of [...words, ...redirectTargets]) {
|
|
1754
|
-
if (tokenTouchesSensitivePath(word)) sensitive = true;
|
|
1968
|
+
if (tokenTouchesSensitivePath(word) || tokenReferencesSecretEnv(word)) sensitive = true;
|
|
1755
1969
|
}
|
|
1756
1970
|
const invocation = commandInvocation(words);
|
|
1757
1971
|
if (!invocation) continue;
|
|
@@ -1765,6 +1979,20 @@ function bashHygiene(command, lex) {
|
|
|
1765
1979
|
script = idx >= 0 ? invocation.rest.slice(idx + 1).join(" ") : void 0;
|
|
1766
1980
|
} else if (invocation.name === "eval" && invocation.rest.length > 0) {
|
|
1767
1981
|
script = invocation.rest.join(" ");
|
|
1982
|
+
} else if (invocation.name === "powershell" || invocation.name === "pwsh") {
|
|
1983
|
+
for (let k = 0; k < invocation.rest.length; k += 1) {
|
|
1984
|
+
const arg = invocation.rest[k];
|
|
1985
|
+
if (/^-(c|command)$/i.test(arg)) {
|
|
1986
|
+
const joined = invocation.rest.slice(k + 1).join(" ");
|
|
1987
|
+
script = joined.length > 0 ? joined : void 0;
|
|
1988
|
+
break;
|
|
1989
|
+
}
|
|
1990
|
+
if (/^-(enc|encodedcommand)$/i.test(arg)) {
|
|
1991
|
+
const encoded = invocation.rest[k + 1];
|
|
1992
|
+
script = encoded === void 0 ? void 0 : decodePowerShellEncoded(encoded) ?? void 0;
|
|
1993
|
+
break;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1768
1996
|
}
|
|
1769
1997
|
if (script !== void 0) visit(lexCached(lex, script), depth + 1);
|
|
1770
1998
|
}
|
|
@@ -1940,21 +2168,23 @@ function sanitizeTitle(raw) {
|
|
|
1940
2168
|
} while (title !== previous);
|
|
1941
2169
|
return title.slice(0, TITLE_MAX_CHARS).trim();
|
|
1942
2170
|
}
|
|
1943
|
-
async function generateSessionTitle(userText, model) {
|
|
1944
|
-
const { text } = await generateText2({
|
|
2171
|
+
async function generateSessionTitle(userText, model, onUsage) {
|
|
2172
|
+
const { text, usage } = await generateText2({
|
|
1945
2173
|
model,
|
|
1946
2174
|
system: TITLE_SYSTEM_PROMPT,
|
|
1947
2175
|
prompt: userText.slice(0, TITLE_INPUT_MAX_CHARS),
|
|
1948
2176
|
maxTokens: TITLE_MAX_TOKENS,
|
|
2177
|
+
maxRetries: 0,
|
|
1949
2178
|
abortSignal: AbortSignal.timeout(TITLE_TIMEOUT_MS)
|
|
1950
2179
|
});
|
|
2180
|
+
onUsage?.(usage);
|
|
1951
2181
|
return sanitizeTitle(text);
|
|
1952
2182
|
}
|
|
1953
|
-
function scheduleSessionTitle(store, userText, model) {
|
|
2183
|
+
function scheduleSessionTitle(store, userText, model, onUsage) {
|
|
1954
2184
|
void (async () => {
|
|
1955
2185
|
const meta = await store.meta();
|
|
1956
2186
|
if (meta.title) return;
|
|
1957
|
-
const title = await generateSessionTitle(userText, model);
|
|
2187
|
+
const title = await generateSessionTitle(userText, model, onUsage);
|
|
1958
2188
|
if (!title) return;
|
|
1959
2189
|
const current = await store.meta();
|
|
1960
2190
|
if (current.title) return;
|
|
@@ -2123,44 +2353,8 @@ var MAX_SUBAGENT_DEPTH = 1;
|
|
|
2123
2353
|
var MAX_REPORT_CHARS = 2e4;
|
|
2124
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.";
|
|
2125
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.";
|
|
2126
|
-
function convertUsagePricing(usage, from, to, config, names) {
|
|
2127
|
-
if (from === to) return usage;
|
|
2128
|
-
const fromCfg = findModelPricing(config, names?.fromName, from.modelId);
|
|
2129
|
-
const toCfg = findModelPricing(config, names?.toName, to.modelId);
|
|
2130
|
-
if (fromCfg?.promptPrice === void 0 || fromCfg.completionPrice === void 0 || toCfg?.promptPrice === void 0 || toCfg.completionPrice === void 0 || toCfg.promptPrice <= 0 || toCfg.completionPrice <= 0) {
|
|
2131
|
-
return usage;
|
|
2132
|
-
}
|
|
2133
|
-
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
2134
|
-
const promptUsd = (usage.promptTokens - cached) * fromCfg.promptPrice;
|
|
2135
|
-
const cachedUsd = cached * (fromCfg.cacheReadPrice ?? fromCfg.promptPrice);
|
|
2136
|
-
const cacheReadUsd = (usage.cacheReadInputTokens ?? 0) * (fromCfg.cacheReadPrice ?? 0);
|
|
2137
|
-
const completionUsd = usage.completionTokens * fromCfg.completionPrice;
|
|
2138
|
-
const scaledCached = cachedUsd / (toCfg.cacheReadPrice ?? toCfg.promptPrice);
|
|
2139
|
-
const promptTokens = Math.round(promptUsd / toCfg.promptPrice + scaledCached);
|
|
2140
|
-
const cachedPromptTokens = Math.round(scaledCached);
|
|
2141
|
-
const toCacheReadPrice = toCfg.cacheReadPrice !== void 0 && toCfg.cacheReadPrice > 0 ? toCfg.cacheReadPrice : void 0;
|
|
2142
|
-
const completionTokens = Math.round(
|
|
2143
|
-
completionUsd / toCfg.completionPrice + (toCacheReadPrice === void 0 ? cacheReadUsd / toCfg.completionPrice : 0)
|
|
2144
|
-
);
|
|
2145
|
-
const cacheReadInputTokens = toCacheReadPrice === void 0 ? 0 : Math.round(cacheReadUsd / toCacheReadPrice);
|
|
2146
|
-
const result = {
|
|
2147
|
-
promptTokens,
|
|
2148
|
-
completionTokens,
|
|
2149
|
-
totalTokens: promptTokens + completionTokens
|
|
2150
|
-
};
|
|
2151
|
-
if (cachedPromptTokens > 0) result.cachedPromptTokens = cachedPromptTokens;
|
|
2152
|
-
if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
|
|
2153
|
-
return result;
|
|
2154
|
-
}
|
|
2155
|
-
function findModelPricing(config, name, modelId) {
|
|
2156
|
-
if (name !== void 0) {
|
|
2157
|
-
const byName = config.models.find((m) => m.name === name);
|
|
2158
|
-
if (byName) return byName;
|
|
2159
|
-
}
|
|
2160
|
-
return config.models.find((m) => m.model === modelId);
|
|
2161
|
-
}
|
|
2162
2356
|
async function runSubagent(deps, args, cwd, signal, agentIdRef) {
|
|
2163
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2357
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-M64D5YII.js");
|
|
2164
2358
|
const child = new AgentLoop2({
|
|
2165
2359
|
model: deps.model,
|
|
2166
2360
|
registry: createDefaultRegistry(),
|
|
@@ -2175,6 +2369,7 @@ ${READ_ONLY_PROMPT}` : ""}` : `${SUBAGENT_PROMPT}${args.read_only ? `
|
|
|
2175
2369
|
${READ_ONLY_PROMPT}` : ""}`,
|
|
2176
2370
|
providerMetadata: deps.providerMetadata,
|
|
2177
2371
|
temperature: deps.temperature,
|
|
2372
|
+
maxTokens: deps.maxTokens,
|
|
2178
2373
|
streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
|
|
2179
2374
|
streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
|
|
2180
2375
|
vision: deps.vision,
|
|
@@ -2376,15 +2571,16 @@ ${context}
|
|
|
2376
2571
|
}
|
|
2377
2572
|
return messages;
|
|
2378
2573
|
}
|
|
2379
|
-
async function checkTaskComplete(model, request, finalReply, signal, temperature) {
|
|
2574
|
+
async function checkTaskComplete(model, request, finalReply, toolSummary, signal, temperature, onUsage) {
|
|
2380
2575
|
const controller = new AbortController();
|
|
2381
2576
|
const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
|
|
2382
2577
|
const onAbort = () => controller.abort();
|
|
2383
2578
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2384
2579
|
try {
|
|
2385
|
-
const { text } = await generateText3({
|
|
2580
|
+
const { text, usage } = await generateText3({
|
|
2386
2581
|
model,
|
|
2387
2582
|
maxTokens: 8,
|
|
2583
|
+
maxRetries: 0,
|
|
2388
2584
|
temperature,
|
|
2389
2585
|
abortSignal: controller.signal,
|
|
2390
2586
|
prompt: [
|
|
@@ -2396,9 +2592,14 @@ ${request.slice(0, 2e3)}
|
|
|
2396
2592
|
`<reply>
|
|
2397
2593
|
${finalReply.slice(0, 2e3)}
|
|
2398
2594
|
</reply>`,
|
|
2399
|
-
"
|
|
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."
|
|
2400
2600
|
].join("\n")
|
|
2401
2601
|
});
|
|
2602
|
+
onUsage(usage);
|
|
2402
2603
|
const verdict = text.trim().toUpperCase();
|
|
2403
2604
|
if (verdict.startsWith("NOT_DONE")) return false;
|
|
2404
2605
|
if (verdict.startsWith("DONE")) return true;
|
|
@@ -2462,15 +2663,6 @@ var AgentLoop = class {
|
|
|
2462
2663
|
// each new user turn restarts the streak.
|
|
2463
2664
|
lastToolSignature = null;
|
|
2464
2665
|
repeatedToolCalls = 0;
|
|
2465
|
-
// Token usage subagents consumed, waiting to fold into this loop's next
|
|
2466
|
-
// finish event with usage: sync children report when their tool call
|
|
2467
|
-
// settles, background ones whenever they finish (drained or not).
|
|
2468
|
-
// Consumers meter only root-loop finish events, so folding here bills
|
|
2469
|
-
// subagent spend with zero consumer changes — /cost, /usage and
|
|
2470
|
-
// sessionBudgetUsd all cover it. A background child still running when the
|
|
2471
|
-
// session's last finish has passed never reports (best effort, noted in
|
|
2472
|
-
// agent-tasks cleanup paths).
|
|
2473
|
-
subagentUsage = null;
|
|
2474
2666
|
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
2475
2667
|
// execution so file snapshots attribute root changes to a turn and mark
|
|
2476
2668
|
// subagent changes as excluded from turn-level retraction.
|
|
@@ -2489,6 +2681,7 @@ var AgentLoop = class {
|
|
|
2489
2681
|
// turn context string. Saves a full toCoreMessages + attachTurnContext copy
|
|
2490
2682
|
// per stream attempt, which retried steps used to pay every time.
|
|
2491
2683
|
requestMessagesCache = null;
|
|
2684
|
+
historyTokenSnapshot = null;
|
|
2492
2685
|
// Memoized per (registry, plan flag): tools register on the registry only
|
|
2493
2686
|
// at loop construction, and the plan filter re-reads the live permission
|
|
2494
2687
|
// mode on every call — so a built map stays valid until either input
|
|
@@ -2531,11 +2724,12 @@ var AgentLoop = class {
|
|
|
2531
2724
|
// their stderr surfaces through this callback (REPL system message / stderr
|
|
2532
2725
|
// in print mode).
|
|
2533
2726
|
onHookWarning;
|
|
2727
|
+
onUsage;
|
|
2534
2728
|
constructor(opts) {
|
|
2535
2729
|
this.opts = opts;
|
|
2536
2730
|
const depth = opts.subagentDepth ?? 0;
|
|
2537
2731
|
if (depth > 0) {
|
|
2538
|
-
opts.registry = new ToolRegistry(new TodoStore());
|
|
2732
|
+
opts.registry = new ToolRegistry(new TodoStore({ persistence: "memory" }));
|
|
2539
2733
|
} else {
|
|
2540
2734
|
setTodoPersistGuard(() => {
|
|
2541
2735
|
const mode = this.opts.config.permissionMode;
|
|
@@ -2551,6 +2745,7 @@ var AgentLoop = class {
|
|
|
2551
2745
|
opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
|
|
2552
2746
|
opts.registry?.register(createRememberTool());
|
|
2553
2747
|
if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
|
|
2748
|
+
const subagentSessionStore = opts.sessionStore;
|
|
2554
2749
|
opts.registry.register(
|
|
2555
2750
|
createSubagentTool({
|
|
2556
2751
|
model: opts.model,
|
|
@@ -2559,13 +2754,14 @@ var AgentLoop = class {
|
|
|
2559
2754
|
system: opts.system,
|
|
2560
2755
|
providerMetadata: opts.providerMetadata,
|
|
2561
2756
|
temperature: opts.temperature,
|
|
2757
|
+
maxTokens: opts.maxTokens,
|
|
2562
2758
|
streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
|
|
2563
2759
|
streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
|
|
2564
2760
|
vision: opts.vision,
|
|
2565
2761
|
depth,
|
|
2566
2762
|
modelName: opts.modelName,
|
|
2567
2763
|
getConfirmHandler: () => this.confirmHandler,
|
|
2568
|
-
onUsage: (usage,
|
|
2764
|
+
onUsage: (usage, _childModel, childModelName) => this.recordUsage(usage, childModelName, subagentSessionStore)
|
|
2569
2765
|
})
|
|
2570
2766
|
);
|
|
2571
2767
|
}
|
|
@@ -2631,8 +2827,25 @@ var AgentLoop = class {
|
|
|
2631
2827
|
// budgets deliberately use only the first three (history + schemas +
|
|
2632
2828
|
// calibration), since in-flight reasoning is never resent and must not
|
|
2633
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
|
+
}
|
|
2634
2847
|
estimateContextTokens() {
|
|
2635
|
-
return
|
|
2848
|
+
return this.historyTokens() + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
|
|
2636
2849
|
}
|
|
2637
2850
|
// Auxiliary model for cheap side calls: config.smallModel, resolved once.
|
|
2638
2851
|
// An unresolvable name/key degrades to the main model instead of breaking
|
|
@@ -2657,24 +2870,30 @@ var AgentLoop = class {
|
|
|
2657
2870
|
this.resolveAuxModel();
|
|
2658
2871
|
return this.auxModelValue ? this.auxTemperatureValue : this.opts.temperature;
|
|
2659
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
|
+
}
|
|
2660
2886
|
// Rebinds the per-call provider metadata (e.g. after the reasoning effort
|
|
2661
2887
|
// for the current model changed via /model) without recreating the loop.
|
|
2662
2888
|
setProviderMetadata(metadata) {
|
|
2663
2889
|
this.opts.providerMetadata = metadata;
|
|
2664
2890
|
}
|
|
2665
2891
|
// Swaps the persistence target (/new starts a fresh session mid-REPL).
|
|
2666
|
-
// Title generation is re-armed so the new session gets one after its first
|
|
2667
|
-
// turn, and snapshot checkpoints now flow to the new store. The redo stack
|
|
2668
|
-
// dies with the old session context: its trees describe working-tree states
|
|
2669
|
-
// that no longer line up with the new session's conversation. Subagent
|
|
2670
|
-
// usage still waiting to fold belongs to the old conversation too — it
|
|
2671
|
-
// must not bill the new session's first finish.
|
|
2672
2892
|
setSessionStore(store) {
|
|
2673
2893
|
this.opts.sessionStore = store;
|
|
2674
2894
|
this.titleScheduled = false;
|
|
2675
2895
|
this.titleInput = void 0;
|
|
2676
2896
|
this.redoStack = [];
|
|
2677
|
-
this.subagentUsage = null;
|
|
2678
2897
|
if (store) this.bindSnapshotHooks(store);
|
|
2679
2898
|
}
|
|
2680
2899
|
bindSnapshotHooks(store) {
|
|
@@ -2696,12 +2915,17 @@ var AgentLoop = class {
|
|
|
2696
2915
|
onRemove: (ids) => store.removeCheckpoints(ids)
|
|
2697
2916
|
});
|
|
2698
2917
|
}
|
|
2918
|
+
resetWholesaleHistoryState() {
|
|
2919
|
+
this.serverTokenBaseline = null;
|
|
2920
|
+
this.requestMessagesCache = null;
|
|
2921
|
+
this.historyTokenSnapshot = null;
|
|
2922
|
+
this.opts.registry?.resetVolatileState();
|
|
2923
|
+
}
|
|
2699
2924
|
async loadMessages(messages) {
|
|
2700
2925
|
this.messages = reconcileStarMessages(messages.map(toStarMessage));
|
|
2701
2926
|
this.turnMarkers = [];
|
|
2702
2927
|
this.redoStack = [];
|
|
2703
|
-
this.
|
|
2704
|
-
this.opts.registry?.resetVolatileState();
|
|
2928
|
+
this.resetWholesaleHistoryState();
|
|
2705
2929
|
}
|
|
2706
2930
|
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2707
2931
|
// drop, which snapshot turn's file changes it would revert (undefined =
|
|
@@ -2735,8 +2959,8 @@ var AgentLoop = class {
|
|
|
2735
2959
|
REDO_LABEL_MAX
|
|
2736
2960
|
);
|
|
2737
2961
|
this.messages = result.messages;
|
|
2738
|
-
await this.
|
|
2739
|
-
this.
|
|
2962
|
+
await this.replacePersisted([...this.messages]);
|
|
2963
|
+
this.resetWholesaleHistoryState();
|
|
2740
2964
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2741
2965
|
let turn;
|
|
2742
2966
|
let tree;
|
|
@@ -2819,8 +3043,8 @@ var AgentLoop = class {
|
|
|
2819
3043
|
const removed = this.messages.length - cut;
|
|
2820
3044
|
if (removed === 0) return 0;
|
|
2821
3045
|
this.messages = this.messages.slice(0, cut);
|
|
2822
|
-
await this.
|
|
2823
|
-
this.
|
|
3046
|
+
await this.replacePersisted([...this.messages]);
|
|
3047
|
+
this.resetWholesaleHistoryState();
|
|
2824
3048
|
this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
|
|
2825
3049
|
this.redoStack = [];
|
|
2826
3050
|
return removed;
|
|
@@ -2831,7 +3055,18 @@ var AgentLoop = class {
|
|
|
2831
3055
|
await this.persist(message);
|
|
2832
3056
|
}
|
|
2833
3057
|
async persist(message, meta) {
|
|
2834
|
-
|
|
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
|
+
}
|
|
2835
3070
|
}
|
|
2836
3071
|
// Replaces oversized image parts in the in-memory history with text
|
|
2837
3072
|
// placeholders and rewrites the persisted session, so the failed request
|
|
@@ -2843,7 +3078,8 @@ var AgentLoop = class {
|
|
|
2843
3078
|
message: messages[i] ?? star.message,
|
|
2844
3079
|
meta: star.meta
|
|
2845
3080
|
}));
|
|
2846
|
-
await this.
|
|
3081
|
+
await this.replacePersisted(this.messages);
|
|
3082
|
+
this.resetWholesaleHistoryState();
|
|
2847
3083
|
return removed;
|
|
2848
3084
|
}
|
|
2849
3085
|
// Same rewrite as above, but for text-only endpoints that reject any image
|
|
@@ -2856,7 +3092,8 @@ var AgentLoop = class {
|
|
|
2856
3092
|
message: messages[i] ?? star.message,
|
|
2857
3093
|
meta: star.meta
|
|
2858
3094
|
}));
|
|
2859
|
-
await this.
|
|
3095
|
+
await this.replacePersisted(this.messages);
|
|
3096
|
+
this.resetWholesaleHistoryState();
|
|
2860
3097
|
return removed;
|
|
2861
3098
|
}
|
|
2862
3099
|
// Background title generation runs once per loop, fed by the first real
|
|
@@ -2872,7 +3109,12 @@ var AgentLoop = class {
|
|
|
2872
3109
|
const input = this.titleInput;
|
|
2873
3110
|
if (!input) return;
|
|
2874
3111
|
this.titleScheduled = true;
|
|
2875
|
-
scheduleSessionTitle(
|
|
3112
|
+
scheduleSessionTitle(
|
|
3113
|
+
store,
|
|
3114
|
+
input,
|
|
3115
|
+
this.getAuxModel(),
|
|
3116
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName(), store)
|
|
3117
|
+
);
|
|
2876
3118
|
}
|
|
2877
3119
|
// A user interrupt (Esc) ends the turn mid-stream, before the normal
|
|
2878
3120
|
// assistant-message persistence runs. Whatever text and tool calls already
|
|
@@ -2910,8 +3152,7 @@ var AgentLoop = class {
|
|
|
2910
3152
|
]
|
|
2911
3153
|
};
|
|
2912
3154
|
this.messages.push({ message: synthetic });
|
|
2913
|
-
await this.persist(synthetic)
|
|
2914
|
-
});
|
|
3155
|
+
await this.persist(synthetic);
|
|
2915
3156
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
2916
3157
|
}
|
|
2917
3158
|
}
|
|
@@ -2971,6 +3212,7 @@ var AgentLoop = class {
|
|
|
2971
3212
|
let autoContinues = 0;
|
|
2972
3213
|
let lastWasNudge = false;
|
|
2973
3214
|
let usedToolsThisTurn = false;
|
|
3215
|
+
const toolOutcomes = [];
|
|
2974
3216
|
let streamCutContinues = 0;
|
|
2975
3217
|
let openTodos = [];
|
|
2976
3218
|
let todoRevision = this.opts.registry.todos.revision;
|
|
@@ -3043,24 +3285,57 @@ ${finished.result}`
|
|
|
3043
3285
|
}
|
|
3044
3286
|
}
|
|
3045
3287
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
3046
|
-
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);
|
|
3047
3291
|
const calibration = this.calibrationDelta();
|
|
3048
|
-
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
|
+
});
|
|
3049
3297
|
if (elided) {
|
|
3050
3298
|
this.messages = elided.messages;
|
|
3051
|
-
await this.
|
|
3052
|
-
this.
|
|
3299
|
+
await this.replacePersisted([...this.messages]);
|
|
3300
|
+
this.resetWholesaleHistoryState();
|
|
3053
3301
|
}
|
|
3054
3302
|
const compacted = compactMessages(this.messages, threshold, {
|
|
3055
3303
|
overheadTokens: toolSchemaTokens + calibration
|
|
3056
3304
|
});
|
|
3057
3305
|
if (compacted.compacted) {
|
|
3058
3306
|
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
3059
|
-
await this.
|
|
3307
|
+
await this.replacePersisted([...this.messages]);
|
|
3060
3308
|
this.turnMarkers = [];
|
|
3061
|
-
this.
|
|
3309
|
+
this.resetWholesaleHistoryState();
|
|
3062
3310
|
}
|
|
3063
|
-
|
|
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
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const requestLocalTokens = this.historyTokens() + toolSchemaTokens;
|
|
3064
3339
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
3065
3340
|
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
3066
3341
|
let text = "";
|
|
@@ -3112,7 +3387,8 @@ ${finished.result}`
|
|
|
3112
3387
|
serverTokens: event.usage.promptTokens
|
|
3113
3388
|
};
|
|
3114
3389
|
}
|
|
3115
|
-
|
|
3390
|
+
if (event.usage) this.recordUsage(event.usage);
|
|
3391
|
+
yield event;
|
|
3116
3392
|
} else if (event.type === "error") {
|
|
3117
3393
|
failure = event.error;
|
|
3118
3394
|
} else {
|
|
@@ -3172,6 +3448,7 @@ ${finished.result}`
|
|
|
3172
3448
|
}
|
|
3173
3449
|
}
|
|
3174
3450
|
if (failure) {
|
|
3451
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
3175
3452
|
yield { type: "error", error: failure };
|
|
3176
3453
|
await this.runStopHooks("error");
|
|
3177
3454
|
return;
|
|
@@ -3270,8 +3547,10 @@ ${finished.result}`
|
|
|
3270
3547
|
this.getAuxModel(),
|
|
3271
3548
|
inputText,
|
|
3272
3549
|
text,
|
|
3550
|
+
toolOutcomes.join("\n"),
|
|
3273
3551
|
signal,
|
|
3274
|
-
this.getAuxTemperature()
|
|
3552
|
+
this.getAuxTemperature(),
|
|
3553
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName())
|
|
3275
3554
|
);
|
|
3276
3555
|
if (complete === false) {
|
|
3277
3556
|
reason = "completion check reports the task unfinished";
|
|
@@ -3366,6 +3645,10 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3366
3645
|
if (call.name === "todo_write" && !result.isError) {
|
|
3367
3646
|
openTodos = pendingTodoTitles(call.args);
|
|
3368
3647
|
}
|
|
3648
|
+
toolOutcomes.push(
|
|
3649
|
+
`${call.name} (${result.isError ? "failed" : "succeeded"}): ${result.content.slice(0, 500)}`
|
|
3650
|
+
);
|
|
3651
|
+
if (toolOutcomes.length > 20) toolOutcomes.shift();
|
|
3369
3652
|
yield {
|
|
3370
3653
|
type: "tool-result",
|
|
3371
3654
|
id: call.id,
|
|
@@ -3390,8 +3673,7 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3390
3673
|
]
|
|
3391
3674
|
};
|
|
3392
3675
|
this.messages.push({ message: synthetic });
|
|
3393
|
-
await this.persist(synthetic)
|
|
3394
|
-
});
|
|
3676
|
+
await this.persist(synthetic);
|
|
3395
3677
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
3396
3678
|
}
|
|
3397
3679
|
for (const { message, meta } of pendingImages) {
|
|
@@ -3453,7 +3735,8 @@ Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
|
3453
3735
|
dropped,
|
|
3454
3736
|
this.getAuxModel(),
|
|
3455
3737
|
signal,
|
|
3456
|
-
this.getAuxTemperature()
|
|
3738
|
+
this.getAuxTemperature(),
|
|
3739
|
+
(usage) => this.recordUsage(usage, this.getAuxModelName())
|
|
3457
3740
|
);
|
|
3458
3741
|
if (summary.trim().length === 0) return compacted.messages;
|
|
3459
3742
|
const messages = compacted.messages.slice();
|
|
@@ -3478,7 +3761,8 @@ ${summary}`
|
|
|
3478
3761
|
providerMetadata: this.opts.providerMetadata,
|
|
3479
3762
|
idleTimeoutMs: (this.opts.streamIdleTimeoutSec ?? this.opts.config.streamIdleTimeoutSec) * 1e3 * timeoutScale,
|
|
3480
3763
|
firstPartTimeoutMs: (this.opts.streamFirstChunkTimeoutSec ?? this.opts.config.streamFirstChunkTimeoutSec) * 1e3 * timeoutScale,
|
|
3481
|
-
temperature: this.opts.temperature
|
|
3764
|
+
temperature: this.opts.temperature,
|
|
3765
|
+
maxTokens: this.opts.maxTokens
|
|
3482
3766
|
})) {
|
|
3483
3767
|
if (event.type === "finish" && event.truncated) {
|
|
3484
3768
|
yield {
|
|
@@ -3564,53 +3848,6 @@ ${summary}`
|
|
|
3564
3848
|
return empty;
|
|
3565
3849
|
}
|
|
3566
3850
|
}
|
|
3567
|
-
// Accumulates a settled subagent's usage. When the child provably ran on a
|
|
3568
|
-
// different, fully priced model, the counts are first converted into
|
|
3569
|
-
// this-model-priced equivalents (convertUsagePricing): session cost is
|
|
3570
|
-
// token totals × the active model's price, so the conversion keeps the $
|
|
3571
|
-
// estimate accurate where raw sums would misprice. Price lookup goes by
|
|
3572
|
-
// the [[models]] entry names both loops were resolved from, not model id.
|
|
3573
|
-
addSubagentUsage(usage, childModel, childModelName) {
|
|
3574
|
-
const converted = convertUsagePricing(usage, childModel, this.opts.model, this.opts.config, {
|
|
3575
|
-
fromName: childModelName,
|
|
3576
|
-
toName: this.opts.modelName
|
|
3577
|
-
});
|
|
3578
|
-
const pending = this.subagentUsage ?? {
|
|
3579
|
-
promptTokens: 0,
|
|
3580
|
-
completionTokens: 0,
|
|
3581
|
-
totalTokens: 0
|
|
3582
|
-
};
|
|
3583
|
-
pending.promptTokens += converted.promptTokens;
|
|
3584
|
-
pending.completionTokens += converted.completionTokens;
|
|
3585
|
-
pending.totalTokens += usageTotalTokens(converted);
|
|
3586
|
-
if (converted.cachedPromptTokens) {
|
|
3587
|
-
pending.cachedPromptTokens = (pending.cachedPromptTokens ?? 0) + converted.cachedPromptTokens;
|
|
3588
|
-
}
|
|
3589
|
-
if (converted.cacheReadInputTokens) {
|
|
3590
|
-
pending.cacheReadInputTokens = (pending.cacheReadInputTokens ?? 0) + converted.cacheReadInputTokens;
|
|
3591
|
-
}
|
|
3592
|
-
this.subagentUsage = pending;
|
|
3593
|
-
}
|
|
3594
|
-
// Folds pending subagent usage into an outgoing finish event (and clears
|
|
3595
|
-
// it). A finish without usage reports nothing to consumers, so the pending
|
|
3596
|
-
// amount is kept for the next finish that carries usage instead. Totals
|
|
3597
|
-
// sum the billed classes (usageTotalTokens), not whatever extra classes a
|
|
3598
|
-
// provider folded into its reported totalTokens.
|
|
3599
|
-
foldSubagentUsage(event) {
|
|
3600
|
-
const pending = this.subagentUsage;
|
|
3601
|
-
if (!pending || !event.usage) return event;
|
|
3602
|
-
this.subagentUsage = null;
|
|
3603
|
-
const usage = {
|
|
3604
|
-
promptTokens: event.usage.promptTokens + pending.promptTokens,
|
|
3605
|
-
completionTokens: event.usage.completionTokens + pending.completionTokens,
|
|
3606
|
-
totalTokens: usageTotalTokens(event.usage) + usageTotalTokens(pending)
|
|
3607
|
-
};
|
|
3608
|
-
const cachedPromptTokens = (event.usage.cachedPromptTokens ?? 0) + (pending.cachedPromptTokens ?? 0);
|
|
3609
|
-
const cacheReadInputTokens = (event.usage.cacheReadInputTokens ?? 0) + (pending.cacheReadInputTokens ?? 0);
|
|
3610
|
-
if (cachedPromptTokens > 0) usage.cachedPromptTokens = cachedPromptTokens;
|
|
3611
|
-
if (cacheReadInputTokens > 0) usage.cacheReadInputTokens = cacheReadInputTokens;
|
|
3612
|
-
return { ...event, usage };
|
|
3613
|
-
}
|
|
3614
3851
|
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3615
3852
|
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3616
3853
|
// times in a row; 0 disables. Any different signature resets the streak.
|
|
@@ -3706,7 +3943,7 @@ export {
|
|
|
3706
3943
|
compactMessages,
|
|
3707
3944
|
WORKING_DIFF_MAX_LINES,
|
|
3708
3945
|
isGitRepo,
|
|
3709
|
-
|
|
3946
|
+
getGitSummaryCachedAsync,
|
|
3710
3947
|
collectCommitContext,
|
|
3711
3948
|
buildCommitPrompt,
|
|
3712
3949
|
collectWorkingDiff,
|
|
@@ -3719,4 +3956,4 @@ export {
|
|
|
3719
3956
|
attachTurnContext,
|
|
3720
3957
|
AgentLoop
|
|
3721
3958
|
};
|
|
3722
|
-
//# sourceMappingURL=chunk-
|
|
3959
|
+
//# sourceMappingURL=chunk-4HANB3DJ.js.map
|