@cryer/star-cli 0.2.2 → 0.3.0

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.
@@ -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";
@@ -702,14 +702,211 @@ async function rewindToSnapshot(id) {
702
702
  return { reverted, messageIndex };
703
703
  }
704
704
 
705
+ // src/tools/todo.ts
706
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
707
+ import path3 from "path";
708
+ import { z } from "zod";
709
+ var todoItemSchema = z.object({
710
+ id: z.number().int(),
711
+ title: z.string(),
712
+ status: z.enum(["pending", "in_progress", "done"])
713
+ });
714
+ var writeSchema = z.object({
715
+ todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
716
+ });
717
+ var readSchema = z.object({});
718
+ function fileFor(cwd) {
719
+ return path3.join(cwd, ".star", "todos.json");
720
+ }
721
+ var TodoStore = class {
722
+ items = /* @__PURE__ */ new Map();
723
+ list() {
724
+ return [...this.items.values()];
725
+ }
726
+ replace(items) {
727
+ this.items.clear();
728
+ for (const item of items) {
729
+ this.items.set(item.id, item);
730
+ }
731
+ }
732
+ async load(cwd) {
733
+ let raw;
734
+ try {
735
+ raw = await readFile2(fileFor(cwd), "utf8");
736
+ } catch {
737
+ return;
738
+ }
739
+ try {
740
+ const parsed = JSON.parse(raw);
741
+ if (Array.isArray(parsed)) {
742
+ this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
743
+ }
744
+ } catch {
745
+ }
746
+ }
747
+ async save(cwd) {
748
+ await mkdir2(path3.join(cwd, ".star"), { recursive: true });
749
+ await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
750
+ `);
751
+ }
752
+ };
753
+ var SYMBOLS = {
754
+ pending: "[ ]",
755
+ in_progress: "[~]",
756
+ done: "[x]"
757
+ };
758
+ function formatTodos(items) {
759
+ if (items.length === 0) {
760
+ return "No todos.";
761
+ }
762
+ const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
763
+ const width = Math.max(...lines.map((l) => l.length));
764
+ return lines.map((line, i) => {
765
+ const status = items[i]?.status;
766
+ return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
767
+ }).join("\n");
768
+ }
769
+ var defaultStore = new TodoStore();
770
+ function parseTodoArgs(args) {
771
+ const parsed = writeSchema.safeParse(args);
772
+ return parsed.success ? parsed.data.todos : null;
773
+ }
774
+ function pendingTodoTitles(args) {
775
+ return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
776
+ }
777
+ async function loadTodos(cwd) {
778
+ await defaultStore.load(cwd);
779
+ return defaultStore.list();
780
+ }
781
+ function createTodoTools(store = defaultStore) {
782
+ const todoWrite = {
783
+ name: "todo_write",
784
+ description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
785
+ permission: "read",
786
+ parameters: writeSchema,
787
+ async execute(args, ctx) {
788
+ const parsed = writeSchema.safeParse(args);
789
+ if (!parsed.success) {
790
+ return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
791
+ }
792
+ await store.load(ctx.cwd);
793
+ store.replace(parsed.data.todos);
794
+ await store.save(ctx.cwd);
795
+ return { content: formatTodos(store.list()) };
796
+ }
797
+ };
798
+ const todoRead = {
799
+ name: "todo_read",
800
+ description: "Read the current todo list.",
801
+ permission: "read",
802
+ parameters: readSchema,
803
+ async execute(_args, ctx) {
804
+ await store.load(ctx.cwd);
805
+ return { content: formatTodos(store.list()) };
806
+ }
807
+ };
808
+ return [todoWrite, todoRead];
809
+ }
810
+
811
+ // src/agent/agent-tasks.ts
812
+ import { EventEmitter } from "events";
813
+ var AgentTaskManager = class extends EventEmitter {
814
+ records = /* @__PURE__ */ new Map();
815
+ counter = 0;
816
+ start(run, meta) {
817
+ const id = `agent-${++this.counter}`;
818
+ const rec = {
819
+ id,
820
+ prompt: meta.prompt,
821
+ description: meta.description,
822
+ status: "running",
823
+ startedAt: Date.now(),
824
+ result: "",
825
+ controller: new AbortController(),
826
+ notified: false
827
+ };
828
+ this.records.set(id, rec);
829
+ void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
830
+ (error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
831
+ );
832
+ this.emitUpdate(rec);
833
+ return this.snapshot(rec);
834
+ }
835
+ get(id) {
836
+ const rec = this.records.get(id);
837
+ return rec ? this.snapshot(rec) : void 0;
838
+ }
839
+ list() {
840
+ return [...this.records.values()].map((rec) => this.snapshot(rec));
841
+ }
842
+ runningCount() {
843
+ let count = 0;
844
+ for (const rec of this.records.values()) {
845
+ if (rec.status === "running") count += 1;
846
+ }
847
+ return count;
848
+ }
849
+ kill(id) {
850
+ const rec = this.records.get(id);
851
+ if (!rec || rec.status !== "running") return void 0;
852
+ rec.controller.abort();
853
+ this.finish(rec, "stopped", rec.result || "Stopped by user.");
854
+ return this.snapshot(rec);
855
+ }
856
+ cleanup() {
857
+ const killed = [];
858
+ for (const rec of this.records.values()) {
859
+ if (rec.status === "running") {
860
+ rec.controller.abort();
861
+ this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
862
+ killed.push(this.snapshot(rec));
863
+ }
864
+ }
865
+ return killed;
866
+ }
867
+ // Finished tasks whose result has not yet been handed to the parent loop.
868
+ drainNotifications() {
869
+ const drained = [];
870
+ for (const rec of this.records.values()) {
871
+ if (rec.status !== "running" && !rec.notified) {
872
+ rec.notified = true;
873
+ drained.push(this.snapshot(rec));
874
+ }
875
+ }
876
+ return drained;
877
+ }
878
+ finish(rec, status, result) {
879
+ if (rec.status !== "running") return;
880
+ rec.status = status;
881
+ rec.result = result;
882
+ rec.endedAt = Date.now();
883
+ this.emitUpdate(rec);
884
+ }
885
+ emitUpdate(rec) {
886
+ this.emit("update", this.snapshot(rec));
887
+ }
888
+ snapshot(rec) {
889
+ return {
890
+ id: rec.id,
891
+ prompt: rec.prompt,
892
+ description: rec.description,
893
+ status: rec.status,
894
+ startedAt: rec.startedAt,
895
+ endedAt: rec.endedAt,
896
+ result: rec.result
897
+ };
898
+ }
899
+ };
900
+ var defaultAgentTasks = new AgentTaskManager();
901
+
705
902
  // src/agent/project-memory.ts
706
903
  import { readFileSync, statSync } from "fs";
707
- import path3 from "path";
904
+ import path4 from "path";
708
905
  var PROJECT_MEMORY_FILE = "AGENTS.md";
709
906
  var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
710
907
  var cache = /* @__PURE__ */ new Map();
711
908
  function readProjectMemory(cwd) {
712
- const file = path3.join(cwd, PROJECT_MEMORY_FILE);
909
+ const file = path4.join(cwd, PROJECT_MEMORY_FILE);
713
910
  let mtimeMs;
714
911
  try {
715
912
  mtimeMs = statSync(file).mtimeMs;
@@ -739,26 +936,26 @@ ${body}`;
739
936
 
740
937
  // src/agent/skills.ts
741
938
  import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
742
- import path5 from "path";
743
- import { z } from "zod";
939
+ import path6 from "path";
940
+ import { z as z2 } from "zod";
744
941
 
745
942
  // src/config/paths.ts
746
943
  import os from "os";
747
- import path4 from "path";
944
+ import path5 from "path";
748
945
  function starHome() {
749
- return process.env.STAR_HOME ?? path4.join(os.homedir(), ".star-cli");
946
+ return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
750
947
  }
751
948
  function globalConfigPath() {
752
- return path4.join(starHome(), "config.toml");
949
+ return path5.join(starHome(), "config.toml");
753
950
  }
754
951
  function sessionsDir() {
755
- return path4.join(starHome(), "sessions");
952
+ return path5.join(starHome(), "sessions");
756
953
  }
757
954
  function userMemoryPath() {
758
- return path4.join(starHome(), "MEMORY.md");
955
+ return path5.join(starHome(), "MEMORY.md");
759
956
  }
760
957
  function projectConfigPath(cwd) {
761
- return path4.join(cwd, ".star", "config.toml");
958
+ return path5.join(cwd, ".star", "config.toml");
762
959
  }
763
960
 
764
961
  // src/agent/skills.ts
@@ -791,7 +988,7 @@ function parseSkillFile(filePath, dirName, scope) {
791
988
  const name = parsed?.meta.name || dirName;
792
989
  const description = parsed?.meta.description;
793
990
  if (parsed && NAME_PATTERN.test(name) && description) {
794
- skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
991
+ skill = { name, description, filePath, dir: path6.dirname(filePath), scope };
795
992
  }
796
993
  } catch {
797
994
  skill = null;
@@ -808,14 +1005,14 @@ function scanDir(base, scope, into) {
808
1005
  }
809
1006
  for (const entry of entries.sort()) {
810
1007
  if (!NAME_PATTERN.test(entry)) continue;
811
- const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
1008
+ const parsed = parseSkillFile(path6.join(base, entry, SKILL_FILE), entry, scope);
812
1009
  if (parsed) into.set(parsed.name, parsed);
813
1010
  }
814
1011
  }
815
1012
  function discoverSkills(cwd, home = starHome()) {
816
1013
  const byName = /* @__PURE__ */ new Map();
817
- scanDir(path5.join(home, "skills"), "user", byName);
818
- scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
1014
+ scanDir(path6.join(home, "skills"), "user", byName);
1015
+ scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
819
1016
  return [...byName.values()];
820
1017
  }
821
1018
  function formatSkillsBlock(skills) {
@@ -846,8 +1043,8 @@ function createSkillTool(deps) {
846
1043
  name: "skill",
847
1044
  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
1045
  permission: "read",
849
- parameters: z.object({
850
- name: z.string().describe("Name of the skill to load, as listed in the system prompt.")
1046
+ parameters: z2.object({
1047
+ name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
851
1048
  }),
852
1049
  async execute(args) {
853
1050
  const skills = discoverSkills(deps.cwd, deps.home);
@@ -882,16 +1079,16 @@ import { z as z12 } from "zod";
882
1079
  // src/tools/bash.ts
883
1080
  import { spawn as spawn3 } from "child_process";
884
1081
  import { existsSync } from "fs";
885
- import path6 from "path";
886
- import { z as z2 } from "zod";
1082
+ import path7 from "path";
1083
+ import { z as z3 } from "zod";
887
1084
 
888
1085
  // src/tasks/manager.ts
889
1086
  import { spawn as spawn2 } from "child_process";
890
- import { EventEmitter } from "events";
1087
+ import { EventEmitter as EventEmitter2 } from "events";
891
1088
  var MAX_TASK_OUTPUT = 1e5;
892
1089
  var DEFAULT_TASK_TIMEOUT = 3600;
893
1090
  var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
894
- var TaskManager = class extends EventEmitter {
1091
+ var TaskManager = class extends EventEmitter2 {
895
1092
  records = /* @__PURE__ */ new Map();
896
1093
  seq = 0;
897
1094
  start(opts) {
@@ -1002,11 +1199,11 @@ var MAX_OUTPUT = 3e4;
1002
1199
  var DEFAULT_TIMEOUT = 120;
1003
1200
  var MAX_TIMEOUT = 600;
1004
1201
  function findOnPath(exe, exclude) {
1005
- for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
1202
+ for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
1006
1203
  if (!dir || exclude?.(dir)) {
1007
1204
  continue;
1008
1205
  }
1009
- const full = path6.join(dir, exe);
1206
+ const full = path7.join(dir, exe);
1010
1207
  if (existsSync(full)) {
1011
1208
  return full;
1012
1209
  }
@@ -1024,7 +1221,7 @@ function resolveShell() {
1024
1221
  }
1025
1222
  const gitExe = findOnPath("git.exe");
1026
1223
  if (gitExe) {
1027
- const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
1224
+ const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
1028
1225
  if (existsSync(sibling)) {
1029
1226
  return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
1030
1227
  }
@@ -1041,11 +1238,11 @@ function resolveShell() {
1041
1238
  const comspec = process.env.ComSpec ?? "cmd.exe";
1042
1239
  return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
1043
1240
  }
1044
- var schema = z2.object({
1045
- command: z2.string().describe("Shell command to execute"),
1046
- timeout: z2.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1047
- description: z2.string().optional().describe("Short description of what the command does"),
1048
- run_in_background: z2.boolean().optional().describe(
1241
+ var schema = z3.object({
1242
+ command: z3.string().describe("Shell command to execute"),
1243
+ timeout: z3.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1244
+ description: z3.string().optional().describe("Short description of what the command does"),
1245
+ run_in_background: z3.boolean().optional().describe(
1049
1246
  "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
1247
  )
1051
1248
  });
@@ -1147,14 +1344,14 @@ Command timed out after ${timeoutSeconds}s`,
1147
1344
  };
1148
1345
 
1149
1346
  // src/tools/fs/edit.ts
1150
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1151
- import path7 from "path";
1152
- import { z as z3 } from "zod";
1153
- var schema2 = z3.object({
1154
- path: z3.string().describe("File path, absolute or relative to the working directory"),
1155
- old_string: z3.string().min(1).describe("Exact text to replace"),
1156
- new_string: z3.string().describe("Replacement text"),
1157
- replace_all: z3.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1347
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1348
+ import path8 from "path";
1349
+ import { z as z4 } from "zod";
1350
+ var schema2 = z4.object({
1351
+ path: z4.string().describe("File path, absolute or relative to the working directory"),
1352
+ old_string: z4.string().min(1).describe("Exact text to replace"),
1353
+ new_string: z4.string().describe("Replacement text"),
1354
+ replace_all: z4.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1158
1355
  });
1159
1356
  var editFileTool = {
1160
1357
  name: "edit_file",
@@ -1162,10 +1359,10 @@ var editFileTool = {
1162
1359
  permission: "write",
1163
1360
  parameters: schema2,
1164
1361
  async execute(args, ctx) {
1165
- const filePath = path7.resolve(ctx.cwd, args.path);
1362
+ const filePath = path8.resolve(ctx.cwd, args.path);
1166
1363
  let content;
1167
1364
  try {
1168
- content = await readFile2(filePath, "utf8");
1365
+ content = await readFile3(filePath, "utf8");
1169
1366
  } catch (err) {
1170
1367
  return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
1171
1368
  }
@@ -1186,7 +1383,7 @@ var editFileTool = {
1186
1383
  }
1187
1384
  const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
1188
1385
  try {
1189
- await writeFile2(filePath, updated, "utf8");
1386
+ await writeFile3(filePath, updated, "utf8");
1190
1387
  } catch (err) {
1191
1388
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1192
1389
  }
@@ -1205,13 +1402,13 @@ var editFileTool = {
1205
1402
  };
1206
1403
 
1207
1404
  // src/tools/fs/glob.ts
1208
- import path9 from "path";
1209
- import { z as z4 } from "zod";
1405
+ import path10 from "path";
1406
+ import { z as z5 } from "zod";
1210
1407
 
1211
1408
  // src/tools/fs/util.ts
1212
1409
  import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1213
1410
  import { readdir, stat } from "fs/promises";
1214
- import path8 from "path";
1411
+ import path9 from "path";
1215
1412
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1216
1413
  async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1217
1414
  const out = [];
@@ -1221,7 +1418,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
1221
1418
  return;
1222
1419
  }
1223
1420
  for (const entry of entries) {
1224
- const abs = path8.join(dir, entry.name);
1421
+ const abs = path9.join(dir, entry.name);
1225
1422
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
1226
1423
  if (entry.isDirectory()) {
1227
1424
  if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
@@ -1301,7 +1498,7 @@ function parseIgnorePatterns(body) {
1301
1498
  return out;
1302
1499
  }
1303
1500
  function loadIgnorePatterns(dir) {
1304
- const file = path8.join(dir, IGNORE_FILE);
1501
+ const file = path9.join(dir, IGNORE_FILE);
1305
1502
  let mtimeMs;
1306
1503
  try {
1307
1504
  mtimeMs = statSync3(file).mtimeMs;
@@ -1326,9 +1523,9 @@ function createIgnorePredicate(cwd) {
1326
1523
  if (patterns.length === 0) {
1327
1524
  return void 0;
1328
1525
  }
1329
- const root = path8.resolve(cwd);
1526
+ const root = path9.resolve(cwd);
1330
1527
  return (absPath, isDir) => {
1331
- const rel = path8.relative(root, absPath).split(path8.sep).join("/");
1528
+ const rel = path9.relative(root, absPath).split(path9.sep).join("/");
1332
1529
  if (!rel || rel.startsWith("..")) {
1333
1530
  return false;
1334
1531
  }
@@ -1344,7 +1541,7 @@ function createIgnorePredicate(cwd) {
1344
1541
  };
1345
1542
  }
1346
1543
  function isSensitivePath2(filePath) {
1347
- const base = path8.basename(filePath);
1544
+ const base = path9.basename(filePath);
1348
1545
  if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
1349
1546
  return false;
1350
1547
  }
@@ -1359,11 +1556,11 @@ function isSensitivePath2(filePath) {
1359
1556
 
1360
1557
  // src/tools/fs/glob.ts
1361
1558
  var MAX_RESULTS = 100;
1362
- var schema3 = z4.object({
1363
- pattern: z4.string().describe(
1559
+ var schema3 = z5.object({
1560
+ pattern: z5.string().describe(
1364
1561
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
1365
1562
  ),
1366
- path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1563
+ path: z5.string().optional().describe("Directory to search, defaults to the working directory")
1367
1564
  });
1368
1565
  var globTool = {
1369
1566
  name: "glob",
@@ -1371,7 +1568,7 @@ var globTool = {
1371
1568
  permission: "read",
1372
1569
  parameters: schema3,
1373
1570
  async execute(args, ctx) {
1374
- const root = path9.resolve(ctx.cwd, args.path ?? ".");
1571
+ const root = path10.resolve(ctx.cwd, args.path ?? ".");
1375
1572
  const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1376
1573
  const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
1377
1574
  if (matched.length === 0) {
@@ -1382,15 +1579,15 @@ var globTool = {
1382
1579
  };
1383
1580
 
1384
1581
  // src/tools/fs/grep.ts
1385
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
1386
- import path10 from "path";
1387
- import { z as z5 } from "zod";
1582
+ import { readFile as readFile4, stat as stat2 } from "fs/promises";
1583
+ import path11 from "path";
1584
+ import { z as z6 } from "zod";
1388
1585
  var MAX_MATCHES = 250;
1389
- var schema4 = z5.object({
1390
- pattern: z5.string().describe("Regular expression to search for"),
1391
- path: z5.string().optional().describe("File or directory to search, defaults to the working directory"),
1392
- glob: z5.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1393
- ignoreCase: z5.boolean().optional().describe("Case-insensitive matching")
1586
+ var schema4 = z6.object({
1587
+ pattern: z6.string().describe("Regular expression to search for"),
1588
+ path: z6.string().optional().describe("File or directory to search, defaults to the working directory"),
1589
+ glob: z6.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1590
+ ignoreCase: z6.boolean().optional().describe("Case-insensitive matching")
1394
1591
  });
1395
1592
  var grepTool = {
1396
1593
  name: "grep",
@@ -1404,12 +1601,12 @@ var grepTool = {
1404
1601
  } catch (err) {
1405
1602
  return { content: `Invalid regular expression: ${err.message}`, isError: true };
1406
1603
  }
1407
- const root = path10.resolve(ctx.cwd, args.path ?? ".");
1604
+ const root = path11.resolve(ctx.cwd, args.path ?? ".");
1408
1605
  let files;
1409
1606
  try {
1410
1607
  const st = await stat2(root);
1411
1608
  if (st.isFile()) {
1412
- files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
1609
+ files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
1413
1610
  } else {
1414
1611
  files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
1415
1612
  }
@@ -1425,7 +1622,7 @@ var grepTool = {
1425
1622
  outer: for (const file of files) {
1426
1623
  let buf;
1427
1624
  try {
1428
- buf = await readFile3(file.abs);
1625
+ buf = await readFile4(file.abs);
1429
1626
  } catch {
1430
1627
  continue;
1431
1628
  }
@@ -1455,15 +1652,15 @@ var grepTool = {
1455
1652
  };
1456
1653
 
1457
1654
  // src/tools/fs/read.ts
1458
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
1459
- import path11 from "path";
1460
- import { z as z6 } from "zod";
1655
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1656
+ import path12 from "path";
1657
+ import { z as z7 } from "zod";
1461
1658
  var DEFAULT_LIMIT = 2e3;
1462
1659
  var MAX_CHARS = 100 * 1024;
1463
- var schema5 = z6.object({
1464
- path: z6.string().describe("File path, absolute or relative to the working directory"),
1465
- offset: z6.number().int().positive().optional().describe("1-based line number to start reading from"),
1466
- limit: z6.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1660
+ var schema5 = z7.object({
1661
+ path: z7.string().describe("File path, absolute or relative to the working directory"),
1662
+ offset: z7.number().int().positive().optional().describe("1-based line number to start reading from"),
1663
+ limit: z7.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1467
1664
  });
1468
1665
  var readFileTool = {
1469
1666
  name: "read_file",
@@ -1471,7 +1668,7 @@ var readFileTool = {
1471
1668
  permission: "read",
1472
1669
  parameters: schema5,
1473
1670
  async execute(args, ctx) {
1474
- const filePath = path11.resolve(ctx.cwd, args.path);
1671
+ const filePath = path12.resolve(ctx.cwd, args.path);
1475
1672
  if (isSensitivePath2(filePath)) {
1476
1673
  return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
1477
1674
  }
@@ -1482,7 +1679,7 @@ var readFileTool = {
1482
1679
  if (st.isDirectory()) {
1483
1680
  return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
1484
1681
  }
1485
- const raw = await readFile4(filePath, "utf8");
1682
+ const raw = await readFile5(filePath, "utf8");
1486
1683
  const lines = raw.split("\n");
1487
1684
  const start = args.offset ?? 1;
1488
1685
  const limit = args.limit ?? DEFAULT_LIMIT;
@@ -1516,12 +1713,12 @@ var readFileTool = {
1516
1713
  };
1517
1714
 
1518
1715
  // src/tools/fs/write.ts
1519
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1520
- import path12 from "path";
1521
- import { z as z7 } from "zod";
1522
- var schema6 = z7.object({
1523
- path: z7.string().describe("File path, absolute or relative to the working directory"),
1524
- content: z7.string().describe("Full content to write to the file")
1716
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
1717
+ import path13 from "path";
1718
+ import { z as z8 } from "zod";
1719
+ var schema6 = z8.object({
1720
+ path: z8.string().describe("File path, absolute or relative to the working directory"),
1721
+ content: z8.string().describe("Full content to write to the file")
1525
1722
  });
1526
1723
  var writeFileTool = {
1527
1724
  name: "write_file",
@@ -1529,11 +1726,11 @@ var writeFileTool = {
1529
1726
  permission: "write",
1530
1727
  parameters: schema6,
1531
1728
  async execute(args, ctx) {
1532
- const filePath = path12.resolve(ctx.cwd, args.path);
1729
+ const filePath = path13.resolve(ctx.cwd, args.path);
1533
1730
  const snapshot = await captureSnapshot(filePath, "write_file");
1534
1731
  try {
1535
- await mkdir2(path12.dirname(filePath), { recursive: true });
1536
- await writeFile3(filePath, args.content, "utf8");
1732
+ await mkdir3(path13.dirname(filePath), { recursive: true });
1733
+ await writeFile4(filePath, args.content, "utf8");
1537
1734
  } catch (err) {
1538
1735
  return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
1539
1736
  }
@@ -1543,7 +1740,7 @@ var writeFileTool = {
1543
1740
  };
1544
1741
 
1545
1742
  // src/tools/tasks.ts
1546
- import { z as z8 } from "zod";
1743
+ import { z as z9 } from "zod";
1547
1744
 
1548
1745
  // src/tasks/format.ts
1549
1746
  function formatDuration(startedAt, endedAt) {
@@ -1589,40 +1786,59 @@ function formatTaskFinished(task) {
1589
1786
  }
1590
1787
 
1591
1788
  // src/tools/tasks.ts
1592
- var idSchema = z8.object({
1593
- id: z8.string().describe("Task id, e.g. task-1")
1789
+ var idSchema = z9.object({
1790
+ id: z9.string().describe("Task id, e.g. task-1 or agent-1")
1594
1791
  });
1792
+ function formatAgentTaskLine(task) {
1793
+ const duration = formatDuration(task.startedAt, task.endedAt);
1794
+ const label = task.description ?? task.prompt.slice(0, 60);
1795
+ const status = task.status === "running" ? `running ${duration}` : `${task.status} after ${duration}`;
1796
+ return `${task.id} [${status}] ${label}`;
1797
+ }
1595
1798
  var taskListTool = {
1596
1799
  name: "task_list",
1597
- description: "List background shell tasks with status, runtime, and exit code.",
1800
+ description: "List background tasks (shell commands and subagents) with status and runtime.",
1598
1801
  permission: "read",
1599
- parameters: z8.object({}),
1802
+ parameters: z9.object({}),
1600
1803
  execute() {
1601
- return Promise.resolve({ content: formatTaskList(defaultTaskManager.list()) });
1804
+ const shell = formatTaskList(defaultTaskManager.list());
1805
+ const agents = defaultAgentTasks.list();
1806
+ const agentLines = agents.length === 0 ? "No background subagents." : `Background subagents:
1807
+ ${agents.map(formatAgentTaskLine).join("\n")}`;
1808
+ return Promise.resolve({ content: `${shell}
1809
+ ${agentLines}` });
1602
1810
  }
1603
1811
  };
1604
1812
  var taskOutputTool = {
1605
1813
  name: "task_output",
1606
- description: "Read the current output of a background shell task (works while still running).",
1814
+ description: "Read the current output of a background task (shell command or subagent; works while still running).",
1607
1815
  permission: "read",
1608
1816
  parameters: idSchema,
1609
1817
  execute(args) {
1610
- const task = defaultTaskManager.get(args.id);
1611
- if (!task) {
1612
- return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1818
+ const shell = defaultTaskManager.get(args.id);
1819
+ if (shell) {
1820
+ const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
1821
+ return Promise.resolve({ content: `${formatTaskLine(shell)}
1822
+ ${body}` });
1613
1823
  }
1614
- const body = task.output.replace(/\s+$/, "") || "(no output yet)";
1615
- return Promise.resolve({ content: `${formatTaskLine(task)}
1824
+ const agent = defaultAgentTasks.get(args.id);
1825
+ if (agent) {
1826
+ const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
1827
+ return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
1616
1828
  ${body}` });
1829
+ }
1830
+ return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1617
1831
  }
1618
1832
  };
1619
1833
  var taskKillTool = {
1620
1834
  name: "task_kill",
1621
- description: "Stop a running background shell task (kills the whole process tree).",
1835
+ description: "Stop a running background task (shell command process tree or subagent).",
1622
1836
  permission: "exec",
1623
1837
  parameters: idSchema,
1624
1838
  execute(args) {
1625
- const task = defaultTaskManager.get(args.id);
1839
+ const shell = defaultTaskManager.get(args.id);
1840
+ const agent = shell ? void 0 : defaultAgentTasks.get(args.id);
1841
+ const task = shell ?? agent;
1626
1842
  if (!task) {
1627
1843
  return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
1628
1844
  }
@@ -1632,105 +1848,14 @@ var taskKillTool = {
1632
1848
  isError: true
1633
1849
  });
1634
1850
  }
1635
- defaultTaskManager.kill(args.id);
1636
- return Promise.resolve({ content: `Task ${args.id} stopped.` });
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 {
1851
+ if (shell) {
1852
+ defaultTaskManager.kill(args.id);
1853
+ } else {
1854
+ defaultAgentTasks.kill(args.id);
1680
1855
  }
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
- `);
1856
+ return Promise.resolve({ content: `Task ${args.id} stopped.` });
1686
1857
  }
1687
1858
  };
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
1859
 
1735
1860
  // src/tools/web/fetch.ts
1736
1861
  import { z as z10 } from "zod";
@@ -2029,69 +2154,86 @@ function createDefaultRegistry() {
2029
2154
  var MAX_SUBAGENT_DEPTH = 1;
2030
2155
  var MAX_REPORT_CHARS = 2e4;
2031
2156
  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.";
2157
+ async function runSubagent(deps, args, cwd, signal) {
2158
+ const { AgentLoop: AgentLoop2 } = await import("./loop-EIODKYPT.js");
2159
+ const child = new AgentLoop2({
2160
+ model: deps.model,
2161
+ registry: createDefaultRegistry(),
2162
+ config: deps.config,
2163
+ cwd,
2164
+ system: deps.system ? `${deps.system}
2165
+
2166
+ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
2167
+ subagentDepth: deps.depth + 1
2168
+ });
2169
+ const confirmHandler = deps.getConfirmHandler?.();
2170
+ if (confirmHandler) child.confirmHandler = confirmHandler;
2171
+ let toolCalls = 0;
2172
+ let error = null;
2173
+ try {
2174
+ for await (const event of child.stream(args.prompt, signal)) {
2175
+ if (event.type === "tool-result") {
2176
+ toolCalls++;
2177
+ } else if (event.type === "error") {
2178
+ error = event.error.message;
2179
+ break;
2180
+ }
2181
+ }
2182
+ } catch (e) {
2183
+ error = e instanceof Error ? e.message : String(e);
2184
+ }
2185
+ let report = "";
2186
+ const messages = child.getMessages();
2187
+ for (let i = messages.length - 1; i >= 0; i--) {
2188
+ const message = messages[i];
2189
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
2190
+ const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
2191
+ if (text) {
2192
+ report = text;
2193
+ break;
2194
+ }
2195
+ }
2196
+ if (report.length > MAX_REPORT_CHARS) {
2197
+ report = `${report.slice(0, MAX_REPORT_CHARS)}
2198
+ ... (truncated)`;
2199
+ }
2200
+ const label = args.description ? ` "${args.description}"` : "";
2201
+ const stats = `[subagent${label}: ${toolCalls} tool call(s)]`;
2202
+ if (error) {
2203
+ throw new Error([`Subagent failed: ${error}`, stats, report].filter(Boolean).join("\n"));
2204
+ }
2205
+ return `${stats}
2206
+ ${report || "(subagent produced no text output)"}`;
2207
+ }
2032
2208
  function createSubagentTool(deps) {
2033
2209
  return {
2034
2210
  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.",
2211
+ 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
2212
  permission: "exec",
2037
2213
  parameters: z12.object({
2038
2214
  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.")
2215
+ description: z12.string().optional().describe("Short label for the subtask, shown in the result header."),
2216
+ run_in_background: z12.boolean().optional().describe(
2217
+ "Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
2218
+ )
2040
2219
  }),
2041
2220
  async execute(args, ctx) {
2042
- const { AgentLoop: AgentLoop2 } = await import("./loop-ADVWZA62.js");
2043
- const child = new AgentLoop2({
2044
- model: deps.model,
2045
- registry: createDefaultRegistry(),
2046
- config: deps.config,
2047
- cwd: ctx.cwd,
2048
- system: deps.system ? `${deps.system}
2049
-
2050
- ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
2051
- subagentDepth: deps.depth + 1
2052
- });
2053
- const confirmHandler = deps.getConfirmHandler?.();
2054
- if (confirmHandler) child.confirmHandler = confirmHandler;
2221
+ if (args.run_in_background) {
2222
+ const task = defaultAgentTasks.start((signal2) => runSubagent(deps, args, ctx.cwd, signal2), {
2223
+ prompt: args.prompt,
2224
+ description: args.description
2225
+ });
2226
+ const label = args.description ? ` "${args.description}"` : "";
2227
+ return {
2228
+ 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.`
2229
+ };
2230
+ }
2055
2231
  const signal = ctx.abortSignal ?? new AbortController().signal;
2056
- let toolCalls = 0;
2057
- let error = null;
2058
2232
  try {
2059
- for await (const event of child.stream(args.prompt, signal)) {
2060
- if (event.type === "tool-result") {
2061
- toolCalls++;
2062
- } else if (event.type === "error") {
2063
- error = event.error.message;
2064
- break;
2065
- }
2066
- }
2233
+ return { content: await runSubagent(deps, args, ctx.cwd, signal) };
2067
2234
  } catch (e) {
2068
- error = e instanceof Error ? e.message : String(e);
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)`;
2084
- }
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
- };
2235
+ return { content: e instanceof Error ? e.message : String(e), isError: true };
2092
2236
  }
2093
- return { content: `${stats}
2094
- ${report || "(subagent produced no text output)"}` };
2095
2237
  }
2096
2238
  };
2097
2239
  }
@@ -2156,6 +2298,72 @@ function createRememberTool(deps = {}) {
2156
2298
 
2157
2299
  // src/agent/loop.ts
2158
2300
  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.";
2301
+ 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;
2302
+ 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.";
2303
+ var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
2304
+ async function checkTaskComplete(model, request, finalReply, signal) {
2305
+ const controller = new AbortController();
2306
+ const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
2307
+ const onAbort = () => controller.abort();
2308
+ signal.addEventListener("abort", onAbort, { once: true });
2309
+ try {
2310
+ const { text } = await generateText3({
2311
+ model,
2312
+ maxTokens: 8,
2313
+ abortSignal: controller.signal,
2314
+ prompt: [
2315
+ "An AI coding agent was given this task by the user:",
2316
+ `<task>
2317
+ ${request.slice(0, 2e3)}
2318
+ </task>`,
2319
+ "The agent has stopped calling tools and ended with this reply:",
2320
+ `<reply>
2321
+ ${finalReply.slice(0, 2e3)}
2322
+ </reply>`,
2323
+ "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."
2324
+ ].join("\n")
2325
+ });
2326
+ const verdict = text.trim().toUpperCase();
2327
+ if (verdict.startsWith("NOT_DONE")) return false;
2328
+ if (verdict.startsWith("DONE")) return true;
2329
+ return null;
2330
+ } catch {
2331
+ return null;
2332
+ } finally {
2333
+ clearTimeout(timer);
2334
+ signal.removeEventListener("abort", onAbort);
2335
+ }
2336
+ }
2337
+ var STREAM_RETRY_BASE_DELAY_MS = 1e3;
2338
+ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
2339
+ "AI_NoSuchToolError",
2340
+ "AI_InvalidToolArgumentsError",
2341
+ "AI_InvalidPromptError",
2342
+ "AI_NoSuchModelError",
2343
+ "AI_LoadAPIKeyError"
2344
+ ]);
2345
+ function isRetryableStreamError(error) {
2346
+ if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
2347
+ const status = error.statusCode;
2348
+ if (typeof status === "number") {
2349
+ return status === 408 || status === 429 || status >= 500;
2350
+ }
2351
+ return true;
2352
+ }
2353
+ function sleep(ms, signal) {
2354
+ if (signal.aborted) return Promise.resolve(false);
2355
+ return new Promise((resolve) => {
2356
+ const onAbort = () => {
2357
+ clearTimeout(timer);
2358
+ resolve(false);
2359
+ };
2360
+ const timer = setTimeout(() => {
2361
+ signal.removeEventListener("abort", onAbort);
2362
+ resolve(true);
2363
+ }, ms);
2364
+ signal.addEventListener("abort", onAbort, { once: true });
2365
+ });
2366
+ }
2159
2367
  var AgentLoop = class {
2160
2368
  messages = [];
2161
2369
  opts;
@@ -2291,7 +2499,7 @@ var AgentLoop = class {
2291
2499
  }
2292
2500
  async *stream(input, signal, opts) {
2293
2501
  this.syncSystemMessage();
2294
- const text = typeof input === "string" ? input : input.text;
2502
+ const inputText = typeof input === "string" ? input : input.text;
2295
2503
  const images = typeof input === "string" ? [] : input.images;
2296
2504
  const userMessage = images.length > 0 ? {
2297
2505
  role: "user",
@@ -2301,9 +2509,9 @@ var AgentLoop = class {
2301
2509
  image: img.data,
2302
2510
  mimeType: img.mimeType
2303
2511
  })),
2304
- { type: "text", text }
2512
+ { type: "text", text: inputText }
2305
2513
  ]
2306
- } : { role: "user", content: text };
2514
+ } : { role: "user", content: inputText };
2307
2515
  this.messages.push(userMessage);
2308
2516
  const seq = (this.opts.subagentDepth ?? 0) === 0 ? beginTurn(this.messages.length - 1) : currentTurnSeq();
2309
2517
  this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
@@ -2312,42 +2520,89 @@ var AgentLoop = class {
2312
2520
  );
2313
2521
  const { config, registry, cwd } = this.opts;
2314
2522
  const aiTools = this.buildAiTools();
2523
+ let autoContinues = 0;
2524
+ let lastWasNudge = false;
2525
+ let usedToolsThisTurn = false;
2526
+ let openTodos = [];
2315
2527
  for (let step = 0; step < config.maxSteps; step++) {
2528
+ if ((this.opts.subagentDepth ?? 0) === 0) {
2529
+ for (const finished of defaultAgentTasks.drainNotifications()) {
2530
+ const label = finished.description ? ` "${finished.description}"` : "";
2531
+ const note = {
2532
+ role: "user",
2533
+ content: `[background subagent ${finished.id}${label} ${finished.status}]
2534
+ ${finished.result}`
2535
+ };
2536
+ this.messages.push(note);
2537
+ await this.persist(note);
2538
+ }
2539
+ }
2316
2540
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
2317
2541
  const compacted = compactMessages([...this.messages], maxTokens);
2318
2542
  if (compacted.compacted) {
2319
2543
  this.messages = await this.applyCompactionSummary(compacted);
2320
2544
  }
2321
- let text2 = "";
2545
+ const maxRetries = Math.max(0, config.streamMaxRetries);
2546
+ const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
2547
+ let text = "";
2322
2548
  const toolCalls = [];
2323
- let failed = false;
2324
- try {
2325
- for await (const event of this.streamOnce(aiTools, signal)) {
2326
- if (event.type === "text-delta") {
2327
- text2 += event.text;
2328
- yield event;
2329
- } else if (event.type === "reasoning") {
2330
- yield event;
2331
- } else if (event.type === "tool-call") {
2332
- toolCalls.push({ id: event.id, name: event.name, args: event.args });
2333
- yield event;
2334
- } else if (event.type === "error") {
2335
- failed = true;
2336
- yield event;
2337
- } else {
2338
- yield event;
2549
+ let failure = null;
2550
+ for (let attempt = 0; ; attempt++) {
2551
+ text = "";
2552
+ toolCalls.length = 0;
2553
+ failure = null;
2554
+ let finishReason;
2555
+ const timeoutScale = Math.min(attempt + 1, 3);
2556
+ try {
2557
+ for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
2558
+ if (event.type === "text-delta") {
2559
+ text += event.text;
2560
+ yield event;
2561
+ } else if (event.type === "reasoning") {
2562
+ yield event;
2563
+ } else if (event.type === "tool-call") {
2564
+ toolCalls.push({ id: event.id, name: event.name, args: event.args });
2565
+ yield event;
2566
+ } else if (event.type === "finish") {
2567
+ finishReason = event.finishReason;
2568
+ yield event;
2569
+ } else if (event.type === "error") {
2570
+ failure = event.error;
2571
+ } else {
2572
+ yield event;
2573
+ }
2339
2574
  }
2575
+ } catch (error) {
2576
+ if (signal.aborted) return;
2577
+ failure = error instanceof Error ? error : new Error(String(error));
2340
2578
  }
2341
- } catch (error) {
2342
2579
  if (signal.aborted) return;
2343
- yield { type: "error", error: error instanceof Error ? error : new Error(String(error)) };
2580
+ if (!failure && (text.length > 0 || toolCalls.length > 0)) break;
2581
+ if (failure && !isRetryableStreamError(failure)) break;
2582
+ if (attempt >= maxRetries) break;
2583
+ yield {
2584
+ type: "retry",
2585
+ attempt: attempt + 2,
2586
+ maxAttempts: maxRetries + 1,
2587
+ reason: failure ? failure.message : `empty response (finish: ${finishReason ?? "none"})`
2588
+ };
2589
+ if (!await sleep(baseDelay * 2 ** attempt, signal)) return;
2590
+ }
2591
+ if (failure) {
2592
+ yield { type: "error", error: failure };
2593
+ return;
2594
+ }
2595
+ if (text.length === 0 && toolCalls.length === 0) {
2596
+ yield {
2597
+ type: "error",
2598
+ error: new Error("The model returned an empty response after retries; ending the turn.")
2599
+ };
2344
2600
  return;
2345
2601
  }
2346
- if (failed) return;
2347
2602
  const assistantMessage = {
2348
2603
  role: "assistant",
2349
2604
  content: [
2350
- ...text2 ? [{ type: "text", text: text2 }] : [],
2605
+ ...text ? [{ type: "text", text }] : [],
2351
2606
  ...toolCalls.map((call) => ({
2352
2607
  type: "tool-call",
2353
2608
  toolCallId: call.id,
@@ -2358,11 +2613,40 @@ var AgentLoop = class {
2358
2613
  };
2359
2614
  this.messages.push(assistantMessage);
2360
2615
  await this.persist(assistantMessage);
2361
- this.maybeScheduleTitle(text2);
2616
+ this.maybeScheduleTitle(text);
2362
2617
  if (toolCalls.length === 0) {
2618
+ let reason = null;
2619
+ let nudgeText = AUTO_CONTINUE_NUDGE;
2620
+ if (openTodos.length > 0) {
2621
+ reason = `${openTodos.length} todo item(s) still open`;
2622
+ nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
2623
+ ", "
2624
+ )}. Complete them now with tool calls, or update the list with todo_write if they are no longer needed.`;
2625
+ } else if (lastWasNudge) {
2626
+ reason = "reply still had no tool calls";
2627
+ } else if (PENDING_WORK_PATTERN.test(text)) {
2628
+ reason = "reply announced unfinished work";
2629
+ } else if (usedToolsThisTurn && !signal.aborted) {
2630
+ const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
2631
+ if (complete === false) reason = "completion check reports the task unfinished";
2632
+ }
2633
+ if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
2634
+ autoContinues++;
2635
+ lastWasNudge = true;
2636
+ yield {
2637
+ type: "notice",
2638
+ message: `${reason}; asking the model to continue (${autoContinues}/${config.maxAutoContinues}).`
2639
+ };
2640
+ const nudge = { role: "user", content: nudgeText };
2641
+ this.messages.push(nudge);
2642
+ await this.persist(nudge);
2643
+ continue;
2644
+ }
2363
2645
  await this.runEventHooks("Stop");
2364
2646
  return;
2365
2647
  }
2648
+ lastWasNudge = false;
2649
+ usedToolsThisTurn = true;
2366
2650
  const answered = /* @__PURE__ */ new Set();
2367
2651
  try {
2368
2652
  for (const call of toolCalls) {
@@ -2382,6 +2666,9 @@ var AgentLoop = class {
2382
2666
  this.messages.push(toolMessage);
2383
2667
  await this.persist(toolMessage);
2384
2668
  answered.add(call.id);
2669
+ if (call.name === "todo_write" && !result.isError) {
2670
+ openTodos = pendingTodoTitles(call.args);
2671
+ }
2385
2672
  yield {
2386
2673
  type: "tool-result",
2387
2674
  id: call.id,
@@ -2472,14 +2759,15 @@ ${summary}`
2472
2759
  return compacted.messages;
2473
2760
  }
2474
2761
  }
2475
- async *streamOnce(aiTools, signal) {
2762
+ async *streamOnce(aiTools, signal, timeoutScale = 1) {
2476
2763
  const { streamChat } = await import("./stream-Y55NRF2S.js");
2477
2764
  yield* streamChat({
2478
2765
  model: this.opts.model,
2479
2766
  messages: this.messages,
2480
2767
  tools: aiTools,
2481
2768
  abortSignal: signal,
2482
- idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
2769
+ idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
2770
+ firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
2483
2771
  });
2484
2772
  }
2485
2773
  buildAiTools() {
@@ -2572,6 +2860,7 @@ ${summary}`
2572
2860
  };
2573
2861
 
2574
2862
  export {
2863
+ defaultAgentTasks,
2575
2864
  estimateTokens,
2576
2865
  summarizeMessages,
2577
2866
  compactMessages,
@@ -2590,6 +2879,10 @@ export {
2590
2879
  hydrateSnapshots,
2591
2880
  undoTurnSnapshots,
2592
2881
  rewindToSnapshot,
2882
+ TodoStore,
2883
+ formatTodos,
2884
+ parseTodoArgs,
2885
+ loadTodos,
2593
2886
  starHome,
2594
2887
  globalConfigPath,
2595
2888
  sessionsDir,
@@ -2605,11 +2898,9 @@ export {
2605
2898
  formatTaskList,
2606
2899
  formatTaskStarted,
2607
2900
  formatTaskFinished,
2608
- TodoStore,
2609
- formatTodos,
2610
2901
  createDefaultRegistry,
2611
2902
  appendUserMemory,
2612
2903
  PLAN_MODE_PROMPT,
2613
2904
  AgentLoop
2614
2905
  };
2615
- //# sourceMappingURL=chunk-GN236FD4.js.map
2906
+ //# sourceMappingURL=chunk-DZWTIMJ2.js.map