@cryer/star-cli 0.3.9 → 0.3.11
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 +11 -2
- package/README.zh-CN.md +11 -2
- package/dist/{chunk-RE3NOULF.js → chunk-63CGUF3F.js} +333 -40
- package/dist/chunk-63CGUF3F.js.map +1 -0
- package/dist/{chunk-VJFWTXWN.js → chunk-GYXF6NPC.js} +32 -7
- package/dist/chunk-GYXF6NPC.js.map +1 -0
- package/dist/{chunk-A5PXPWHB.js → chunk-LSYGHLRU.js} +13 -1
- package/dist/chunk-LSYGHLRU.js.map +1 -0
- package/dist/{chunk-56ZIA57D.js → chunk-N4MIQSNZ.js} +17 -6
- package/dist/chunk-N4MIQSNZ.js.map +1 -0
- package/dist/{chunk-OIJYNQFZ.js → chunk-Q5FJN4LN.js} +305 -50
- package/dist/chunk-Q5FJN4LN.js.map +1 -0
- package/dist/{loop-PVOSF6LR.js → loop-AY4PXRVP.js} +5 -5
- package/dist/main.js +7 -8
- package/dist/main.js.map +1 -1
- package/dist/{repl-OO7C6V7M.js → repl-YBNTJOSB.js} +287 -51
- package/dist/repl-YBNTJOSB.js.map +1 -0
- package/dist/{tools-JDTACJYX.js → tools-BH55BVIS.js} +3 -3
- package/package.json +26 -16
- package/dist/chunk-56ZIA57D.js.map +0 -1
- package/dist/chunk-A5PXPWHB.js.map +0 -1
- package/dist/chunk-OIJYNQFZ.js.map +0 -1
- package/dist/chunk-RE3NOULF.js.map +0 -1
- package/dist/chunk-VJFWTXWN.js.map +0 -1
- package/dist/repl-OO7C6V7M.js.map +0 -1
- /package/dist/{loop-PVOSF6LR.js.map → loop-AY4PXRVP.js.map} +0 -0
- /package/dist/{tools-JDTACJYX.js.map → tools-BH55BVIS.js.map} +0 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
toCoreMessages,
|
|
13
13
|
toStarMessage,
|
|
14
14
|
trackTree
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-N4MIQSNZ.js";
|
|
16
16
|
import {
|
|
17
17
|
createModel,
|
|
18
18
|
streamActivity
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
ToolRegistry,
|
|
25
25
|
createDefaultRegistry,
|
|
26
26
|
debugStreamLog
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-Q5FJN4LN.js";
|
|
28
28
|
import {
|
|
29
29
|
TodoStore,
|
|
30
30
|
beginTurn,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
stripOversizedImages,
|
|
44
44
|
undoTurnSnapshots,
|
|
45
45
|
userMemoryPath
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-LSYGHLRU.js";
|
|
47
47
|
|
|
48
48
|
// src/agent/loop.ts
|
|
49
49
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -53,9 +53,19 @@ import { generateText } from "ai";
|
|
|
53
53
|
|
|
54
54
|
// src/context/tokens.ts
|
|
55
55
|
import { zodSchema } from "ai";
|
|
56
|
+
import { encode } from "gpt-tokenizer/encoding/cl100k_base";
|
|
56
57
|
var CHARS_PER_TOKEN = 4;
|
|
57
58
|
var MESSAGE_OVERHEAD = 4;
|
|
58
59
|
var IMAGE_PART_TOKENS = 1024;
|
|
60
|
+
var ENCODE_OPTIONS = { disallowedSpecial: /* @__PURE__ */ new Set() };
|
|
61
|
+
function countTextTokens(text) {
|
|
62
|
+
if (text.length === 0) return 0;
|
|
63
|
+
try {
|
|
64
|
+
return encode(text, ENCODE_OPTIONS).length;
|
|
65
|
+
} catch {
|
|
66
|
+
return Math.ceil(textWeight(text) / CHARS_PER_TOKEN);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
function isCjkCode(code) {
|
|
60
70
|
return code >= 12288 && code <= 12543 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519;
|
|
61
71
|
}
|
|
@@ -66,26 +76,29 @@ function textWeight(text) {
|
|
|
66
76
|
}
|
|
67
77
|
return text.length + cjk * (CHARS_PER_TOKEN - 1);
|
|
68
78
|
}
|
|
69
|
-
function
|
|
79
|
+
function roughTextTokens(text) {
|
|
80
|
+
return Math.ceil(textWeight(text) / CHARS_PER_TOKEN);
|
|
81
|
+
}
|
|
82
|
+
function contentTokenLength(message) {
|
|
70
83
|
if (message.role === "tool") {
|
|
71
|
-
let
|
|
84
|
+
let tokens2 = 0;
|
|
72
85
|
for (const part of message.content) {
|
|
73
|
-
|
|
86
|
+
tokens2 += countTextTokens(JSON.stringify(part.result ?? null));
|
|
74
87
|
}
|
|
75
|
-
return
|
|
88
|
+
return tokens2;
|
|
76
89
|
}
|
|
77
90
|
if (typeof message.content === "string") {
|
|
78
|
-
return
|
|
91
|
+
return countTextTokens(message.content);
|
|
79
92
|
}
|
|
80
|
-
let
|
|
93
|
+
let tokens = 0;
|
|
81
94
|
for (const part of message.content) {
|
|
82
95
|
if (part.type === "text" || part.type === "reasoning") {
|
|
83
|
-
|
|
96
|
+
tokens += countTextTokens(part.text);
|
|
84
97
|
} else if (part.type === "tool-call") {
|
|
85
|
-
|
|
98
|
+
tokens += countTextTokens(part.toolName) + countTextTokens(JSON.stringify(part.args ?? null));
|
|
86
99
|
}
|
|
87
100
|
}
|
|
88
|
-
return
|
|
101
|
+
return tokens;
|
|
89
102
|
}
|
|
90
103
|
function imagePartCount(message) {
|
|
91
104
|
if (typeof message.content === "string" || message.role === "tool") return 0;
|
|
@@ -95,7 +108,7 @@ var tokenCache = /* @__PURE__ */ new WeakMap();
|
|
|
95
108
|
function estimateMessageTokens(message) {
|
|
96
109
|
const cached = tokenCache.get(message);
|
|
97
110
|
if (cached !== void 0) return cached;
|
|
98
|
-
const tokens =
|
|
111
|
+
const tokens = contentTokenLength(message) + MESSAGE_OVERHEAD + imagePartCount(message) * IMAGE_PART_TOKENS;
|
|
99
112
|
tokenCache.set(message, tokens);
|
|
100
113
|
return tokens;
|
|
101
114
|
}
|
|
@@ -123,14 +136,14 @@ function parametersWireJson(parameters) {
|
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
function estimateToolSchemaTokens(tools) {
|
|
126
|
-
let
|
|
139
|
+
let tokens = 0;
|
|
127
140
|
for (const [name, tool] of Object.entries(tools)) {
|
|
128
|
-
|
|
141
|
+
tokens += countTextTokens(name);
|
|
129
142
|
const description = tool.description;
|
|
130
|
-
if (typeof description === "string")
|
|
131
|
-
|
|
143
|
+
if (typeof description === "string") tokens += countTextTokens(description);
|
|
144
|
+
tokens += countTextTokens(parametersWireJson(tool.parameters));
|
|
132
145
|
}
|
|
133
|
-
return
|
|
146
|
+
return tokens;
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
// src/context/compaction.ts
|
|
@@ -231,6 +244,7 @@ ${transcript}`,
|
|
|
231
244
|
}
|
|
232
245
|
function compactMessages(messages, maxTokens, opts = {}) {
|
|
233
246
|
const limit = opts.force ? 0 : maxTokens;
|
|
247
|
+
const minKept = opts.force ? 1 : MIN_KEPT_MESSAGES;
|
|
234
248
|
const overhead = opts.overheadTokens ?? 0;
|
|
235
249
|
const tokens = messages.map((star) => estimateMessageTokens(star.message));
|
|
236
250
|
let total = overhead;
|
|
@@ -259,7 +273,7 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
259
273
|
let droppedCount = 0;
|
|
260
274
|
let droppedTokens = 0;
|
|
261
275
|
for (const turn of turns) {
|
|
262
|
-
if (restCount - droppedCount - turn.length <
|
|
276
|
+
if (restCount - droppedCount - turn.length < minKept) {
|
|
263
277
|
break;
|
|
264
278
|
}
|
|
265
279
|
droppedCount += turn.length;
|
|
@@ -283,6 +297,69 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
283
297
|
};
|
|
284
298
|
}
|
|
285
299
|
|
|
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
|
+
|
|
286
363
|
// src/core/events.ts
|
|
287
364
|
function usageTotalTokens(usage) {
|
|
288
365
|
const summed = usage.promptTokens + usage.completionTokens;
|
|
@@ -650,10 +727,10 @@ function summarizeStreamError(error) {
|
|
|
650
727
|
}
|
|
651
728
|
|
|
652
729
|
// src/llm/stream.ts
|
|
653
|
-
import { streamText } from "ai";
|
|
654
|
-
var DEFAULT_IDLE_TIMEOUT_MS =
|
|
730
|
+
import { InvalidToolArgumentsError, streamText } from "ai";
|
|
731
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 6e4;
|
|
655
732
|
var DEFAULT_FIRST_PART_TIMEOUT_MS = 12e4;
|
|
656
|
-
var
|
|
733
|
+
var CONTENT_PART_TYPES = /* @__PURE__ */ new Set(["text-delta", "tool-call", "tool-call-delta"]);
|
|
657
734
|
var MAX_KEEPALIVE_SILENCE_MS = 10 * 6e4;
|
|
658
735
|
var CACHE_READ_KEYS = ["cacheReadInputTokens", "cache_read_input_tokens"];
|
|
659
736
|
var CACHED_PROMPT_KEYS = ["cachedPromptTokens", "cached_prompt_tokens", "cachedTokens"];
|
|
@@ -732,6 +809,7 @@ async function* streamChat(opts) {
|
|
|
732
809
|
let deltas = 0;
|
|
733
810
|
let hasContent = false;
|
|
734
811
|
const argsProgress = /* @__PURE__ */ new Map();
|
|
812
|
+
let invalidArgsCounter = 0;
|
|
735
813
|
let lastPartAt = Date.now();
|
|
736
814
|
debugStreamLog("request", {
|
|
737
815
|
messages: opts.messages.length,
|
|
@@ -791,7 +869,7 @@ async function* streamChat(opts) {
|
|
|
791
869
|
}
|
|
792
870
|
const part = next.value;
|
|
793
871
|
lastPartAt = Date.now();
|
|
794
|
-
if (
|
|
872
|
+
if (CONTENT_PART_TYPES.has(part.type)) seenContent = true;
|
|
795
873
|
switch (part.type) {
|
|
796
874
|
case "text-delta":
|
|
797
875
|
deltas++;
|
|
@@ -804,6 +882,7 @@ async function* streamChat(opts) {
|
|
|
804
882
|
break;
|
|
805
883
|
case "tool-call":
|
|
806
884
|
hasContent = true;
|
|
885
|
+
argsProgress.delete(part.toolCallId);
|
|
807
886
|
yield {
|
|
808
887
|
type: "tool-call",
|
|
809
888
|
id: part.toolCallId,
|
|
@@ -843,6 +922,22 @@ async function* streamChat(opts) {
|
|
|
843
922
|
usage: part.usage,
|
|
844
923
|
deltas
|
|
845
924
|
});
|
|
925
|
+
for (const [id, progress] of argsProgress) {
|
|
926
|
+
debugStreamLog("truncated-tool-call", {
|
|
927
|
+
toolCallId: id,
|
|
928
|
+
toolName: progress.name,
|
|
929
|
+
chars: progress.chars,
|
|
930
|
+
finishReason: part.finishReason
|
|
931
|
+
});
|
|
932
|
+
yield {
|
|
933
|
+
type: "tool-call",
|
|
934
|
+
id,
|
|
935
|
+
name: progress.name,
|
|
936
|
+
args: {},
|
|
937
|
+
invalidArgs: `the arguments were cut off mid-stream after ${progress.chars} characters (finish reason: ${part.finishReason}) \u2014 the reply hit the output or context limit before the JSON completed. Reissue ${progress.name} with complete arguments; for large content, split it into several smaller calls.`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
argsProgress.clear();
|
|
846
941
|
yield {
|
|
847
942
|
type: "finish",
|
|
848
943
|
finishReason: part.finishReason,
|
|
@@ -857,6 +952,29 @@ async function* streamChat(opts) {
|
|
|
857
952
|
}
|
|
858
953
|
case "error": {
|
|
859
954
|
if (opts.abortSignal?.aborted) return;
|
|
955
|
+
if (InvalidToolArgumentsError.isInstance(part.error)) {
|
|
956
|
+
const invalid = part.error;
|
|
957
|
+
let args = invalid.toolArgs;
|
|
958
|
+
try {
|
|
959
|
+
args = JSON.parse(invalid.toolArgs);
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
const started = [...argsProgress.entries()].reverse().find(([, progress]) => progress.name === invalid.toolName);
|
|
963
|
+
debugStreamLog("invalid-tool-args", {
|
|
964
|
+
toolName: invalid.toolName,
|
|
965
|
+
recoveredId: started?.[0] ?? null
|
|
966
|
+
});
|
|
967
|
+
yield {
|
|
968
|
+
type: "tool-call",
|
|
969
|
+
id: started?.[0] ?? `invalid-args-${++invalidArgsCounter}`,
|
|
970
|
+
name: invalid.toolName,
|
|
971
|
+
args,
|
|
972
|
+
// The zod detail tells the model exactly which field to fix;
|
|
973
|
+
// capped because the message embeds the full rejected value.
|
|
974
|
+
invalidArgs: invalid.message.length > 2e3 ? `${invalid.message.slice(0, 2e3)}\u2026` : invalid.message
|
|
975
|
+
};
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
860
978
|
const error = toStreamErrorInfo(part.error);
|
|
861
979
|
debugStreamLog("error-part", { seenContent, deltas, error: summarizeStreamError(error) });
|
|
862
980
|
yield { type: "error", error };
|
|
@@ -1209,7 +1327,15 @@ function buildAllowRule(req) {
|
|
|
1209
1327
|
}
|
|
1210
1328
|
|
|
1211
1329
|
// src/permissions/gate.ts
|
|
1212
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
1330
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
1331
|
+
"write_file",
|
|
1332
|
+
"edit_file",
|
|
1333
|
+
"read_file",
|
|
1334
|
+
"read_image",
|
|
1335
|
+
"grep",
|
|
1336
|
+
"glob",
|
|
1337
|
+
"code_outline"
|
|
1338
|
+
]);
|
|
1213
1339
|
var FORK_BOMB = /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/;
|
|
1214
1340
|
var MAX_ANALYSIS_DEPTH = 8;
|
|
1215
1341
|
var SH_FAMILY = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ash"]);
|
|
@@ -1981,7 +2107,8 @@ import { randomUUID } from "crypto";
|
|
|
1981
2107
|
import { z as z2 } from "zod";
|
|
1982
2108
|
var MAX_SUBAGENT_DEPTH = 1;
|
|
1983
2109
|
var MAX_REPORT_CHARS = 2e4;
|
|
1984
|
-
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
|
|
2110
|
+
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.";
|
|
2111
|
+
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.";
|
|
1985
2112
|
function convertUsagePricing(usage, from, to, config, names) {
|
|
1986
2113
|
if (from === to) return usage;
|
|
1987
2114
|
const fromCfg = findModelPricing(config, names?.fromName, from.modelId);
|
|
@@ -2019,7 +2146,7 @@ function findModelPricing(config, name, modelId) {
|
|
|
2019
2146
|
return config.models.find((m) => m.model === modelId);
|
|
2020
2147
|
}
|
|
2021
2148
|
async function runSubagent(deps, args, cwd, signal, agentIdRef) {
|
|
2022
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2149
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-AY4PXRVP.js");
|
|
2023
2150
|
const child = new AgentLoop2({
|
|
2024
2151
|
model: deps.model,
|
|
2025
2152
|
registry: createDefaultRegistry(),
|
|
@@ -2027,13 +2154,18 @@ async function runSubagent(deps, args, cwd, signal, agentIdRef) {
|
|
|
2027
2154
|
cwd,
|
|
2028
2155
|
system: deps.system ? `${deps.system}
|
|
2029
2156
|
|
|
2030
|
-
${SUBAGENT_PROMPT}
|
|
2157
|
+
${SUBAGENT_PROMPT}${args.read_only ? `
|
|
2158
|
+
|
|
2159
|
+
${READ_ONLY_PROMPT}` : ""}` : `${SUBAGENT_PROMPT}${args.read_only ? `
|
|
2160
|
+
|
|
2161
|
+
${READ_ONLY_PROMPT}` : ""}`,
|
|
2031
2162
|
providerMetadata: deps.providerMetadata,
|
|
2032
2163
|
temperature: deps.temperature,
|
|
2033
2164
|
streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
|
|
2034
2165
|
streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
|
|
2035
2166
|
vision: deps.vision,
|
|
2036
2167
|
subagentDepth: deps.depth + 1,
|
|
2168
|
+
readOnly: args.read_only,
|
|
2037
2169
|
modelName: deps.modelName,
|
|
2038
2170
|
// Read after the dynamic-import await: defaultAgentTasks.start() has
|
|
2039
2171
|
// returned by then, so a background spawn's ref.id is already assigned.
|
|
@@ -2083,8 +2215,11 @@ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
|
2083
2215
|
report = `${report.slice(0, MAX_REPORT_CHARS)}
|
|
2084
2216
|
... (truncated)`;
|
|
2085
2217
|
}
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2218
|
+
const tags = [
|
|
2219
|
+
args.description ? `"${args.description}"` : null,
|
|
2220
|
+
args.read_only ? "read-only" : null
|
|
2221
|
+
].filter(Boolean).join(" ");
|
|
2222
|
+
const stats = `[subagent${tags ? ` ${tags}` : ""}: ${toolCalls} tool call(s)]`;
|
|
2088
2223
|
if (error) {
|
|
2089
2224
|
throw new Error([`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"));
|
|
2090
2225
|
}
|
|
@@ -2094,11 +2229,16 @@ ${report || "(subagent produced no text output)"}`;
|
|
|
2094
2229
|
function createSubagentTool(deps) {
|
|
2095
2230
|
return {
|
|
2096
2231
|
name: "subagent",
|
|
2097
|
-
description: "Spawn a subagent with its own agent loop to handle a focused subtask (research, exploration, or an isolated change). The subagent has the same tools but cannot spawn further subagents. Returns the subagent's final report. Prefer this for self-contained work that would otherwise clutter the main conversation. Set run_in_background to start it without blocking: independent subtasks then run concurrently and their reports are delivered to you automatically when they finish (also inspectable via task_list/task_output/task_kill).",
|
|
2232
|
+
description: "Spawn a subagent with its own agent loop to handle a focused subtask (research, exploration, or an isolated change). The subagent has the same tools but cannot spawn further subagents, and it starts with zero context beyond what you write \u2014 brief it like a colleague who just walked in: state the goal, hand over what you already know (paths, identifiers, prior findings), and say exactly what the report should contain. Returns the subagent's final report. Prefer this for self-contained work that would otherwise clutter the main conversation. Set read_only for research, exploration, or review subtasks that must not modify anything. Set run_in_background to start it without blocking: independent subtasks then run concurrently and their reports are delivered to you automatically when they finish (also inspectable via task_list/task_output/task_kill).",
|
|
2098
2233
|
permission: "exec",
|
|
2099
2234
|
parameters: z2.object({
|
|
2100
|
-
prompt: z2.string().describe(
|
|
2235
|
+
prompt: z2.string().describe(
|
|
2236
|
+
"Complete, self-contained instructions for the subagent: the goal, everything you already know that it needs, and the expected report."
|
|
2237
|
+
),
|
|
2101
2238
|
description: z2.string().optional().describe("Short label for the subtask, shown in the result header."),
|
|
2239
|
+
read_only: z2.boolean().optional().describe(
|
|
2240
|
+
"Restrict the subagent to read-level tools \u2014 no file writes, no shell commands, no task kills (default false). Use for research, exploration, and review subtasks."
|
|
2241
|
+
),
|
|
2102
2242
|
run_in_background: z2.boolean().optional().describe(
|
|
2103
2243
|
"Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
|
|
2104
2244
|
)
|
|
@@ -2203,6 +2343,7 @@ function finalNudgeWarning(autoContinues, maxAutoContinues) {
|
|
|
2203
2343
|
return autoContinues >= maxAutoContinues ? " This is the final automatic continuation \u2014 if your next reply again has no tool calls, nudging stops and the turn is handed back to the user. Finish the work now or summarize what remains." : "";
|
|
2204
2344
|
}
|
|
2205
2345
|
var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
|
|
2346
|
+
var TODO_STALE_NUDGE_STEPS = 20;
|
|
2206
2347
|
function attachTurnContext(messages, context) {
|
|
2207
2348
|
if (!context) return messages;
|
|
2208
2349
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -2241,7 +2382,7 @@ ${request.slice(0, 2e3)}
|
|
|
2241
2382
|
`<reply>
|
|
2242
2383
|
${finalReply.slice(0, 2e3)}
|
|
2243
2384
|
</reply>`,
|
|
2244
|
-
"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? Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
|
|
2385
|
+
"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? 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 \u2014 a reply that claims success without an observed green check is not complete. Or is it only describing, planning, or reporting progress with work still left? Answer with exactly one word: DONE or NOT_DONE."
|
|
2245
2386
|
].join("\n")
|
|
2246
2387
|
});
|
|
2247
2388
|
const verdict = text.trim().toUpperCase();
|
|
@@ -2341,6 +2482,29 @@ var AgentLoop = class {
|
|
|
2341
2482
|
// (the tool map's share of the request window) is computed alongside since
|
|
2342
2483
|
// it depends on the same inputs.
|
|
2343
2484
|
aiToolsCache = null;
|
|
2485
|
+
// Server-calibration baseline for the context estimate: the latest finish
|
|
2486
|
+
// event's provider-reported promptTokens paired with the local estimate of
|
|
2487
|
+
// the exact request that produced it. Non-OpenAI tokenizers can count the
|
|
2488
|
+
// same history far higher than the local cl100k estimate (a qwen endpoint
|
|
2489
|
+
// counted ~30% more on a Chinese-heavy code session), which let the ctx%
|
|
2490
|
+
// display and the auto-compaction trigger lag the real window fill until
|
|
2491
|
+
// the provider itself started truncating replies. The delta refreshes on
|
|
2492
|
+
// every step's finish, so drift is bounded to one step's new content.
|
|
2493
|
+
serverTokenBaseline = null;
|
|
2494
|
+
// Text streamed by the in-flight reply (visible text + reasoning +
|
|
2495
|
+
// completed tool-call args). estimateContextTokens folds a rough token
|
|
2496
|
+
// estimate of it into the status-bar ctx% so the display tracks the
|
|
2497
|
+
// server's real-time occupancy — prompt PLUS generated-so-far, which is
|
|
2498
|
+
// what llama.cpp-style monitors show — instead of lagging it by the whole
|
|
2499
|
+
// in-flight generation (10k reasoning tokens ≈ 8% of a 128k window).
|
|
2500
|
+
// Reset when each stream attempt ends: the persisted reply is then counted
|
|
2501
|
+
// through the history estimate, and reasoning — never resent — drops out.
|
|
2502
|
+
// User steering prompts submitted while a turn is running (the REPL's
|
|
2503
|
+
// "insert now" choice). Drained into the history as synthetic user
|
|
2504
|
+
// messages at the next step boundary — the in-flight model reply and its
|
|
2505
|
+
// tool calls always finish first, so a steer never interrupts mid-step.
|
|
2506
|
+
pendingSteers = [];
|
|
2507
|
+
inflightStreamText = "";
|
|
2344
2508
|
// Lazily resolved auxiliary model (config.smallModel) for cheap side calls
|
|
2345
2509
|
// — compaction summaries, session titles, completion checks. Undefined =
|
|
2346
2510
|
// use the main model; a resolution failure is surfaced once as a notice.
|
|
@@ -2391,6 +2555,9 @@ var AgentLoop = class {
|
|
|
2391
2555
|
})
|
|
2392
2556
|
);
|
|
2393
2557
|
}
|
|
2558
|
+
if (opts.readOnly && depth > 0) {
|
|
2559
|
+
opts.registry?.retain((tool) => tool.permission === "read");
|
|
2560
|
+
}
|
|
2394
2561
|
}
|
|
2395
2562
|
// CoreMessage view of the history (meta stripped): what the model, the
|
|
2396
2563
|
// display and the token estimator consume. Element identity is preserved,
|
|
@@ -2403,12 +2570,56 @@ var AgentLoop = class {
|
|
|
2403
2570
|
getStarMessages() {
|
|
2404
2571
|
return this.messages;
|
|
2405
2572
|
}
|
|
2573
|
+
// Queue a user steering prompt for the running turn. Delivery happens at
|
|
2574
|
+
// the next step boundary (see drainSteers); if the turn ends first the
|
|
2575
|
+
// text stays queued and takeUndeliveredSteers hands it back to the caller.
|
|
2576
|
+
injectUserMessage(text) {
|
|
2577
|
+
const trimmed = text.trim();
|
|
2578
|
+
if (trimmed.length > 0) this.pendingSteers.push(trimmed);
|
|
2579
|
+
}
|
|
2580
|
+
// Steers that never reached a step boundary (turn ended, aborted or
|
|
2581
|
+
// errored first). The caller decides their fate — the REPL re-queues them
|
|
2582
|
+
// as ordinary prompts so user input is never dropped.
|
|
2583
|
+
takeUndeliveredSteers() {
|
|
2584
|
+
return this.pendingSteers.splice(0);
|
|
2585
|
+
}
|
|
2586
|
+
// Append queued steers to the history as synthetic user messages: they
|
|
2587
|
+
// belong to the turn in progress, so /undo, /rewind and compaction must
|
|
2588
|
+
// not treat them as turn boundaries. Returns the number delivered.
|
|
2589
|
+
async drainSteers() {
|
|
2590
|
+
const steers = this.pendingSteers.splice(0);
|
|
2591
|
+
for (const text of steers) {
|
|
2592
|
+
const message = { role: "user", content: text };
|
|
2593
|
+
const meta = { synthetic: "steer" };
|
|
2594
|
+
this.messages.push({ message, meta });
|
|
2595
|
+
await this.persist(message, meta);
|
|
2596
|
+
}
|
|
2597
|
+
return steers.length;
|
|
2598
|
+
}
|
|
2406
2599
|
// Wire-schema token overhead of the active tool set (memoized by
|
|
2407
2600
|
// buildAiTools). The auto-compact budget includes it; surfaced here so the
|
|
2408
2601
|
// status bar ctx% can show the same total the compaction decision uses.
|
|
2409
2602
|
getToolSchemaTokens() {
|
|
2410
2603
|
return this.buildAiTools().schemaTokens;
|
|
2411
2604
|
}
|
|
2605
|
+
// Additive correction from serverTokenBaseline, clamped ≥ 0: an
|
|
2606
|
+
// under-reading provider estimate must pull compaction earlier (the bug
|
|
2607
|
+
// this fixes), but an over-reading one must never push it later than the
|
|
2608
|
+
// local estimate alone would — compacting early is safe, late is fatal.
|
|
2609
|
+
calibrationDelta() {
|
|
2610
|
+
const baseline = this.serverTokenBaseline;
|
|
2611
|
+
if (!baseline) return 0;
|
|
2612
|
+
return Math.max(0, baseline.serverTokens - baseline.localTokens);
|
|
2613
|
+
}
|
|
2614
|
+
// Effective context-window usage in tokens: local BPE estimate + tool
|
|
2615
|
+
// schemas + the server-calibration delta + the in-flight reply's streamed
|
|
2616
|
+
// output. This is the status bar's live ctx% view; the elision/compaction
|
|
2617
|
+
// budgets deliberately use only the first three (history + schemas +
|
|
2618
|
+
// calibration), since in-flight reasoning is never resent and must not
|
|
2619
|
+
// trigger a compaction the next request won't need.
|
|
2620
|
+
estimateContextTokens() {
|
|
2621
|
+
return estimateTokens(this.getMessages()) + this.getToolSchemaTokens() + this.calibrationDelta() + (this.inflightStreamText.length > 0 ? roughTextTokens(this.inflightStreamText) : 0);
|
|
2622
|
+
}
|
|
2412
2623
|
// Auxiliary model for cheap side calls: config.smallModel, resolved once.
|
|
2413
2624
|
// An unresolvable name/key degrades to the main model instead of breaking
|
|
2414
2625
|
// turns; the failure rides out once as a notice at the next turn start.
|
|
@@ -2748,6 +2959,9 @@ var AgentLoop = class {
|
|
|
2748
2959
|
let usedToolsThisTurn = false;
|
|
2749
2960
|
let streamCutContinues = 0;
|
|
2750
2961
|
let openTodos = [];
|
|
2962
|
+
let todoRevision = this.opts.registry.todos.revision;
|
|
2963
|
+
let stepsSinceTodoChange = 0;
|
|
2964
|
+
const todoStaleNudgeSteps = this.opts.todoStaleNudgeSteps ?? TODO_STALE_NUDGE_STEPS;
|
|
2751
2965
|
const maxSteps = config.maxSteps;
|
|
2752
2966
|
let step = 0;
|
|
2753
2967
|
let toolExecutedSinceCheckpoint = false;
|
|
@@ -2784,9 +2998,47 @@ ${finished.result}`
|
|
|
2784
2998
|
await this.persist(note, meta);
|
|
2785
2999
|
}
|
|
2786
3000
|
}
|
|
3001
|
+
const steered = await this.drainSteers();
|
|
3002
|
+
if (steered > 0) {
|
|
3003
|
+
yield {
|
|
3004
|
+
type: "notice",
|
|
3005
|
+
message: `${steered} steering message(s) delivered to the model; taking effect from this step.`
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
const currentTodoRevision = registry.todos.revision;
|
|
3009
|
+
if (currentTodoRevision !== todoRevision) {
|
|
3010
|
+
todoRevision = currentTodoRevision;
|
|
3011
|
+
stepsSinceTodoChange = 0;
|
|
3012
|
+
} else if (stepsSinceTodoChange >= todoStaleNudgeSteps) {
|
|
3013
|
+
const open = registry.todos.list().filter((t) => t.status !== "done");
|
|
3014
|
+
if (open.length > 0) {
|
|
3015
|
+
stepsSinceTodoChange = 0;
|
|
3016
|
+
yield {
|
|
3017
|
+
type: "notice",
|
|
3018
|
+
message: `todo list unchanged for ${todoStaleNudgeSteps} steps with ${open.length} open item(s); reminding the model to update it.`
|
|
3019
|
+
};
|
|
3020
|
+
const reminder = {
|
|
3021
|
+
role: "user",
|
|
3022
|
+
content: `[todo reminder] Your todo list still has ${open.length} open item(s), unchanged for ${todoStaleNudgeSteps} steps: ${open.slice(0, 5).map((t) => `"${t.title}"`).join(
|
|
3023
|
+
", "
|
|
3024
|
+
)}${open.length > 5 ? ", \u2026" : ""}. Update the list with todo_write so it reflects what is done and what you are working on now, then continue working.`
|
|
3025
|
+
};
|
|
3026
|
+
const meta = { synthetic: "nudge" };
|
|
3027
|
+
this.messages.push({ message: reminder, meta });
|
|
3028
|
+
await this.persist(reminder, meta);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2787
3031
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
2788
|
-
const
|
|
2789
|
-
|
|
3032
|
+
const threshold = resolveCompactThreshold(config, maxTokens);
|
|
3033
|
+
const calibration = this.calibrationDelta();
|
|
3034
|
+
const elided = elideStaleContent(this.messages, threshold, toolSchemaTokens + calibration);
|
|
3035
|
+
if (elided) {
|
|
3036
|
+
this.messages = elided.messages;
|
|
3037
|
+
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
3038
|
+
this.opts.registry?.resetVolatileState();
|
|
3039
|
+
}
|
|
3040
|
+
const compacted = compactMessages(this.messages, threshold, {
|
|
3041
|
+
overheadTokens: toolSchemaTokens + calibration
|
|
2790
3042
|
});
|
|
2791
3043
|
if (compacted.compacted) {
|
|
2792
3044
|
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
@@ -2794,6 +3046,7 @@ ${finished.result}`
|
|
|
2794
3046
|
this.turnMarkers = [];
|
|
2795
3047
|
this.opts.registry?.resetVolatileState();
|
|
2796
3048
|
}
|
|
3049
|
+
const requestLocalTokens = estimateTokens(this.getMessages()) + toolSchemaTokens;
|
|
2797
3050
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
2798
3051
|
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
2799
3052
|
let text = "";
|
|
@@ -2814,22 +3067,37 @@ ${finished.result}`
|
|
|
2814
3067
|
lastUsage = void 0;
|
|
2815
3068
|
sawReasoning = false;
|
|
2816
3069
|
wasTruncated = false;
|
|
3070
|
+
this.inflightStreamText = "";
|
|
2817
3071
|
const timeoutScale = Math.min(attempt + 1, 3);
|
|
2818
3072
|
try {
|
|
2819
3073
|
for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
|
|
2820
3074
|
if (event.type === "text-delta") {
|
|
2821
3075
|
text += event.text;
|
|
3076
|
+
this.inflightStreamText += event.text;
|
|
2822
3077
|
yield event;
|
|
2823
3078
|
} else if (event.type === "reasoning") {
|
|
2824
3079
|
sawReasoning = true;
|
|
3080
|
+
this.inflightStreamText += event.text;
|
|
2825
3081
|
yield event;
|
|
2826
3082
|
} else if (event.type === "tool-call") {
|
|
2827
|
-
|
|
3083
|
+
this.inflightStreamText += JSON.stringify(event.args ?? null);
|
|
3084
|
+
toolCalls.push({
|
|
3085
|
+
id: event.id,
|
|
3086
|
+
name: event.name,
|
|
3087
|
+
args: event.args,
|
|
3088
|
+
...event.invalidArgs !== void 0 ? { invalidArgs: event.invalidArgs } : {}
|
|
3089
|
+
});
|
|
2828
3090
|
yield event;
|
|
2829
3091
|
} else if (event.type === "finish") {
|
|
2830
3092
|
lastFinishReason = event.finishReason;
|
|
2831
3093
|
if (event.truncated) wasTruncated = true;
|
|
2832
3094
|
lastUsage = event.usage;
|
|
3095
|
+
if (event.usage && event.usage.promptTokens > 0) {
|
|
3096
|
+
this.serverTokenBaseline = {
|
|
3097
|
+
localTokens: requestLocalTokens,
|
|
3098
|
+
serverTokens: event.usage.promptTokens
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
2833
3101
|
yield this.foldSubagentUsage(event);
|
|
2834
3102
|
} else if (event.type === "error") {
|
|
2835
3103
|
failure = event.error;
|
|
@@ -2842,6 +3110,7 @@ ${finished.result}`
|
|
|
2842
3110
|
failure = toStreamErrorInfo(error);
|
|
2843
3111
|
}
|
|
2844
3112
|
}
|
|
3113
|
+
this.inflightStreamText = "";
|
|
2845
3114
|
if (signal.aborted) {
|
|
2846
3115
|
yield* this.persistInterrupted(text, toolCalls);
|
|
2847
3116
|
await this.runStopHooks("aborted");
|
|
@@ -2894,6 +3163,14 @@ ${finished.result}`
|
|
|
2894
3163
|
return;
|
|
2895
3164
|
}
|
|
2896
3165
|
if (!hasOutput()) {
|
|
3166
|
+
const steered2 = await this.drainSteers();
|
|
3167
|
+
if (steered2 > 0) {
|
|
3168
|
+
yield {
|
|
3169
|
+
type: "notice",
|
|
3170
|
+
message: `${steered2} steering message(s) delivered to the model after an empty reply.`
|
|
3171
|
+
};
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
2897
3174
|
if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
2898
3175
|
autoContinues++;
|
|
2899
3176
|
lastWasNudge = true;
|
|
@@ -2935,6 +3212,16 @@ ${finished.result}`
|
|
|
2935
3212
|
this.messages.push({ message: assistantMessage });
|
|
2936
3213
|
await this.persist(assistantMessage);
|
|
2937
3214
|
if (toolCalls.length === 0) {
|
|
3215
|
+
const steered2 = await this.drainSteers();
|
|
3216
|
+
if (steered2 > 0) {
|
|
3217
|
+
autoContinues = 0;
|
|
3218
|
+
lastWasNudge = false;
|
|
3219
|
+
yield {
|
|
3220
|
+
type: "notice",
|
|
3221
|
+
message: `${steered2} steering message(s) delivered to the model; the turn continues.`
|
|
3222
|
+
};
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
2938
3225
|
if (wasTruncated) {
|
|
2939
3226
|
if (config.permissionMode !== "plan" && streamCutContinues < config.streamMaxRetries) {
|
|
2940
3227
|
streamCutContinues++;
|
|
@@ -3019,10 +3306,12 @@ ${finished.result}`
|
|
|
3019
3306
|
turnTreeReady = null;
|
|
3020
3307
|
}
|
|
3021
3308
|
const answered = /* @__PURE__ */ new Set();
|
|
3309
|
+
const pendingImages = [];
|
|
3022
3310
|
try {
|
|
3023
3311
|
for (const call of toolCalls) {
|
|
3024
3312
|
if (signal.aborted) break;
|
|
3025
|
-
const refusal =
|
|
3313
|
+
const refusal = call.invalidArgs !== void 0 ? `Tool call not executed \u2014 ${call.invalidArgs}
|
|
3314
|
+
Reissue ${call.name} with valid arguments.` : this.doomLoopRefusal(call);
|
|
3026
3315
|
if (refusal !== null) {
|
|
3027
3316
|
yield { type: "notice", message: refusal };
|
|
3028
3317
|
} else {
|
|
@@ -3058,9 +3347,7 @@ ${finished.result}`
|
|
|
3058
3347
|
}))
|
|
3059
3348
|
]
|
|
3060
3349
|
};
|
|
3061
|
-
|
|
3062
|
-
this.messages.push({ message: imageMessage, meta });
|
|
3063
|
-
await this.persist(imageMessage, meta);
|
|
3350
|
+
pendingImages.push({ message: imageMessage, meta: { synthetic: "tool-image" } });
|
|
3064
3351
|
}
|
|
3065
3352
|
if (call.name === "todo_write" && !result.isError) {
|
|
3066
3353
|
openTodos = pendingTodoTitles(call.args);
|
|
@@ -3093,12 +3380,18 @@ ${finished.result}`
|
|
|
3093
3380
|
});
|
|
3094
3381
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
3095
3382
|
}
|
|
3383
|
+
for (const { message, meta } of pendingImages) {
|
|
3384
|
+
this.messages.push({ message, meta });
|
|
3385
|
+
await this.persist(message, meta).catch(() => {
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3096
3388
|
}
|
|
3097
3389
|
if (signal.aborted) {
|
|
3098
3390
|
await this.runStopHooks("aborted");
|
|
3099
3391
|
return;
|
|
3100
3392
|
}
|
|
3101
3393
|
step++;
|
|
3394
|
+
stepsSinceTodoChange++;
|
|
3102
3395
|
}
|
|
3103
3396
|
}
|
|
3104
3397
|
// The permission mode can change at runtime, so the system message is
|
|
@@ -3412,4 +3705,4 @@ export {
|
|
|
3412
3705
|
attachTurnContext,
|
|
3413
3706
|
AgentLoop
|
|
3414
3707
|
};
|
|
3415
|
-
//# sourceMappingURL=chunk-
|
|
3708
|
+
//# sourceMappingURL=chunk-63CGUF3F.js.map
|