@cryer/star-cli 0.2.2 → 0.3.1
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 +37 -15
- package/dist/{chunk-GN236FD4.js → chunk-K4SIXWCI.js} +824 -274
- package/dist/chunk-K4SIXWCI.js.map +1 -0
- package/dist/{loop-ADVWZA62.js → loop-ED6AAMWV.js} +2 -2
- package/dist/main.js +1719 -380
- package/dist/main.js.map +1 -1
- package/dist/{stream-Y55NRF2S.js → stream-YQCRUBO3.js} +26 -2
- package/dist/stream-YQCRUBO3.js.map +1 -0
- package/package.json +25 -16
- package/dist/chunk-GN236FD4.js.map +0 -1
- package/dist/stream-Y55NRF2S.js.map +0 -1
- /package/dist/{loop-ADVWZA62.js.map → loop-ED6AAMWV.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/agent/loop.ts
|
|
4
|
-
import { tool as aiTool } from "ai";
|
|
4
|
+
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
5
5
|
|
|
6
6
|
// src/context/compaction.ts
|
|
7
7
|
import { generateText } from "ai";
|
|
@@ -235,6 +235,133 @@ ${unstaged}`);
|
|
|
235
235
|
return { status, diff: text, truncated };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// src/core/image.ts
|
|
239
|
+
var MAX_IMAGE_DIMENSION = 2e3;
|
|
240
|
+
var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
|
|
241
|
+
var IMAGE_REMOVED_PLACEHOLDER = "[image removed: too large for the model]";
|
|
242
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
243
|
+
function probePng(buf) {
|
|
244
|
+
if (buf.length < 24) return null;
|
|
245
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
246
|
+
if (buf[i] !== PNG_SIGNATURE[i]) return null;
|
|
247
|
+
}
|
|
248
|
+
if (buf.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
249
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
250
|
+
}
|
|
251
|
+
var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
|
|
252
|
+
192,
|
|
253
|
+
193,
|
|
254
|
+
194,
|
|
255
|
+
195,
|
|
256
|
+
197,
|
|
257
|
+
198,
|
|
258
|
+
199,
|
|
259
|
+
201,
|
|
260
|
+
202,
|
|
261
|
+
203,
|
|
262
|
+
205,
|
|
263
|
+
206,
|
|
264
|
+
207
|
|
265
|
+
]);
|
|
266
|
+
function probeJpeg(buf) {
|
|
267
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
268
|
+
let offset = 2;
|
|
269
|
+
while (offset + 3 < buf.length) {
|
|
270
|
+
if (buf[offset] !== 255) return null;
|
|
271
|
+
const marker = buf[offset + 1];
|
|
272
|
+
if (marker === void 0) return null;
|
|
273
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
274
|
+
offset += 2;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const length = buf.readUInt16BE(offset + 2);
|
|
278
|
+
if (length < 2) return null;
|
|
279
|
+
if (JPEG_SOF_MARKERS.has(marker)) {
|
|
280
|
+
if (offset + 9 > buf.length) return null;
|
|
281
|
+
return { height: buf.readUInt16BE(offset + 5), width: buf.readUInt16BE(offset + 7) };
|
|
282
|
+
}
|
|
283
|
+
offset += 2 + length;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function probeGif(buf) {
|
|
288
|
+
if (buf.length < 10) return null;
|
|
289
|
+
const signature = buf.toString("ascii", 0, 6);
|
|
290
|
+
if (signature !== "GIF87a" && signature !== "GIF89a") return null;
|
|
291
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
292
|
+
}
|
|
293
|
+
function probeWebp(buf) {
|
|
294
|
+
if (buf.length < 30) return null;
|
|
295
|
+
if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WEBP") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
299
|
+
if (chunk === "VP8X") {
|
|
300
|
+
return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
301
|
+
}
|
|
302
|
+
if (chunk === "VP8 ") {
|
|
303
|
+
return {
|
|
304
|
+
width: buf.readUInt16LE(26) & 16383,
|
|
305
|
+
height: buf.readUInt16LE(28) & 16383
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (chunk === "VP8L") {
|
|
309
|
+
if (buf[20] !== 47) return null;
|
|
310
|
+
const b1 = buf[21];
|
|
311
|
+
const b2 = buf[22];
|
|
312
|
+
const b3 = buf[23];
|
|
313
|
+
const b4 = buf[24];
|
|
314
|
+
return {
|
|
315
|
+
width: 1 + ((b2 & 63) << 8 | b1),
|
|
316
|
+
height: 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
function probeImageDimensions(buf) {
|
|
322
|
+
return probePng(buf) ?? probeJpeg(buf) ?? probeGif(buf) ?? probeWebp(buf);
|
|
323
|
+
}
|
|
324
|
+
var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|size\s*limit|max(?:imum)?[\s-]*size/i;
|
|
325
|
+
function isOversizedImageError(error) {
|
|
326
|
+
const status = error.statusCode;
|
|
327
|
+
if (typeof status !== "number" || status < 400 || status >= 500) return false;
|
|
328
|
+
return /image/i.test(error.message) && IMAGE_SIZE_KEYWORDS.test(error.message);
|
|
329
|
+
}
|
|
330
|
+
function imagePartBytes(image) {
|
|
331
|
+
if (typeof image === "string") {
|
|
332
|
+
const base64 = image.startsWith("data:") ? image.slice(image.indexOf(",") + 1) : image;
|
|
333
|
+
try {
|
|
334
|
+
return Buffer.from(base64, "base64");
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (image instanceof Uint8Array) return Buffer.from(image);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function isOversizedImageData(buf) {
|
|
343
|
+
const dims = probeImageDimensions(buf);
|
|
344
|
+
if (dims) return Math.max(dims.width, dims.height) > MAX_IMAGE_DIMENSION;
|
|
345
|
+
return buf.length > OVERSIZED_IMAGE_FALLBACK_BYTES;
|
|
346
|
+
}
|
|
347
|
+
function stripOversizedImages(messages) {
|
|
348
|
+
let removed = 0;
|
|
349
|
+
const result = messages.map((message) => {
|
|
350
|
+
if (!Array.isArray(message.content)) return message;
|
|
351
|
+
let changed = false;
|
|
352
|
+
const content = message.content.map((part) => {
|
|
353
|
+
if (part.type !== "image") return part;
|
|
354
|
+
const bytes = imagePartBytes(part.image);
|
|
355
|
+
if (!bytes || !isOversizedImageData(bytes)) return part;
|
|
356
|
+
changed = true;
|
|
357
|
+
removed++;
|
|
358
|
+
return { type: "text", text: IMAGE_REMOVED_PLACEHOLDER };
|
|
359
|
+
});
|
|
360
|
+
return changed ? { ...message, content } : message;
|
|
361
|
+
});
|
|
362
|
+
return { messages: removed > 0 ? result : messages, removed };
|
|
363
|
+
}
|
|
364
|
+
|
|
238
365
|
// src/core/messages.ts
|
|
239
366
|
var MISSING_TOOL_RESULT_TEXT = "tool result missing (session was interrupted)";
|
|
240
367
|
function reconcileToolCalls(messages) {
|
|
@@ -664,6 +791,12 @@ async function revert(snapshot) {
|
|
|
664
791
|
await rm(snapshot.path, { force: true });
|
|
665
792
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
666
793
|
}
|
|
794
|
+
function listTurnSnapshots(turn) {
|
|
795
|
+
return stack.filter((snapshot) => snapshot.turn === turn).reverse();
|
|
796
|
+
}
|
|
797
|
+
function resolveSnapshotContent(snapshot) {
|
|
798
|
+
return resolveContent(snapshot);
|
|
799
|
+
}
|
|
667
800
|
async function undoTurnSnapshots(turn) {
|
|
668
801
|
const matches = [];
|
|
669
802
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
@@ -702,14 +835,214 @@ async function rewindToSnapshot(id) {
|
|
|
702
835
|
return { reverted, messageIndex };
|
|
703
836
|
}
|
|
704
837
|
|
|
838
|
+
// src/tools/todo.ts
|
|
839
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
840
|
+
import path3 from "path";
|
|
841
|
+
import { z } from "zod";
|
|
842
|
+
var todoItemSchema = z.object({
|
|
843
|
+
id: z.number().int(),
|
|
844
|
+
title: z.string(),
|
|
845
|
+
status: z.enum(["pending", "in_progress", "done"])
|
|
846
|
+
});
|
|
847
|
+
var writeSchema = z.object({
|
|
848
|
+
todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
849
|
+
});
|
|
850
|
+
var readSchema = z.object({});
|
|
851
|
+
function fileFor(cwd) {
|
|
852
|
+
return path3.join(cwd, ".star", "todos.json");
|
|
853
|
+
}
|
|
854
|
+
var TodoStore = class {
|
|
855
|
+
items = /* @__PURE__ */ new Map();
|
|
856
|
+
list() {
|
|
857
|
+
return [...this.items.values()];
|
|
858
|
+
}
|
|
859
|
+
replace(items) {
|
|
860
|
+
this.items.clear();
|
|
861
|
+
for (const item of items) {
|
|
862
|
+
this.items.set(item.id, item);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async load(cwd) {
|
|
866
|
+
let raw;
|
|
867
|
+
try {
|
|
868
|
+
raw = await readFile2(fileFor(cwd), "utf8");
|
|
869
|
+
} catch {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(raw);
|
|
874
|
+
if (Array.isArray(parsed)) {
|
|
875
|
+
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async save(cwd) {
|
|
881
|
+
await mkdir2(path3.join(cwd, ".star"), { recursive: true });
|
|
882
|
+
await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
883
|
+
`);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var SYMBOLS = {
|
|
887
|
+
pending: "[ ]",
|
|
888
|
+
in_progress: "[~]",
|
|
889
|
+
done: "[x]"
|
|
890
|
+
};
|
|
891
|
+
function formatTodos(items) {
|
|
892
|
+
if (items.length === 0) {
|
|
893
|
+
return "No todos.";
|
|
894
|
+
}
|
|
895
|
+
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
896
|
+
const width = Math.max(...lines.map((l) => l.length));
|
|
897
|
+
return lines.map((line, i) => {
|
|
898
|
+
const status = items[i]?.status;
|
|
899
|
+
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
900
|
+
}).join("\n");
|
|
901
|
+
}
|
|
902
|
+
var defaultStore = new TodoStore();
|
|
903
|
+
function parseTodoArgs(args) {
|
|
904
|
+
const parsed = writeSchema.safeParse(args);
|
|
905
|
+
return parsed.success ? parsed.data.todos : null;
|
|
906
|
+
}
|
|
907
|
+
function pendingTodoTitles(args) {
|
|
908
|
+
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
909
|
+
}
|
|
910
|
+
async function loadTodos(cwd) {
|
|
911
|
+
await defaultStore.load(cwd);
|
|
912
|
+
return defaultStore.list();
|
|
913
|
+
}
|
|
914
|
+
function resetTodos() {
|
|
915
|
+
defaultStore.replace([]);
|
|
916
|
+
}
|
|
917
|
+
function createTodoTools(store = defaultStore) {
|
|
918
|
+
const todoWrite = {
|
|
919
|
+
name: "todo_write",
|
|
920
|
+
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
921
|
+
permission: "read",
|
|
922
|
+
parameters: writeSchema,
|
|
923
|
+
async execute(args, ctx) {
|
|
924
|
+
const parsed = writeSchema.safeParse(args);
|
|
925
|
+
if (!parsed.success) {
|
|
926
|
+
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
927
|
+
}
|
|
928
|
+
store.replace(parsed.data.todos);
|
|
929
|
+
await store.save(ctx.cwd);
|
|
930
|
+
const items = store.list();
|
|
931
|
+
const done = items.filter((it) => it.status === "done").length;
|
|
932
|
+
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const todoRead = {
|
|
936
|
+
name: "todo_read",
|
|
937
|
+
description: "Read the current todo list.",
|
|
938
|
+
permission: "read",
|
|
939
|
+
parameters: readSchema,
|
|
940
|
+
async execute() {
|
|
941
|
+
return { content: formatTodos(store.list()) };
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
return [todoWrite, todoRead];
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/agent/agent-tasks.ts
|
|
948
|
+
import { EventEmitter } from "events";
|
|
949
|
+
var AgentTaskManager = class extends EventEmitter {
|
|
950
|
+
records = /* @__PURE__ */ new Map();
|
|
951
|
+
counter = 0;
|
|
952
|
+
start(run, meta) {
|
|
953
|
+
const id = `agent-${++this.counter}`;
|
|
954
|
+
const rec = {
|
|
955
|
+
id,
|
|
956
|
+
prompt: meta.prompt,
|
|
957
|
+
description: meta.description,
|
|
958
|
+
status: "running",
|
|
959
|
+
startedAt: Date.now(),
|
|
960
|
+
result: "",
|
|
961
|
+
controller: new AbortController(),
|
|
962
|
+
notified: false
|
|
963
|
+
};
|
|
964
|
+
this.records.set(id, rec);
|
|
965
|
+
void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
|
|
966
|
+
(error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
|
|
967
|
+
);
|
|
968
|
+
this.emitUpdate(rec);
|
|
969
|
+
return this.snapshot(rec);
|
|
970
|
+
}
|
|
971
|
+
get(id) {
|
|
972
|
+
const rec = this.records.get(id);
|
|
973
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
974
|
+
}
|
|
975
|
+
list() {
|
|
976
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
977
|
+
}
|
|
978
|
+
runningCount() {
|
|
979
|
+
let count = 0;
|
|
980
|
+
for (const rec of this.records.values()) {
|
|
981
|
+
if (rec.status === "running") count += 1;
|
|
982
|
+
}
|
|
983
|
+
return count;
|
|
984
|
+
}
|
|
985
|
+
kill(id) {
|
|
986
|
+
const rec = this.records.get(id);
|
|
987
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
988
|
+
rec.controller.abort();
|
|
989
|
+
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
990
|
+
return this.snapshot(rec);
|
|
991
|
+
}
|
|
992
|
+
cleanup() {
|
|
993
|
+
const killed = [];
|
|
994
|
+
for (const rec of this.records.values()) {
|
|
995
|
+
if (rec.status === "running") {
|
|
996
|
+
rec.controller.abort();
|
|
997
|
+
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
998
|
+
killed.push(this.snapshot(rec));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return killed;
|
|
1002
|
+
}
|
|
1003
|
+
// Finished tasks whose result has not yet been handed to the parent loop.
|
|
1004
|
+
drainNotifications() {
|
|
1005
|
+
const drained = [];
|
|
1006
|
+
for (const rec of this.records.values()) {
|
|
1007
|
+
if (rec.status !== "running" && !rec.notified) {
|
|
1008
|
+
rec.notified = true;
|
|
1009
|
+
drained.push(this.snapshot(rec));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return drained;
|
|
1013
|
+
}
|
|
1014
|
+
finish(rec, status, result) {
|
|
1015
|
+
if (rec.status !== "running") return;
|
|
1016
|
+
rec.status = status;
|
|
1017
|
+
rec.result = result;
|
|
1018
|
+
rec.endedAt = Date.now();
|
|
1019
|
+
this.emitUpdate(rec);
|
|
1020
|
+
}
|
|
1021
|
+
emitUpdate(rec) {
|
|
1022
|
+
this.emit("update", this.snapshot(rec));
|
|
1023
|
+
}
|
|
1024
|
+
snapshot(rec) {
|
|
1025
|
+
return {
|
|
1026
|
+
id: rec.id,
|
|
1027
|
+
prompt: rec.prompt,
|
|
1028
|
+
description: rec.description,
|
|
1029
|
+
status: rec.status,
|
|
1030
|
+
startedAt: rec.startedAt,
|
|
1031
|
+
endedAt: rec.endedAt,
|
|
1032
|
+
result: rec.result
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
var defaultAgentTasks = new AgentTaskManager();
|
|
1037
|
+
|
|
705
1038
|
// src/agent/project-memory.ts
|
|
706
1039
|
import { readFileSync, statSync } from "fs";
|
|
707
|
-
import
|
|
1040
|
+
import path4 from "path";
|
|
708
1041
|
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
709
1042
|
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
710
1043
|
var cache = /* @__PURE__ */ new Map();
|
|
711
1044
|
function readProjectMemory(cwd) {
|
|
712
|
-
const file =
|
|
1045
|
+
const file = path4.join(cwd, PROJECT_MEMORY_FILE);
|
|
713
1046
|
let mtimeMs;
|
|
714
1047
|
try {
|
|
715
1048
|
mtimeMs = statSync(file).mtimeMs;
|
|
@@ -739,26 +1072,29 @@ ${body}`;
|
|
|
739
1072
|
|
|
740
1073
|
// src/agent/skills.ts
|
|
741
1074
|
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
742
|
-
import
|
|
743
|
-
import { z } from "zod";
|
|
1075
|
+
import path6 from "path";
|
|
1076
|
+
import { z as z2 } from "zod";
|
|
744
1077
|
|
|
745
1078
|
// src/config/paths.ts
|
|
746
1079
|
import os from "os";
|
|
747
|
-
import
|
|
1080
|
+
import path5 from "path";
|
|
748
1081
|
function starHome() {
|
|
749
|
-
return process.env.STAR_HOME ??
|
|
1082
|
+
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
750
1083
|
}
|
|
751
1084
|
function globalConfigPath() {
|
|
752
|
-
return
|
|
1085
|
+
return path5.join(starHome(), "config.toml");
|
|
753
1086
|
}
|
|
754
1087
|
function sessionsDir() {
|
|
755
|
-
return
|
|
1088
|
+
return path5.join(starHome(), "sessions");
|
|
756
1089
|
}
|
|
757
1090
|
function userMemoryPath() {
|
|
758
|
-
return
|
|
1091
|
+
return path5.join(starHome(), "MEMORY.md");
|
|
1092
|
+
}
|
|
1093
|
+
function envFilePath() {
|
|
1094
|
+
return path5.join(starHome(), ".env");
|
|
759
1095
|
}
|
|
760
1096
|
function projectConfigPath(cwd) {
|
|
761
|
-
return
|
|
1097
|
+
return path5.join(cwd, ".star", "config.toml");
|
|
762
1098
|
}
|
|
763
1099
|
|
|
764
1100
|
// src/agent/skills.ts
|
|
@@ -791,7 +1127,7 @@ function parseSkillFile(filePath, dirName, scope) {
|
|
|
791
1127
|
const name = parsed?.meta.name || dirName;
|
|
792
1128
|
const description = parsed?.meta.description;
|
|
793
1129
|
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
794
|
-
skill = { name, description, filePath, dir:
|
|
1130
|
+
skill = { name, description, filePath, dir: path6.dirname(filePath), scope };
|
|
795
1131
|
}
|
|
796
1132
|
} catch {
|
|
797
1133
|
skill = null;
|
|
@@ -808,14 +1144,14 @@ function scanDir(base, scope, into) {
|
|
|
808
1144
|
}
|
|
809
1145
|
for (const entry of entries.sort()) {
|
|
810
1146
|
if (!NAME_PATTERN.test(entry)) continue;
|
|
811
|
-
const parsed = parseSkillFile(
|
|
1147
|
+
const parsed = parseSkillFile(path6.join(base, entry, SKILL_FILE), entry, scope);
|
|
812
1148
|
if (parsed) into.set(parsed.name, parsed);
|
|
813
1149
|
}
|
|
814
1150
|
}
|
|
815
1151
|
function discoverSkills(cwd, home = starHome()) {
|
|
816
1152
|
const byName = /* @__PURE__ */ new Map();
|
|
817
|
-
scanDir(
|
|
818
|
-
scanDir(
|
|
1153
|
+
scanDir(path6.join(home, "skills"), "user", byName);
|
|
1154
|
+
scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
|
|
819
1155
|
return [...byName.values()];
|
|
820
1156
|
}
|
|
821
1157
|
function formatSkillsBlock(skills) {
|
|
@@ -846,8 +1182,8 @@ function createSkillTool(deps) {
|
|
|
846
1182
|
name: "skill",
|
|
847
1183
|
description: "Load the full instructions of a skill by name. Available skills are listed in the system prompt when present; call this before starting work that matches one of them.",
|
|
848
1184
|
permission: "read",
|
|
849
|
-
parameters:
|
|
850
|
-
name:
|
|
1185
|
+
parameters: z2.object({
|
|
1186
|
+
name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
851
1187
|
}),
|
|
852
1188
|
async execute(args) {
|
|
853
1189
|
const skills = discoverSkills(deps.cwd, deps.home);
|
|
@@ -882,16 +1218,16 @@ import { z as z12 } from "zod";
|
|
|
882
1218
|
// src/tools/bash.ts
|
|
883
1219
|
import { spawn as spawn3 } from "child_process";
|
|
884
1220
|
import { existsSync } from "fs";
|
|
885
|
-
import
|
|
886
|
-
import { z as
|
|
1221
|
+
import path7 from "path";
|
|
1222
|
+
import { z as z3 } from "zod";
|
|
887
1223
|
|
|
888
1224
|
// src/tasks/manager.ts
|
|
889
1225
|
import { spawn as spawn2 } from "child_process";
|
|
890
|
-
import { EventEmitter } from "events";
|
|
1226
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
891
1227
|
var MAX_TASK_OUTPUT = 1e5;
|
|
892
1228
|
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
893
1229
|
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
894
|
-
var TaskManager = class extends
|
|
1230
|
+
var TaskManager = class extends EventEmitter2 {
|
|
895
1231
|
records = /* @__PURE__ */ new Map();
|
|
896
1232
|
seq = 0;
|
|
897
1233
|
start(opts) {
|
|
@@ -1002,11 +1338,11 @@ var MAX_OUTPUT = 3e4;
|
|
|
1002
1338
|
var DEFAULT_TIMEOUT = 120;
|
|
1003
1339
|
var MAX_TIMEOUT = 600;
|
|
1004
1340
|
function findOnPath(exe, exclude) {
|
|
1005
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1341
|
+
for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
|
|
1006
1342
|
if (!dir || exclude?.(dir)) {
|
|
1007
1343
|
continue;
|
|
1008
1344
|
}
|
|
1009
|
-
const full =
|
|
1345
|
+
const full = path7.join(dir, exe);
|
|
1010
1346
|
if (existsSync(full)) {
|
|
1011
1347
|
return full;
|
|
1012
1348
|
}
|
|
@@ -1024,7 +1360,7 @@ function resolveShell() {
|
|
|
1024
1360
|
}
|
|
1025
1361
|
const gitExe = findOnPath("git.exe");
|
|
1026
1362
|
if (gitExe) {
|
|
1027
|
-
const sibling =
|
|
1363
|
+
const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
|
|
1028
1364
|
if (existsSync(sibling)) {
|
|
1029
1365
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1030
1366
|
}
|
|
@@ -1041,11 +1377,11 @@ function resolveShell() {
|
|
|
1041
1377
|
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
1042
1378
|
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
1043
1379
|
}
|
|
1044
|
-
var schema =
|
|
1045
|
-
command:
|
|
1046
|
-
timeout:
|
|
1047
|
-
description:
|
|
1048
|
-
run_in_background:
|
|
1380
|
+
var schema = z3.object({
|
|
1381
|
+
command: z3.string().describe("Shell command to execute"),
|
|
1382
|
+
timeout: z3.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
1383
|
+
description: z3.string().optional().describe("Short description of what the command does"),
|
|
1384
|
+
run_in_background: z3.boolean().optional().describe(
|
|
1049
1385
|
"Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
|
|
1050
1386
|
)
|
|
1051
1387
|
});
|
|
@@ -1147,14 +1483,14 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1147
1483
|
};
|
|
1148
1484
|
|
|
1149
1485
|
// src/tools/fs/edit.ts
|
|
1150
|
-
import { readFile as
|
|
1151
|
-
import
|
|
1152
|
-
import { z as
|
|
1153
|
-
var schema2 =
|
|
1154
|
-
path:
|
|
1155
|
-
old_string:
|
|
1156
|
-
new_string:
|
|
1157
|
-
replace_all:
|
|
1486
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1487
|
+
import path8 from "path";
|
|
1488
|
+
import { z as z4 } from "zod";
|
|
1489
|
+
var schema2 = z4.object({
|
|
1490
|
+
path: z4.string().describe("File path, absolute or relative to the working directory"),
|
|
1491
|
+
old_string: z4.string().min(1).describe("Exact text to replace"),
|
|
1492
|
+
new_string: z4.string().describe("Replacement text"),
|
|
1493
|
+
replace_all: z4.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
|
|
1158
1494
|
});
|
|
1159
1495
|
var editFileTool = {
|
|
1160
1496
|
name: "edit_file",
|
|
@@ -1162,10 +1498,10 @@ var editFileTool = {
|
|
|
1162
1498
|
permission: "write",
|
|
1163
1499
|
parameters: schema2,
|
|
1164
1500
|
async execute(args, ctx) {
|
|
1165
|
-
const filePath =
|
|
1501
|
+
const filePath = path8.resolve(ctx.cwd, args.path);
|
|
1166
1502
|
let content;
|
|
1167
1503
|
try {
|
|
1168
|
-
content = await
|
|
1504
|
+
content = await readFile3(filePath, "utf8");
|
|
1169
1505
|
} catch (err) {
|
|
1170
1506
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1171
1507
|
}
|
|
@@ -1186,7 +1522,7 @@ var editFileTool = {
|
|
|
1186
1522
|
}
|
|
1187
1523
|
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1188
1524
|
try {
|
|
1189
|
-
await
|
|
1525
|
+
await writeFile3(filePath, updated, "utf8");
|
|
1190
1526
|
} catch (err) {
|
|
1191
1527
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1192
1528
|
}
|
|
@@ -1205,13 +1541,13 @@ var editFileTool = {
|
|
|
1205
1541
|
};
|
|
1206
1542
|
|
|
1207
1543
|
// src/tools/fs/glob.ts
|
|
1208
|
-
import
|
|
1209
|
-
import { z as
|
|
1544
|
+
import path10 from "path";
|
|
1545
|
+
import { z as z5 } from "zod";
|
|
1210
1546
|
|
|
1211
1547
|
// src/tools/fs/util.ts
|
|
1212
1548
|
import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
1213
1549
|
import { readdir, stat } from "fs/promises";
|
|
1214
|
-
import
|
|
1550
|
+
import path9 from "path";
|
|
1215
1551
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1216
1552
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1217
1553
|
const out = [];
|
|
@@ -1221,7 +1557,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1221
1557
|
return;
|
|
1222
1558
|
}
|
|
1223
1559
|
for (const entry of entries) {
|
|
1224
|
-
const abs =
|
|
1560
|
+
const abs = path9.join(dir, entry.name);
|
|
1225
1561
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1226
1562
|
if (entry.isDirectory()) {
|
|
1227
1563
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1301,7 +1637,7 @@ function parseIgnorePatterns(body) {
|
|
|
1301
1637
|
return out;
|
|
1302
1638
|
}
|
|
1303
1639
|
function loadIgnorePatterns(dir) {
|
|
1304
|
-
const file =
|
|
1640
|
+
const file = path9.join(dir, IGNORE_FILE);
|
|
1305
1641
|
let mtimeMs;
|
|
1306
1642
|
try {
|
|
1307
1643
|
mtimeMs = statSync3(file).mtimeMs;
|
|
@@ -1326,9 +1662,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1326
1662
|
if (patterns.length === 0) {
|
|
1327
1663
|
return void 0;
|
|
1328
1664
|
}
|
|
1329
|
-
const root =
|
|
1665
|
+
const root = path9.resolve(cwd);
|
|
1330
1666
|
return (absPath, isDir) => {
|
|
1331
|
-
const rel =
|
|
1667
|
+
const rel = path9.relative(root, absPath).split(path9.sep).join("/");
|
|
1332
1668
|
if (!rel || rel.startsWith("..")) {
|
|
1333
1669
|
return false;
|
|
1334
1670
|
}
|
|
@@ -1344,7 +1680,7 @@ function createIgnorePredicate(cwd) {
|
|
|
1344
1680
|
};
|
|
1345
1681
|
}
|
|
1346
1682
|
function isSensitivePath2(filePath) {
|
|
1347
|
-
const base =
|
|
1683
|
+
const base = path9.basename(filePath);
|
|
1348
1684
|
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
1349
1685
|
return false;
|
|
1350
1686
|
}
|
|
@@ -1359,11 +1695,11 @@ function isSensitivePath2(filePath) {
|
|
|
1359
1695
|
|
|
1360
1696
|
// src/tools/fs/glob.ts
|
|
1361
1697
|
var MAX_RESULTS = 100;
|
|
1362
|
-
var schema3 =
|
|
1363
|
-
pattern:
|
|
1698
|
+
var schema3 = z5.object({
|
|
1699
|
+
pattern: z5.string().describe(
|
|
1364
1700
|
"Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
|
|
1365
1701
|
),
|
|
1366
|
-
path:
|
|
1702
|
+
path: z5.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1367
1703
|
});
|
|
1368
1704
|
var globTool = {
|
|
1369
1705
|
name: "glob",
|
|
@@ -1371,7 +1707,7 @@ var globTool = {
|
|
|
1371
1707
|
permission: "read",
|
|
1372
1708
|
parameters: schema3,
|
|
1373
1709
|
async execute(args, ctx) {
|
|
1374
|
-
const root =
|
|
1710
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1375
1711
|
const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1376
1712
|
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1377
1713
|
if (matched.length === 0) {
|
|
@@ -1382,15 +1718,15 @@ var globTool = {
|
|
|
1382
1718
|
};
|
|
1383
1719
|
|
|
1384
1720
|
// src/tools/fs/grep.ts
|
|
1385
|
-
import { readFile as
|
|
1386
|
-
import
|
|
1387
|
-
import { z as
|
|
1721
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1722
|
+
import path11 from "path";
|
|
1723
|
+
import { z as z6 } from "zod";
|
|
1388
1724
|
var MAX_MATCHES = 250;
|
|
1389
|
-
var schema4 =
|
|
1390
|
-
pattern:
|
|
1391
|
-
path:
|
|
1392
|
-
glob:
|
|
1393
|
-
ignoreCase:
|
|
1725
|
+
var schema4 = z6.object({
|
|
1726
|
+
pattern: z6.string().describe("Regular expression to search for"),
|
|
1727
|
+
path: z6.string().optional().describe("File or directory to search, defaults to the working directory"),
|
|
1728
|
+
glob: z6.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
|
|
1729
|
+
ignoreCase: z6.boolean().optional().describe("Case-insensitive matching")
|
|
1394
1730
|
});
|
|
1395
1731
|
var grepTool = {
|
|
1396
1732
|
name: "grep",
|
|
@@ -1404,12 +1740,12 @@ var grepTool = {
|
|
|
1404
1740
|
} catch (err) {
|
|
1405
1741
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1406
1742
|
}
|
|
1407
|
-
const root =
|
|
1743
|
+
const root = path11.resolve(ctx.cwd, args.path ?? ".");
|
|
1408
1744
|
let files;
|
|
1409
1745
|
try {
|
|
1410
1746
|
const st = await stat2(root);
|
|
1411
1747
|
if (st.isFile()) {
|
|
1412
|
-
files = [{ abs: root, rel:
|
|
1748
|
+
files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
|
|
1413
1749
|
} else {
|
|
1414
1750
|
files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1415
1751
|
}
|
|
@@ -1425,7 +1761,7 @@ var grepTool = {
|
|
|
1425
1761
|
outer: for (const file of files) {
|
|
1426
1762
|
let buf;
|
|
1427
1763
|
try {
|
|
1428
|
-
buf = await
|
|
1764
|
+
buf = await readFile4(file.abs);
|
|
1429
1765
|
} catch {
|
|
1430
1766
|
continue;
|
|
1431
1767
|
}
|
|
@@ -1455,15 +1791,15 @@ var grepTool = {
|
|
|
1455
1791
|
};
|
|
1456
1792
|
|
|
1457
1793
|
// src/tools/fs/read.ts
|
|
1458
|
-
import { readFile as
|
|
1459
|
-
import
|
|
1460
|
-
import { z as
|
|
1794
|
+
import { readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
1795
|
+
import path12 from "path";
|
|
1796
|
+
import { z as z7 } from "zod";
|
|
1461
1797
|
var DEFAULT_LIMIT = 2e3;
|
|
1462
1798
|
var MAX_CHARS = 100 * 1024;
|
|
1463
|
-
var schema5 =
|
|
1464
|
-
path:
|
|
1465
|
-
offset:
|
|
1466
|
-
limit:
|
|
1799
|
+
var schema5 = z7.object({
|
|
1800
|
+
path: z7.string().describe("File path, absolute or relative to the working directory"),
|
|
1801
|
+
offset: z7.number().int().positive().optional().describe("1-based line number to start reading from"),
|
|
1802
|
+
limit: z7.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
|
|
1467
1803
|
});
|
|
1468
1804
|
var readFileTool = {
|
|
1469
1805
|
name: "read_file",
|
|
@@ -1471,7 +1807,7 @@ var readFileTool = {
|
|
|
1471
1807
|
permission: "read",
|
|
1472
1808
|
parameters: schema5,
|
|
1473
1809
|
async execute(args, ctx) {
|
|
1474
|
-
const filePath =
|
|
1810
|
+
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1475
1811
|
if (isSensitivePath2(filePath)) {
|
|
1476
1812
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1477
1813
|
}
|
|
@@ -1482,7 +1818,7 @@ var readFileTool = {
|
|
|
1482
1818
|
if (st.isDirectory()) {
|
|
1483
1819
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1484
1820
|
}
|
|
1485
|
-
const raw = await
|
|
1821
|
+
const raw = await readFile5(filePath, "utf8");
|
|
1486
1822
|
const lines = raw.split("\n");
|
|
1487
1823
|
const start = args.offset ?? 1;
|
|
1488
1824
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
@@ -1516,12 +1852,12 @@ var readFileTool = {
|
|
|
1516
1852
|
};
|
|
1517
1853
|
|
|
1518
1854
|
// src/tools/fs/write.ts
|
|
1519
|
-
import { mkdir as
|
|
1520
|
-
import
|
|
1521
|
-
import { z as
|
|
1522
|
-
var schema6 =
|
|
1523
|
-
path:
|
|
1524
|
-
content:
|
|
1855
|
+
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1856
|
+
import path13 from "path";
|
|
1857
|
+
import { z as z8 } from "zod";
|
|
1858
|
+
var schema6 = z8.object({
|
|
1859
|
+
path: z8.string().describe("File path, absolute or relative to the working directory"),
|
|
1860
|
+
content: z8.string().describe("Full content to write to the file")
|
|
1525
1861
|
});
|
|
1526
1862
|
var writeFileTool = {
|
|
1527
1863
|
name: "write_file",
|
|
@@ -1529,11 +1865,11 @@ var writeFileTool = {
|
|
|
1529
1865
|
permission: "write",
|
|
1530
1866
|
parameters: schema6,
|
|
1531
1867
|
async execute(args, ctx) {
|
|
1532
|
-
const filePath =
|
|
1868
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1533
1869
|
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1534
1870
|
try {
|
|
1535
|
-
await
|
|
1536
|
-
await
|
|
1871
|
+
await mkdir3(path13.dirname(filePath), { recursive: true });
|
|
1872
|
+
await writeFile4(filePath, args.content, "utf8");
|
|
1537
1873
|
} catch (err) {
|
|
1538
1874
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1539
1875
|
}
|
|
@@ -1543,7 +1879,7 @@ var writeFileTool = {
|
|
|
1543
1879
|
};
|
|
1544
1880
|
|
|
1545
1881
|
// src/tools/tasks.ts
|
|
1546
|
-
import { z as
|
|
1882
|
+
import { z as z9 } from "zod";
|
|
1547
1883
|
|
|
1548
1884
|
// src/tasks/format.ts
|
|
1549
1885
|
function formatDuration(startedAt, endedAt) {
|
|
@@ -1589,40 +1925,59 @@ function formatTaskFinished(task) {
|
|
|
1589
1925
|
}
|
|
1590
1926
|
|
|
1591
1927
|
// src/tools/tasks.ts
|
|
1592
|
-
var idSchema =
|
|
1593
|
-
id:
|
|
1928
|
+
var idSchema = z9.object({
|
|
1929
|
+
id: z9.string().describe("Task id, e.g. task-1 or agent-1")
|
|
1594
1930
|
});
|
|
1931
|
+
function formatAgentTaskLine(task) {
|
|
1932
|
+
const duration = formatDuration(task.startedAt, task.endedAt);
|
|
1933
|
+
const label = task.description ?? task.prompt.slice(0, 60);
|
|
1934
|
+
const status = task.status === "running" ? `running ${duration}` : `${task.status} after ${duration}`;
|
|
1935
|
+
return `${task.id} [${status}] ${label}`;
|
|
1936
|
+
}
|
|
1595
1937
|
var taskListTool = {
|
|
1596
1938
|
name: "task_list",
|
|
1597
|
-
description: "List background shell
|
|
1939
|
+
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
1598
1940
|
permission: "read",
|
|
1599
|
-
parameters:
|
|
1941
|
+
parameters: z9.object({}),
|
|
1600
1942
|
execute() {
|
|
1601
|
-
|
|
1943
|
+
const shell = formatTaskList(defaultTaskManager.list());
|
|
1944
|
+
const agents = defaultAgentTasks.list();
|
|
1945
|
+
const agentLines = agents.length === 0 ? "No background subagents." : `Background subagents:
|
|
1946
|
+
${agents.map(formatAgentTaskLine).join("\n")}`;
|
|
1947
|
+
return Promise.resolve({ content: `${shell}
|
|
1948
|
+
${agentLines}` });
|
|
1602
1949
|
}
|
|
1603
1950
|
};
|
|
1604
1951
|
var taskOutputTool = {
|
|
1605
1952
|
name: "task_output",
|
|
1606
|
-
description: "Read the current output of a background shell
|
|
1953
|
+
description: "Read the current output of a background task (shell command or subagent; works while still running).",
|
|
1607
1954
|
permission: "read",
|
|
1608
1955
|
parameters: idSchema,
|
|
1609
1956
|
execute(args) {
|
|
1610
|
-
const
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
1957
|
+
const shell = defaultTaskManager.get(args.id);
|
|
1958
|
+
if (shell) {
|
|
1959
|
+
const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
|
|
1960
|
+
return Promise.resolve({ content: `${formatTaskLine(shell)}
|
|
1961
|
+
${body}` });
|
|
1613
1962
|
}
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1963
|
+
const agent = defaultAgentTasks.get(args.id);
|
|
1964
|
+
if (agent) {
|
|
1965
|
+
const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
|
|
1966
|
+
return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
|
|
1616
1967
|
${body}` });
|
|
1968
|
+
}
|
|
1969
|
+
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1617
1970
|
}
|
|
1618
1971
|
};
|
|
1619
1972
|
var taskKillTool = {
|
|
1620
1973
|
name: "task_kill",
|
|
1621
|
-
description: "Stop a running background
|
|
1974
|
+
description: "Stop a running background task (shell command process tree or subagent).",
|
|
1622
1975
|
permission: "exec",
|
|
1623
1976
|
parameters: idSchema,
|
|
1624
1977
|
execute(args) {
|
|
1625
|
-
const
|
|
1978
|
+
const shell = defaultTaskManager.get(args.id);
|
|
1979
|
+
const agent = shell ? void 0 : defaultAgentTasks.get(args.id);
|
|
1980
|
+
const task = shell ?? agent;
|
|
1626
1981
|
if (!task) {
|
|
1627
1982
|
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1628
1983
|
}
|
|
@@ -1632,105 +1987,14 @@ var taskKillTool = {
|
|
|
1632
1987
|
isError: true
|
|
1633
1988
|
});
|
|
1634
1989
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
// src/tools/todo.ts
|
|
1641
|
-
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1642
|
-
import path13 from "path";
|
|
1643
|
-
import { z as z9 } from "zod";
|
|
1644
|
-
var todoItemSchema = z9.object({
|
|
1645
|
-
id: z9.number().int(),
|
|
1646
|
-
title: z9.string(),
|
|
1647
|
-
status: z9.enum(["pending", "in_progress", "done"])
|
|
1648
|
-
});
|
|
1649
|
-
var writeSchema = z9.object({
|
|
1650
|
-
todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
1651
|
-
});
|
|
1652
|
-
var readSchema = z9.object({});
|
|
1653
|
-
function fileFor(cwd) {
|
|
1654
|
-
return path13.join(cwd, ".star", "todos.json");
|
|
1655
|
-
}
|
|
1656
|
-
var TodoStore = class {
|
|
1657
|
-
items = /* @__PURE__ */ new Map();
|
|
1658
|
-
list() {
|
|
1659
|
-
return [...this.items.values()];
|
|
1660
|
-
}
|
|
1661
|
-
replace(items) {
|
|
1662
|
-
this.items.clear();
|
|
1663
|
-
for (const item of items) {
|
|
1664
|
-
this.items.set(item.id, item);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
async load(cwd) {
|
|
1668
|
-
let raw;
|
|
1669
|
-
try {
|
|
1670
|
-
raw = await readFile5(fileFor(cwd), "utf8");
|
|
1671
|
-
} catch {
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
try {
|
|
1675
|
-
const parsed = JSON.parse(raw);
|
|
1676
|
-
if (Array.isArray(parsed)) {
|
|
1677
|
-
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
1678
|
-
}
|
|
1679
|
-
} catch {
|
|
1990
|
+
if (shell) {
|
|
1991
|
+
defaultTaskManager.kill(args.id);
|
|
1992
|
+
} else {
|
|
1993
|
+
defaultAgentTasks.kill(args.id);
|
|
1680
1994
|
}
|
|
1681
|
-
|
|
1682
|
-
async save(cwd) {
|
|
1683
|
-
await mkdir3(path13.join(cwd, ".star"), { recursive: true });
|
|
1684
|
-
await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
1685
|
-
`);
|
|
1995
|
+
return Promise.resolve({ content: `Task ${args.id} stopped.` });
|
|
1686
1996
|
}
|
|
1687
1997
|
};
|
|
1688
|
-
var SYMBOLS = {
|
|
1689
|
-
pending: "[ ]",
|
|
1690
|
-
in_progress: "[~]",
|
|
1691
|
-
done: "[x]"
|
|
1692
|
-
};
|
|
1693
|
-
function formatTodos(items) {
|
|
1694
|
-
if (items.length === 0) {
|
|
1695
|
-
return "No todos.";
|
|
1696
|
-
}
|
|
1697
|
-
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
1698
|
-
const width = Math.max(...lines.map((l) => l.length));
|
|
1699
|
-
return lines.map((line, i) => {
|
|
1700
|
-
const status = items[i]?.status;
|
|
1701
|
-
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
1702
|
-
}).join("\n");
|
|
1703
|
-
}
|
|
1704
|
-
var defaultStore = new TodoStore();
|
|
1705
|
-
function createTodoTools(store = defaultStore) {
|
|
1706
|
-
const todoWrite = {
|
|
1707
|
-
name: "todo_write",
|
|
1708
|
-
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
1709
|
-
permission: "read",
|
|
1710
|
-
parameters: writeSchema,
|
|
1711
|
-
async execute(args, ctx) {
|
|
1712
|
-
const parsed = writeSchema.safeParse(args);
|
|
1713
|
-
if (!parsed.success) {
|
|
1714
|
-
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
1715
|
-
}
|
|
1716
|
-
await store.load(ctx.cwd);
|
|
1717
|
-
store.replace(parsed.data.todos);
|
|
1718
|
-
await store.save(ctx.cwd);
|
|
1719
|
-
return { content: formatTodos(store.list()) };
|
|
1720
|
-
}
|
|
1721
|
-
};
|
|
1722
|
-
const todoRead = {
|
|
1723
|
-
name: "todo_read",
|
|
1724
|
-
description: "Read the current todo list.",
|
|
1725
|
-
permission: "read",
|
|
1726
|
-
parameters: readSchema,
|
|
1727
|
-
async execute(_args, ctx) {
|
|
1728
|
-
await store.load(ctx.cwd);
|
|
1729
|
-
return { content: formatTodos(store.list()) };
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
return [todoWrite, todoRead];
|
|
1733
|
-
}
|
|
1734
1998
|
|
|
1735
1999
|
// src/tools/web/fetch.ts
|
|
1736
2000
|
import { z as z10 } from "zod";
|
|
@@ -2029,69 +2293,86 @@ function createDefaultRegistry() {
|
|
|
2029
2293
|
var MAX_SUBAGENT_DEPTH = 1;
|
|
2030
2294
|
var MAX_REPORT_CHARS = 2e4;
|
|
2031
2295
|
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, then finish with a concise report of what you found or did. Your final text is returned to the main agent as the tool result \u2014 make it self-contained.";
|
|
2296
|
+
async function runSubagent(deps, args, cwd, signal) {
|
|
2297
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-ED6AAMWV.js");
|
|
2298
|
+
const child = new AgentLoop2({
|
|
2299
|
+
model: deps.model,
|
|
2300
|
+
registry: createDefaultRegistry(),
|
|
2301
|
+
config: deps.config,
|
|
2302
|
+
cwd,
|
|
2303
|
+
system: deps.system ? `${deps.system}
|
|
2304
|
+
|
|
2305
|
+
${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
2306
|
+
subagentDepth: deps.depth + 1
|
|
2307
|
+
});
|
|
2308
|
+
const confirmHandler = deps.getConfirmHandler?.();
|
|
2309
|
+
if (confirmHandler) child.confirmHandler = confirmHandler;
|
|
2310
|
+
let toolCalls = 0;
|
|
2311
|
+
let error = null;
|
|
2312
|
+
try {
|
|
2313
|
+
for await (const event of child.stream(args.prompt, signal)) {
|
|
2314
|
+
if (event.type === "tool-result") {
|
|
2315
|
+
toolCalls++;
|
|
2316
|
+
} else if (event.type === "error") {
|
|
2317
|
+
error = event.error.message;
|
|
2318
|
+
break;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
} catch (e) {
|
|
2322
|
+
error = e instanceof Error ? e.message : String(e);
|
|
2323
|
+
}
|
|
2324
|
+
let report = "";
|
|
2325
|
+
const messages = child.getMessages();
|
|
2326
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2327
|
+
const message = messages[i];
|
|
2328
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
2329
|
+
const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
2330
|
+
if (text) {
|
|
2331
|
+
report = text;
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
if (report.length > MAX_REPORT_CHARS) {
|
|
2336
|
+
report = `${report.slice(0, MAX_REPORT_CHARS)}
|
|
2337
|
+
... (truncated)`;
|
|
2338
|
+
}
|
|
2339
|
+
const label = args.description ? ` "${args.description}"` : "";
|
|
2340
|
+
const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
|
|
2341
|
+
if (error) {
|
|
2342
|
+
throw new Error([`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"));
|
|
2343
|
+
}
|
|
2344
|
+
return `${stats}
|
|
2345
|
+
${report || "(subagent produced no text output)"}`;
|
|
2346
|
+
}
|
|
2032
2347
|
function createSubagentTool(deps) {
|
|
2033
2348
|
return {
|
|
2034
2349
|
name: "subagent",
|
|
2035
|
-
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.",
|
|
2350
|
+
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).",
|
|
2036
2351
|
permission: "exec",
|
|
2037
2352
|
parameters: z12.object({
|
|
2038
2353
|
prompt: z12.string().describe("Complete, self-contained instructions for the subagent."),
|
|
2039
|
-
description: z12.string().optional().describe("Short label for the subtask, shown in the result header.")
|
|
2354
|
+
description: z12.string().optional().describe("Short label for the subtask, shown in the result header."),
|
|
2355
|
+
run_in_background: z12.boolean().optional().describe(
|
|
2356
|
+
"Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
|
|
2357
|
+
)
|
|
2040
2358
|
}),
|
|
2041
2359
|
async execute(args, ctx) {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
});
|
|
2053
|
-
const confirmHandler = deps.getConfirmHandler?.();
|
|
2054
|
-
if (confirmHandler) child.confirmHandler = confirmHandler;
|
|
2360
|
+
if (args.run_in_background) {
|
|
2361
|
+
const task = defaultAgentTasks.start((signal2) => runSubagent(deps, args, ctx.cwd, signal2), {
|
|
2362
|
+
prompt: args.prompt,
|
|
2363
|
+
description: args.description
|
|
2364
|
+
});
|
|
2365
|
+
const label = args.description ? ` "${args.description}"` : "";
|
|
2366
|
+
return {
|
|
2367
|
+
content: `Background subagent ${task.id}${label} started. Its report will be delivered as a message when it finishes; use task_list/task_output to check progress.`
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2055
2370
|
const signal = ctx.abortSignal ?? new AbortController().signal;
|
|
2056
|
-
let toolCalls = 0;
|
|
2057
|
-
let error = null;
|
|
2058
2371
|
try {
|
|
2059
|
-
|
|
2060
|
-
if (event.type === "tool-result") {
|
|
2061
|
-
toolCalls++;
|
|
2062
|
-
} else if (event.type === "error") {
|
|
2063
|
-
error = event.error.message;
|
|
2064
|
-
break;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2372
|
+
return { content: await runSubagent(deps, args, ctx.cwd, signal) };
|
|
2067
2373
|
} catch (e) {
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2070
|
-
let report = "";
|
|
2071
|
-
const messages = child.getMessages();
|
|
2072
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2073
|
-
const message = messages[i];
|
|
2074
|
-
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
2075
|
-
const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
2076
|
-
if (text) {
|
|
2077
|
-
report = text;
|
|
2078
|
-
break;
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
if (report.length > MAX_REPORT_CHARS) {
|
|
2082
|
-
report = `${report.slice(0, MAX_REPORT_CHARS)}
|
|
2083
|
-
... (truncated)`;
|
|
2374
|
+
return { content: e instanceof Error ? e.message : String(e), isError: true };
|
|
2084
2375
|
}
|
|
2085
|
-
const label = args.description ? ` "${args.description}"` : "";
|
|
2086
|
-
const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
|
|
2087
|
-
if (error) {
|
|
2088
|
-
return {
|
|
2089
|
-
content: [`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"),
|
|
2090
|
-
isError: true
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
return { content: `${stats}
|
|
2094
|
-
${report || "(subagent produced no text output)"}` };
|
|
2095
2376
|
}
|
|
2096
2377
|
};
|
|
2097
2378
|
}
|
|
@@ -2156,6 +2437,79 @@ function createRememberTool(deps = {}) {
|
|
|
2156
2437
|
|
|
2157
2438
|
// src/agent/loop.ts
|
|
2158
2439
|
var PLAN_MODE_PROMPT = "\n\nYou are currently in PLAN MODE. Research the task using only the read-only tools available to you, then present a concrete, step-by-step implementation plan as your final answer. You must not modify files, run shell commands, or otherwise change the system \u2014 write/exec tools are unavailable in this mode. Do not ask the user to run commands for you; note manual steps in the plan instead. The user will review your plan and approve it before any execution begins.";
|
|
2440
|
+
var PENDING_WORK_PATTERN = /(?:我将|我会|我现在|接下来|下一步|下面(?:我|将)|稍后|随后|准备开始|现在开始|马上|即将|这就|待会|开始(?:执行|进行|处理|转换|动手|生成|写入|运行|创建)|(?:完成后|然后|接着)会|让我(?:们)?(?:来)?|I will|I'll|I am going to|I'm going to|I shall|let me|next,? I|I will now)/i;
|
|
2441
|
+
var AUTO_CONTINUE_NUDGE = "[auto-continue] You ended your turn with words instead of actions. Do not describe or restate the plan \u2014 continue the task NOW by calling tools. Reply with text only if the task is already fully complete.";
|
|
2442
|
+
var EMPTY_REPLY_NUDGE = "[auto-continue] Your previous reply was empty \u2014 no text and no tool calls. If the task is fully complete, reply with one short confirmation; otherwise continue NOW by calling tools.";
|
|
2443
|
+
function isSteerableEmptyFinish(finishReason) {
|
|
2444
|
+
return finishReason === "stop" || finishReason === "content-filter";
|
|
2445
|
+
}
|
|
2446
|
+
function finalNudgeWarning(autoContinues, maxAutoContinues) {
|
|
2447
|
+
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." : "";
|
|
2448
|
+
}
|
|
2449
|
+
var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
|
|
2450
|
+
async function checkTaskComplete(model, request, finalReply, signal) {
|
|
2451
|
+
const controller = new AbortController();
|
|
2452
|
+
const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
|
|
2453
|
+
const onAbort = () => controller.abort();
|
|
2454
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2455
|
+
try {
|
|
2456
|
+
const { text } = await generateText3({
|
|
2457
|
+
model,
|
|
2458
|
+
maxTokens: 8,
|
|
2459
|
+
abortSignal: controller.signal,
|
|
2460
|
+
prompt: [
|
|
2461
|
+
"An AI coding agent was given this task by the user:",
|
|
2462
|
+
`<task>
|
|
2463
|
+
${request.slice(0, 2e3)}
|
|
2464
|
+
</task>`,
|
|
2465
|
+
"The agent has stopped calling tools and ended with this reply:",
|
|
2466
|
+
`<reply>
|
|
2467
|
+
${finalReply.slice(0, 2e3)}
|
|
2468
|
+
</reply>`,
|
|
2469
|
+
"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."
|
|
2470
|
+
].join("\n")
|
|
2471
|
+
});
|
|
2472
|
+
const verdict = text.trim().toUpperCase();
|
|
2473
|
+
if (verdict.startsWith("NOT_DONE")) return false;
|
|
2474
|
+
if (verdict.startsWith("DONE")) return true;
|
|
2475
|
+
return null;
|
|
2476
|
+
} catch {
|
|
2477
|
+
return null;
|
|
2478
|
+
} finally {
|
|
2479
|
+
clearTimeout(timer);
|
|
2480
|
+
signal.removeEventListener("abort", onAbort);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
var STREAM_RETRY_BASE_DELAY_MS = 1e3;
|
|
2484
|
+
var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
2485
|
+
"AI_NoSuchToolError",
|
|
2486
|
+
"AI_InvalidToolArgumentsError",
|
|
2487
|
+
"AI_InvalidPromptError",
|
|
2488
|
+
"AI_NoSuchModelError",
|
|
2489
|
+
"AI_LoadAPIKeyError"
|
|
2490
|
+
]);
|
|
2491
|
+
function isRetryableStreamError(error) {
|
|
2492
|
+
if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
|
|
2493
|
+
const status = error.statusCode;
|
|
2494
|
+
if (typeof status === "number") {
|
|
2495
|
+
return status === 408 || status === 429 || status >= 500;
|
|
2496
|
+
}
|
|
2497
|
+
return true;
|
|
2498
|
+
}
|
|
2499
|
+
function sleep(ms, signal) {
|
|
2500
|
+
if (signal.aborted) return Promise.resolve(false);
|
|
2501
|
+
return new Promise((resolve) => {
|
|
2502
|
+
const onAbort = () => {
|
|
2503
|
+
clearTimeout(timer);
|
|
2504
|
+
resolve(false);
|
|
2505
|
+
};
|
|
2506
|
+
const timer = setTimeout(() => {
|
|
2507
|
+
signal.removeEventListener("abort", onAbort);
|
|
2508
|
+
resolve(true);
|
|
2509
|
+
}, ms);
|
|
2510
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2159
2513
|
var AgentLoop = class {
|
|
2160
2514
|
messages = [];
|
|
2161
2515
|
opts;
|
|
@@ -2226,6 +2580,18 @@ var AgentLoop = class {
|
|
|
2226
2580
|
this.messages = reconcileToolCalls(messages);
|
|
2227
2581
|
this.turnMarkers = [];
|
|
2228
2582
|
}
|
|
2583
|
+
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2584
|
+
// drop and which snapshot turn's file changes it would revert (undefined =
|
|
2585
|
+
// no verified turn, so no file snapshots may be reverted). Mutates nothing,
|
|
2586
|
+
// so the REPL can show the preview before the user confirms.
|
|
2587
|
+
previewLastTurnRetraction() {
|
|
2588
|
+
const result = retractLastTurn([...this.messages]);
|
|
2589
|
+
if (result.removed === 0) return { removed: 0 };
|
|
2590
|
+
const userIndex = result.messages.length;
|
|
2591
|
+
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2592
|
+
const turn = last && last.userIndex === userIndex ? last.seq : void 0;
|
|
2593
|
+
return { removed: result.removed, turn };
|
|
2594
|
+
}
|
|
2229
2595
|
// Drops the final user message and everything after it, and persists the
|
|
2230
2596
|
// trimmed history so a later /resume does not bring the turn back.
|
|
2231
2597
|
// `turn` is the retracted turn's snapshot seq when it could be verified
|
|
@@ -2279,6 +2645,16 @@ var AgentLoop = class {
|
|
|
2279
2645
|
async persist(message) {
|
|
2280
2646
|
await this.opts.sessionStore?.append(message);
|
|
2281
2647
|
}
|
|
2648
|
+
// Replaces oversized image parts in the in-memory history with text
|
|
2649
|
+
// placeholders and rewrites the persisted session, so the failed request
|
|
2650
|
+
// can be resent and a later resume never carries the offending images.
|
|
2651
|
+
async stripOversizedImages() {
|
|
2652
|
+
const { messages, removed } = stripOversizedImages(this.messages);
|
|
2653
|
+
if (removed === 0) return 0;
|
|
2654
|
+
this.messages = messages;
|
|
2655
|
+
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2656
|
+
return removed;
|
|
2657
|
+
}
|
|
2282
2658
|
// After the first assistant reply, kick off background title generation
|
|
2283
2659
|
// for the session. Runs once per loop; the store-level title check makes
|
|
2284
2660
|
// resumed sessions that already have a title a no-op.
|
|
@@ -2289,9 +2665,50 @@ var AgentLoop = class {
|
|
|
2289
2665
|
this.titleScheduled = true;
|
|
2290
2666
|
scheduleSessionTitle(store, input, this.opts.model);
|
|
2291
2667
|
}
|
|
2668
|
+
// A user interrupt (Esc) ends the turn mid-stream, before the normal
|
|
2669
|
+
// assistant-message persistence runs. Whatever text and tool calls already
|
|
2670
|
+
// arrived are still persisted — the text marked with "[interrupted]" so the
|
|
2671
|
+
// cut-off stays visible after a resume and the model can tell its reply was
|
|
2672
|
+
// cut short — and streamed tool calls are closed with synthetic results so
|
|
2673
|
+
// the stored history stays valid for the API.
|
|
2674
|
+
async *persistInterrupted(text, toolCalls) {
|
|
2675
|
+
if (text.length === 0 && toolCalls.length === 0) return;
|
|
2676
|
+
const assistantMessage = {
|
|
2677
|
+
role: "assistant",
|
|
2678
|
+
content: [
|
|
2679
|
+
...text ? [{ type: "text", text: `${text} [interrupted]` }] : [],
|
|
2680
|
+
...toolCalls.map((call) => ({
|
|
2681
|
+
type: "tool-call",
|
|
2682
|
+
toolCallId: call.id,
|
|
2683
|
+
toolName: call.name,
|
|
2684
|
+
args: call.args
|
|
2685
|
+
}))
|
|
2686
|
+
]
|
|
2687
|
+
};
|
|
2688
|
+
this.messages.push(assistantMessage);
|
|
2689
|
+
await this.persist(assistantMessage);
|
|
2690
|
+
for (const call of toolCalls) {
|
|
2691
|
+
const content = "Tool execution interrupted by user.";
|
|
2692
|
+
const synthetic = {
|
|
2693
|
+
role: "tool",
|
|
2694
|
+
content: [
|
|
2695
|
+
{
|
|
2696
|
+
type: "tool-result",
|
|
2697
|
+
toolCallId: call.id,
|
|
2698
|
+
toolName: call.name,
|
|
2699
|
+
result: content
|
|
2700
|
+
}
|
|
2701
|
+
]
|
|
2702
|
+
};
|
|
2703
|
+
this.messages.push(synthetic);
|
|
2704
|
+
await this.persist(synthetic).catch(() => {
|
|
2705
|
+
});
|
|
2706
|
+
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2292
2709
|
async *stream(input, signal, opts) {
|
|
2293
2710
|
this.syncSystemMessage();
|
|
2294
|
-
const
|
|
2711
|
+
const inputText = typeof input === "string" ? input : input.text;
|
|
2295
2712
|
const images = typeof input === "string" ? [] : input.images;
|
|
2296
2713
|
const userMessage = images.length > 0 ? {
|
|
2297
2714
|
role: "user",
|
|
@@ -2301,9 +2718,9 @@ var AgentLoop = class {
|
|
|
2301
2718
|
image: img.data,
|
|
2302
2719
|
mimeType: img.mimeType
|
|
2303
2720
|
})),
|
|
2304
|
-
{ type: "text", text }
|
|
2721
|
+
{ type: "text", text: inputText }
|
|
2305
2722
|
]
|
|
2306
|
-
} : { role: "user", content:
|
|
2723
|
+
} : { role: "user", content: inputText };
|
|
2307
2724
|
this.messages.push(userMessage);
|
|
2308
2725
|
const seq = (this.opts.subagentDepth ?? 0) === 0 ? beginTurn(this.messages.length - 1) : currentTurnSeq();
|
|
2309
2726
|
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
|
|
@@ -2312,42 +2729,123 @@ var AgentLoop = class {
|
|
|
2312
2729
|
);
|
|
2313
2730
|
const { config, registry, cwd } = this.opts;
|
|
2314
2731
|
const aiTools = this.buildAiTools();
|
|
2732
|
+
let autoContinues = 0;
|
|
2733
|
+
let lastWasNudge = false;
|
|
2734
|
+
let usedToolsThisTurn = false;
|
|
2735
|
+
let openTodos = [];
|
|
2315
2736
|
for (let step = 0; step < config.maxSteps; step++) {
|
|
2737
|
+
if ((this.opts.subagentDepth ?? 0) === 0) {
|
|
2738
|
+
for (const finished of defaultAgentTasks.drainNotifications()) {
|
|
2739
|
+
const label = finished.description ? ` "${finished.description}"` : "";
|
|
2740
|
+
const note = {
|
|
2741
|
+
role: "user",
|
|
2742
|
+
content: `[background subagent ${finished.id}${label} ${finished.status}]
|
|
2743
|
+
${finished.result}`
|
|
2744
|
+
};
|
|
2745
|
+
this.messages.push(note);
|
|
2746
|
+
await this.persist(note);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2316
2749
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
2317
2750
|
const compacted = compactMessages([...this.messages], maxTokens);
|
|
2318
2751
|
if (compacted.compacted) {
|
|
2319
2752
|
this.messages = await this.applyCompactionSummary(compacted);
|
|
2320
2753
|
}
|
|
2321
|
-
|
|
2754
|
+
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
2755
|
+
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
2756
|
+
let text = "";
|
|
2322
2757
|
const toolCalls = [];
|
|
2323
|
-
let
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2758
|
+
let failure = null;
|
|
2759
|
+
let lastFinishReason;
|
|
2760
|
+
let oversizedImagesStripped = false;
|
|
2761
|
+
for (let attempt = 0; ; attempt++) {
|
|
2762
|
+
text = "";
|
|
2763
|
+
toolCalls.length = 0;
|
|
2764
|
+
failure = null;
|
|
2765
|
+
lastFinishReason = void 0;
|
|
2766
|
+
const timeoutScale = Math.min(attempt + 1, 3);
|
|
2767
|
+
try {
|
|
2768
|
+
for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
|
|
2769
|
+
if (event.type === "text-delta") {
|
|
2770
|
+
text += event.text;
|
|
2771
|
+
yield event;
|
|
2772
|
+
} else if (event.type === "reasoning") {
|
|
2773
|
+
yield event;
|
|
2774
|
+
} else if (event.type === "tool-call") {
|
|
2775
|
+
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
2776
|
+
yield event;
|
|
2777
|
+
} else if (event.type === "finish") {
|
|
2778
|
+
lastFinishReason = event.finishReason;
|
|
2779
|
+
yield event;
|
|
2780
|
+
} else if (event.type === "error") {
|
|
2781
|
+
failure = event.error;
|
|
2782
|
+
} else {
|
|
2783
|
+
yield event;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
if (!signal.aborted) {
|
|
2788
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (signal.aborted) {
|
|
2792
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (!failure && (text.length > 0 || toolCalls.length > 0)) break;
|
|
2796
|
+
if (failure && !oversizedImagesStripped && isOversizedImageError(failure)) {
|
|
2797
|
+
oversizedImagesStripped = true;
|
|
2798
|
+
const removed = await this.stripOversizedImages();
|
|
2799
|
+
if (removed > 0) {
|
|
2800
|
+
yield {
|
|
2801
|
+
type: "notice",
|
|
2802
|
+
message: `Removed ${removed} oversized image(s) the model rejected; retrying the request.`
|
|
2803
|
+
};
|
|
2804
|
+
continue;
|
|
2339
2805
|
}
|
|
2340
2806
|
}
|
|
2341
|
-
|
|
2342
|
-
if (
|
|
2343
|
-
|
|
2807
|
+
if (failure && !isRetryableStreamError(failure)) break;
|
|
2808
|
+
if (attempt >= maxRetries) break;
|
|
2809
|
+
if (!failure && text.length === 0 && toolCalls.length === 0 && attempt >= 1 && isSteerableEmptyFinish(lastFinishReason))
|
|
2810
|
+
break;
|
|
2811
|
+
yield {
|
|
2812
|
+
type: "retry",
|
|
2813
|
+
attempt: attempt + 2,
|
|
2814
|
+
maxAttempts: maxRetries + 1,
|
|
2815
|
+
reason: failure ? failure.message : `empty response (finish: ${lastFinishReason ?? "none"})`
|
|
2816
|
+
};
|
|
2817
|
+
if (!await sleep(baseDelay * 2 ** attempt, signal)) return;
|
|
2818
|
+
}
|
|
2819
|
+
if (failure) {
|
|
2820
|
+
yield { type: "error", error: failure };
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
if (text.length === 0 && toolCalls.length === 0) {
|
|
2824
|
+
if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
2825
|
+
autoContinues++;
|
|
2826
|
+
lastWasNudge = true;
|
|
2827
|
+
yield {
|
|
2828
|
+
type: "notice",
|
|
2829
|
+
message: `the model returned an empty reply; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
|
|
2830
|
+
};
|
|
2831
|
+
const nudge = {
|
|
2832
|
+
role: "user",
|
|
2833
|
+
content: EMPTY_REPLY_NUDGE + finalNudgeWarning(autoContinues, config.maxAutoContinues)
|
|
2834
|
+
};
|
|
2835
|
+
this.messages.push(nudge);
|
|
2836
|
+
await this.persist(nudge);
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
yield {
|
|
2840
|
+
type: "error",
|
|
2841
|
+
error: new Error("The model returned an empty response after retries; ending the turn.")
|
|
2842
|
+
};
|
|
2344
2843
|
return;
|
|
2345
2844
|
}
|
|
2346
|
-
if (failed) return;
|
|
2347
2845
|
const assistantMessage = {
|
|
2348
2846
|
role: "assistant",
|
|
2349
2847
|
content: [
|
|
2350
|
-
...
|
|
2848
|
+
...text ? [{ type: "text", text }] : [],
|
|
2351
2849
|
...toolCalls.map((call) => ({
|
|
2352
2850
|
type: "tool-call",
|
|
2353
2851
|
toolCallId: call.id,
|
|
@@ -2358,11 +2856,50 @@ var AgentLoop = class {
|
|
|
2358
2856
|
};
|
|
2359
2857
|
this.messages.push(assistantMessage);
|
|
2360
2858
|
await this.persist(assistantMessage);
|
|
2361
|
-
this.maybeScheduleTitle(
|
|
2859
|
+
this.maybeScheduleTitle(text);
|
|
2362
2860
|
if (toolCalls.length === 0) {
|
|
2861
|
+
let reason = null;
|
|
2862
|
+
let nudgeText = AUTO_CONTINUE_NUDGE;
|
|
2863
|
+
if (openTodos.length > 0) {
|
|
2864
|
+
reason = `${openTodos.length} todo item(s) still open`;
|
|
2865
|
+
nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
|
|
2866
|
+
", "
|
|
2867
|
+
)}. Complete them now with tool calls, or update the list with todo_write if they are no longer needed.`;
|
|
2868
|
+
} else if (lastWasNudge) {
|
|
2869
|
+
reason = "reply still had no tool calls";
|
|
2870
|
+
} else if (PENDING_WORK_PATTERN.test(text)) {
|
|
2871
|
+
reason = "reply announced unfinished work";
|
|
2872
|
+
} else if (usedToolsThisTurn && !signal.aborted) {
|
|
2873
|
+
const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
|
|
2874
|
+
if (complete === false) reason = "completion check reports the task unfinished";
|
|
2875
|
+
}
|
|
2876
|
+
if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
2877
|
+
autoContinues++;
|
|
2878
|
+
lastWasNudge = true;
|
|
2879
|
+
yield {
|
|
2880
|
+
type: "notice",
|
|
2881
|
+
message: `${reason}; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
|
|
2882
|
+
};
|
|
2883
|
+
const nudge = {
|
|
2884
|
+
role: "user",
|
|
2885
|
+
content: nudgeText + finalNudgeWarning(autoContinues, config.maxAutoContinues)
|
|
2886
|
+
};
|
|
2887
|
+
this.messages.push(nudge);
|
|
2888
|
+
await this.persist(nudge);
|
|
2889
|
+
continue;
|
|
2890
|
+
}
|
|
2891
|
+
if (reason !== null && config.permissionMode !== "plan") {
|
|
2892
|
+
yield {
|
|
2893
|
+
type: "notice",
|
|
2894
|
+
message: `${reason}; auto-continue limit reached, handing the turn back \u2014 reply "continue" to keep going.`
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2363
2897
|
await this.runEventHooks("Stop");
|
|
2364
2898
|
return;
|
|
2365
2899
|
}
|
|
2900
|
+
lastWasNudge = false;
|
|
2901
|
+
usedToolsThisTurn = true;
|
|
2902
|
+
autoContinues = 0;
|
|
2366
2903
|
const answered = /* @__PURE__ */ new Set();
|
|
2367
2904
|
try {
|
|
2368
2905
|
for (const call of toolCalls) {
|
|
@@ -2382,6 +2919,9 @@ var AgentLoop = class {
|
|
|
2382
2919
|
this.messages.push(toolMessage);
|
|
2383
2920
|
await this.persist(toolMessage);
|
|
2384
2921
|
answered.add(call.id);
|
|
2922
|
+
if (call.name === "todo_write" && !result.isError) {
|
|
2923
|
+
openTodos = pendingTodoTitles(call.args);
|
|
2924
|
+
}
|
|
2385
2925
|
yield {
|
|
2386
2926
|
type: "tool-result",
|
|
2387
2927
|
id: call.id,
|
|
@@ -2472,14 +3012,15 @@ ${summary}`
|
|
|
2472
3012
|
return compacted.messages;
|
|
2473
3013
|
}
|
|
2474
3014
|
}
|
|
2475
|
-
async *streamOnce(aiTools, signal) {
|
|
2476
|
-
const { streamChat } = await import("./stream-
|
|
3015
|
+
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
3016
|
+
const { streamChat } = await import("./stream-YQCRUBO3.js");
|
|
2477
3017
|
yield* streamChat({
|
|
2478
3018
|
model: this.opts.model,
|
|
2479
3019
|
messages: this.messages,
|
|
2480
3020
|
tools: aiTools,
|
|
2481
3021
|
abortSignal: signal,
|
|
2482
|
-
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
|
|
3022
|
+
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
|
|
3023
|
+
firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
|
|
2483
3024
|
});
|
|
2484
3025
|
}
|
|
2485
3026
|
buildAiTools() {
|
|
@@ -2572,6 +3113,7 @@ ${summary}`
|
|
|
2572
3113
|
};
|
|
2573
3114
|
|
|
2574
3115
|
export {
|
|
3116
|
+
defaultAgentTasks,
|
|
2575
3117
|
estimateTokens,
|
|
2576
3118
|
summarizeMessages,
|
|
2577
3119
|
compactMessages,
|
|
@@ -2581,6 +3123,8 @@ export {
|
|
|
2581
3123
|
collectCommitContext,
|
|
2582
3124
|
buildCommitPrompt,
|
|
2583
3125
|
collectWorkingDiff,
|
|
3126
|
+
MAX_IMAGE_DIMENSION,
|
|
3127
|
+
probeImageDimensions,
|
|
2584
3128
|
reconcileToolCalls,
|
|
2585
3129
|
isAllowedByRules,
|
|
2586
3130
|
buildAllowRule,
|
|
@@ -2588,12 +3132,20 @@ export {
|
|
|
2588
3132
|
listSnapshots,
|
|
2589
3133
|
clearSnapshots,
|
|
2590
3134
|
hydrateSnapshots,
|
|
3135
|
+
listTurnSnapshots,
|
|
3136
|
+
resolveSnapshotContent,
|
|
2591
3137
|
undoTurnSnapshots,
|
|
2592
3138
|
rewindToSnapshot,
|
|
3139
|
+
TodoStore,
|
|
3140
|
+
formatTodos,
|
|
3141
|
+
parseTodoArgs,
|
|
3142
|
+
loadTodos,
|
|
3143
|
+
resetTodos,
|
|
2593
3144
|
starHome,
|
|
2594
3145
|
globalConfigPath,
|
|
2595
3146
|
sessionsDir,
|
|
2596
3147
|
userMemoryPath,
|
|
3148
|
+
envFilePath,
|
|
2597
3149
|
projectConfigPath,
|
|
2598
3150
|
discoverSkills,
|
|
2599
3151
|
defaultTaskManager,
|
|
@@ -2605,11 +3157,9 @@ export {
|
|
|
2605
3157
|
formatTaskList,
|
|
2606
3158
|
formatTaskStarted,
|
|
2607
3159
|
formatTaskFinished,
|
|
2608
|
-
TodoStore,
|
|
2609
|
-
formatTodos,
|
|
2610
3160
|
createDefaultRegistry,
|
|
2611
3161
|
appendUserMemory,
|
|
2612
3162
|
PLAN_MODE_PROMPT,
|
|
2613
3163
|
AgentLoop
|
|
2614
3164
|
};
|
|
2615
|
-
//# sourceMappingURL=chunk-
|
|
3165
|
+
//# sourceMappingURL=chunk-K4SIXWCI.js.map
|