@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.
- package/README.md +23 -9
- package/dist/{chunk-GN236FD4.js → chunk-DZWTIMJ2.js} +563 -272
- package/dist/chunk-DZWTIMJ2.js.map +1 -0
- package/dist/{loop-ADVWZA62.js → loop-EIODKYPT.js} +2 -2
- package/dist/main.js +185 -65
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GN236FD4.js.map +0 -1
- /package/dist/{loop-ADVWZA62.js.map → loop-EIODKYPT.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";
|
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
|
944
|
+
import path5 from "path";
|
|
748
945
|
function starHome() {
|
|
749
|
-
return process.env.STAR_HOME ??
|
|
946
|
+
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
750
947
|
}
|
|
751
948
|
function globalConfigPath() {
|
|
752
|
-
return
|
|
949
|
+
return path5.join(starHome(), "config.toml");
|
|
753
950
|
}
|
|
754
951
|
function sessionsDir() {
|
|
755
|
-
return
|
|
952
|
+
return path5.join(starHome(), "sessions");
|
|
756
953
|
}
|
|
757
954
|
function userMemoryPath() {
|
|
758
|
-
return
|
|
955
|
+
return path5.join(starHome(), "MEMORY.md");
|
|
759
956
|
}
|
|
760
957
|
function projectConfigPath(cwd) {
|
|
761
|
-
return
|
|
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:
|
|
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(
|
|
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(
|
|
818
|
-
scanDir(
|
|
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:
|
|
850
|
-
name:
|
|
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
|
|
886
|
-
import { z as
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1045
|
-
command:
|
|
1046
|
-
timeout:
|
|
1047
|
-
description:
|
|
1048
|
-
run_in_background:
|
|
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
|
|
1151
|
-
import
|
|
1152
|
-
import { z as
|
|
1153
|
-
var schema2 =
|
|
1154
|
-
path:
|
|
1155
|
-
old_string:
|
|
1156
|
-
new_string:
|
|
1157
|
-
replace_all:
|
|
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 =
|
|
1362
|
+
const filePath = path8.resolve(ctx.cwd, args.path);
|
|
1166
1363
|
let content;
|
|
1167
1364
|
try {
|
|
1168
|
-
content = await
|
|
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
|
|
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
|
|
1209
|
-
import { z as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1526
|
+
const root = path9.resolve(cwd);
|
|
1330
1527
|
return (absPath, isDir) => {
|
|
1331
|
-
const rel =
|
|
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 =
|
|
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 =
|
|
1363
|
-
pattern:
|
|
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:
|
|
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 =
|
|
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
|
|
1386
|
-
import
|
|
1387
|
-
import { z as
|
|
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 =
|
|
1390
|
-
pattern:
|
|
1391
|
-
path:
|
|
1392
|
-
glob:
|
|
1393
|
-
ignoreCase:
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
1459
|
-
import
|
|
1460
|
-
import { z as
|
|
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 =
|
|
1464
|
-
path:
|
|
1465
|
-
offset:
|
|
1466
|
-
limit:
|
|
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 =
|
|
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
|
|
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
|
|
1520
|
-
import
|
|
1521
|
-
import { z as
|
|
1522
|
-
var schema6 =
|
|
1523
|
-
path:
|
|
1524
|
-
content:
|
|
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 =
|
|
1729
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1533
1730
|
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1534
1731
|
try {
|
|
1535
|
-
await
|
|
1536
|
-
await
|
|
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
|
|
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 =
|
|
1593
|
-
id:
|
|
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
|
|
1800
|
+
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
1598
1801
|
permission: "read",
|
|
1599
|
-
parameters:
|
|
1802
|
+
parameters: z9.object({}),
|
|
1600
1803
|
execute() {
|
|
1601
|
-
|
|
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
|
|
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
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
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
|
|
1615
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
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-
|
|
2906
|
+
//# sourceMappingURL=chunk-DZWTIMJ2.js.map
|