@cryer/star-cli 0.2.1 → 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-MJ2O7NWI.js → chunk-DZWTIMJ2.js} +713 -276
- package/dist/chunk-DZWTIMJ2.js.map +1 -0
- package/dist/{loop-R3RQCNFT.js → loop-EIODKYPT.js} +2 -2
- package/dist/main.js +733 -170
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MJ2O7NWI.js.map +0 -1
- /package/dist/{loop-R3RQCNFT.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";
|
|
@@ -433,6 +433,12 @@ function matchAllowRule(rule, req) {
|
|
|
433
433
|
return target !== void 0 && globMatch(rule.pattern, target);
|
|
434
434
|
}
|
|
435
435
|
function isAllowedByRules(rules, req) {
|
|
436
|
+
return matchesAnyRule(rules, req);
|
|
437
|
+
}
|
|
438
|
+
function isDeniedByRules(rules, req) {
|
|
439
|
+
return matchesAnyRule(rules, req);
|
|
440
|
+
}
|
|
441
|
+
function matchesAnyRule(rules, req) {
|
|
436
442
|
return rules.some((raw) => {
|
|
437
443
|
const rule = parseAllowRule(raw);
|
|
438
444
|
return rule !== null && matchAllowRule(rule, req);
|
|
@@ -484,7 +490,7 @@ function isSensitivePath(p) {
|
|
|
484
490
|
}
|
|
485
491
|
return base === ".env" || base.startsWith(".env.") || base === "id_rsa" || base.endsWith(".pem");
|
|
486
492
|
}
|
|
487
|
-
function checkPermission(mode, req, ctx, allowRules = []) {
|
|
493
|
+
function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
488
494
|
if (mode === "yolo") {
|
|
489
495
|
return "allow";
|
|
490
496
|
}
|
|
@@ -504,6 +510,9 @@ function checkPermission(mode, req, ctx, allowRules = []) {
|
|
|
504
510
|
return "deny";
|
|
505
511
|
}
|
|
506
512
|
}
|
|
513
|
+
if (isDeniedByRules(denyRules, req)) {
|
|
514
|
+
return "deny";
|
|
515
|
+
}
|
|
507
516
|
if (mode === "auto") {
|
|
508
517
|
return "allow";
|
|
509
518
|
}
|
|
@@ -693,14 +702,211 @@ async function rewindToSnapshot(id) {
|
|
|
693
702
|
return { reverted, messageIndex };
|
|
694
703
|
}
|
|
695
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
|
+
|
|
696
902
|
// src/agent/project-memory.ts
|
|
697
903
|
import { readFileSync, statSync } from "fs";
|
|
698
|
-
import
|
|
904
|
+
import path4 from "path";
|
|
699
905
|
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
700
906
|
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
701
907
|
var cache = /* @__PURE__ */ new Map();
|
|
702
908
|
function readProjectMemory(cwd) {
|
|
703
|
-
const file =
|
|
909
|
+
const file = path4.join(cwd, PROJECT_MEMORY_FILE);
|
|
704
910
|
let mtimeMs;
|
|
705
911
|
try {
|
|
706
912
|
mtimeMs = statSync(file).mtimeMs;
|
|
@@ -730,23 +936,26 @@ ${body}`;
|
|
|
730
936
|
|
|
731
937
|
// src/agent/skills.ts
|
|
732
938
|
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
733
|
-
import
|
|
734
|
-
import { z } from "zod";
|
|
939
|
+
import path6 from "path";
|
|
940
|
+
import { z as z2 } from "zod";
|
|
735
941
|
|
|
736
942
|
// src/config/paths.ts
|
|
737
943
|
import os from "os";
|
|
738
|
-
import
|
|
944
|
+
import path5 from "path";
|
|
739
945
|
function starHome() {
|
|
740
|
-
return process.env.STAR_HOME ??
|
|
946
|
+
return process.env.STAR_HOME ?? path5.join(os.homedir(), ".star-cli");
|
|
741
947
|
}
|
|
742
948
|
function globalConfigPath() {
|
|
743
|
-
return
|
|
949
|
+
return path5.join(starHome(), "config.toml");
|
|
744
950
|
}
|
|
745
951
|
function sessionsDir() {
|
|
746
|
-
return
|
|
952
|
+
return path5.join(starHome(), "sessions");
|
|
953
|
+
}
|
|
954
|
+
function userMemoryPath() {
|
|
955
|
+
return path5.join(starHome(), "MEMORY.md");
|
|
747
956
|
}
|
|
748
957
|
function projectConfigPath(cwd) {
|
|
749
|
-
return
|
|
958
|
+
return path5.join(cwd, ".star", "config.toml");
|
|
750
959
|
}
|
|
751
960
|
|
|
752
961
|
// src/agent/skills.ts
|
|
@@ -779,7 +988,7 @@ function parseSkillFile(filePath, dirName, scope) {
|
|
|
779
988
|
const name = parsed?.meta.name || dirName;
|
|
780
989
|
const description = parsed?.meta.description;
|
|
781
990
|
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
782
|
-
skill = { name, description, filePath, dir:
|
|
991
|
+
skill = { name, description, filePath, dir: path6.dirname(filePath), scope };
|
|
783
992
|
}
|
|
784
993
|
} catch {
|
|
785
994
|
skill = null;
|
|
@@ -796,14 +1005,14 @@ function scanDir(base, scope, into) {
|
|
|
796
1005
|
}
|
|
797
1006
|
for (const entry of entries.sort()) {
|
|
798
1007
|
if (!NAME_PATTERN.test(entry)) continue;
|
|
799
|
-
const parsed = parseSkillFile(
|
|
1008
|
+
const parsed = parseSkillFile(path6.join(base, entry, SKILL_FILE), entry, scope);
|
|
800
1009
|
if (parsed) into.set(parsed.name, parsed);
|
|
801
1010
|
}
|
|
802
1011
|
}
|
|
803
1012
|
function discoverSkills(cwd, home = starHome()) {
|
|
804
1013
|
const byName = /* @__PURE__ */ new Map();
|
|
805
|
-
scanDir(
|
|
806
|
-
scanDir(
|
|
1014
|
+
scanDir(path6.join(home, "skills"), "user", byName);
|
|
1015
|
+
scanDir(path6.join(cwd, ".star", "skills"), "project", byName);
|
|
807
1016
|
return [...byName.values()];
|
|
808
1017
|
}
|
|
809
1018
|
function formatSkillsBlock(skills) {
|
|
@@ -834,8 +1043,8 @@ function createSkillTool(deps) {
|
|
|
834
1043
|
name: "skill",
|
|
835
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.",
|
|
836
1045
|
permission: "read",
|
|
837
|
-
parameters:
|
|
838
|
-
name:
|
|
1046
|
+
parameters: z2.object({
|
|
1047
|
+
name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
839
1048
|
}),
|
|
840
1049
|
async execute(args) {
|
|
841
1050
|
const skills = discoverSkills(deps.cwd, deps.home);
|
|
@@ -870,16 +1079,16 @@ import { z as z12 } from "zod";
|
|
|
870
1079
|
// src/tools/bash.ts
|
|
871
1080
|
import { spawn as spawn3 } from "child_process";
|
|
872
1081
|
import { existsSync } from "fs";
|
|
873
|
-
import
|
|
874
|
-
import { z as
|
|
1082
|
+
import path7 from "path";
|
|
1083
|
+
import { z as z3 } from "zod";
|
|
875
1084
|
|
|
876
1085
|
// src/tasks/manager.ts
|
|
877
1086
|
import { spawn as spawn2 } from "child_process";
|
|
878
|
-
import { EventEmitter } from "events";
|
|
1087
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
879
1088
|
var MAX_TASK_OUTPUT = 1e5;
|
|
880
1089
|
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
881
1090
|
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
882
|
-
var TaskManager = class extends
|
|
1091
|
+
var TaskManager = class extends EventEmitter2 {
|
|
883
1092
|
records = /* @__PURE__ */ new Map();
|
|
884
1093
|
seq = 0;
|
|
885
1094
|
start(opts) {
|
|
@@ -990,11 +1199,11 @@ var MAX_OUTPUT = 3e4;
|
|
|
990
1199
|
var DEFAULT_TIMEOUT = 120;
|
|
991
1200
|
var MAX_TIMEOUT = 600;
|
|
992
1201
|
function findOnPath(exe, exclude) {
|
|
993
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1202
|
+
for (const dir of (process.env.PATH ?? "").split(path7.delimiter)) {
|
|
994
1203
|
if (!dir || exclude?.(dir)) {
|
|
995
1204
|
continue;
|
|
996
1205
|
}
|
|
997
|
-
const full =
|
|
1206
|
+
const full = path7.join(dir, exe);
|
|
998
1207
|
if (existsSync(full)) {
|
|
999
1208
|
return full;
|
|
1000
1209
|
}
|
|
@@ -1012,7 +1221,7 @@ function resolveShell() {
|
|
|
1012
1221
|
}
|
|
1013
1222
|
const gitExe = findOnPath("git.exe");
|
|
1014
1223
|
if (gitExe) {
|
|
1015
|
-
const sibling =
|
|
1224
|
+
const sibling = path7.join(path7.dirname(path7.dirname(gitExe)), "bin", "bash.exe");
|
|
1016
1225
|
if (existsSync(sibling)) {
|
|
1017
1226
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1018
1227
|
}
|
|
@@ -1029,11 +1238,11 @@ function resolveShell() {
|
|
|
1029
1238
|
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
1030
1239
|
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
1031
1240
|
}
|
|
1032
|
-
var schema =
|
|
1033
|
-
command:
|
|
1034
|
-
timeout:
|
|
1035
|
-
description:
|
|
1036
|
-
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(
|
|
1037
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."
|
|
1038
1247
|
)
|
|
1039
1248
|
});
|
|
@@ -1135,14 +1344,14 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1135
1344
|
};
|
|
1136
1345
|
|
|
1137
1346
|
// src/tools/fs/edit.ts
|
|
1138
|
-
import { readFile as
|
|
1139
|
-
import
|
|
1140
|
-
import { z as
|
|
1141
|
-
var schema2 =
|
|
1142
|
-
path:
|
|
1143
|
-
old_string:
|
|
1144
|
-
new_string:
|
|
1145
|
-
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")
|
|
1146
1355
|
});
|
|
1147
1356
|
var editFileTool = {
|
|
1148
1357
|
name: "edit_file",
|
|
@@ -1150,10 +1359,10 @@ var editFileTool = {
|
|
|
1150
1359
|
permission: "write",
|
|
1151
1360
|
parameters: schema2,
|
|
1152
1361
|
async execute(args, ctx) {
|
|
1153
|
-
const filePath =
|
|
1362
|
+
const filePath = path8.resolve(ctx.cwd, args.path);
|
|
1154
1363
|
let content;
|
|
1155
1364
|
try {
|
|
1156
|
-
content = await
|
|
1365
|
+
content = await readFile3(filePath, "utf8");
|
|
1157
1366
|
} catch (err) {
|
|
1158
1367
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1159
1368
|
}
|
|
@@ -1174,7 +1383,7 @@ var editFileTool = {
|
|
|
1174
1383
|
}
|
|
1175
1384
|
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1176
1385
|
try {
|
|
1177
|
-
await
|
|
1386
|
+
await writeFile3(filePath, updated, "utf8");
|
|
1178
1387
|
} catch (err) {
|
|
1179
1388
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1180
1389
|
}
|
|
@@ -1193,14 +1402,15 @@ var editFileTool = {
|
|
|
1193
1402
|
};
|
|
1194
1403
|
|
|
1195
1404
|
// src/tools/fs/glob.ts
|
|
1196
|
-
import
|
|
1197
|
-
import { z as
|
|
1405
|
+
import path10 from "path";
|
|
1406
|
+
import { z as z5 } from "zod";
|
|
1198
1407
|
|
|
1199
1408
|
// src/tools/fs/util.ts
|
|
1409
|
+
import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
1200
1410
|
import { readdir, stat } from "fs/promises";
|
|
1201
|
-
import
|
|
1411
|
+
import path9 from "path";
|
|
1202
1412
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1203
|
-
async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
1413
|
+
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1204
1414
|
const out = [];
|
|
1205
1415
|
async function walk(dir, relBase) {
|
|
1206
1416
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
@@ -1208,13 +1418,17 @@ async function walkFiles(root, skipDirs = SKIP_DIRS) {
|
|
|
1208
1418
|
return;
|
|
1209
1419
|
}
|
|
1210
1420
|
for (const entry of entries) {
|
|
1211
|
-
const abs =
|
|
1421
|
+
const abs = path9.join(dir, entry.name);
|
|
1212
1422
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1213
1423
|
if (entry.isDirectory()) {
|
|
1214
|
-
if (
|
|
1215
|
-
|
|
1424
|
+
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
1425
|
+
continue;
|
|
1216
1426
|
}
|
|
1427
|
+
await walk(abs, rel);
|
|
1217
1428
|
} else if (entry.isFile()) {
|
|
1429
|
+
if (options.ignore?.(abs, false)) {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1218
1432
|
try {
|
|
1219
1433
|
const st = await stat(abs);
|
|
1220
1434
|
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
@@ -1263,8 +1477,71 @@ function matchesGlob(pattern, relPath) {
|
|
|
1263
1477
|
}
|
|
1264
1478
|
return globToRegExp(normalized).test(relPath);
|
|
1265
1479
|
}
|
|
1480
|
+
var IGNORE_FILE = ".starignore";
|
|
1481
|
+
var ignoreCache = /* @__PURE__ */ new Map();
|
|
1482
|
+
function parseIgnorePatterns(body) {
|
|
1483
|
+
const out = [];
|
|
1484
|
+
for (const raw of body.split("\n")) {
|
|
1485
|
+
const line = raw.trim();
|
|
1486
|
+
if (!line || line.startsWith("#")) {
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (line.endsWith("/")) {
|
|
1490
|
+
const pattern = line.slice(0, -1);
|
|
1491
|
+
if (pattern) {
|
|
1492
|
+
out.push({ pattern, dirOnly: true });
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
out.push({ pattern: line, dirOnly: false });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
function loadIgnorePatterns(dir) {
|
|
1501
|
+
const file = path9.join(dir, IGNORE_FILE);
|
|
1502
|
+
let mtimeMs;
|
|
1503
|
+
try {
|
|
1504
|
+
mtimeMs = statSync3(file).mtimeMs;
|
|
1505
|
+
} catch {
|
|
1506
|
+
return [];
|
|
1507
|
+
}
|
|
1508
|
+
const hit = ignoreCache.get(file);
|
|
1509
|
+
if (hit && hit.mtimeMs === mtimeMs) {
|
|
1510
|
+
return hit.patterns;
|
|
1511
|
+
}
|
|
1512
|
+
let patterns = [];
|
|
1513
|
+
try {
|
|
1514
|
+
patterns = parseIgnorePatterns(readFileSync3(file, "utf8"));
|
|
1515
|
+
} catch {
|
|
1516
|
+
patterns = [];
|
|
1517
|
+
}
|
|
1518
|
+
ignoreCache.set(file, { mtimeMs, patterns });
|
|
1519
|
+
return patterns;
|
|
1520
|
+
}
|
|
1521
|
+
function createIgnorePredicate(cwd) {
|
|
1522
|
+
const patterns = loadIgnorePatterns(cwd);
|
|
1523
|
+
if (patterns.length === 0) {
|
|
1524
|
+
return void 0;
|
|
1525
|
+
}
|
|
1526
|
+
const root = path9.resolve(cwd);
|
|
1527
|
+
return (absPath, isDir) => {
|
|
1528
|
+
const rel = path9.relative(root, absPath).split(path9.sep).join("/");
|
|
1529
|
+
if (!rel || rel.startsWith("..")) {
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
for (const { pattern, dirOnly } of patterns) {
|
|
1533
|
+
if (dirOnly && !isDir) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (matchesGlob(pattern, rel)) {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1266
1543
|
function isSensitivePath2(filePath) {
|
|
1267
|
-
const base =
|
|
1544
|
+
const base = path9.basename(filePath);
|
|
1268
1545
|
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
1269
1546
|
return false;
|
|
1270
1547
|
}
|
|
@@ -1279,11 +1556,11 @@ function isSensitivePath2(filePath) {
|
|
|
1279
1556
|
|
|
1280
1557
|
// src/tools/fs/glob.ts
|
|
1281
1558
|
var MAX_RESULTS = 100;
|
|
1282
|
-
var schema3 =
|
|
1283
|
-
pattern:
|
|
1559
|
+
var schema3 = z5.object({
|
|
1560
|
+
pattern: z5.string().describe(
|
|
1284
1561
|
"Glob pattern, e.g. 'src/**/*.ts'. Supports **, * and ?. A bare pattern like '*.ts' matches basenames recursively."
|
|
1285
1562
|
),
|
|
1286
|
-
path:
|
|
1563
|
+
path: z5.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1287
1564
|
});
|
|
1288
1565
|
var globTool = {
|
|
1289
1566
|
name: "glob",
|
|
@@ -1291,8 +1568,8 @@ var globTool = {
|
|
|
1291
1568
|
permission: "read",
|
|
1292
1569
|
parameters: schema3,
|
|
1293
1570
|
async execute(args, ctx) {
|
|
1294
|
-
const root =
|
|
1295
|
-
const files = await walkFiles(root);
|
|
1571
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1572
|
+
const files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1296
1573
|
const matched = files.filter((f) => matchesGlob(args.pattern, f.rel)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1297
1574
|
if (matched.length === 0) {
|
|
1298
1575
|
return { content: `No files matched pattern: ${args.pattern}` };
|
|
@@ -1302,15 +1579,15 @@ var globTool = {
|
|
|
1302
1579
|
};
|
|
1303
1580
|
|
|
1304
1581
|
// src/tools/fs/grep.ts
|
|
1305
|
-
import { readFile as
|
|
1306
|
-
import
|
|
1307
|
-
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";
|
|
1308
1585
|
var MAX_MATCHES = 250;
|
|
1309
|
-
var schema4 =
|
|
1310
|
-
pattern:
|
|
1311
|
-
path:
|
|
1312
|
-
glob:
|
|
1313
|
-
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")
|
|
1314
1591
|
});
|
|
1315
1592
|
var grepTool = {
|
|
1316
1593
|
name: "grep",
|
|
@@ -1324,14 +1601,14 @@ var grepTool = {
|
|
|
1324
1601
|
} catch (err) {
|
|
1325
1602
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1326
1603
|
}
|
|
1327
|
-
const root =
|
|
1604
|
+
const root = path11.resolve(ctx.cwd, args.path ?? ".");
|
|
1328
1605
|
let files;
|
|
1329
1606
|
try {
|
|
1330
1607
|
const st = await stat2(root);
|
|
1331
1608
|
if (st.isFile()) {
|
|
1332
|
-
files = [{ abs: root, rel:
|
|
1609
|
+
files = [{ abs: root, rel: path11.basename(root), mtimeMs: st.mtimeMs }];
|
|
1333
1610
|
} else {
|
|
1334
|
-
files = await walkFiles(root);
|
|
1611
|
+
files = await walkFiles(root, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1335
1612
|
}
|
|
1336
1613
|
} catch {
|
|
1337
1614
|
return { content: `Path not found: ${args.path ?? "."}`, isError: true };
|
|
@@ -1345,7 +1622,7 @@ var grepTool = {
|
|
|
1345
1622
|
outer: for (const file of files) {
|
|
1346
1623
|
let buf;
|
|
1347
1624
|
try {
|
|
1348
|
-
buf = await
|
|
1625
|
+
buf = await readFile4(file.abs);
|
|
1349
1626
|
} catch {
|
|
1350
1627
|
continue;
|
|
1351
1628
|
}
|
|
@@ -1375,15 +1652,15 @@ var grepTool = {
|
|
|
1375
1652
|
};
|
|
1376
1653
|
|
|
1377
1654
|
// src/tools/fs/read.ts
|
|
1378
|
-
import { readFile as
|
|
1379
|
-
import
|
|
1380
|
-
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";
|
|
1381
1658
|
var DEFAULT_LIMIT = 2e3;
|
|
1382
1659
|
var MAX_CHARS = 100 * 1024;
|
|
1383
|
-
var schema5 =
|
|
1384
|
-
path:
|
|
1385
|
-
offset:
|
|
1386
|
-
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)")
|
|
1387
1664
|
});
|
|
1388
1665
|
var readFileTool = {
|
|
1389
1666
|
name: "read_file",
|
|
@@ -1391,7 +1668,7 @@ var readFileTool = {
|
|
|
1391
1668
|
permission: "read",
|
|
1392
1669
|
parameters: schema5,
|
|
1393
1670
|
async execute(args, ctx) {
|
|
1394
|
-
const filePath =
|
|
1671
|
+
const filePath = path12.resolve(ctx.cwd, args.path);
|
|
1395
1672
|
if (isSensitivePath2(filePath)) {
|
|
1396
1673
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1397
1674
|
}
|
|
@@ -1402,7 +1679,7 @@ var readFileTool = {
|
|
|
1402
1679
|
if (st.isDirectory()) {
|
|
1403
1680
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1404
1681
|
}
|
|
1405
|
-
const raw = await
|
|
1682
|
+
const raw = await readFile5(filePath, "utf8");
|
|
1406
1683
|
const lines = raw.split("\n");
|
|
1407
1684
|
const start = args.offset ?? 1;
|
|
1408
1685
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
@@ -1436,12 +1713,12 @@ var readFileTool = {
|
|
|
1436
1713
|
};
|
|
1437
1714
|
|
|
1438
1715
|
// src/tools/fs/write.ts
|
|
1439
|
-
import { mkdir as
|
|
1440
|
-
import
|
|
1441
|
-
import { z as
|
|
1442
|
-
var schema6 =
|
|
1443
|
-
path:
|
|
1444
|
-
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")
|
|
1445
1722
|
});
|
|
1446
1723
|
var writeFileTool = {
|
|
1447
1724
|
name: "write_file",
|
|
@@ -1449,11 +1726,11 @@ var writeFileTool = {
|
|
|
1449
1726
|
permission: "write",
|
|
1450
1727
|
parameters: schema6,
|
|
1451
1728
|
async execute(args, ctx) {
|
|
1452
|
-
const filePath =
|
|
1729
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
1453
1730
|
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
1454
1731
|
try {
|
|
1455
|
-
await
|
|
1456
|
-
await
|
|
1732
|
+
await mkdir3(path13.dirname(filePath), { recursive: true });
|
|
1733
|
+
await writeFile4(filePath, args.content, "utf8");
|
|
1457
1734
|
} catch (err) {
|
|
1458
1735
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1459
1736
|
}
|
|
@@ -1463,7 +1740,7 @@ var writeFileTool = {
|
|
|
1463
1740
|
};
|
|
1464
1741
|
|
|
1465
1742
|
// src/tools/tasks.ts
|
|
1466
|
-
import { z as
|
|
1743
|
+
import { z as z9 } from "zod";
|
|
1467
1744
|
|
|
1468
1745
|
// src/tasks/format.ts
|
|
1469
1746
|
function formatDuration(startedAt, endedAt) {
|
|
@@ -1509,40 +1786,59 @@ function formatTaskFinished(task) {
|
|
|
1509
1786
|
}
|
|
1510
1787
|
|
|
1511
1788
|
// src/tools/tasks.ts
|
|
1512
|
-
var idSchema =
|
|
1513
|
-
id:
|
|
1789
|
+
var idSchema = z9.object({
|
|
1790
|
+
id: z9.string().describe("Task id, e.g. task-1 or agent-1")
|
|
1514
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
|
+
}
|
|
1515
1798
|
var taskListTool = {
|
|
1516
1799
|
name: "task_list",
|
|
1517
|
-
description: "List background shell
|
|
1800
|
+
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
1518
1801
|
permission: "read",
|
|
1519
|
-
parameters:
|
|
1802
|
+
parameters: z9.object({}),
|
|
1520
1803
|
execute() {
|
|
1521
|
-
|
|
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}` });
|
|
1522
1810
|
}
|
|
1523
1811
|
};
|
|
1524
1812
|
var taskOutputTool = {
|
|
1525
1813
|
name: "task_output",
|
|
1526
|
-
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).",
|
|
1527
1815
|
permission: "read",
|
|
1528
1816
|
parameters: idSchema,
|
|
1529
1817
|
execute(args) {
|
|
1530
|
-
const
|
|
1531
|
-
if (
|
|
1532
|
-
|
|
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}` });
|
|
1533
1823
|
}
|
|
1534
|
-
const
|
|
1535
|
-
|
|
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)}
|
|
1536
1828
|
${body}` });
|
|
1829
|
+
}
|
|
1830
|
+
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1537
1831
|
}
|
|
1538
1832
|
};
|
|
1539
1833
|
var taskKillTool = {
|
|
1540
1834
|
name: "task_kill",
|
|
1541
|
-
description: "Stop a running background
|
|
1835
|
+
description: "Stop a running background task (shell command process tree or subagent).",
|
|
1542
1836
|
permission: "exec",
|
|
1543
1837
|
parameters: idSchema,
|
|
1544
1838
|
execute(args) {
|
|
1545
|
-
const
|
|
1839
|
+
const shell = defaultTaskManager.get(args.id);
|
|
1840
|
+
const agent = shell ? void 0 : defaultAgentTasks.get(args.id);
|
|
1841
|
+
const task = shell ?? agent;
|
|
1546
1842
|
if (!task) {
|
|
1547
1843
|
return Promise.resolve({ content: `Unknown task: ${args.id}`, isError: true });
|
|
1548
1844
|
}
|
|
@@ -1552,106 +1848,15 @@ var taskKillTool = {
|
|
|
1552
1848
|
isError: true
|
|
1553
1849
|
});
|
|
1554
1850
|
}
|
|
1555
|
-
|
|
1851
|
+
if (shell) {
|
|
1852
|
+
defaultTaskManager.kill(args.id);
|
|
1853
|
+
} else {
|
|
1854
|
+
defaultAgentTasks.kill(args.id);
|
|
1855
|
+
}
|
|
1556
1856
|
return Promise.resolve({ content: `Task ${args.id} stopped.` });
|
|
1557
1857
|
}
|
|
1558
1858
|
};
|
|
1559
1859
|
|
|
1560
|
-
// src/tools/todo.ts
|
|
1561
|
-
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1562
|
-
import path13 from "path";
|
|
1563
|
-
import { z as z9 } from "zod";
|
|
1564
|
-
var todoItemSchema = z9.object({
|
|
1565
|
-
id: z9.number().int(),
|
|
1566
|
-
title: z9.string(),
|
|
1567
|
-
status: z9.enum(["pending", "in_progress", "done"])
|
|
1568
|
-
});
|
|
1569
|
-
var writeSchema = z9.object({
|
|
1570
|
-
todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
1571
|
-
});
|
|
1572
|
-
var readSchema = z9.object({});
|
|
1573
|
-
function fileFor(cwd) {
|
|
1574
|
-
return path13.join(cwd, ".star", "todos.json");
|
|
1575
|
-
}
|
|
1576
|
-
var TodoStore = class {
|
|
1577
|
-
items = /* @__PURE__ */ new Map();
|
|
1578
|
-
list() {
|
|
1579
|
-
return [...this.items.values()];
|
|
1580
|
-
}
|
|
1581
|
-
replace(items) {
|
|
1582
|
-
this.items.clear();
|
|
1583
|
-
for (const item of items) {
|
|
1584
|
-
this.items.set(item.id, item);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
async load(cwd) {
|
|
1588
|
-
let raw;
|
|
1589
|
-
try {
|
|
1590
|
-
raw = await readFile5(fileFor(cwd), "utf8");
|
|
1591
|
-
} catch {
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
try {
|
|
1595
|
-
const parsed = JSON.parse(raw);
|
|
1596
|
-
if (Array.isArray(parsed)) {
|
|
1597
|
-
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
1598
|
-
}
|
|
1599
|
-
} catch {
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
async save(cwd) {
|
|
1603
|
-
await mkdir3(path13.join(cwd, ".star"), { recursive: true });
|
|
1604
|
-
await writeFile4(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
1605
|
-
`);
|
|
1606
|
-
}
|
|
1607
|
-
};
|
|
1608
|
-
var SYMBOLS = {
|
|
1609
|
-
pending: "[ ]",
|
|
1610
|
-
in_progress: "[~]",
|
|
1611
|
-
done: "[x]"
|
|
1612
|
-
};
|
|
1613
|
-
function formatTodos(items) {
|
|
1614
|
-
if (items.length === 0) {
|
|
1615
|
-
return "No todos.";
|
|
1616
|
-
}
|
|
1617
|
-
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
1618
|
-
const width = Math.max(...lines.map((l) => l.length));
|
|
1619
|
-
return lines.map((line, i) => {
|
|
1620
|
-
const status = items[i]?.status;
|
|
1621
|
-
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
1622
|
-
}).join("\n");
|
|
1623
|
-
}
|
|
1624
|
-
var defaultStore = new TodoStore();
|
|
1625
|
-
function createTodoTools(store = defaultStore) {
|
|
1626
|
-
const todoWrite = {
|
|
1627
|
-
name: "todo_write",
|
|
1628
|
-
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
1629
|
-
permission: "read",
|
|
1630
|
-
parameters: writeSchema,
|
|
1631
|
-
async execute(args, ctx) {
|
|
1632
|
-
const parsed = writeSchema.safeParse(args);
|
|
1633
|
-
if (!parsed.success) {
|
|
1634
|
-
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
1635
|
-
}
|
|
1636
|
-
await store.load(ctx.cwd);
|
|
1637
|
-
store.replace(parsed.data.todos);
|
|
1638
|
-
await store.save(ctx.cwd);
|
|
1639
|
-
return { content: formatTodos(store.list()) };
|
|
1640
|
-
}
|
|
1641
|
-
};
|
|
1642
|
-
const todoRead = {
|
|
1643
|
-
name: "todo_read",
|
|
1644
|
-
description: "Read the current todo list.",
|
|
1645
|
-
permission: "read",
|
|
1646
|
-
parameters: readSchema,
|
|
1647
|
-
async execute(_args, ctx) {
|
|
1648
|
-
await store.load(ctx.cwd);
|
|
1649
|
-
return { content: formatTodos(store.list()) };
|
|
1650
|
-
}
|
|
1651
|
-
};
|
|
1652
|
-
return [todoWrite, todoRead];
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
1860
|
// src/tools/web/fetch.ts
|
|
1656
1861
|
import { z as z10 } from "zod";
|
|
1657
1862
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
@@ -1949,75 +2154,216 @@ function createDefaultRegistry() {
|
|
|
1949
2154
|
var MAX_SUBAGENT_DEPTH = 1;
|
|
1950
2155
|
var MAX_REPORT_CHARS = 2e4;
|
|
1951
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
|
+
}
|
|
1952
2208
|
function createSubagentTool(deps) {
|
|
1953
2209
|
return {
|
|
1954
2210
|
name: "subagent",
|
|
1955
|
-
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).",
|
|
1956
2212
|
permission: "exec",
|
|
1957
2213
|
parameters: z12.object({
|
|
1958
2214
|
prompt: z12.string().describe("Complete, self-contained instructions for the subagent."),
|
|
1959
|
-
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
|
+
)
|
|
1960
2219
|
}),
|
|
1961
2220
|
async execute(args, ctx) {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
});
|
|
1973
|
-
const confirmHandler = deps.getConfirmHandler?.();
|
|
1974
|
-
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
|
+
}
|
|
1975
2231
|
const signal = ctx.abortSignal ?? new AbortController().signal;
|
|
1976
|
-
let toolCalls = 0;
|
|
1977
|
-
let error = null;
|
|
1978
2232
|
try {
|
|
1979
|
-
|
|
1980
|
-
if (event.type === "tool-result") {
|
|
1981
|
-
toolCalls++;
|
|
1982
|
-
} else if (event.type === "error") {
|
|
1983
|
-
error = event.error.message;
|
|
1984
|
-
break;
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
2233
|
+
return { content: await runSubagent(deps, args, ctx.cwd, signal) };
|
|
1987
2234
|
} catch (e) {
|
|
1988
|
-
|
|
2235
|
+
return { content: e instanceof Error ? e.message : String(e), isError: true };
|
|
1989
2236
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/agent/user-memory.ts
|
|
2242
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
2243
|
+
import path14 from "path";
|
|
2244
|
+
import { z as z13 } from "zod";
|
|
2245
|
+
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2246
|
+
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
2247
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
2248
|
+
function readUserMemory(home = starHome()) {
|
|
2249
|
+
const file = path14.join(home, "MEMORY.md");
|
|
2250
|
+
let mtimeMs;
|
|
2251
|
+
try {
|
|
2252
|
+
mtimeMs = statSync4(file).mtimeMs;
|
|
2253
|
+
} catch {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
const hit = cache3.get(file);
|
|
2257
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.block;
|
|
2258
|
+
let block = null;
|
|
2259
|
+
try {
|
|
2260
|
+
let body = readFileSync4(file, "utf8").trim();
|
|
2261
|
+
if (body) {
|
|
2262
|
+
if (body.length > USER_MEMORY_MAX_CHARS) {
|
|
2263
|
+
body = `${body.slice(0, USER_MEMORY_MAX_CHARS)}
|
|
2264
|
+
|
|
2265
|
+
[MEMORY.md truncated to ${USER_MEMORY_MAX_CHARS} characters]`;
|
|
2012
2266
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2267
|
+
block = [
|
|
2268
|
+
"# User memory (MEMORY.md)",
|
|
2269
|
+
"The following notes were saved by the user across sessions. Respect them unless they conflict with the current request.",
|
|
2270
|
+
body
|
|
2271
|
+
].join("\n");
|
|
2272
|
+
}
|
|
2273
|
+
} catch {
|
|
2274
|
+
block = null;
|
|
2275
|
+
}
|
|
2276
|
+
cache3.set(file, { mtimeMs, block });
|
|
2277
|
+
return block;
|
|
2278
|
+
}
|
|
2279
|
+
function appendUserMemory(text, home = starHome()) {
|
|
2280
|
+
mkdirSync(home, { recursive: true });
|
|
2281
|
+
appendFileSync(path14.join(home, "MEMORY.md"), `- ${text}
|
|
2282
|
+
`, "utf8");
|
|
2283
|
+
}
|
|
2284
|
+
function createRememberTool(deps = {}) {
|
|
2285
|
+
return {
|
|
2286
|
+
name: "remember",
|
|
2287
|
+
description: `Save a note to the user's long-term memory (${userMemoryPath()}), injected into every future session. ONLY use when the user explicitly asks to remember or save something. Never use autonomously.`,
|
|
2288
|
+
permission: "write",
|
|
2289
|
+
parameters: z13.object({
|
|
2290
|
+
text: z13.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
|
|
2291
|
+
}),
|
|
2292
|
+
async execute(args) {
|
|
2293
|
+
appendUserMemory(args.text, deps.home);
|
|
2294
|
+
return { content: `Saved to user memory: ${args.text}` };
|
|
2015
2295
|
}
|
|
2016
2296
|
};
|
|
2017
2297
|
}
|
|
2018
2298
|
|
|
2019
2299
|
// src/agent/loop.ts
|
|
2020
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
|
+
}
|
|
2021
2367
|
var AgentLoop = class {
|
|
2022
2368
|
messages = [];
|
|
2023
2369
|
opts;
|
|
@@ -2041,6 +2387,7 @@ var AgentLoop = class {
|
|
|
2041
2387
|
this.bindSnapshotHooks(opts.sessionStore);
|
|
2042
2388
|
}
|
|
2043
2389
|
opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
|
|
2390
|
+
opts.registry?.register(createRememberTool());
|
|
2044
2391
|
const depth = opts.subagentDepth ?? 0;
|
|
2045
2392
|
if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
|
|
2046
2393
|
opts.registry.register(
|
|
@@ -2152,7 +2499,7 @@ var AgentLoop = class {
|
|
|
2152
2499
|
}
|
|
2153
2500
|
async *stream(input, signal, opts) {
|
|
2154
2501
|
this.syncSystemMessage();
|
|
2155
|
-
const
|
|
2502
|
+
const inputText = typeof input === "string" ? input : input.text;
|
|
2156
2503
|
const images = typeof input === "string" ? [] : input.images;
|
|
2157
2504
|
const userMessage = images.length > 0 ? {
|
|
2158
2505
|
role: "user",
|
|
@@ -2162,9 +2509,9 @@ var AgentLoop = class {
|
|
|
2162
2509
|
image: img.data,
|
|
2163
2510
|
mimeType: img.mimeType
|
|
2164
2511
|
})),
|
|
2165
|
-
{ type: "text", text }
|
|
2512
|
+
{ type: "text", text: inputText }
|
|
2166
2513
|
]
|
|
2167
|
-
} : { role: "user", content:
|
|
2514
|
+
} : { role: "user", content: inputText };
|
|
2168
2515
|
this.messages.push(userMessage);
|
|
2169
2516
|
const seq = (this.opts.subagentDepth ?? 0) === 0 ? beginTurn(this.messages.length - 1) : currentTurnSeq();
|
|
2170
2517
|
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
|
|
@@ -2173,42 +2520,89 @@ var AgentLoop = class {
|
|
|
2173
2520
|
);
|
|
2174
2521
|
const { config, registry, cwd } = this.opts;
|
|
2175
2522
|
const aiTools = this.buildAiTools();
|
|
2523
|
+
let autoContinues = 0;
|
|
2524
|
+
let lastWasNudge = false;
|
|
2525
|
+
let usedToolsThisTurn = false;
|
|
2526
|
+
let openTodos = [];
|
|
2176
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
|
+
}
|
|
2177
2540
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
2178
2541
|
const compacted = compactMessages([...this.messages], maxTokens);
|
|
2179
2542
|
if (compacted.compacted) {
|
|
2180
2543
|
this.messages = await this.applyCompactionSummary(compacted);
|
|
2181
2544
|
}
|
|
2182
|
-
|
|
2545
|
+
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
2546
|
+
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
2547
|
+
let text = "";
|
|
2183
2548
|
const toolCalls = [];
|
|
2184
|
-
let
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
+
}
|
|
2200
2574
|
}
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
if (signal.aborted) return;
|
|
2577
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
2201
2578
|
}
|
|
2202
|
-
} catch (error) {
|
|
2203
2579
|
if (signal.aborted) return;
|
|
2204
|
-
|
|
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
|
+
};
|
|
2205
2600
|
return;
|
|
2206
2601
|
}
|
|
2207
|
-
if (failed) return;
|
|
2208
2602
|
const assistantMessage = {
|
|
2209
2603
|
role: "assistant",
|
|
2210
2604
|
content: [
|
|
2211
|
-
...
|
|
2605
|
+
...text ? [{ type: "text", text }] : [],
|
|
2212
2606
|
...toolCalls.map((call) => ({
|
|
2213
2607
|
type: "tool-call",
|
|
2214
2608
|
toolCallId: call.id,
|
|
@@ -2219,11 +2613,40 @@ var AgentLoop = class {
|
|
|
2219
2613
|
};
|
|
2220
2614
|
this.messages.push(assistantMessage);
|
|
2221
2615
|
await this.persist(assistantMessage);
|
|
2222
|
-
this.maybeScheduleTitle(
|
|
2616
|
+
this.maybeScheduleTitle(text);
|
|
2223
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
|
+
}
|
|
2224
2645
|
await this.runEventHooks("Stop");
|
|
2225
2646
|
return;
|
|
2226
2647
|
}
|
|
2648
|
+
lastWasNudge = false;
|
|
2649
|
+
usedToolsThisTurn = true;
|
|
2227
2650
|
const answered = /* @__PURE__ */ new Set();
|
|
2228
2651
|
try {
|
|
2229
2652
|
for (const call of toolCalls) {
|
|
@@ -2243,6 +2666,9 @@ var AgentLoop = class {
|
|
|
2243
2666
|
this.messages.push(toolMessage);
|
|
2244
2667
|
await this.persist(toolMessage);
|
|
2245
2668
|
answered.add(call.id);
|
|
2669
|
+
if (call.name === "todo_write" && !result.isError) {
|
|
2670
|
+
openTodos = pendingTodoTitles(call.args);
|
|
2671
|
+
}
|
|
2246
2672
|
yield {
|
|
2247
2673
|
type: "tool-result",
|
|
2248
2674
|
id: call.id,
|
|
@@ -2285,8 +2711,9 @@ var AgentLoop = class {
|
|
|
2285
2711
|
// /model switch) replaced the history with one that has none. Git context
|
|
2286
2712
|
// (branch, dirty count, recent commits) is refreshed here too, so the model
|
|
2287
2713
|
// always sees the current repo state; any git failure is silently skipped.
|
|
2288
|
-
// Project memory (AGENTS.md in the cwd)
|
|
2289
|
-
//
|
|
2714
|
+
// Project memory (AGENTS.md in the cwd) and user memory (~/.star-cli/
|
|
2715
|
+
// MEMORY.md) are appended as delimited blocks; both cache by mtime, so this
|
|
2716
|
+
// stays cheap per turn. The
|
|
2290
2717
|
// skills listing is refreshed here as well, so skills added mid-session
|
|
2291
2718
|
// show up in the next turn's system prompt.
|
|
2292
2719
|
syncSystemMessage() {
|
|
@@ -2297,6 +2724,8 @@ var AgentLoop = class {
|
|
|
2297
2724
|
if (plan) parts.push(PLAN_MODE_PROMPT.trim());
|
|
2298
2725
|
const memory = readProjectMemory(this.opts.cwd);
|
|
2299
2726
|
if (memory) parts.push(memory);
|
|
2727
|
+
const userMemory = readUserMemory();
|
|
2728
|
+
if (userMemory) parts.push(userMemory);
|
|
2300
2729
|
const skills = discoverSkills(this.opts.cwd);
|
|
2301
2730
|
if (skills.length > 0) parts.push(formatSkillsBlock(skills));
|
|
2302
2731
|
const git = getGitSummary(this.opts.cwd);
|
|
@@ -2330,14 +2759,15 @@ ${summary}`
|
|
|
2330
2759
|
return compacted.messages;
|
|
2331
2760
|
}
|
|
2332
2761
|
}
|
|
2333
|
-
async *streamOnce(aiTools, signal) {
|
|
2762
|
+
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
2334
2763
|
const { streamChat } = await import("./stream-Y55NRF2S.js");
|
|
2335
2764
|
yield* streamChat({
|
|
2336
2765
|
model: this.opts.model,
|
|
2337
2766
|
messages: this.messages,
|
|
2338
2767
|
tools: aiTools,
|
|
2339
2768
|
abortSignal: signal,
|
|
2340
|
-
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3
|
|
2769
|
+
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
|
|
2770
|
+
firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
|
|
2341
2771
|
});
|
|
2342
2772
|
}
|
|
2343
2773
|
buildAiTools() {
|
|
@@ -2378,7 +2808,8 @@ ${summary}`
|
|
|
2378
2808
|
config.permissionMode,
|
|
2379
2809
|
{ toolName: call.name, args: call.args, level: tool.permission },
|
|
2380
2810
|
{ cwd },
|
|
2381
|
-
config.permissions.allow
|
|
2811
|
+
config.permissions.allow,
|
|
2812
|
+
config.permissions.deny
|
|
2382
2813
|
);
|
|
2383
2814
|
if (decision === "deny") {
|
|
2384
2815
|
return { content: `Permission denied for tool "${call.name}".`, isError: true };
|
|
@@ -2429,6 +2860,7 @@ ${summary}`
|
|
|
2429
2860
|
};
|
|
2430
2861
|
|
|
2431
2862
|
export {
|
|
2863
|
+
defaultAgentTasks,
|
|
2432
2864
|
estimateTokens,
|
|
2433
2865
|
summarizeMessages,
|
|
2434
2866
|
compactMessages,
|
|
@@ -2447,23 +2879,28 @@ export {
|
|
|
2447
2879
|
hydrateSnapshots,
|
|
2448
2880
|
undoTurnSnapshots,
|
|
2449
2881
|
rewindToSnapshot,
|
|
2882
|
+
TodoStore,
|
|
2883
|
+
formatTodos,
|
|
2884
|
+
parseTodoArgs,
|
|
2885
|
+
loadTodos,
|
|
2450
2886
|
starHome,
|
|
2451
2887
|
globalConfigPath,
|
|
2452
2888
|
sessionsDir,
|
|
2889
|
+
userMemoryPath,
|
|
2453
2890
|
projectConfigPath,
|
|
2454
2891
|
discoverSkills,
|
|
2455
2892
|
defaultTaskManager,
|
|
2456
2893
|
resolveShell,
|
|
2457
2894
|
bashTool,
|
|
2458
2895
|
SKIP_DIRS,
|
|
2896
|
+
createIgnorePredicate,
|
|
2459
2897
|
isSensitivePath2 as isSensitivePath,
|
|
2460
2898
|
formatTaskList,
|
|
2461
2899
|
formatTaskStarted,
|
|
2462
2900
|
formatTaskFinished,
|
|
2463
|
-
TodoStore,
|
|
2464
|
-
formatTodos,
|
|
2465
2901
|
createDefaultRegistry,
|
|
2902
|
+
appendUserMemory,
|
|
2466
2903
|
PLAN_MODE_PROMPT,
|
|
2467
2904
|
AgentLoop
|
|
2468
2905
|
};
|
|
2469
|
-
//# sourceMappingURL=chunk-
|
|
2906
|
+
//# sourceMappingURL=chunk-DZWTIMJ2.js.map
|