@cryer/star-cli 0.3.2 → 0.3.4
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 +5 -1
- package/README.zh-CN.md +319 -0
- package/dist/{chunk-LADQCYKL.js → chunk-B6P4VUBE.js} +16 -7
- package/dist/chunk-B6P4VUBE.js.map +1 -0
- package/dist/{chunk-BV4PLM6X.js → chunk-K3R42LOX.js} +1487 -670
- package/dist/chunk-K3R42LOX.js.map +1 -0
- package/dist/{loop-XKC5HQBT.js → loop-SDZVEQND.js} +3 -3
- package/dist/main.js +579 -285
- package/dist/main.js.map +1 -1
- package/dist/{stream-OY2MKWYE.js → stream-H4BYBVAZ.js} +22 -4
- package/dist/stream-H4BYBVAZ.js.map +1 -0
- package/package.json +25 -16
- package/dist/chunk-BV4PLM6X.js.map +0 -1
- package/dist/chunk-LADQCYKL.js.map +0 -1
- package/dist/stream-OY2MKWYE.js.map +0 -1
- /package/dist/{loop-XKC5HQBT.js.map → loop-SDZVEQND.js.map} +0 -0
|
@@ -1,15 +1,134 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
computeRetryDelayMs,
|
|
4
|
+
gitTreesDir,
|
|
4
5
|
isRetryableStreamError,
|
|
6
|
+
responseBodyOf,
|
|
5
7
|
starHome,
|
|
6
8
|
summarizeStreamError,
|
|
7
9
|
userMemoryPath
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-B6P4VUBE.js";
|
|
9
11
|
|
|
10
12
|
// src/agent/loop.ts
|
|
11
13
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
12
14
|
|
|
15
|
+
// src/config/schema.ts
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
var ProviderConfigSchema = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
// openai-responses talks to the Responses API (/responses) instead of chat
|
|
20
|
+
// completions — the wire format codex uses, and the native API of
|
|
21
|
+
// reasoning-model relays that otherwise hide reasoning behind empty chat
|
|
22
|
+
// completions. streamChat sends strictSchemas:false + store:false with it.
|
|
23
|
+
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
24
|
+
baseURL: z.string(),
|
|
25
|
+
apiKeyEnv: z.string().optional(),
|
|
26
|
+
apiKey: z.string().optional(),
|
|
27
|
+
headers: z.record(z.string()).optional()
|
|
28
|
+
});
|
|
29
|
+
var ModelConfigSchema = z.object({
|
|
30
|
+
name: z.string(),
|
|
31
|
+
provider: z.string(),
|
|
32
|
+
model: z.string(),
|
|
33
|
+
maxTokens: z.number().int().positive().optional(),
|
|
34
|
+
// Per-model context window; overrides the top-level contextMaxTokens for
|
|
35
|
+
// compaction and the status bar when set.
|
|
36
|
+
contextMaxTokens: z.number().int().positive().optional(),
|
|
37
|
+
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
38
|
+
// the model is treated as unpriced.
|
|
39
|
+
promptPrice: z.number().nonnegative().optional(),
|
|
40
|
+
completionPrice: z.number().nonnegative().optional(),
|
|
41
|
+
// Optional price (USD per 1M tokens) for cache-read prompt tokens. When
|
|
42
|
+
// unset, OpenAI-style cached tokens bill at promptPrice and Anthropic-style
|
|
43
|
+
// cache reads are not billed — set this for accurate cache pricing.
|
|
44
|
+
cacheReadPrice: z.number().nonnegative().optional()
|
|
45
|
+
});
|
|
46
|
+
var PermissionsConfigSchema = z.object({
|
|
47
|
+
allow: z.array(z.string()).default([]),
|
|
48
|
+
deny: z.array(z.string()).default([])
|
|
49
|
+
});
|
|
50
|
+
var HookConfigSchema = z.object({
|
|
51
|
+
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
52
|
+
matcher: z.string().refine(
|
|
53
|
+
(pattern) => {
|
|
54
|
+
try {
|
|
55
|
+
new RegExp(pattern);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{ message: "invalid regular expression" }
|
|
62
|
+
).optional(),
|
|
63
|
+
command: z.string().min(1),
|
|
64
|
+
timeoutSec: z.number().positive().default(30)
|
|
65
|
+
});
|
|
66
|
+
var ConfigSchema = z.object({
|
|
67
|
+
defaultModel: z.string().default(""),
|
|
68
|
+
permissionMode: z.enum(["auto", "ask", "readonly", "yolo", "plan"]).default("ask"),
|
|
69
|
+
providers: z.array(ProviderConfigSchema).default([]),
|
|
70
|
+
models: z.array(ModelConfigSchema).default([]),
|
|
71
|
+
maxSteps: z.number().int().positive().default(50),
|
|
72
|
+
contextMaxTokens: z.number().int().positive().default(1e5),
|
|
73
|
+
// Token threshold at which the agent loop auto-compacts the history.
|
|
74
|
+
// Defaults to the effective context window (compact only when full); set a
|
|
75
|
+
// lower value to compact earlier and keep headroom. Manual /compact ignores
|
|
76
|
+
// this — it always compacts.
|
|
77
|
+
compactThresholdTokens: z.number().int().positive().optional(),
|
|
78
|
+
// Seconds without any stream part before a stalled response is ended
|
|
79
|
+
// gracefully (some relays never send the terminal chunks). Applies once
|
|
80
|
+
// streaming has started; the first part gets a longer, fixed allowance.
|
|
81
|
+
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
82
|
+
// Seconds to wait for the very first stream part before giving up; slow
|
|
83
|
+
// thinking-model relays can buffer for minutes before sending anything.
|
|
84
|
+
streamFirstChunkTimeoutSec: z.number().positive().default(300),
|
|
85
|
+
// Extra attempts per model request when a stream fails transiently
|
|
86
|
+
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
87
|
+
// relay hiccup does not silently end a half-finished turn. 0 disables.
|
|
88
|
+
// The wait between attempts honors Retry-After headers and otherwise backs
|
|
89
|
+
// off exponentially with jitter (llm/retry.ts); retries after a
|
|
90
|
+
// timeout/empty failure also scale both stream timeouts up (attempt number
|
|
91
|
+
// ×, capped at 3x) since the relay is likely overloaded. Default 5 matches
|
|
92
|
+
// opencode/Codex retry budgets and rides out flaky relays (中转站) better
|
|
93
|
+
// than a minimal budget.
|
|
94
|
+
streamMaxRetries: z.number().int().min(0).default(5),
|
|
95
|
+
// How many consecutive unproductive replies may be nudged before the turn
|
|
96
|
+
// is handed back: a text-only reply that announces pending work ("我将…",
|
|
97
|
+
// "I will…") or answers a nudge with more words, or an empty reply that
|
|
98
|
+
// survived the stream retry budget (steered with a nudge — a changed
|
|
99
|
+
// request — since identical resends already failed). Tool calls reset the
|
|
100
|
+
// count, so a productive task keeps going; exhaustion surfaces a notice
|
|
101
|
+
// instead of stopping silently. Never fires in plan mode. 0 disables.
|
|
102
|
+
maxAutoContinues: z.number().int().min(0).default(2),
|
|
103
|
+
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
104
|
+
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
105
|
+
// once the estimated cost for the session reaches this amount.
|
|
106
|
+
sessionBudgetUsd: z.number().positive().optional(),
|
|
107
|
+
// Terminal bell (REPL only): ring when a turn takes longer than
|
|
108
|
+
// notifyBellThresholdSec, and when a background task finishes.
|
|
109
|
+
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
110
|
+
notifyBell: z.boolean().default(true),
|
|
111
|
+
notifyBellThresholdSec: z.number().positive().default(10),
|
|
112
|
+
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
113
|
+
hooks: z.array(HookConfigSchema).default([]),
|
|
114
|
+
// Consecutive identical tool calls (same tool + same arguments) allowed
|
|
115
|
+
// before the loop refuses to execute the repeat and tells the model to
|
|
116
|
+
// change approach — a guard against agents stuck retrying a failing call
|
|
117
|
+
// (opencode's doom_loop). 0 disables.
|
|
118
|
+
doomLoopThreshold: z.number().int().min(0).default(3),
|
|
119
|
+
// Track the whole working tree with an internal git repo (outside the
|
|
120
|
+
// user's .git) at every turn boundary, so /undo and /redo can restore
|
|
121
|
+
// changes made by ANY tool (bash included), not just write_file/edit_file.
|
|
122
|
+
// Falls back silently to per-file snapshots when git is unavailable.
|
|
123
|
+
gitSnapshots: z.boolean().default(true)
|
|
124
|
+
});
|
|
125
|
+
function contextWindowTokens(config, modelName) {
|
|
126
|
+
return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
|
|
127
|
+
}
|
|
128
|
+
function resolveCompactThreshold(config, windowTokens) {
|
|
129
|
+
return Math.min(config.compactThresholdTokens ?? windowTokens, windowTokens);
|
|
130
|
+
}
|
|
131
|
+
|
|
13
132
|
// src/context/compaction.ts
|
|
14
133
|
import { generateText } from "ai";
|
|
15
134
|
|
|
@@ -89,19 +208,24 @@ function serializeMessage(message) {
|
|
|
89
208
|
});
|
|
90
209
|
return `${message.role}: ${parts.join("\n")}`;
|
|
91
210
|
}
|
|
92
|
-
async function summarizeMessages(messages, model) {
|
|
211
|
+
async function summarizeMessages(messages, model, signal) {
|
|
93
212
|
const transcript = messages.map(serializeMessage).join("\n");
|
|
94
213
|
const { text } = await generateText({
|
|
95
214
|
model,
|
|
96
215
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
97
216
|
prompt: `Summarize this conversation so far:
|
|
98
217
|
|
|
99
|
-
${transcript}
|
|
218
|
+
${transcript}`,
|
|
219
|
+
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
220
|
+
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
221
|
+
// falls back to the truncation placeholder on any failure.
|
|
222
|
+
abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
|
|
100
223
|
});
|
|
101
224
|
return text.trim();
|
|
102
225
|
}
|
|
103
|
-
function compactMessages(messages, maxTokens) {
|
|
104
|
-
|
|
226
|
+
function compactMessages(messages, maxTokens, opts = {}) {
|
|
227
|
+
const limit = opts.force ? 0 : maxTokens;
|
|
228
|
+
if (estimateTokens(messages) <= limit) {
|
|
105
229
|
return { messages, compacted: false, droppedCount: 0 };
|
|
106
230
|
}
|
|
107
231
|
const first = messages[0];
|
|
@@ -127,7 +251,7 @@ function compactMessages(messages, maxTokens) {
|
|
|
127
251
|
droppedCount += turn.length;
|
|
128
252
|
turnIndex += 1;
|
|
129
253
|
const candidate = [...head, placeholderMessage(droppedCount), ...rest.slice(droppedCount)];
|
|
130
|
-
if (estimateTokens(candidate) <=
|
|
254
|
+
if (estimateTokens(candidate) <= limit) {
|
|
131
255
|
break;
|
|
132
256
|
}
|
|
133
257
|
}
|
|
@@ -172,6 +296,16 @@ function getGitSummary(cwd) {
|
|
|
172
296
|
const log = runGit(["log", "--oneline", "-3"], cwd) ?? "";
|
|
173
297
|
return { branch, dirtyCount, recentCommits: log === "" ? [] : log.split("\n") };
|
|
174
298
|
}
|
|
299
|
+
var GIT_SUMMARY_CACHE_TTL_MS = 15e3;
|
|
300
|
+
var gitSummaryCache = /* @__PURE__ */ new Map();
|
|
301
|
+
function getGitSummaryCached(cwd) {
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const hit = gitSummaryCache.get(cwd);
|
|
304
|
+
if (hit && now - hit.at < GIT_SUMMARY_CACHE_TTL_MS) return hit.summary;
|
|
305
|
+
const summary = getGitSummary(cwd);
|
|
306
|
+
gitSummaryCache.set(cwd, { at: now, summary });
|
|
307
|
+
return summary;
|
|
308
|
+
}
|
|
175
309
|
function formatGitSummary(summary) {
|
|
176
310
|
const lines = [
|
|
177
311
|
"Git context for the working directory (refreshed each turn):",
|
|
@@ -332,7 +466,8 @@ var IMAGE_SIZE_KEYWORDS = /too[\s-]*(large|big)|exceeds?|dimensions?|resolution|
|
|
|
332
466
|
function isOversizedImageError(error) {
|
|
333
467
|
const status = error.statusCode;
|
|
334
468
|
if (typeof status !== "number" || status < 400 || status >= 500) return false;
|
|
335
|
-
|
|
469
|
+
const text = [error.message, responseBodyOf(error)].filter((part) => typeof part === "string").join("\n");
|
|
470
|
+
return /image/i.test(text) && IMAGE_SIZE_KEYWORDS.test(text);
|
|
336
471
|
}
|
|
337
472
|
function imagePartBytes(image) {
|
|
338
473
|
if (typeof image === "string") {
|
|
@@ -541,8 +676,50 @@ async function runHooks(event, hooks2, ctx) {
|
|
|
541
676
|
}
|
|
542
677
|
|
|
543
678
|
// src/permissions/gate.ts
|
|
679
|
+
import fs from "fs";
|
|
544
680
|
import path from "path";
|
|
545
681
|
|
|
682
|
+
// src/core/sensitive.ts
|
|
683
|
+
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
684
|
+
".env",
|
|
685
|
+
"id_rsa",
|
|
686
|
+
"id_dsa",
|
|
687
|
+
"id_ecdsa",
|
|
688
|
+
"id_ed25519",
|
|
689
|
+
".npmrc",
|
|
690
|
+
".netrc",
|
|
691
|
+
".pgpass"
|
|
692
|
+
]);
|
|
693
|
+
var SENSITIVE_SUFFIXES = [".pem", ".key", ".p12", ".pfx"];
|
|
694
|
+
var SENSITIVE_ENV_EXEMPTIONS = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
695
|
+
function pathSegments(p) {
|
|
696
|
+
return p.replace(/\\/g, "/").split("/").filter(Boolean).map((s) => s.toLowerCase());
|
|
697
|
+
}
|
|
698
|
+
function isSensitivePath(p) {
|
|
699
|
+
const segments = pathSegments(p);
|
|
700
|
+
const base = segments[segments.length - 1] ?? "";
|
|
701
|
+
if (SENSITIVE_ENV_EXEMPTIONS.has(base)) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
if (base === ".env" || base.startsWith(".env.")) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
if (SENSITIVE_BASENAMES.has(base)) {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
if (SENSITIVE_SUFFIXES.some((suffix) => base.endsWith(suffix))) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
const dir = segments[segments.length - 2] ?? "";
|
|
714
|
+
if (dir === ".aws" && base === "credentials") {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
if (dir === ".kube" && base === "config") {
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
546
723
|
// src/permissions/allow.ts
|
|
547
724
|
function parseAllowRule(rule) {
|
|
548
725
|
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
@@ -566,11 +743,32 @@ function matchAllowRule(rule, req) {
|
|
|
566
743
|
const target = requestTarget(req);
|
|
567
744
|
return target !== void 0 && globMatch(rule.pattern, target);
|
|
568
745
|
}
|
|
746
|
+
function splitCommandChain(command) {
|
|
747
|
+
if (command.includes("$(") || command.includes("`")) return null;
|
|
748
|
+
return command.split(/&&|\|\||[;|\n]/).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
749
|
+
}
|
|
750
|
+
function withCommand(req, command) {
|
|
751
|
+
return {
|
|
752
|
+
...req,
|
|
753
|
+
args: { ...req.args ?? {}, command }
|
|
754
|
+
};
|
|
755
|
+
}
|
|
569
756
|
function isAllowedByRules(rules, req) {
|
|
570
|
-
return matchesAnyRule(rules, req);
|
|
757
|
+
if (req.toolName !== "bash") return matchesAnyRule(rules, req);
|
|
758
|
+
const command = requestTarget(req);
|
|
759
|
+
if (command === void 0) return matchesAnyRule(rules, req);
|
|
760
|
+
const segments = splitCommandChain(command);
|
|
761
|
+
if (segments === null) return false;
|
|
762
|
+
if (segments.length === 0) return matchesAnyRule(rules, req);
|
|
763
|
+
return segments.every((segment) => matchesAnyRule(rules, withCommand(req, segment)));
|
|
571
764
|
}
|
|
572
765
|
function isDeniedByRules(rules, req) {
|
|
573
|
-
return matchesAnyRule(rules, req);
|
|
766
|
+
if (req.toolName !== "bash") return matchesAnyRule(rules, req);
|
|
767
|
+
const command = requestTarget(req);
|
|
768
|
+
if (command === void 0) return matchesAnyRule(rules, req);
|
|
769
|
+
const segments = splitCommandChain(command);
|
|
770
|
+
if (segments === null || segments.length === 0) return matchesAnyRule(rules, req);
|
|
771
|
+
return segments.some((segment) => matchesAnyRule(rules, withCommand(req, segment)));
|
|
574
772
|
}
|
|
575
773
|
function matchesAnyRule(rules, req) {
|
|
576
774
|
return rules.some((raw) => {
|
|
@@ -584,10 +782,17 @@ function buildAllowRule(req) {
|
|
|
584
782
|
}
|
|
585
783
|
|
|
586
784
|
// src/permissions/gate.ts
|
|
587
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file"]);
|
|
785
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file", "grep", "glob"]);
|
|
588
786
|
var DANGEROUS_COMMAND_PATTERNS = [
|
|
589
787
|
/rm\s+-rf\s+\/(?:\s|$|;|&)/i,
|
|
590
788
|
/rm\s+-rf\s+~(?:\s|\/|$|;|&)/i,
|
|
789
|
+
/rm\s+-rf\s+\.\/?(?:\s|$|;|&)/i,
|
|
790
|
+
/rm\s+-rf\s+\*(?:\s|$|;|&)/i,
|
|
791
|
+
/\bsudo\s+rm\b/i,
|
|
792
|
+
/\bdel\s+\/s\b/i,
|
|
793
|
+
/\brd\s+\/s\b/i,
|
|
794
|
+
/\bformat\s+[a-z]:/i,
|
|
795
|
+
/\bdiskpart\b/i,
|
|
591
796
|
/:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
|
|
592
797
|
/\bmkfs\b/i,
|
|
593
798
|
/dd\s+[^|;]*\bif=/i,
|
|
@@ -617,12 +822,30 @@ function isOutsideCwd(p, cwd) {
|
|
|
617
822
|
const base = normalizeAbsolute(cwd, cwd);
|
|
618
823
|
return abs !== base && !abs.startsWith(`${base}/`);
|
|
619
824
|
}
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
825
|
+
function resolveRealPath(p, cwd) {
|
|
826
|
+
let current = path.isAbsolute(p) ? path.normalize(p) : path.resolve(cwd, p);
|
|
827
|
+
const missing = [];
|
|
828
|
+
for (; ; ) {
|
|
829
|
+
try {
|
|
830
|
+
return path.join(fs.realpathSync(current), ...missing.reverse());
|
|
831
|
+
} catch {
|
|
832
|
+
const parent = path.dirname(current);
|
|
833
|
+
if (parent === current) return null;
|
|
834
|
+
missing.push(path.basename(current));
|
|
835
|
+
current = parent;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function isOutsideForWrite(p, cwd) {
|
|
840
|
+
const resolved = resolveRealPath(p, cwd);
|
|
841
|
+
if (resolved === null) return isOutsideCwd(p, cwd);
|
|
842
|
+
let realCwd;
|
|
843
|
+
try {
|
|
844
|
+
realCwd = fs.realpathSync(cwd);
|
|
845
|
+
} catch {
|
|
846
|
+
return isOutsideCwd(p, cwd);
|
|
624
847
|
}
|
|
625
|
-
return
|
|
848
|
+
return isOutsideCwd(resolved, realCwd);
|
|
626
849
|
}
|
|
627
850
|
function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
628
851
|
if (mode === "yolo") {
|
|
@@ -634,10 +857,12 @@ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
|
634
857
|
return "deny";
|
|
635
858
|
}
|
|
636
859
|
if (FILE_TOOLS.has(req.toolName) && filePath !== void 0) {
|
|
637
|
-
|
|
860
|
+
const isWriteTool = req.toolName === "write_file" || req.toolName === "edit_file";
|
|
861
|
+
if (isWriteTool && isSensitivePath(filePath)) {
|
|
638
862
|
return "deny";
|
|
639
863
|
}
|
|
640
|
-
|
|
864
|
+
const outside = isWriteTool ? isOutsideForWrite(filePath, ctx.cwd) : isOutsideCwd(filePath, ctx.cwd);
|
|
865
|
+
if (outside) {
|
|
641
866
|
if (mode === "ask" && req.level === "read") {
|
|
642
867
|
return "ask";
|
|
643
868
|
}
|
|
@@ -708,10 +933,93 @@ function scheduleSessionTitle(store, userText, model) {
|
|
|
708
933
|
});
|
|
709
934
|
}
|
|
710
935
|
|
|
711
|
-
// src/
|
|
712
|
-
import {
|
|
936
|
+
// src/snapshot/git-tree.ts
|
|
937
|
+
import { execFile } from "child_process";
|
|
938
|
+
import { existsSync } from "fs";
|
|
939
|
+
import { mkdir } from "fs/promises";
|
|
940
|
+
import os from "os";
|
|
713
941
|
import path2 from "path";
|
|
942
|
+
var GIT_TIMEOUT_MS2 = 1e4;
|
|
943
|
+
var GIT_MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
944
|
+
function runGit2(args) {
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
execFile(
|
|
947
|
+
"git",
|
|
948
|
+
args,
|
|
949
|
+
{ timeout: GIT_TIMEOUT_MS2, maxBuffer: GIT_MAX_BUFFER2, encoding: "utf8" },
|
|
950
|
+
(error, stdout) => {
|
|
951
|
+
resolve(error ? null : stdout.trim());
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
var gitAvailable = null;
|
|
957
|
+
async function isGitAvailable() {
|
|
958
|
+
if (gitAvailable === null) {
|
|
959
|
+
gitAvailable = await runGit2(["--version"]) !== null;
|
|
960
|
+
}
|
|
961
|
+
return gitAvailable;
|
|
962
|
+
}
|
|
963
|
+
function treeRepoDir(cwd) {
|
|
964
|
+
const key = Buffer.from(path2.resolve(cwd), "utf8").toString("base64url");
|
|
965
|
+
return path2.join(gitTreesDir(), key);
|
|
966
|
+
}
|
|
967
|
+
function isGuardedDir(cwd) {
|
|
968
|
+
const resolved = path2.resolve(cwd);
|
|
969
|
+
const home = path2.resolve(os.homedir());
|
|
970
|
+
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
971
|
+
return same(resolved, home) || same(resolved, path2.parse(resolved).root);
|
|
972
|
+
}
|
|
973
|
+
async function ensureTreeRepo(cwd) {
|
|
974
|
+
const dir = treeRepoDir(cwd);
|
|
975
|
+
if (existsSync(path2.join(dir, "HEAD"))) return dir;
|
|
976
|
+
await mkdir(path2.dirname(dir), { recursive: true });
|
|
977
|
+
const out = await runGit2(["init", "--bare", dir]);
|
|
978
|
+
return out === null ? null : dir;
|
|
979
|
+
}
|
|
980
|
+
function repoArgs(dir, cwd) {
|
|
981
|
+
return [`--git-dir=${dir}`, `--work-tree=${cwd}`];
|
|
982
|
+
}
|
|
983
|
+
async function trackTree(cwd) {
|
|
984
|
+
if (isGuardedDir(cwd)) return null;
|
|
985
|
+
if (!await isGitAvailable()) return null;
|
|
986
|
+
const dir = await ensureTreeRepo(cwd);
|
|
987
|
+
if (!dir) return null;
|
|
988
|
+
const base = repoArgs(dir, cwd);
|
|
989
|
+
if (await runGit2([...base, "add", "-A"]) === null) return null;
|
|
990
|
+
return runGit2([...base, "write-tree"]);
|
|
991
|
+
}
|
|
992
|
+
async function restoreTree(cwd, hash) {
|
|
993
|
+
if (isGuardedDir(cwd)) return false;
|
|
994
|
+
if (!await isGitAvailable()) return false;
|
|
995
|
+
const dir = treeRepoDir(cwd);
|
|
996
|
+
if (!existsSync(path2.join(dir, "HEAD"))) return false;
|
|
997
|
+
const base = repoArgs(dir, cwd);
|
|
998
|
+
if (await runGit2([...base, "read-tree", hash]) === null) return false;
|
|
999
|
+
if (await runGit2([...base, "checkout-index", "-f", "-a"]) === null) return false;
|
|
1000
|
+
if (await runGit2([...base, "clean", "-fd"]) === null) return false;
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
async function diffTreeNames(cwd, hash) {
|
|
1004
|
+
if (!await isGitAvailable()) return null;
|
|
1005
|
+
const dir = treeRepoDir(cwd);
|
|
1006
|
+
if (!existsSync(path2.join(dir, "HEAD"))) return null;
|
|
1007
|
+
const base = repoArgs(dir, cwd);
|
|
1008
|
+
const changed = await runGit2([...base, "diff", "--name-only", hash]);
|
|
1009
|
+
const untracked = await runGit2([...base, "ls-files", "--others", "--exclude-standard"]);
|
|
1010
|
+
if (changed === null || untracked === null) return null;
|
|
1011
|
+
const names = /* @__PURE__ */ new Set();
|
|
1012
|
+
for (const line of [...changed.split("\n"), ...untracked.split("\n")]) {
|
|
1013
|
+
if (line !== "") names.add(line);
|
|
1014
|
+
}
|
|
1015
|
+
return [...names];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/tools/fs/snapshots.ts
|
|
1019
|
+
import { mkdir as mkdir2, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1020
|
+
import path3 from "path";
|
|
714
1021
|
var MAX_SNAPSHOTS = 50;
|
|
1022
|
+
var MAX_SNAPSHOT_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
715
1023
|
var stack = [];
|
|
716
1024
|
var activeTurn = 0;
|
|
717
1025
|
var activeTurnMessageIndex = -1;
|
|
@@ -733,28 +1041,48 @@ function currentTurnSeq() {
|
|
|
733
1041
|
function currentTurnMessageIndex() {
|
|
734
1042
|
return activeTurnMessageIndex;
|
|
735
1043
|
}
|
|
736
|
-
|
|
1044
|
+
function ownerOf(snapshot) {
|
|
1045
|
+
return snapshot.owner ?? "root";
|
|
1046
|
+
}
|
|
1047
|
+
async function captureSnapshot(filePath, toolName, snapshotContext) {
|
|
737
1048
|
let content = null;
|
|
738
1049
|
let existed = false;
|
|
739
|
-
|
|
740
|
-
|
|
1050
|
+
let contentTooLarge = false;
|
|
1051
|
+
const st = await stat(filePath).catch(() => null);
|
|
1052
|
+
if (st?.isFile()) {
|
|
741
1053
|
existed = true;
|
|
742
|
-
|
|
1054
|
+
if (st.size > MAX_SNAPSHOT_CONTENT_BYTES) {
|
|
1055
|
+
contentTooLarge = true;
|
|
1056
|
+
} else {
|
|
1057
|
+
try {
|
|
1058
|
+
content = await readFile(filePath, "utf8");
|
|
1059
|
+
} catch {
|
|
1060
|
+
existed = false;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
743
1063
|
}
|
|
744
1064
|
return {
|
|
745
1065
|
id: nextSnapshotId(),
|
|
746
1066
|
path: filePath,
|
|
747
1067
|
existed,
|
|
748
1068
|
content,
|
|
1069
|
+
contentTooLarge: contentTooLarge || void 0,
|
|
749
1070
|
toolName,
|
|
750
1071
|
timestamp: Date.now(),
|
|
751
|
-
turn: activeTurn,
|
|
752
|
-
messageIndex: activeTurnMessageIndex
|
|
1072
|
+
turn: snapshotContext?.turn ?? activeTurn,
|
|
1073
|
+
messageIndex: snapshotContext?.messageIndex ?? activeTurnMessageIndex,
|
|
1074
|
+
owner: snapshotContext?.owner ?? "root"
|
|
753
1075
|
};
|
|
754
1076
|
}
|
|
755
1077
|
async function pushSnapshot(snapshot) {
|
|
756
1078
|
stack.push(snapshot);
|
|
757
|
-
|
|
1079
|
+
try {
|
|
1080
|
+
await hooks?.onPush?.(snapshot);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
if (process.env.STAR_DEBUG) {
|
|
1083
|
+
console.error("[star] failed to persist snapshot checkpoint:", err);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
758
1086
|
if (stack.length > MAX_SNAPSHOTS) {
|
|
759
1087
|
const evicted = stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
760
1088
|
await removeFromStore(evicted.map((s) => s.id));
|
|
@@ -787,191 +1115,156 @@ async function resolveContent(snapshot) {
|
|
|
787
1115
|
}
|
|
788
1116
|
async function revert(snapshot) {
|
|
789
1117
|
if (snapshot.existed) {
|
|
1118
|
+
if (snapshot.contentTooLarge) {
|
|
1119
|
+
return `Skipped ${snapshot.path}: content too large, not restored.`;
|
|
1120
|
+
}
|
|
790
1121
|
const content = await resolveContent(snapshot);
|
|
791
1122
|
if (content === null) {
|
|
792
1123
|
return `Skipped ${snapshot.path}: the snapshot content is no longer available.`;
|
|
793
1124
|
}
|
|
794
|
-
await
|
|
1125
|
+
await mkdir2(path3.dirname(snapshot.path), { recursive: true });
|
|
795
1126
|
await writeFile(snapshot.path, content, "utf8");
|
|
796
1127
|
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
797
1128
|
}
|
|
798
1129
|
await rm(snapshot.path, { force: true });
|
|
799
1130
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
800
1131
|
}
|
|
801
|
-
function listTurnSnapshots(turn) {
|
|
802
|
-
return stack.filter(
|
|
1132
|
+
function listTurnSnapshots(turn, includeSubagent = false) {
|
|
1133
|
+
return stack.filter(
|
|
1134
|
+
(snapshot) => snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")
|
|
1135
|
+
).reverse();
|
|
803
1136
|
}
|
|
804
1137
|
function resolveSnapshotContent(snapshot) {
|
|
805
1138
|
return resolveContent(snapshot);
|
|
806
1139
|
}
|
|
807
|
-
async function undoTurnSnapshots(turn) {
|
|
1140
|
+
async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
808
1141
|
const matches = [];
|
|
809
1142
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
810
1143
|
const snapshot = stack[i];
|
|
811
|
-
if (snapshot && snapshot.turn === turn) {
|
|
1144
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
812
1145
|
matches.push(snapshot);
|
|
813
|
-
stack.splice(i, 1);
|
|
814
1146
|
}
|
|
815
1147
|
}
|
|
816
|
-
await removeFromStore(matches.map((s) => s.id));
|
|
817
1148
|
const messages = [];
|
|
1149
|
+
const revertedIds = [];
|
|
818
1150
|
for (const snapshot of matches) {
|
|
819
|
-
|
|
1151
|
+
try {
|
|
1152
|
+
messages.push(await revert(snapshot));
|
|
1153
|
+
const idx = stack.indexOf(snapshot);
|
|
1154
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
1155
|
+
revertedIds.push(snapshot.id);
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
messages.push(
|
|
1158
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
820
1161
|
}
|
|
1162
|
+
await removeFromStore(revertedIds);
|
|
821
1163
|
return messages;
|
|
822
1164
|
}
|
|
823
|
-
async function
|
|
1165
|
+
async function dropTurnSnapshots(turn, includeSubagent = false) {
|
|
1166
|
+
const dropped = [];
|
|
1167
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1168
|
+
const snapshot = stack[i];
|
|
1169
|
+
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
1170
|
+
dropped.push(snapshot.id);
|
|
1171
|
+
stack.splice(i, 1);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
await removeFromStore(dropped);
|
|
1175
|
+
return dropped.length;
|
|
1176
|
+
}
|
|
1177
|
+
async function rewindToSnapshot(id, includeSubagent = false) {
|
|
824
1178
|
if (!stack.some((snapshot) => snapshot.id === id)) return null;
|
|
825
1179
|
const matches = [];
|
|
826
1180
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
827
1181
|
const snapshot = stack[i];
|
|
828
|
-
if (snapshot && snapshot.id >= id) {
|
|
1182
|
+
if (snapshot && snapshot.id >= id && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
829
1183
|
matches.push(snapshot);
|
|
830
|
-
stack.splice(i, 1);
|
|
831
1184
|
}
|
|
832
1185
|
}
|
|
833
|
-
await removeFromStore(matches.map((s) => s.id));
|
|
834
1186
|
const reverted = [];
|
|
1187
|
+
const revertedIds = [];
|
|
835
1188
|
let messageIndex = null;
|
|
836
1189
|
for (const snapshot of matches) {
|
|
837
|
-
reverted.push(await revert(snapshot));
|
|
838
|
-
if (snapshot.messageIndex >= 0) {
|
|
839
|
-
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return { reverted, messageIndex };
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// src/tools/todo.ts
|
|
846
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
847
|
-
import path3 from "path";
|
|
848
|
-
import { z } from "zod";
|
|
849
|
-
var todoItemSchema = z.object({
|
|
850
|
-
id: z.number().int(),
|
|
851
|
-
title: z.string(),
|
|
852
|
-
status: z.enum(["pending", "in_progress", "done"])
|
|
853
|
-
});
|
|
854
|
-
var writeSchema = z.object({
|
|
855
|
-
todos: z.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
856
|
-
});
|
|
857
|
-
var readSchema = z.object({});
|
|
858
|
-
function fileFor(cwd) {
|
|
859
|
-
return path3.join(cwd, ".star", "todos.json");
|
|
860
|
-
}
|
|
861
|
-
var TodoStore = class {
|
|
862
|
-
items = /* @__PURE__ */ new Map();
|
|
863
|
-
list() {
|
|
864
|
-
return [...this.items.values()];
|
|
865
|
-
}
|
|
866
|
-
replace(items) {
|
|
867
|
-
this.items.clear();
|
|
868
|
-
for (const item of items) {
|
|
869
|
-
this.items.set(item.id, item);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
async load(cwd) {
|
|
873
|
-
let raw;
|
|
874
|
-
try {
|
|
875
|
-
raw = await readFile2(fileFor(cwd), "utf8");
|
|
876
|
-
} catch {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
1190
|
try {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1191
|
+
reverted.push(await revert(snapshot));
|
|
1192
|
+
const idx = stack.indexOf(snapshot);
|
|
1193
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
1194
|
+
revertedIds.push(snapshot.id);
|
|
1195
|
+
if (snapshot.messageIndex >= 0) {
|
|
1196
|
+
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
883
1197
|
}
|
|
884
|
-
} catch {
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
reverted.push(
|
|
1200
|
+
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
1201
|
+
);
|
|
885
1202
|
}
|
|
886
1203
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
await writeFile2(fileFor(cwd), `${JSON.stringify(this.list(), null, 2)}
|
|
890
|
-
`);
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
var SYMBOLS = {
|
|
894
|
-
pending: "[ ]",
|
|
895
|
-
in_progress: "[~]",
|
|
896
|
-
done: "[x]"
|
|
897
|
-
};
|
|
898
|
-
function formatTodos(items) {
|
|
899
|
-
if (items.length === 0) {
|
|
900
|
-
return "No todos.";
|
|
901
|
-
}
|
|
902
|
-
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
903
|
-
const width = Math.max(...lines.map((l) => l.length));
|
|
904
|
-
return lines.map((line, i) => {
|
|
905
|
-
const status = items[i]?.status;
|
|
906
|
-
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
907
|
-
}).join("\n");
|
|
908
|
-
}
|
|
909
|
-
var defaultStore = new TodoStore();
|
|
910
|
-
function parseTodoArgs(args) {
|
|
911
|
-
const parsed = writeSchema.safeParse(args);
|
|
912
|
-
return parsed.success ? parsed.data.todos : null;
|
|
913
|
-
}
|
|
914
|
-
function pendingTodoTitles(args) {
|
|
915
|
-
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
916
|
-
}
|
|
917
|
-
async function loadTodos(cwd) {
|
|
918
|
-
await defaultStore.load(cwd);
|
|
919
|
-
return defaultStore.list();
|
|
920
|
-
}
|
|
921
|
-
function resetTodos() {
|
|
922
|
-
defaultStore.replace([]);
|
|
923
|
-
}
|
|
924
|
-
function createTodoTools(store = defaultStore) {
|
|
925
|
-
const todoWrite = {
|
|
926
|
-
name: "todo_write",
|
|
927
|
-
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
928
|
-
permission: "read",
|
|
929
|
-
parameters: writeSchema,
|
|
930
|
-
async execute(args, ctx) {
|
|
931
|
-
const parsed = writeSchema.safeParse(args);
|
|
932
|
-
if (!parsed.success) {
|
|
933
|
-
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
934
|
-
}
|
|
935
|
-
store.replace(parsed.data.todos);
|
|
936
|
-
await store.save(ctx.cwd);
|
|
937
|
-
const items = store.list();
|
|
938
|
-
const done = items.filter((it) => it.status === "done").length;
|
|
939
|
-
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
const todoRead = {
|
|
943
|
-
name: "todo_read",
|
|
944
|
-
description: "Read the current todo list.",
|
|
945
|
-
permission: "read",
|
|
946
|
-
parameters: readSchema,
|
|
947
|
-
async execute() {
|
|
948
|
-
return { content: formatTodos(store.list()) };
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
return [todoWrite, todoRead];
|
|
1204
|
+
await removeFromStore(revertedIds);
|
|
1205
|
+
return { reverted, messageIndex };
|
|
952
1206
|
}
|
|
953
1207
|
|
|
954
|
-
// src/
|
|
1208
|
+
// src/tools/bash.ts
|
|
1209
|
+
import { spawn as spawn3 } from "child_process";
|
|
1210
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1211
|
+
import path4 from "path";
|
|
1212
|
+
import { z as z2 } from "zod";
|
|
1213
|
+
|
|
1214
|
+
// src/tasks/manager.ts
|
|
1215
|
+
import { spawn as spawn2 } from "child_process";
|
|
955
1216
|
import { EventEmitter } from "events";
|
|
956
|
-
var
|
|
1217
|
+
var MAX_TASK_OUTPUT = 1e5;
|
|
1218
|
+
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1219
|
+
var MAX_FINISHED_RECORDS = 50;
|
|
1220
|
+
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1221
|
+
var TaskManager = class extends EventEmitter {
|
|
957
1222
|
records = /* @__PURE__ */ new Map();
|
|
958
|
-
|
|
959
|
-
start(
|
|
960
|
-
const id = `
|
|
1223
|
+
seq = 0;
|
|
1224
|
+
start(opts) {
|
|
1225
|
+
const id = `task-${++this.seq}`;
|
|
1226
|
+
const spec = resolveShell();
|
|
1227
|
+
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1228
|
+
cwd: opts.cwd,
|
|
1229
|
+
windowsHide: true,
|
|
1230
|
+
// POSIX: the child leads its own process group so killTree can SIGKILL
|
|
1231
|
+
// the whole tree with a negative pid (Windows uses taskkill /t).
|
|
1232
|
+
detached: process.platform !== "win32"
|
|
1233
|
+
});
|
|
961
1234
|
const rec = {
|
|
962
1235
|
id,
|
|
963
|
-
|
|
964
|
-
description:
|
|
1236
|
+
command: opts.command,
|
|
1237
|
+
description: opts.description,
|
|
965
1238
|
status: "running",
|
|
966
1239
|
startedAt: Date.now(),
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1240
|
+
output: "",
|
|
1241
|
+
truncated: false,
|
|
1242
|
+
child
|
|
970
1243
|
};
|
|
971
1244
|
this.records.set(id, rec);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1245
|
+
const append = (d) => {
|
|
1246
|
+
rec.output += d.toString("utf8");
|
|
1247
|
+
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1248
|
+
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1249
|
+
rec.truncated = true;
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
child.stdout.on("data", append);
|
|
1253
|
+
child.stderr.on("data", append);
|
|
1254
|
+
child.on("error", (err) => {
|
|
1255
|
+
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1256
|
+
`));
|
|
1257
|
+
if (rec.status === "running") {
|
|
1258
|
+
this.finish(rec, "failed", null);
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
child.on("close", (code) => {
|
|
1262
|
+
if (rec.status !== "running") return;
|
|
1263
|
+
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1264
|
+
});
|
|
1265
|
+
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1266
|
+
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1267
|
+
rec.timer.unref();
|
|
975
1268
|
this.emitUpdate(rec);
|
|
976
1269
|
return this.snapshot(rec);
|
|
977
1270
|
}
|
|
@@ -992,312 +1285,39 @@ var AgentTaskManager = class extends EventEmitter {
|
|
|
992
1285
|
kill(id) {
|
|
993
1286
|
const rec = this.records.get(id);
|
|
994
1287
|
if (!rec || rec.status !== "running") return void 0;
|
|
995
|
-
|
|
996
|
-
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
1288
|
+
this.terminate(rec, "stopped");
|
|
997
1289
|
return this.snapshot(rec);
|
|
998
1290
|
}
|
|
999
1291
|
cleanup() {
|
|
1000
1292
|
const killed = [];
|
|
1001
1293
|
for (const rec of this.records.values()) {
|
|
1002
1294
|
if (rec.status === "running") {
|
|
1003
|
-
|
|
1004
|
-
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
1295
|
+
this.terminate(rec, "stopped");
|
|
1005
1296
|
killed.push(this.snapshot(rec));
|
|
1006
1297
|
}
|
|
1007
1298
|
}
|
|
1008
1299
|
return killed;
|
|
1009
1300
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
for (const rec of this.records.values()) {
|
|
1014
|
-
if (rec.status !== "running" && !rec.notified) {
|
|
1015
|
-
rec.notified = true;
|
|
1016
|
-
drained.push(this.snapshot(rec));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return drained;
|
|
1020
|
-
}
|
|
1021
|
-
finish(rec, status, result) {
|
|
1022
|
-
if (rec.status !== "running") return;
|
|
1023
|
-
rec.status = status;
|
|
1024
|
-
rec.result = result;
|
|
1025
|
-
rec.endedAt = Date.now();
|
|
1026
|
-
this.emitUpdate(rec);
|
|
1027
|
-
}
|
|
1028
|
-
emitUpdate(rec) {
|
|
1029
|
-
this.emit("update", this.snapshot(rec));
|
|
1030
|
-
}
|
|
1031
|
-
snapshot(rec) {
|
|
1032
|
-
return {
|
|
1033
|
-
id: rec.id,
|
|
1034
|
-
prompt: rec.prompt,
|
|
1035
|
-
description: rec.description,
|
|
1036
|
-
status: rec.status,
|
|
1037
|
-
startedAt: rec.startedAt,
|
|
1038
|
-
endedAt: rec.endedAt,
|
|
1039
|
-
result: rec.result
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
var defaultAgentTasks = new AgentTaskManager();
|
|
1044
|
-
|
|
1045
|
-
// src/agent/project-memory.ts
|
|
1046
|
-
import { readFileSync, statSync } from "fs";
|
|
1047
|
-
import path4 from "path";
|
|
1048
|
-
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
1049
|
-
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
1050
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1051
|
-
function readProjectMemory(cwd) {
|
|
1052
|
-
const file = path4.join(cwd, PROJECT_MEMORY_FILE);
|
|
1053
|
-
let mtimeMs;
|
|
1054
|
-
try {
|
|
1055
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
1056
|
-
} catch {
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
|
-
const hit = cache.get(file);
|
|
1060
|
-
if (hit && hit.mtimeMs === mtimeMs) return hit.block;
|
|
1061
|
-
let block = null;
|
|
1062
|
-
try {
|
|
1063
|
-
let body = readFileSync(file, "utf8").trim();
|
|
1064
|
-
if (body) {
|
|
1065
|
-
if (body.length > PROJECT_MEMORY_MAX_CHARS) {
|
|
1066
|
-
body = `${body.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
1067
|
-
|
|
1068
|
-
[AGENTS.md truncated to ${PROJECT_MEMORY_MAX_CHARS} characters]`;
|
|
1069
|
-
}
|
|
1070
|
-
block = `# Project instructions (AGENTS.md)
|
|
1071
|
-
${body}`;
|
|
1072
|
-
}
|
|
1073
|
-
} catch {
|
|
1074
|
-
block = null;
|
|
1075
|
-
}
|
|
1076
|
-
cache.set(file, { mtimeMs, block });
|
|
1077
|
-
return block;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// src/agent/skills.ts
|
|
1081
|
-
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
1082
|
-
import path5 from "path";
|
|
1083
|
-
import { z as z2 } from "zod";
|
|
1084
|
-
var SKILL_FILE = "SKILL.md";
|
|
1085
|
-
var SKILL_MAX_CHARS = 32 * 1024;
|
|
1086
|
-
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
1087
|
-
function parseFrontmatter(raw) {
|
|
1088
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
1089
|
-
if (!match) return null;
|
|
1090
|
-
const meta = {};
|
|
1091
|
-
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
1092
|
-
const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
1093
|
-
if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
|
|
1094
|
-
}
|
|
1095
|
-
return { meta, body: raw.slice(match[0].length).trim() };
|
|
1096
|
-
}
|
|
1097
|
-
var cache2 = /* @__PURE__ */ new Map();
|
|
1098
|
-
function parseSkillFile(filePath, dirName, scope) {
|
|
1099
|
-
let mtimeMs;
|
|
1100
|
-
try {
|
|
1101
|
-
mtimeMs = statSync2(filePath).mtimeMs;
|
|
1102
|
-
} catch {
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
const hit = cache2.get(filePath);
|
|
1106
|
-
if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
|
|
1107
|
-
let skill = null;
|
|
1108
|
-
try {
|
|
1109
|
-
const parsed = parseFrontmatter(readFileSync2(filePath, "utf8"));
|
|
1110
|
-
const name = parsed?.meta.name || dirName;
|
|
1111
|
-
const description = parsed?.meta.description;
|
|
1112
|
-
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
1113
|
-
skill = { name, description, filePath, dir: path5.dirname(filePath), scope };
|
|
1114
|
-
}
|
|
1115
|
-
} catch {
|
|
1116
|
-
skill = null;
|
|
1117
|
-
}
|
|
1118
|
-
cache2.set(filePath, { mtimeMs, skill });
|
|
1119
|
-
return skill;
|
|
1120
|
-
}
|
|
1121
|
-
function scanDir(base, scope, into) {
|
|
1122
|
-
let entries;
|
|
1123
|
-
try {
|
|
1124
|
-
entries = readdirSync(base);
|
|
1125
|
-
} catch {
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
for (const entry of entries.sort()) {
|
|
1129
|
-
if (!NAME_PATTERN.test(entry)) continue;
|
|
1130
|
-
const parsed = parseSkillFile(path5.join(base, entry, SKILL_FILE), entry, scope);
|
|
1131
|
-
if (parsed) into.set(parsed.name, parsed);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
function discoverSkills(cwd, home = starHome()) {
|
|
1135
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1136
|
-
scanDir(path5.join(home, "skills"), "user", byName);
|
|
1137
|
-
scanDir(path5.join(cwd, ".star", "skills"), "project", byName);
|
|
1138
|
-
return [...byName.values()];
|
|
1139
|
-
}
|
|
1140
|
-
function formatSkillsBlock(skills) {
|
|
1141
|
-
const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
|
|
1142
|
-
return [
|
|
1143
|
-
"# Available skills",
|
|
1144
|
-
"When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
|
|
1145
|
-
...lines
|
|
1146
|
-
].join("\n");
|
|
1147
|
-
}
|
|
1148
|
-
function loadSkillBody(skill) {
|
|
1149
|
-
let parsed = null;
|
|
1150
|
-
try {
|
|
1151
|
-
parsed = parseFrontmatter(readFileSync2(skill.filePath, "utf8"));
|
|
1152
|
-
} catch {
|
|
1153
|
-
return null;
|
|
1154
|
-
}
|
|
1155
|
-
if (!parsed?.body) return null;
|
|
1156
|
-
if (parsed.body.length > SKILL_MAX_CHARS) {
|
|
1157
|
-
return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
|
|
1158
|
-
|
|
1159
|
-
[skill truncated to ${SKILL_MAX_CHARS} characters]`;
|
|
1160
|
-
}
|
|
1161
|
-
return parsed.body;
|
|
1162
|
-
}
|
|
1163
|
-
function createSkillTool(deps) {
|
|
1164
|
-
return {
|
|
1165
|
-
name: "skill",
|
|
1166
|
-
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.",
|
|
1167
|
-
permission: "read",
|
|
1168
|
-
parameters: z2.object({
|
|
1169
|
-
name: z2.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
1170
|
-
}),
|
|
1171
|
-
async execute(args) {
|
|
1172
|
-
const skills = discoverSkills(deps.cwd, deps.home);
|
|
1173
|
-
const skill = skills.find((s) => s.name === args.name);
|
|
1174
|
-
if (!skill) {
|
|
1175
|
-
const available = skills.map((s) => s.name).join(", ") || "none";
|
|
1176
|
-
return {
|
|
1177
|
-
content: `Unknown skill: "${args.name}". Available skills: ${available}`,
|
|
1178
|
-
isError: true
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
const body = loadSkillBody(skill);
|
|
1182
|
-
if (!body) {
|
|
1183
|
-
return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
|
|
1184
|
-
}
|
|
1185
|
-
return {
|
|
1186
|
-
content: [
|
|
1187
|
-
`# Skill: ${skill.name}`,
|
|
1188
|
-
"",
|
|
1189
|
-
body,
|
|
1190
|
-
"",
|
|
1191
|
-
`Base directory for relative file references in this skill: ${skill.dir}`
|
|
1192
|
-
].join("\n")
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// src/agent/subagent.ts
|
|
1199
|
-
import { z as z12 } from "zod";
|
|
1200
|
-
|
|
1201
|
-
// src/tools/bash.ts
|
|
1202
|
-
import { spawn as spawn3 } from "child_process";
|
|
1203
|
-
import { existsSync } from "fs";
|
|
1204
|
-
import path6 from "path";
|
|
1205
|
-
import { z as z3 } from "zod";
|
|
1206
|
-
|
|
1207
|
-
// src/tasks/manager.ts
|
|
1208
|
-
import { spawn as spawn2 } from "child_process";
|
|
1209
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
1210
|
-
var MAX_TASK_OUTPUT = 1e5;
|
|
1211
|
-
var DEFAULT_TASK_TIMEOUT = 3600;
|
|
1212
|
-
var TRUNCATED_PREFIX = "[... earlier output truncated ...]\n";
|
|
1213
|
-
var TaskManager = class extends EventEmitter2 {
|
|
1214
|
-
records = /* @__PURE__ */ new Map();
|
|
1215
|
-
seq = 0;
|
|
1216
|
-
start(opts) {
|
|
1217
|
-
const id = `task-${++this.seq}`;
|
|
1218
|
-
const spec = resolveShell();
|
|
1219
|
-
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1220
|
-
cwd: opts.cwd,
|
|
1221
|
-
windowsHide: true
|
|
1222
|
-
});
|
|
1223
|
-
const rec = {
|
|
1224
|
-
id,
|
|
1225
|
-
command: opts.command,
|
|
1226
|
-
description: opts.description,
|
|
1227
|
-
status: "running",
|
|
1228
|
-
startedAt: Date.now(),
|
|
1229
|
-
output: "",
|
|
1230
|
-
truncated: false,
|
|
1231
|
-
child
|
|
1232
|
-
};
|
|
1233
|
-
this.records.set(id, rec);
|
|
1234
|
-
const append = (d) => {
|
|
1235
|
-
rec.output += d.toString("utf8");
|
|
1236
|
-
if (rec.output.length > MAX_TASK_OUTPUT) {
|
|
1237
|
-
rec.output = rec.output.slice(-MAX_TASK_OUTPUT);
|
|
1238
|
-
rec.truncated = true;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
child.stdout.on("data", append);
|
|
1242
|
-
child.stderr.on("data", append);
|
|
1243
|
-
child.on("error", (err) => {
|
|
1244
|
-
append(Buffer.from(`Failed to start shell '${spec.label}': ${err.message}
|
|
1245
|
-
`));
|
|
1246
|
-
if (rec.status === "running") {
|
|
1247
|
-
this.finish(rec, "failed", null);
|
|
1248
|
-
}
|
|
1249
|
-
});
|
|
1250
|
-
child.on("close", (code) => {
|
|
1251
|
-
if (rec.status !== "running") return;
|
|
1252
|
-
this.finish(rec, code === 0 ? "completed" : "failed", code);
|
|
1253
|
-
});
|
|
1254
|
-
const timeoutSeconds = opts.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT;
|
|
1255
|
-
rec.timer = setTimeout(() => this.terminate(rec, "timed_out"), timeoutSeconds * 1e3);
|
|
1256
|
-
rec.timer.unref();
|
|
1257
|
-
this.emitUpdate(rec);
|
|
1258
|
-
return this.snapshot(rec);
|
|
1259
|
-
}
|
|
1260
|
-
get(id) {
|
|
1261
|
-
const rec = this.records.get(id);
|
|
1262
|
-
return rec ? this.snapshot(rec) : void 0;
|
|
1263
|
-
}
|
|
1264
|
-
list() {
|
|
1265
|
-
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
1266
|
-
}
|
|
1267
|
-
runningCount() {
|
|
1268
|
-
let count = 0;
|
|
1269
|
-
for (const rec of this.records.values()) {
|
|
1270
|
-
if (rec.status === "running") count += 1;
|
|
1271
|
-
}
|
|
1272
|
-
return count;
|
|
1273
|
-
}
|
|
1274
|
-
kill(id) {
|
|
1275
|
-
const rec = this.records.get(id);
|
|
1276
|
-
if (!rec || rec.status !== "running") return void 0;
|
|
1277
|
-
this.terminate(rec, "stopped");
|
|
1278
|
-
return this.snapshot(rec);
|
|
1279
|
-
}
|
|
1280
|
-
cleanup() {
|
|
1281
|
-
const killed = [];
|
|
1282
|
-
for (const rec of this.records.values()) {
|
|
1283
|
-
if (rec.status === "running") {
|
|
1284
|
-
this.terminate(rec, "stopped");
|
|
1285
|
-
killed.push(this.snapshot(rec));
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return killed;
|
|
1289
|
-
}
|
|
1290
|
-
terminate(rec, status) {
|
|
1291
|
-
killTree2(rec.child);
|
|
1292
|
-
this.finish(rec, status, null);
|
|
1301
|
+
terminate(rec, status) {
|
|
1302
|
+
killTree2(rec.child);
|
|
1303
|
+
this.finish(rec, status, null);
|
|
1293
1304
|
}
|
|
1294
1305
|
finish(rec, status, exitCode) {
|
|
1295
1306
|
if (rec.timer) clearTimeout(rec.timer);
|
|
1296
1307
|
rec.status = status;
|
|
1297
1308
|
rec.exitCode = exitCode;
|
|
1298
1309
|
rec.endedAt = Date.now();
|
|
1310
|
+
this.pruneFinished();
|
|
1299
1311
|
this.emitUpdate(rec);
|
|
1300
1312
|
}
|
|
1313
|
+
pruneFinished() {
|
|
1314
|
+
const finished = [...this.records.values()].filter((rec) => rec.status !== "running");
|
|
1315
|
+
if (finished.length <= MAX_FINISHED_RECORDS) return;
|
|
1316
|
+
finished.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
|
|
1317
|
+
for (const rec of finished.slice(0, finished.length - MAX_FINISHED_RECORDS)) {
|
|
1318
|
+
this.records.delete(rec.id);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1301
1321
|
emitUpdate(rec) {
|
|
1302
1322
|
this.emit("update", this.snapshot(rec));
|
|
1303
1323
|
}
|
|
@@ -1318,21 +1338,22 @@ var defaultTaskManager = new TaskManager();
|
|
|
1318
1338
|
|
|
1319
1339
|
// src/tools/bash.ts
|
|
1320
1340
|
var MAX_OUTPUT = 3e4;
|
|
1341
|
+
var HALF_OUTPUT = MAX_OUTPUT / 2;
|
|
1321
1342
|
var DEFAULT_TIMEOUT = 120;
|
|
1322
1343
|
var MAX_TIMEOUT = 600;
|
|
1323
1344
|
function findOnPath(exe, exclude) {
|
|
1324
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1345
|
+
for (const dir of (process.env.PATH ?? "").split(path4.delimiter)) {
|
|
1325
1346
|
if (!dir || exclude?.(dir)) {
|
|
1326
1347
|
continue;
|
|
1327
1348
|
}
|
|
1328
|
-
const full =
|
|
1329
|
-
if (
|
|
1349
|
+
const full = path4.join(dir, exe);
|
|
1350
|
+
if (existsSync2(full)) {
|
|
1330
1351
|
return full;
|
|
1331
1352
|
}
|
|
1332
1353
|
}
|
|
1333
1354
|
return null;
|
|
1334
1355
|
}
|
|
1335
|
-
function
|
|
1356
|
+
function resolveShellUncached() {
|
|
1336
1357
|
if (process.platform !== "win32") {
|
|
1337
1358
|
return { shell: "sh", wrap: (c) => ["-c", c], label: "sh" };
|
|
1338
1359
|
}
|
|
@@ -1343,8 +1364,8 @@ function resolveShell() {
|
|
|
1343
1364
|
}
|
|
1344
1365
|
const gitExe = findOnPath("git.exe");
|
|
1345
1366
|
if (gitExe) {
|
|
1346
|
-
const sibling =
|
|
1347
|
-
if (
|
|
1367
|
+
const sibling = path4.join(path4.dirname(path4.dirname(gitExe)), "bin", "bash.exe");
|
|
1368
|
+
if (existsSync2(sibling)) {
|
|
1348
1369
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1349
1370
|
}
|
|
1350
1371
|
}
|
|
@@ -1353,42 +1374,82 @@ function resolveShell() {
|
|
|
1353
1374
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1354
1375
|
"D:\\Git\\bin\\bash.exe"
|
|
1355
1376
|
]) {
|
|
1356
|
-
if (
|
|
1377
|
+
if (existsSync2(candidate)) {
|
|
1357
1378
|
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
1358
1379
|
}
|
|
1359
1380
|
}
|
|
1360
1381
|
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
1361
1382
|
return { shell: comspec, wrap: (c) => ["/d", "/s", "/c", c], label: "cmd" };
|
|
1362
1383
|
}
|
|
1363
|
-
var
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1384
|
+
var cachedSpec = null;
|
|
1385
|
+
function resolveShell() {
|
|
1386
|
+
if (!cachedSpec) {
|
|
1387
|
+
cachedSpec = resolveShellUncached();
|
|
1388
|
+
}
|
|
1389
|
+
return cachedSpec;
|
|
1390
|
+
}
|
|
1391
|
+
var schema = z2.object({
|
|
1392
|
+
command: z2.string().describe("Shell command to execute"),
|
|
1393
|
+
timeout: z2.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
|
|
1394
|
+
description: z2.string().optional().describe("Short description of what the command does"),
|
|
1395
|
+
run_in_background: z2.boolean().optional().describe(
|
|
1368
1396
|
"Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
|
|
1369
1397
|
)
|
|
1370
1398
|
});
|
|
1371
1399
|
function killTree2(child) {
|
|
1372
|
-
if (process.platform === "win32"
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1400
|
+
if (process.platform === "win32") {
|
|
1401
|
+
if (child.pid) {
|
|
1402
|
+
spawn3("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
1403
|
+
windowsHide: true,
|
|
1404
|
+
stdio: "ignore"
|
|
1405
|
+
}).unref();
|
|
1406
|
+
}
|
|
1377
1407
|
return;
|
|
1378
1408
|
}
|
|
1379
|
-
child.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1409
|
+
if (child.pid) {
|
|
1410
|
+
try {
|
|
1411
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1412
|
+
return;
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
child.kill("SIGKILL");
|
|
1417
|
+
}
|
|
1418
|
+
var BoundedOutput = class {
|
|
1419
|
+
head = "";
|
|
1420
|
+
tail = "";
|
|
1421
|
+
dropped = 0;
|
|
1422
|
+
overflow = false;
|
|
1423
|
+
append(text) {
|
|
1424
|
+
if (!this.overflow) {
|
|
1425
|
+
this.head += text;
|
|
1426
|
+
if (this.head.length <= MAX_OUTPUT) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
this.overflow = true;
|
|
1430
|
+
this.tail = this.head.slice(HALF_OUTPUT);
|
|
1431
|
+
this.head = this.head.slice(0, HALF_OUTPUT);
|
|
1432
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
1433
|
+
this.dropped = this.tail.length - HALF_OUTPUT;
|
|
1434
|
+
this.tail = this.tail.slice(-HALF_OUTPUT);
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
this.tail += text;
|
|
1439
|
+
if (this.tail.length > HALF_OUTPUT) {
|
|
1440
|
+
this.dropped += this.tail.length - HALF_OUTPUT;
|
|
1441
|
+
this.tail = this.tail.slice(-HALF_OUTPUT);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
toString() {
|
|
1445
|
+
if (!this.overflow) {
|
|
1446
|
+
return this.head;
|
|
1447
|
+
}
|
|
1448
|
+
return `${this.head}
|
|
1449
|
+
... [${this.dropped} characters truncated] ...
|
|
1450
|
+
${this.tail}`;
|
|
1384
1451
|
}
|
|
1385
|
-
|
|
1386
|
-
const head = s.slice(0, half);
|
|
1387
|
-
const tail = s.slice(-half);
|
|
1388
|
-
return `${head}
|
|
1389
|
-
... [${s.length - MAX_OUTPUT} characters truncated] ...
|
|
1390
|
-
${tail}`;
|
|
1391
|
-
}
|
|
1452
|
+
};
|
|
1392
1453
|
var bashTool = {
|
|
1393
1454
|
name: "bash",
|
|
1394
1455
|
description: "Execute a shell command (Git Bash on Windows when available, otherwise cmd; sh elsewhere). Stdout and stderr are merged. Output is truncated to 30000 characters. Set run_in_background for long-running commands; it returns a task id for task_list/task_output/task_kill.",
|
|
@@ -1413,9 +1474,10 @@ description: ${args.description ?? "(none)"}`
|
|
|
1413
1474
|
return new Promise((resolve) => {
|
|
1414
1475
|
const child = spawn3(spec.shell, spec.wrap(args.command), {
|
|
1415
1476
|
cwd: ctx.cwd,
|
|
1416
|
-
windowsHide: true
|
|
1477
|
+
windowsHide: true,
|
|
1478
|
+
detached: process.platform !== "win32"
|
|
1417
1479
|
});
|
|
1418
|
-
|
|
1480
|
+
const output = new BoundedOutput();
|
|
1419
1481
|
let settled = false;
|
|
1420
1482
|
const finish = (result) => {
|
|
1421
1483
|
if (settled) {
|
|
@@ -1428,36 +1490,32 @@ description: ${args.description ?? "(none)"}`
|
|
|
1428
1490
|
};
|
|
1429
1491
|
const onAbort = () => {
|
|
1430
1492
|
killTree2(child);
|
|
1431
|
-
finish({ content: `${
|
|
1493
|
+
finish({ content: `${output}
|
|
1432
1494
|
Command aborted`, isError: true });
|
|
1433
1495
|
};
|
|
1434
1496
|
const timer = setTimeout(() => {
|
|
1435
1497
|
killTree2(child);
|
|
1436
1498
|
finish({
|
|
1437
|
-
content: `${
|
|
1499
|
+
content: `${output}
|
|
1438
1500
|
Command timed out after ${timeoutSeconds}s`,
|
|
1439
1501
|
isError: true
|
|
1440
1502
|
});
|
|
1441
1503
|
}, timeoutSeconds * 1e3);
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
child.stderr.on("data", (d) => {
|
|
1446
|
-
output += d.toString("utf8");
|
|
1447
|
-
});
|
|
1504
|
+
timer.unref();
|
|
1505
|
+
child.stdout.on("data", (d) => output.append(d.toString("utf8")));
|
|
1506
|
+
child.stderr.on("data", (d) => output.append(d.toString("utf8")));
|
|
1448
1507
|
child.on("error", (err) => {
|
|
1449
1508
|
finish({ content: `Failed to start shell '${spec.label}': ${err.message}`, isError: true });
|
|
1450
1509
|
});
|
|
1451
1510
|
ctx.abortSignal?.addEventListener("abort", onAbort);
|
|
1452
1511
|
child.on("close", (code) => {
|
|
1453
|
-
const trimmed = output.replace(/\s+$/, "");
|
|
1512
|
+
const trimmed = output.toString().replace(/\s+$/, "");
|
|
1454
1513
|
if (code === 0) {
|
|
1455
|
-
finish({ content:
|
|
1514
|
+
finish({ content: trimmed || "(no output)" });
|
|
1456
1515
|
return;
|
|
1457
1516
|
}
|
|
1458
|
-
const body = truncateMiddle(trimmed);
|
|
1459
1517
|
finish({
|
|
1460
|
-
content: `${
|
|
1518
|
+
content: `${trimmed}${trimmed ? "\n" : ""}Exit code: ${code ?? "unknown"}`,
|
|
1461
1519
|
isError: true
|
|
1462
1520
|
});
|
|
1463
1521
|
});
|
|
@@ -1466,14 +1524,14 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1466
1524
|
};
|
|
1467
1525
|
|
|
1468
1526
|
// src/tools/fs/edit.ts
|
|
1469
|
-
import { readFile as
|
|
1470
|
-
import
|
|
1471
|
-
import { z as
|
|
1472
|
-
var schema2 =
|
|
1473
|
-
path:
|
|
1474
|
-
old_string:
|
|
1475
|
-
new_string:
|
|
1476
|
-
replace_all:
|
|
1527
|
+
import { readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1528
|
+
import path5 from "path";
|
|
1529
|
+
import { z as z3 } from "zod";
|
|
1530
|
+
var schema2 = z3.object({
|
|
1531
|
+
path: z3.string().describe("File path, absolute or relative to the working directory"),
|
|
1532
|
+
old_string: z3.string().min(1).describe("Exact text to replace"),
|
|
1533
|
+
new_string: z3.string().describe("Replacement text"),
|
|
1534
|
+
replace_all: z3.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
|
|
1477
1535
|
});
|
|
1478
1536
|
var editFileTool = {
|
|
1479
1537
|
name: "edit_file",
|
|
@@ -1481,10 +1539,12 @@ var editFileTool = {
|
|
|
1481
1539
|
permission: "write",
|
|
1482
1540
|
parameters: schema2,
|
|
1483
1541
|
async execute(args, ctx) {
|
|
1484
|
-
const filePath =
|
|
1542
|
+
const filePath = path5.resolve(ctx.cwd, args.path);
|
|
1485
1543
|
let content;
|
|
1544
|
+
let readMtimeMs;
|
|
1486
1545
|
try {
|
|
1487
|
-
content = await
|
|
1546
|
+
content = await readFile2(filePath, "utf8");
|
|
1547
|
+
readMtimeMs = (await stat2(filePath)).mtimeMs;
|
|
1488
1548
|
} catch (err) {
|
|
1489
1549
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1490
1550
|
}
|
|
@@ -1503,34 +1563,44 @@ var editFileTool = {
|
|
|
1503
1563
|
isError: true
|
|
1504
1564
|
};
|
|
1505
1565
|
}
|
|
1506
|
-
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, args.new_string);
|
|
1566
|
+
const updated = args.replace_all ? content.split(args.old_string).join(args.new_string) : content.replace(args.old_string, () => args.new_string);
|
|
1507
1567
|
try {
|
|
1508
|
-
await
|
|
1568
|
+
const now = await stat2(filePath).catch(() => null);
|
|
1569
|
+
if (!now || now.mtimeMs !== readMtimeMs) {
|
|
1570
|
+
return {
|
|
1571
|
+
content: `Failed to edit ${args.path}: file changed since it was read; re-read and retry`,
|
|
1572
|
+
isError: true
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
await writeFile2(filePath, updated, "utf8");
|
|
1509
1576
|
} catch (err) {
|
|
1510
1577
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1511
1578
|
}
|
|
1579
|
+
const contentTooLarge = Buffer.byteLength(content, "utf8") > MAX_SNAPSHOT_CONTENT_BYTES;
|
|
1512
1580
|
await pushSnapshot({
|
|
1513
1581
|
id: nextSnapshotId(),
|
|
1514
1582
|
path: filePath,
|
|
1515
1583
|
existed: true,
|
|
1516
|
-
content,
|
|
1584
|
+
content: contentTooLarge ? null : content,
|
|
1585
|
+
contentTooLarge: contentTooLarge || void 0,
|
|
1517
1586
|
toolName: "edit_file",
|
|
1518
1587
|
timestamp: Date.now(),
|
|
1519
|
-
turn: currentTurnSeq(),
|
|
1520
|
-
messageIndex: currentTurnMessageIndex()
|
|
1588
|
+
turn: ctx.snapshotContext?.turn ?? currentTurnSeq(),
|
|
1589
|
+
messageIndex: ctx.snapshotContext?.messageIndex ?? currentTurnMessageIndex(),
|
|
1590
|
+
owner: ctx.snapshotContext?.owner ?? "root"
|
|
1521
1591
|
});
|
|
1522
1592
|
return { content: `Edited ${args.path}: ${count} replacement${count > 1 ? "s" : ""}` };
|
|
1523
1593
|
}
|
|
1524
1594
|
};
|
|
1525
1595
|
|
|
1526
1596
|
// src/tools/fs/glob.ts
|
|
1527
|
-
import
|
|
1528
|
-
import { z as
|
|
1597
|
+
import path7 from "path";
|
|
1598
|
+
import { z as z4 } from "zod";
|
|
1529
1599
|
|
|
1530
1600
|
// src/tools/fs/util.ts
|
|
1531
|
-
import { readFileSync
|
|
1532
|
-
import { readdir, stat } from "fs/promises";
|
|
1533
|
-
import
|
|
1601
|
+
import { readFileSync, statSync } from "fs";
|
|
1602
|
+
import { readdir, stat as stat3 } from "fs/promises";
|
|
1603
|
+
import path6 from "path";
|
|
1534
1604
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1535
1605
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1536
1606
|
const out = [];
|
|
@@ -1540,7 +1610,7 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1540
1610
|
return;
|
|
1541
1611
|
}
|
|
1542
1612
|
for (const entry of entries) {
|
|
1543
|
-
const abs =
|
|
1613
|
+
const abs = path6.join(dir, entry.name);
|
|
1544
1614
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1545
1615
|
if (entry.isDirectory()) {
|
|
1546
1616
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1551,8 +1621,12 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1551
1621
|
if (options.ignore?.(abs, false)) {
|
|
1552
1622
|
continue;
|
|
1553
1623
|
}
|
|
1624
|
+
if (options.withMtime === false) {
|
|
1625
|
+
out.push({ abs, rel, mtimeMs: 0 });
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1554
1628
|
try {
|
|
1555
|
-
const st = await
|
|
1629
|
+
const st = await stat3(abs);
|
|
1556
1630
|
out.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
1557
1631
|
} catch {
|
|
1558
1632
|
}
|
|
@@ -1563,32 +1637,107 @@ async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
|
1563
1637
|
return out;
|
|
1564
1638
|
}
|
|
1565
1639
|
var REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
1640
|
+
function escapeRe(c) {
|
|
1641
|
+
return c.replace(REGEX_SPECIALS, "\\$&");
|
|
1642
|
+
}
|
|
1566
1643
|
function globToRegExp(pattern) {
|
|
1567
|
-
let re = "";
|
|
1568
1644
|
let i = 0;
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1645
|
+
function parseClass() {
|
|
1646
|
+
let j = i + 1;
|
|
1647
|
+
let negate = false;
|
|
1648
|
+
if (pattern.charAt(j) === "!") {
|
|
1649
|
+
negate = true;
|
|
1650
|
+
j += 1;
|
|
1651
|
+
}
|
|
1652
|
+
let body = "";
|
|
1653
|
+
if (pattern.charAt(j) === "]") {
|
|
1654
|
+
body += "\\]";
|
|
1655
|
+
j += 1;
|
|
1656
|
+
}
|
|
1657
|
+
while (j < pattern.length && pattern.charAt(j) !== "]") {
|
|
1658
|
+
const ch = pattern.charAt(j);
|
|
1659
|
+
body += ch === "\\" ? "\\\\" : ch;
|
|
1660
|
+
j += 1;
|
|
1661
|
+
}
|
|
1662
|
+
if (j < pattern.length) {
|
|
1663
|
+
i = j + 1;
|
|
1664
|
+
if (body.startsWith("^")) body = `\\${body}`;
|
|
1665
|
+
return `[${negate ? "^" : ""}${body}]`;
|
|
1666
|
+
}
|
|
1667
|
+
i += 1;
|
|
1668
|
+
return "\\[";
|
|
1669
|
+
}
|
|
1670
|
+
function parseSegment(depth) {
|
|
1671
|
+
let re2 = "";
|
|
1672
|
+
while (i < pattern.length) {
|
|
1673
|
+
const c = pattern.charAt(i);
|
|
1674
|
+
if (depth > 0 && (c === "," || c === "}")) {
|
|
1675
|
+
return { re: re2, end: c };
|
|
1676
|
+
}
|
|
1677
|
+
if (c === "*") {
|
|
1678
|
+
if (pattern.charAt(i + 1) === "*") {
|
|
1679
|
+
i += 2;
|
|
1680
|
+
if (pattern.charAt(i) === "/") {
|
|
1681
|
+
i += 1;
|
|
1682
|
+
re2 += "(?:[^/]+/)*";
|
|
1683
|
+
while (pattern.startsWith("**/", i)) {
|
|
1684
|
+
i += 3;
|
|
1685
|
+
}
|
|
1686
|
+
} else {
|
|
1687
|
+
re2 += ".*";
|
|
1688
|
+
}
|
|
1577
1689
|
} else {
|
|
1578
|
-
|
|
1690
|
+
re2 += "[^/]*";
|
|
1691
|
+
i += 1;
|
|
1579
1692
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (c === "?") {
|
|
1696
|
+
re2 += "[^/]";
|
|
1582
1697
|
i += 1;
|
|
1698
|
+
continue;
|
|
1583
1699
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1700
|
+
if (c === "[") {
|
|
1701
|
+
re2 += parseClass();
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (c === "{" && depth < 2) {
|
|
1705
|
+
const close = braceEnd(i);
|
|
1706
|
+
if (close === -1) {
|
|
1707
|
+
re2 += "\\{";
|
|
1708
|
+
i += 1;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
i += 1;
|
|
1712
|
+
const alts = [];
|
|
1713
|
+
for (; ; ) {
|
|
1714
|
+
const part = parseSegment(depth + 1);
|
|
1715
|
+
alts.push(part.re);
|
|
1716
|
+
if (part.end !== ",") break;
|
|
1717
|
+
i += 1;
|
|
1718
|
+
}
|
|
1719
|
+
i += 1;
|
|
1720
|
+
re2 += `(?:${alts.join("|")})`;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
re2 += escapeRe(c);
|
|
1589
1724
|
i += 1;
|
|
1590
1725
|
}
|
|
1726
|
+
return { re: re2, end: "" };
|
|
1727
|
+
}
|
|
1728
|
+
function braceEnd(from) {
|
|
1729
|
+
let depth = 0;
|
|
1730
|
+
for (let j = from; j < pattern.length; j++) {
|
|
1731
|
+
const ch = pattern.charAt(j);
|
|
1732
|
+
if (ch === "{") depth += 1;
|
|
1733
|
+
else if (ch === "}") {
|
|
1734
|
+
depth -= 1;
|
|
1735
|
+
if (depth === 0) return j;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return -1;
|
|
1591
1739
|
}
|
|
1740
|
+
const { re } = parseSegment(0);
|
|
1592
1741
|
return new RegExp(`^${re}$`);
|
|
1593
1742
|
}
|
|
1594
1743
|
function matchesGlob(pattern, relPath) {
|
|
@@ -1620,10 +1769,10 @@ function parseIgnorePatterns(body) {
|
|
|
1620
1769
|
return out;
|
|
1621
1770
|
}
|
|
1622
1771
|
function loadIgnorePatterns(dir) {
|
|
1623
|
-
const file =
|
|
1772
|
+
const file = path6.join(dir, IGNORE_FILE);
|
|
1624
1773
|
let mtimeMs;
|
|
1625
1774
|
try {
|
|
1626
|
-
mtimeMs =
|
|
1775
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
1627
1776
|
} catch {
|
|
1628
1777
|
return [];
|
|
1629
1778
|
}
|
|
@@ -1633,7 +1782,7 @@ function loadIgnorePatterns(dir) {
|
|
|
1633
1782
|
}
|
|
1634
1783
|
let patterns = [];
|
|
1635
1784
|
try {
|
|
1636
|
-
patterns = parseIgnorePatterns(
|
|
1785
|
+
patterns = parseIgnorePatterns(readFileSync(file, "utf8"));
|
|
1637
1786
|
} catch {
|
|
1638
1787
|
patterns = [];
|
|
1639
1788
|
}
|
|
@@ -1645,9 +1794,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1645
1794
|
if (patterns.length === 0) {
|
|
1646
1795
|
return void 0;
|
|
1647
1796
|
}
|
|
1648
|
-
const root =
|
|
1797
|
+
const root = path6.resolve(cwd);
|
|
1649
1798
|
return (absPath, isDir) => {
|
|
1650
|
-
const rel =
|
|
1799
|
+
const rel = path6.relative(root, absPath).split(path6.sep).join("/");
|
|
1651
1800
|
if (!rel || rel.startsWith("..")) {
|
|
1652
1801
|
return false;
|
|
1653
1802
|
}
|
|
@@ -1662,27 +1811,26 @@ function createIgnorePredicate(cwd) {
|
|
|
1662
1811
|
return false;
|
|
1663
1812
|
};
|
|
1664
1813
|
}
|
|
1665
|
-
function isSensitivePath2(filePath) {
|
|
1666
|
-
const base = path8.basename(filePath);
|
|
1667
|
-
if (base === ".env.example" || base === ".env.sample" || base === ".env.template") {
|
|
1668
|
-
return false;
|
|
1669
|
-
}
|
|
1670
|
-
if (base === ".env" || base.startsWith(".env.")) {
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
if (base === "id_rsa" || base.endsWith(".pem")) {
|
|
1674
|
-
return true;
|
|
1675
|
-
}
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
1814
|
|
|
1679
1815
|
// src/tools/fs/glob.ts
|
|
1680
1816
|
var MAX_RESULTS = 100;
|
|
1681
|
-
var
|
|
1682
|
-
|
|
1683
|
-
|
|
1817
|
+
var GLOB_MAGIC = /[*?[{]/;
|
|
1818
|
+
function literalDirPrefix(pattern) {
|
|
1819
|
+
const segments = pattern.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
1820
|
+
const out = [];
|
|
1821
|
+
for (const segment of segments) {
|
|
1822
|
+
if (segment === "" || GLOB_MAGIC.test(segment)) {
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
out.push(segment);
|
|
1826
|
+
}
|
|
1827
|
+
return out;
|
|
1828
|
+
}
|
|
1829
|
+
var schema3 = z4.object({
|
|
1830
|
+
pattern: z4.string().describe(
|
|
1831
|
+
"Glob pattern, e.g. 'src/**/*.ts'. Supports **, *, ?, {a,b} braces and [abc] character classes. A bare pattern like '*.ts' matches basenames recursively."
|
|
1684
1832
|
),
|
|
1685
|
-
path:
|
|
1833
|
+
path: z4.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1686
1834
|
});
|
|
1687
1835
|
var globTool = {
|
|
1688
1836
|
name: "glob",
|
|
@@ -1690,47 +1838,72 @@ var globTool = {
|
|
|
1690
1838
|
permission: "read",
|
|
1691
1839
|
parameters: schema3,
|
|
1692
1840
|
async execute(args, ctx) {
|
|
1693
|
-
const root =
|
|
1694
|
-
const
|
|
1695
|
-
const
|
|
1841
|
+
const root = path7.resolve(ctx.cwd, args.path ?? ".");
|
|
1842
|
+
const prefixSegments = literalDirPrefix(args.pattern);
|
|
1843
|
+
const walkRoot = prefixSegments.length > 0 ? path7.join(root, ...prefixSegments) : root;
|
|
1844
|
+
const prefix = prefixSegments.length > 0 ? `${prefixSegments.join("/")}/` : "";
|
|
1845
|
+
const files = await walkFiles(walkRoot, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1846
|
+
const matched = files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
1696
1847
|
if (matched.length === 0) {
|
|
1697
1848
|
return { content: `No files matched pattern: ${args.pattern}` };
|
|
1698
1849
|
}
|
|
1699
|
-
return { content: matched.map((f) => f.rel).join("\n") };
|
|
1850
|
+
return { content: matched.map((f) => `${prefix}${f.rel}`).join("\n") };
|
|
1700
1851
|
}
|
|
1701
1852
|
};
|
|
1702
1853
|
|
|
1703
1854
|
// src/tools/fs/grep.ts
|
|
1704
|
-
import { readFile as
|
|
1705
|
-
import
|
|
1706
|
-
import { z as
|
|
1855
|
+
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
1856
|
+
import path8 from "path";
|
|
1857
|
+
import { z as z5 } from "zod";
|
|
1707
1858
|
var MAX_MATCHES = 250;
|
|
1708
|
-
var
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1859
|
+
var MAX_LINE_CHARS = 500;
|
|
1860
|
+
var MAX_PATTERN_CHARS = 200;
|
|
1861
|
+
var MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
1862
|
+
function isTooComplexPattern(pattern) {
|
|
1863
|
+
return pattern.length > MAX_PATTERN_CHARS || /\)[+*]/.test(pattern) || /\)\{\d+(,\d*)?\}/.test(pattern);
|
|
1864
|
+
}
|
|
1865
|
+
var schema4 = z5.object({
|
|
1866
|
+
pattern: z5.string().describe("Regular expression to search for"),
|
|
1867
|
+
path: z5.string().optional().describe("File or directory to search, defaults to the working directory"),
|
|
1868
|
+
glob: z5.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
|
|
1869
|
+
ignoreCase: z5.boolean().optional().describe("Case-insensitive matching")
|
|
1713
1870
|
});
|
|
1714
1871
|
var grepTool = {
|
|
1715
1872
|
name: "grep",
|
|
1716
|
-
description: "Search file contents with a regular expression. Outputs 'file:line:content', up to 250 matches. Skips binary files, node_modules and .git.",
|
|
1873
|
+
description: "Search file contents with a regular expression. Outputs 'file:line:content', up to 250 matches. Skips binary files, files over 5MB, sensitive files, node_modules and .git.",
|
|
1717
1874
|
permission: "read",
|
|
1718
1875
|
parameters: schema4,
|
|
1719
1876
|
async execute(args, ctx) {
|
|
1877
|
+
if (isTooComplexPattern(args.pattern)) {
|
|
1878
|
+
return {
|
|
1879
|
+
content: `pattern too complex: keep it under ${MAX_PATTERN_CHARS} characters and avoid quantified groups like (a+)+`,
|
|
1880
|
+
isError: true
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1720
1883
|
let re;
|
|
1721
1884
|
try {
|
|
1722
1885
|
re = new RegExp(args.pattern, args.ignoreCase ? "i" : "");
|
|
1723
1886
|
} catch (err) {
|
|
1724
1887
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1725
1888
|
}
|
|
1726
|
-
const root =
|
|
1889
|
+
const root = path8.resolve(ctx.cwd, args.path ?? ".");
|
|
1727
1890
|
let files;
|
|
1728
1891
|
try {
|
|
1729
|
-
const st = await
|
|
1892
|
+
const st = await stat4(root);
|
|
1730
1893
|
if (st.isFile()) {
|
|
1731
|
-
|
|
1894
|
+
if (isSensitivePath(root)) {
|
|
1895
|
+
return {
|
|
1896
|
+
content: `Refused to grep sensitive file: ${args.path ?? "."}`,
|
|
1897
|
+
isError: true
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
files = [{ abs: root, rel: path8.basename(root), mtimeMs: st.mtimeMs }];
|
|
1732
1901
|
} else {
|
|
1733
|
-
|
|
1902
|
+
const walked = await walkFiles(root, void 0, {
|
|
1903
|
+
ignore: createIgnorePredicate(ctx.cwd),
|
|
1904
|
+
withMtime: false
|
|
1905
|
+
});
|
|
1906
|
+
files = walked.filter((f) => !isSensitivePath(f.abs));
|
|
1734
1907
|
}
|
|
1735
1908
|
} catch {
|
|
1736
1909
|
return { content: `Path not found: ${args.path ?? "."}`, isError: true };
|
|
@@ -1741,10 +1914,19 @@ var grepTool = {
|
|
|
1741
1914
|
}
|
|
1742
1915
|
const out = [];
|
|
1743
1916
|
let truncated = false;
|
|
1917
|
+
let skippedLarge = 0;
|
|
1744
1918
|
outer: for (const file of files) {
|
|
1919
|
+
const st = await stat4(file.abs).catch(() => null);
|
|
1920
|
+
if (!st) {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
1924
|
+
skippedLarge += 1;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1745
1927
|
let buf;
|
|
1746
1928
|
try {
|
|
1747
|
-
buf = await
|
|
1929
|
+
buf = await readFile3(file.abs);
|
|
1748
1930
|
} catch {
|
|
1749
1931
|
continue;
|
|
1750
1932
|
}
|
|
@@ -1755,7 +1937,8 @@ var grepTool = {
|
|
|
1755
1937
|
for (let i = 0; i < lines.length; i++) {
|
|
1756
1938
|
const line = lines[i] ?? "";
|
|
1757
1939
|
if (re.test(line)) {
|
|
1758
|
-
|
|
1940
|
+
const shown = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
1941
|
+
out.push(`${file.rel}:${i + 1}:${shown}`);
|
|
1759
1942
|
if (out.length >= MAX_MATCHES) {
|
|
1760
1943
|
truncated = true;
|
|
1761
1944
|
break outer;
|
|
@@ -1763,26 +1946,55 @@ var grepTool = {
|
|
|
1763
1946
|
}
|
|
1764
1947
|
}
|
|
1765
1948
|
}
|
|
1949
|
+
const skippedNote = skippedLarge > 0 ? `(skipped ${skippedLarge} file(s) larger than 5MB)` : null;
|
|
1766
1950
|
if (out.length === 0) {
|
|
1767
|
-
return {
|
|
1951
|
+
return {
|
|
1952
|
+
content: `No matches for pattern: ${args.pattern}${skippedNote ? ` ${skippedNote}` : ""}`
|
|
1953
|
+
};
|
|
1768
1954
|
}
|
|
1769
1955
|
if (truncated) {
|
|
1770
1956
|
out.push(`... (truncated: more than ${MAX_MATCHES} matches)`);
|
|
1771
1957
|
}
|
|
1958
|
+
if (skippedNote) {
|
|
1959
|
+
out.push(`... ${skippedNote}`);
|
|
1960
|
+
}
|
|
1772
1961
|
return { content: out.join("\n") };
|
|
1773
1962
|
}
|
|
1774
1963
|
};
|
|
1775
1964
|
|
|
1776
1965
|
// src/tools/fs/read.ts
|
|
1777
|
-
import {
|
|
1778
|
-
import
|
|
1779
|
-
import { z as
|
|
1966
|
+
import { open, stat as stat5 } from "fs/promises";
|
|
1967
|
+
import path9 from "path";
|
|
1968
|
+
import { z as z6 } from "zod";
|
|
1780
1969
|
var DEFAULT_LIMIT = 2e3;
|
|
1781
1970
|
var MAX_CHARS = 100 * 1024;
|
|
1782
|
-
var
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1971
|
+
var MAX_READ_BYTES = 1024 * 1024;
|
|
1972
|
+
var BINARY_PROBE_BYTES = 8192;
|
|
1973
|
+
function decodeUtf16be(buf) {
|
|
1974
|
+
const len = buf.length - buf.length % 2;
|
|
1975
|
+
const swapped = Buffer.allocUnsafe(len);
|
|
1976
|
+
for (let k = 0; k < len; k += 2) {
|
|
1977
|
+
swapped[k] = buf[k + 1] ?? 0;
|
|
1978
|
+
swapped[k + 1] = buf[k] ?? 0;
|
|
1979
|
+
}
|
|
1980
|
+
return swapped.toString("utf16le");
|
|
1981
|
+
}
|
|
1982
|
+
function decodeTextBuffer(buf) {
|
|
1983
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
1984
|
+
return { text: buf.subarray(2).toString("utf16le") };
|
|
1985
|
+
}
|
|
1986
|
+
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
1987
|
+
return { text: decodeUtf16be(buf.subarray(2)) };
|
|
1988
|
+
}
|
|
1989
|
+
if (buf.subarray(0, BINARY_PROBE_BYTES).includes(0)) {
|
|
1990
|
+
return { binary: true };
|
|
1991
|
+
}
|
|
1992
|
+
return { text: buf.toString("utf8") };
|
|
1993
|
+
}
|
|
1994
|
+
var schema5 = z6.object({
|
|
1995
|
+
path: z6.string().describe("File path, absolute or relative to the working directory"),
|
|
1996
|
+
offset: z6.number().int().positive().optional().describe("1-based line number to start reading from"),
|
|
1997
|
+
limit: z6.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
|
|
1786
1998
|
});
|
|
1787
1999
|
var readFileTool = {
|
|
1788
2000
|
name: "read_file",
|
|
@@ -1790,30 +2002,48 @@ var readFileTool = {
|
|
|
1790
2002
|
permission: "read",
|
|
1791
2003
|
parameters: schema5,
|
|
1792
2004
|
async execute(args, ctx) {
|
|
1793
|
-
const filePath =
|
|
1794
|
-
if (
|
|
2005
|
+
const filePath = path9.resolve(ctx.cwd, args.path);
|
|
2006
|
+
if (isSensitivePath(filePath)) {
|
|
1795
2007
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
1796
2008
|
}
|
|
1797
|
-
const st = await
|
|
2009
|
+
const st = await stat5(filePath).catch(() => null);
|
|
1798
2010
|
if (!st) {
|
|
1799
2011
|
return { content: `File not found: ${args.path}`, isError: true };
|
|
1800
2012
|
}
|
|
1801
2013
|
if (st.isDirectory()) {
|
|
1802
2014
|
return { content: `Path is a directory, not a file: ${args.path}`, isError: true };
|
|
1803
2015
|
}
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
2016
|
+
const bytesToRead = Math.min(st.size, MAX_READ_BYTES);
|
|
2017
|
+
const fileCapped = st.size > MAX_READ_BYTES;
|
|
2018
|
+
const handle = await open(filePath, "r").catch(() => null);
|
|
2019
|
+
if (!handle) {
|
|
2020
|
+
return { content: `Failed to read ${args.path}`, isError: true };
|
|
2021
|
+
}
|
|
2022
|
+
let buf;
|
|
2023
|
+
try {
|
|
2024
|
+
const { bytesRead, buffer } = await handle.read(Buffer.alloc(bytesToRead), 0, bytesToRead, 0);
|
|
2025
|
+
buf = buffer.subarray(0, bytesRead);
|
|
2026
|
+
} finally {
|
|
2027
|
+
await handle.close().catch(() => {
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
const decoded = decodeTextBuffer(buf);
|
|
2031
|
+
if ("binary" in decoded) {
|
|
2032
|
+
return { content: `Cannot read binary file: ${args.path}`, isError: true };
|
|
2033
|
+
}
|
|
2034
|
+
const lines = decoded.text.split("\n");
|
|
2035
|
+
const totalDesc = fileCapped ? `at least ${lines.length}` : `${lines.length}`;
|
|
1806
2036
|
const start = args.offset ?? 1;
|
|
1807
2037
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
2038
|
+
if (start > lines.length) {
|
|
2039
|
+
return { content: `(offset ${start} beyond end of file; file has ${totalDesc} lines)` };
|
|
2040
|
+
}
|
|
1808
2041
|
const end = Math.min(lines.length, start - 1 + limit);
|
|
1809
2042
|
const out = [];
|
|
1810
2043
|
let size = 0;
|
|
1811
2044
|
let truncatedBySize = false;
|
|
1812
2045
|
for (let i = start - 1; i < end; i++) {
|
|
1813
|
-
const line = lines[i];
|
|
1814
|
-
if (line === void 0) {
|
|
1815
|
-
break;
|
|
1816
|
-
}
|
|
2046
|
+
const line = (lines[i] ?? "").replace(/\r$/, "");
|
|
1817
2047
|
const numbered = `${i + 1} ${line}`;
|
|
1818
2048
|
if (size + numbered.length > MAX_CHARS) {
|
|
1819
2049
|
truncatedBySize = true;
|
|
@@ -1825,22 +2055,22 @@ var readFileTool = {
|
|
|
1825
2055
|
const lastShown = start - 1 + out.length;
|
|
1826
2056
|
if (truncatedBySize) {
|
|
1827
2057
|
out.push(
|
|
1828
|
-
`... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${
|
|
2058
|
+
`... (truncated: output exceeds 100KB, showing lines ${start}-${lastShown} of ${totalDesc})`
|
|
1829
2059
|
);
|
|
1830
|
-
} else if (end < lines.length) {
|
|
1831
|
-
out.push(`... (truncated: showing lines ${start}-${end} of ${
|
|
2060
|
+
} else if (end < lines.length || fileCapped) {
|
|
2061
|
+
out.push(`... (truncated: showing lines ${start}-${end} of ${totalDesc})`);
|
|
1832
2062
|
}
|
|
1833
2063
|
return { content: out.join("\n") };
|
|
1834
2064
|
}
|
|
1835
2065
|
};
|
|
1836
2066
|
|
|
1837
2067
|
// src/tools/fs/write.ts
|
|
1838
|
-
import { mkdir as mkdir3, writeFile as
|
|
1839
|
-
import
|
|
1840
|
-
import { z as
|
|
1841
|
-
var schema6 =
|
|
1842
|
-
path:
|
|
1843
|
-
content:
|
|
2068
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
2069
|
+
import path10 from "path";
|
|
2070
|
+
import { z as z7 } from "zod";
|
|
2071
|
+
var schema6 = z7.object({
|
|
2072
|
+
path: z7.string().describe("File path, absolute or relative to the working directory"),
|
|
2073
|
+
content: z7.string().describe("Full content to write to the file")
|
|
1844
2074
|
});
|
|
1845
2075
|
var writeFileTool = {
|
|
1846
2076
|
name: "write_file",
|
|
@@ -1848,11 +2078,11 @@ var writeFileTool = {
|
|
|
1848
2078
|
permission: "write",
|
|
1849
2079
|
parameters: schema6,
|
|
1850
2080
|
async execute(args, ctx) {
|
|
1851
|
-
const filePath =
|
|
1852
|
-
const snapshot = await captureSnapshot(filePath, "write_file");
|
|
2081
|
+
const filePath = path10.resolve(ctx.cwd, args.path);
|
|
2082
|
+
const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
|
|
1853
2083
|
try {
|
|
1854
|
-
await mkdir3(
|
|
1855
|
-
await
|
|
2084
|
+
await mkdir3(path10.dirname(filePath), { recursive: true });
|
|
2085
|
+
await writeFile3(filePath, args.content, "utf8");
|
|
1856
2086
|
} catch (err) {
|
|
1857
2087
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1858
2088
|
}
|
|
@@ -1862,7 +2092,132 @@ var writeFileTool = {
|
|
|
1862
2092
|
};
|
|
1863
2093
|
|
|
1864
2094
|
// src/tools/tasks.ts
|
|
1865
|
-
import { z as
|
|
2095
|
+
import { z as z8 } from "zod";
|
|
2096
|
+
|
|
2097
|
+
// src/agent/agent-tasks.ts
|
|
2098
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
2099
|
+
var MAX_FINISHED_RECORDS2 = 50;
|
|
2100
|
+
var AgentTaskManager = class extends EventEmitter2 {
|
|
2101
|
+
records = /* @__PURE__ */ new Map();
|
|
2102
|
+
counter = 0;
|
|
2103
|
+
start(run, meta) {
|
|
2104
|
+
const id = `agent-${++this.counter}`;
|
|
2105
|
+
const rec = {
|
|
2106
|
+
id,
|
|
2107
|
+
prompt: meta.prompt,
|
|
2108
|
+
description: meta.description,
|
|
2109
|
+
status: "running",
|
|
2110
|
+
startedAt: Date.now(),
|
|
2111
|
+
result: "",
|
|
2112
|
+
controller: new AbortController(),
|
|
2113
|
+
notified: false
|
|
2114
|
+
};
|
|
2115
|
+
this.records.set(id, rec);
|
|
2116
|
+
void run(rec.controller.signal).then((result) => this.finish(rec, "completed", result)).catch(
|
|
2117
|
+
(error) => this.finish(rec, "failed", error instanceof Error ? error.message : String(error))
|
|
2118
|
+
);
|
|
2119
|
+
this.emitUpdate(rec);
|
|
2120
|
+
return this.snapshot(rec);
|
|
2121
|
+
}
|
|
2122
|
+
get(id) {
|
|
2123
|
+
const rec = this.records.get(id);
|
|
2124
|
+
return rec ? this.snapshot(rec) : void 0;
|
|
2125
|
+
}
|
|
2126
|
+
list() {
|
|
2127
|
+
return [...this.records.values()].map((rec) => this.snapshot(rec));
|
|
2128
|
+
}
|
|
2129
|
+
runningCount() {
|
|
2130
|
+
let count = 0;
|
|
2131
|
+
for (const rec of this.records.values()) {
|
|
2132
|
+
if (rec.status === "running") count += 1;
|
|
2133
|
+
}
|
|
2134
|
+
return count;
|
|
2135
|
+
}
|
|
2136
|
+
kill(id) {
|
|
2137
|
+
const rec = this.records.get(id);
|
|
2138
|
+
if (!rec || rec.status !== "running") return void 0;
|
|
2139
|
+
rec.controller.abort();
|
|
2140
|
+
this.finish(rec, "stopped", rec.result || "Stopped by user.");
|
|
2141
|
+
return this.snapshot(rec);
|
|
2142
|
+
}
|
|
2143
|
+
cleanup() {
|
|
2144
|
+
const killed = [];
|
|
2145
|
+
for (const rec of this.records.values()) {
|
|
2146
|
+
if (rec.status === "running") {
|
|
2147
|
+
rec.controller.abort();
|
|
2148
|
+
this.finish(rec, "stopped", rec.result || "Stopped on shutdown.");
|
|
2149
|
+
killed.push(this.snapshot(rec));
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return killed;
|
|
2153
|
+
}
|
|
2154
|
+
// Finished tasks whose result has not yet been handed to the parent loop.
|
|
2155
|
+
drainNotifications() {
|
|
2156
|
+
const drained = [];
|
|
2157
|
+
for (const rec of this.records.values()) {
|
|
2158
|
+
if (rec.status !== "running" && !rec.notified) {
|
|
2159
|
+
rec.notified = true;
|
|
2160
|
+
drained.push(this.snapshot(rec));
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
this.pruneFinished();
|
|
2164
|
+
return drained;
|
|
2165
|
+
}
|
|
2166
|
+
// Map iteration is insertion-ordered, so the first finished records met are
|
|
2167
|
+
// the oldest. Never drop an un-notified record: the parent has not seen its
|
|
2168
|
+
// result yet. Running records are untouched.
|
|
2169
|
+
pruneFinished() {
|
|
2170
|
+
let finished = 0;
|
|
2171
|
+
for (const rec of this.records.values()) {
|
|
2172
|
+
if (rec.status !== "running") finished += 1;
|
|
2173
|
+
}
|
|
2174
|
+
let excess = finished - MAX_FINISHED_RECORDS2;
|
|
2175
|
+
if (excess <= 0) return;
|
|
2176
|
+
for (const rec of this.records.values()) {
|
|
2177
|
+
if (excess <= 0) return;
|
|
2178
|
+
if (rec.status !== "running" && rec.notified) {
|
|
2179
|
+
this.records.delete(rec.id);
|
|
2180
|
+
excess -= 1;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
finish(rec, status, result) {
|
|
2185
|
+
if (rec.status !== "running") return;
|
|
2186
|
+
rec.status = status;
|
|
2187
|
+
rec.result = result;
|
|
2188
|
+
rec.endedAt = Date.now();
|
|
2189
|
+
this.emitUpdate(rec);
|
|
2190
|
+
}
|
|
2191
|
+
emitUpdate(rec) {
|
|
2192
|
+
this.emit("update", this.snapshot(rec));
|
|
2193
|
+
}
|
|
2194
|
+
snapshot(rec) {
|
|
2195
|
+
return {
|
|
2196
|
+
id: rec.id,
|
|
2197
|
+
prompt: rec.prompt,
|
|
2198
|
+
description: rec.description,
|
|
2199
|
+
status: rec.status,
|
|
2200
|
+
startedAt: rec.startedAt,
|
|
2201
|
+
endedAt: rec.endedAt,
|
|
2202
|
+
result: rec.result
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
var defaultAgentTasks = new AgentTaskManager();
|
|
2207
|
+
function formatAgentTaskUpdate(task) {
|
|
2208
|
+
const label = task.description ? `: ${task.description}` : "";
|
|
2209
|
+
if (task.status === "running") {
|
|
2210
|
+
return `Background subagent ${task.id} started${label}`;
|
|
2211
|
+
}
|
|
2212
|
+
let text = `Background subagent ${task.id} ${task.status}${label}`;
|
|
2213
|
+
if (task.status === "failed") {
|
|
2214
|
+
const reason = (task.result.split("\n")[0] ?? "").replace(/^Subagent failed:\s*/, "");
|
|
2215
|
+
if (reason) {
|
|
2216
|
+
text += ` \u2014 ${reason.length > 140 ? `${reason.slice(0, 140)}\u2026` : reason}`;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return text;
|
|
2220
|
+
}
|
|
1866
2221
|
|
|
1867
2222
|
// src/tasks/format.ts
|
|
1868
2223
|
function formatDuration(startedAt, endedAt) {
|
|
@@ -1908,9 +2263,14 @@ function formatTaskFinished(task) {
|
|
|
1908
2263
|
}
|
|
1909
2264
|
|
|
1910
2265
|
// src/tools/tasks.ts
|
|
1911
|
-
var idSchema =
|
|
1912
|
-
id:
|
|
2266
|
+
var idSchema = z8.object({
|
|
2267
|
+
id: z8.string().describe("Task id, e.g. task-1 or agent-1")
|
|
1913
2268
|
});
|
|
2269
|
+
function truncateOutput(s) {
|
|
2270
|
+
if (s.length <= MAX_OUTPUT) return s;
|
|
2271
|
+
return `[... earlier output truncated; showing the last ${MAX_OUTPUT} characters ...]
|
|
2272
|
+
${s.slice(-MAX_OUTPUT)}`;
|
|
2273
|
+
}
|
|
1914
2274
|
function formatAgentTaskLine(task) {
|
|
1915
2275
|
const duration = formatDuration(task.startedAt, task.endedAt);
|
|
1916
2276
|
const label = task.description ?? task.prompt.slice(0, 60);
|
|
@@ -1921,7 +2281,7 @@ var taskListTool = {
|
|
|
1921
2281
|
name: "task_list",
|
|
1922
2282
|
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
1923
2283
|
permission: "read",
|
|
1924
|
-
parameters:
|
|
2284
|
+
parameters: z8.object({}),
|
|
1925
2285
|
execute() {
|
|
1926
2286
|
const shell = formatTaskList(defaultTaskManager.list());
|
|
1927
2287
|
const agents = defaultAgentTasks.list();
|
|
@@ -1939,13 +2299,13 @@ var taskOutputTool = {
|
|
|
1939
2299
|
execute(args) {
|
|
1940
2300
|
const shell = defaultTaskManager.get(args.id);
|
|
1941
2301
|
if (shell) {
|
|
1942
|
-
const body = shell.output.replace(/\s+$/, "") || "(no output yet)";
|
|
2302
|
+
const body = truncateOutput(shell.output.replace(/\s+$/, "") || "(no output yet)");
|
|
1943
2303
|
return Promise.resolve({ content: `${formatTaskLine(shell)}
|
|
1944
2304
|
${body}` });
|
|
1945
2305
|
}
|
|
1946
2306
|
const agent = defaultAgentTasks.get(args.id);
|
|
1947
2307
|
if (agent) {
|
|
1948
|
-
const body = agent.status === "running" ? "(still running)" : agent.result.replace(/\s+$/, "") || "(no report)";
|
|
2308
|
+
const body = agent.status === "running" ? "(still running)" : truncateOutput(agent.result.replace(/\s+$/, "") || "(no report)");
|
|
1949
2309
|
return Promise.resolve({ content: `${formatAgentTaskLine(agent)}
|
|
1950
2310
|
${body}` });
|
|
1951
2311
|
}
|
|
@@ -1979,6 +2339,133 @@ var taskKillTool = {
|
|
|
1979
2339
|
}
|
|
1980
2340
|
};
|
|
1981
2341
|
|
|
2342
|
+
// src/tools/todo.ts
|
|
2343
|
+
import { mkdir as mkdir4, readFile as readFile4, rename, writeFile as writeFile4 } from "fs/promises";
|
|
2344
|
+
import path11 from "path";
|
|
2345
|
+
import { z as z9 } from "zod";
|
|
2346
|
+
var todoItemSchema = z9.object({
|
|
2347
|
+
id: z9.number().int(),
|
|
2348
|
+
title: z9.string(),
|
|
2349
|
+
status: z9.enum(["pending", "in_progress", "done"])
|
|
2350
|
+
});
|
|
2351
|
+
var writeSchema = z9.object({
|
|
2352
|
+
todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
2353
|
+
});
|
|
2354
|
+
var readSchema = z9.object({});
|
|
2355
|
+
function fileFor(cwd) {
|
|
2356
|
+
return path11.join(cwd, ".star", "todos.json");
|
|
2357
|
+
}
|
|
2358
|
+
var persistGuard = () => true;
|
|
2359
|
+
function setTodoPersistGuard(guard) {
|
|
2360
|
+
persistGuard = guard;
|
|
2361
|
+
}
|
|
2362
|
+
var TodoStore = class {
|
|
2363
|
+
items = /* @__PURE__ */ new Map();
|
|
2364
|
+
list() {
|
|
2365
|
+
return [...this.items.values()];
|
|
2366
|
+
}
|
|
2367
|
+
replace(items) {
|
|
2368
|
+
this.items.clear();
|
|
2369
|
+
for (const item of items) {
|
|
2370
|
+
this.items.set(item.id, item);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async load(cwd) {
|
|
2374
|
+
let raw;
|
|
2375
|
+
try {
|
|
2376
|
+
raw = await readFile4(fileFor(cwd), "utf8");
|
|
2377
|
+
} catch {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
const parsed = JSON.parse(raw);
|
|
2382
|
+
if (Array.isArray(parsed)) {
|
|
2383
|
+
this.replace(parsed.filter((it) => todoItemSchema.safeParse(it).success));
|
|
2384
|
+
}
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
// Atomic write (tmp + rename): a crash or failed save can never leave a
|
|
2389
|
+
// truncated todos.json behind.
|
|
2390
|
+
async save(cwd) {
|
|
2391
|
+
if (!persistGuard()) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
await mkdir4(path11.join(cwd, ".star"), { recursive: true });
|
|
2395
|
+
const file = fileFor(cwd);
|
|
2396
|
+
const tmp = `${file}.tmp`;
|
|
2397
|
+
await writeFile4(tmp, `${JSON.stringify(this.list(), null, 2)}
|
|
2398
|
+
`);
|
|
2399
|
+
await rename(tmp, file);
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
var SYMBOLS = {
|
|
2403
|
+
pending: "[ ]",
|
|
2404
|
+
in_progress: "[~]",
|
|
2405
|
+
done: "[x]"
|
|
2406
|
+
};
|
|
2407
|
+
function formatTodos(items) {
|
|
2408
|
+
if (items.length === 0) {
|
|
2409
|
+
return "No todos.";
|
|
2410
|
+
}
|
|
2411
|
+
const lines = items.map((it) => `${SYMBOLS[it.status]} ${it.id}. ${it.title}`);
|
|
2412
|
+
const width = Math.max(...lines.map((l) => l.length));
|
|
2413
|
+
return lines.map((line, i) => {
|
|
2414
|
+
const status = items[i]?.status;
|
|
2415
|
+
return status === "pending" ? line : `${line.padEnd(width + 4)}(${status})`;
|
|
2416
|
+
}).join("\n");
|
|
2417
|
+
}
|
|
2418
|
+
var defaultStore = new TodoStore();
|
|
2419
|
+
function parseTodoArgs(args) {
|
|
2420
|
+
const parsed = writeSchema.safeParse(args);
|
|
2421
|
+
return parsed.success ? parsed.data.todos : null;
|
|
2422
|
+
}
|
|
2423
|
+
function pendingTodoTitles(args) {
|
|
2424
|
+
return (parseTodoArgs(args) ?? []).filter((t) => t.status !== "done").map((t) => t.title);
|
|
2425
|
+
}
|
|
2426
|
+
async function loadTodos(cwd) {
|
|
2427
|
+
await defaultStore.load(cwd);
|
|
2428
|
+
return defaultStore.list();
|
|
2429
|
+
}
|
|
2430
|
+
function resetTodos() {
|
|
2431
|
+
defaultStore.replace([]);
|
|
2432
|
+
}
|
|
2433
|
+
function createTodoTools(store = defaultStore) {
|
|
2434
|
+
const todoWrite = {
|
|
2435
|
+
name: "todo_write",
|
|
2436
|
+
description: "Replace the current todo list with the given items. Each item has an id, title, and status (pending, in_progress, done).",
|
|
2437
|
+
permission: "read",
|
|
2438
|
+
parameters: writeSchema,
|
|
2439
|
+
async execute(args, ctx) {
|
|
2440
|
+
const parsed = writeSchema.safeParse(args);
|
|
2441
|
+
if (!parsed.success) {
|
|
2442
|
+
return { content: `Invalid todos: ${parsed.error.message}`, isError: true };
|
|
2443
|
+
}
|
|
2444
|
+
const previous = store.list();
|
|
2445
|
+
store.replace(parsed.data.todos);
|
|
2446
|
+
try {
|
|
2447
|
+
await store.save(ctx.cwd);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
store.replace(previous);
|
|
2450
|
+
return { content: `Failed to save todos: ${err.message}`, isError: true };
|
|
2451
|
+
}
|
|
2452
|
+
const items = store.list();
|
|
2453
|
+
const done = items.filter((it) => it.status === "done").length;
|
|
2454
|
+
return { content: `Todo list updated (${done}/${items.length} done).` };
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
const todoRead = {
|
|
2458
|
+
name: "todo_read",
|
|
2459
|
+
description: "Read the current todo list.",
|
|
2460
|
+
permission: "read",
|
|
2461
|
+
parameters: readSchema,
|
|
2462
|
+
async execute() {
|
|
2463
|
+
return { content: formatTodos(store.list()) };
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
return [todoWrite, todoRead];
|
|
2467
|
+
}
|
|
2468
|
+
|
|
1982
2469
|
// src/tools/web/fetch.ts
|
|
1983
2470
|
import { z as z10 } from "zod";
|
|
1984
2471
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
@@ -1988,8 +2475,14 @@ var schema7 = z10.object({
|
|
|
1988
2475
|
url: z10.string().describe("The http(s) URL to fetch"),
|
|
1989
2476
|
maxChars: z10.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
|
|
1990
2477
|
});
|
|
2478
|
+
function safeCodePoint(n, raw) {
|
|
2479
|
+
if (!Number.isInteger(n) || n < 0 || n > 1114111) {
|
|
2480
|
+
return raw;
|
|
2481
|
+
}
|
|
2482
|
+
return String.fromCodePoint(n);
|
|
2483
|
+
}
|
|
1991
2484
|
function decodeEntities(s) {
|
|
1992
|
-
return s.replace(/&#(\d+);/g, (
|
|
2485
|
+
return s.replace(/&#(\d+);/g, (m, n) => safeCodePoint(Number(n), m)).replace(/&#x([0-9a-fA-F]+);/gi, (m, n) => safeCodePoint(Number.parseInt(n, 16), m)).replace(/&(quot|lt|gt|nbsp);/g, (m) => {
|
|
1993
2486
|
switch (m) {
|
|
1994
2487
|
case """:
|
|
1995
2488
|
return '"';
|
|
@@ -2049,7 +2542,7 @@ function truncate2(s, maxChars) {
|
|
|
2049
2542
|
}
|
|
2050
2543
|
var webFetchTool = {
|
|
2051
2544
|
name: "web_fetch",
|
|
2052
|
-
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed). Output is truncated to maxChars (default 20000).",
|
|
2545
|
+
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed). Output is truncated to maxChars (default 20000). Can reach internal/intranet addresses, so do not use it for untrusted URLs.",
|
|
2053
2546
|
permission: "read",
|
|
2054
2547
|
parameters: schema7,
|
|
2055
2548
|
async execute(args, ctx) {
|
|
@@ -2231,7 +2724,9 @@ var webSearchTool = {
|
|
|
2231
2724
|
// src/tools/registry.ts
|
|
2232
2725
|
var ToolRegistry = class {
|
|
2233
2726
|
tools = /* @__PURE__ */ new Map();
|
|
2234
|
-
|
|
2727
|
+
// todoStore defaults to the module-level shared store; subagent loops pass
|
|
2728
|
+
// their own instance so a child's todo_write cannot clobber the parent list.
|
|
2729
|
+
constructor(todoStore) {
|
|
2235
2730
|
for (const tool of [
|
|
2236
2731
|
readFileTool,
|
|
2237
2732
|
writeFileTool,
|
|
@@ -2244,7 +2739,7 @@ var ToolRegistry = class {
|
|
|
2244
2739
|
taskListTool,
|
|
2245
2740
|
taskOutputTool,
|
|
2246
2741
|
taskKillTool,
|
|
2247
|
-
...createTodoTools()
|
|
2742
|
+
...createTodoTools(todoStore)
|
|
2248
2743
|
]) {
|
|
2249
2744
|
this.register(tool);
|
|
2250
2745
|
}
|
|
@@ -2263,13 +2758,165 @@ var ToolRegistry = class {
|
|
|
2263
2758
|
}
|
|
2264
2759
|
};
|
|
2265
2760
|
|
|
2266
|
-
// src/
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2761
|
+
// src/agent/project-memory.ts
|
|
2762
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2763
|
+
import path12 from "path";
|
|
2764
|
+
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
2765
|
+
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2766
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2767
|
+
function readProjectMemory(cwd) {
|
|
2768
|
+
const file = path12.join(cwd, PROJECT_MEMORY_FILE);
|
|
2769
|
+
let mtimeMs;
|
|
2770
|
+
try {
|
|
2771
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
2772
|
+
} catch {
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
const hit = cache.get(file);
|
|
2776
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.block;
|
|
2777
|
+
let block = null;
|
|
2778
|
+
try {
|
|
2779
|
+
let body = readFileSync2(file, "utf8").trim();
|
|
2780
|
+
if (body) {
|
|
2781
|
+
if (body.length > PROJECT_MEMORY_MAX_CHARS) {
|
|
2782
|
+
body = `${body.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
2783
|
+
|
|
2784
|
+
[AGENTS.md truncated to ${PROJECT_MEMORY_MAX_CHARS} characters]`;
|
|
2785
|
+
}
|
|
2786
|
+
block = `# Project instructions (AGENTS.md)
|
|
2787
|
+
${body}`;
|
|
2788
|
+
}
|
|
2789
|
+
} catch {
|
|
2790
|
+
block = null;
|
|
2791
|
+
}
|
|
2792
|
+
cache.set(file, { mtimeMs, block });
|
|
2793
|
+
return block;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// src/agent/skills.ts
|
|
2797
|
+
import { readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "fs";
|
|
2798
|
+
import path13 from "path";
|
|
2799
|
+
import { z as z12 } from "zod";
|
|
2800
|
+
var SKILL_FILE = "SKILL.md";
|
|
2801
|
+
var SKILL_MAX_CHARS = 32 * 1024;
|
|
2802
|
+
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
2803
|
+
function parseFrontmatter(raw) {
|
|
2804
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
2805
|
+
if (!match) return null;
|
|
2806
|
+
const meta = {};
|
|
2807
|
+
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
2808
|
+
const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
2809
|
+
if (kv?.[1]) meta[kv[1]] = (kv[2] ?? "").trim();
|
|
2810
|
+
}
|
|
2811
|
+
return { meta, body: raw.slice(match[0].length).trim() };
|
|
2812
|
+
}
|
|
2813
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
2814
|
+
function parseSkillFile(filePath, dirName, scope) {
|
|
2815
|
+
let mtimeMs;
|
|
2816
|
+
try {
|
|
2817
|
+
mtimeMs = statSync3(filePath).mtimeMs;
|
|
2818
|
+
} catch {
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
const hit = cache2.get(filePath);
|
|
2822
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.skill;
|
|
2823
|
+
let skill = null;
|
|
2824
|
+
try {
|
|
2825
|
+
const parsed = parseFrontmatter(readFileSync3(filePath, "utf8"));
|
|
2826
|
+
const name = parsed?.meta.name || dirName;
|
|
2827
|
+
const description = parsed?.meta.description;
|
|
2828
|
+
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
2829
|
+
skill = { name, description, filePath, dir: path13.dirname(filePath), scope };
|
|
2830
|
+
}
|
|
2831
|
+
} catch {
|
|
2832
|
+
skill = null;
|
|
2271
2833
|
}
|
|
2272
|
-
|
|
2834
|
+
cache2.set(filePath, { mtimeMs, skill });
|
|
2835
|
+
return skill;
|
|
2836
|
+
}
|
|
2837
|
+
function scanDir(base, scope, into) {
|
|
2838
|
+
let entries;
|
|
2839
|
+
try {
|
|
2840
|
+
entries = readdirSync(base);
|
|
2841
|
+
} catch {
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
for (const entry of entries.sort()) {
|
|
2845
|
+
if (!NAME_PATTERN.test(entry)) continue;
|
|
2846
|
+
const parsed = parseSkillFile(path13.join(base, entry, SKILL_FILE), entry, scope);
|
|
2847
|
+
if (parsed) into.set(parsed.name, parsed);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function discoverSkills(cwd, home = starHome()) {
|
|
2851
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2852
|
+
scanDir(path13.join(home, "skills"), "user", byName);
|
|
2853
|
+
scanDir(path13.join(cwd, ".star", "skills"), "project", byName);
|
|
2854
|
+
return [...byName.values()];
|
|
2855
|
+
}
|
|
2856
|
+
function formatSkillsBlock(skills) {
|
|
2857
|
+
const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
|
|
2858
|
+
return [
|
|
2859
|
+
"# Available skills",
|
|
2860
|
+
"When the user's request matches one of these skills, call the skill tool with its name to load the full instructions before proceeding:",
|
|
2861
|
+
...lines
|
|
2862
|
+
].join("\n");
|
|
2863
|
+
}
|
|
2864
|
+
function loadSkillBody(skill) {
|
|
2865
|
+
let parsed = null;
|
|
2866
|
+
try {
|
|
2867
|
+
parsed = parseFrontmatter(readFileSync3(skill.filePath, "utf8"));
|
|
2868
|
+
} catch {
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
if (!parsed?.body) return null;
|
|
2872
|
+
if (parsed.body.length > SKILL_MAX_CHARS) {
|
|
2873
|
+
return `${parsed.body.slice(0, SKILL_MAX_CHARS)}
|
|
2874
|
+
|
|
2875
|
+
[skill truncated to ${SKILL_MAX_CHARS} characters]`;
|
|
2876
|
+
}
|
|
2877
|
+
return parsed.body;
|
|
2878
|
+
}
|
|
2879
|
+
function createSkillTool(deps) {
|
|
2880
|
+
return {
|
|
2881
|
+
name: "skill",
|
|
2882
|
+
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.",
|
|
2883
|
+
permission: "read",
|
|
2884
|
+
parameters: z12.object({
|
|
2885
|
+
name: z12.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
2886
|
+
}),
|
|
2887
|
+
async execute(args) {
|
|
2888
|
+
const skills = discoverSkills(deps.cwd, deps.home);
|
|
2889
|
+
const skill = skills.find((s) => s.name === args.name);
|
|
2890
|
+
if (!skill) {
|
|
2891
|
+
const available = skills.map((s) => s.name).join(", ") || "none";
|
|
2892
|
+
return {
|
|
2893
|
+
content: `Unknown skill: "${args.name}". Available skills: ${available}`,
|
|
2894
|
+
isError: true
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
const body = loadSkillBody(skill);
|
|
2898
|
+
if (!body) {
|
|
2899
|
+
return { content: `Skill "${skill.name}" is empty or unreadable.`, isError: true };
|
|
2900
|
+
}
|
|
2901
|
+
return {
|
|
2902
|
+
content: [
|
|
2903
|
+
`# Skill: ${skill.name}`,
|
|
2904
|
+
"",
|
|
2905
|
+
body,
|
|
2906
|
+
"",
|
|
2907
|
+
`Base directory for relative file references in this skill: ${skill.dir}`
|
|
2908
|
+
].join("\n")
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// src/agent/subagent.ts
|
|
2915
|
+
import { z as z13 } from "zod";
|
|
2916
|
+
|
|
2917
|
+
// src/tools/index.ts
|
|
2918
|
+
function createDefaultRegistry(todoStore) {
|
|
2919
|
+
return new ToolRegistry(todoStore);
|
|
2273
2920
|
}
|
|
2274
2921
|
|
|
2275
2922
|
// src/agent/subagent.ts
|
|
@@ -2277,7 +2924,7 @@ var MAX_SUBAGENT_DEPTH = 1;
|
|
|
2277
2924
|
var MAX_REPORT_CHARS = 2e4;
|
|
2278
2925
|
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.";
|
|
2279
2926
|
async function runSubagent(deps, args, cwd, signal) {
|
|
2280
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
2927
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-SDZVEQND.js");
|
|
2281
2928
|
const child = new AgentLoop2({
|
|
2282
2929
|
model: deps.model,
|
|
2283
2930
|
registry: createDefaultRegistry(),
|
|
@@ -2304,6 +2951,9 @@ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
|
2304
2951
|
} catch (e) {
|
|
2305
2952
|
error = e instanceof Error ? e.message : String(e);
|
|
2306
2953
|
}
|
|
2954
|
+
if (signal.aborted) {
|
|
2955
|
+
throw new Error("Tool execution aborted.");
|
|
2956
|
+
}
|
|
2307
2957
|
let report = "";
|
|
2308
2958
|
const messages = child.getMessages();
|
|
2309
2959
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -2332,10 +2982,10 @@ function createSubagentTool(deps) {
|
|
|
2332
2982
|
name: "subagent",
|
|
2333
2983
|
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).",
|
|
2334
2984
|
permission: "exec",
|
|
2335
|
-
parameters:
|
|
2336
|
-
prompt:
|
|
2337
|
-
description:
|
|
2338
|
-
run_in_background:
|
|
2985
|
+
parameters: z13.object({
|
|
2986
|
+
prompt: z13.string().describe("Complete, self-contained instructions for the subagent."),
|
|
2987
|
+
description: z13.string().optional().describe("Short label for the subtask, shown in the result header."),
|
|
2988
|
+
run_in_background: z13.boolean().optional().describe(
|
|
2339
2989
|
"Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
|
|
2340
2990
|
)
|
|
2341
2991
|
}),
|
|
@@ -2362,13 +3012,13 @@ function createSubagentTool(deps) {
|
|
|
2362
3012
|
|
|
2363
3013
|
// src/agent/user-memory.ts
|
|
2364
3014
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
2365
|
-
import
|
|
2366
|
-
import { z as
|
|
3015
|
+
import path14 from "path";
|
|
3016
|
+
import { z as z14 } from "zod";
|
|
2367
3017
|
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2368
3018
|
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
2369
3019
|
var cache3 = /* @__PURE__ */ new Map();
|
|
2370
3020
|
function readUserMemory(home = starHome()) {
|
|
2371
|
-
const file =
|
|
3021
|
+
const file = path14.join(home, "MEMORY.md");
|
|
2372
3022
|
let mtimeMs;
|
|
2373
3023
|
try {
|
|
2374
3024
|
mtimeMs = statSync4(file).mtimeMs;
|
|
@@ -2400,7 +3050,7 @@ function readUserMemory(home = starHome()) {
|
|
|
2400
3050
|
}
|
|
2401
3051
|
function appendUserMemory(text, home = starHome()) {
|
|
2402
3052
|
mkdirSync(home, { recursive: true });
|
|
2403
|
-
appendFileSync(
|
|
3053
|
+
appendFileSync(path14.join(home, "MEMORY.md"), `- ${text}
|
|
2404
3054
|
`, "utf8");
|
|
2405
3055
|
}
|
|
2406
3056
|
function createRememberTool(deps = {}) {
|
|
@@ -2408,8 +3058,8 @@ function createRememberTool(deps = {}) {
|
|
|
2408
3058
|
name: "remember",
|
|
2409
3059
|
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.`,
|
|
2410
3060
|
permission: "write",
|
|
2411
|
-
parameters:
|
|
2412
|
-
text:
|
|
3061
|
+
parameters: z14.object({
|
|
3062
|
+
text: z14.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
|
|
2413
3063
|
}),
|
|
2414
3064
|
async execute(args) {
|
|
2415
3065
|
appendUserMemory(args.text, deps.home);
|
|
@@ -2467,6 +3117,24 @@ ${finalReply.slice(0, 2e3)}
|
|
|
2467
3117
|
}
|
|
2468
3118
|
}
|
|
2469
3119
|
var STREAM_RETRY_BASE_DELAY_MS = 1e3;
|
|
3120
|
+
var REDO_LABEL_MAX = 40;
|
|
3121
|
+
function messageText(message) {
|
|
3122
|
+
if (!message) return "";
|
|
3123
|
+
const content = message.content;
|
|
3124
|
+
if (typeof content === "string") return content;
|
|
3125
|
+
if (Array.isArray(content)) {
|
|
3126
|
+
return content.filter((part) => part.type === "text").map((part) => "text" in part ? part.text : "").join(" ");
|
|
3127
|
+
}
|
|
3128
|
+
return "";
|
|
3129
|
+
}
|
|
3130
|
+
function stableStringify(value) {
|
|
3131
|
+
if (value === void 0) return "undefined";
|
|
3132
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
3133
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
3134
|
+
const record = value;
|
|
3135
|
+
const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
3136
|
+
return `{${entries.join(",")}}`;
|
|
3137
|
+
}
|
|
2470
3138
|
function sleep(ms, signal) {
|
|
2471
3139
|
if (signal.aborted) return Promise.resolve(false);
|
|
2472
3140
|
return new Promise((resolve) => {
|
|
@@ -2485,11 +3153,32 @@ var AgentLoop = class {
|
|
|
2485
3153
|
messages = [];
|
|
2486
3154
|
opts;
|
|
2487
3155
|
// Seq + user-message index of each turn started via stream(); lets /undo
|
|
2488
|
-
// match a retracted turn to the file snapshots it produced.
|
|
2489
|
-
//
|
|
2490
|
-
//
|
|
3156
|
+
// match a retracted turn to the file snapshots it produced. `tree` is the
|
|
3157
|
+
// whole-tree git snapshot captured at the turn's start (root loop only,
|
|
3158
|
+
// config.gitSnapshots) — when present, /undo restores it instead of
|
|
3159
|
+
// replaying per-file snapshots, which also covers bash-made changes.
|
|
3160
|
+
// Cleared when history is replaced wholesale (resume/compact), because
|
|
3161
|
+
// indices no longer line up — in that case /undo only retracts messages,
|
|
3162
|
+
// never wrong files.
|
|
2491
3163
|
turnMarkers = [];
|
|
3164
|
+
// Pre-undo tree snapshots, latest last: a git-path /undo pushes the
|
|
3165
|
+
// pre-undo working tree here so /redo can restore it exactly. Only the git
|
|
3166
|
+
// path feeds this — per-file snapshot undos stay non-redoable.
|
|
3167
|
+
redoStack = [];
|
|
3168
|
+
// Start of the most recently retracted turn's user message; undoLastTurn
|
|
3169
|
+
// consumes it as the redo label right after retractLastTurn sets it.
|
|
3170
|
+
lastUndoneLabel = "";
|
|
3171
|
+
// Doom-loop guard: signature of the most recent tool call and how many
|
|
3172
|
+
// times in a row it has repeated. Any different call resets the count.
|
|
3173
|
+
lastToolSignature = null;
|
|
3174
|
+
repeatedToolCalls = 0;
|
|
3175
|
+
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
3176
|
+
// execution so file snapshots attribute root changes to a turn and mark
|
|
3177
|
+
// subagent changes as excluded from turn-level retraction.
|
|
3178
|
+
activeTurnSeq = 0;
|
|
3179
|
+
activeTurnUserIndex = -1;
|
|
2492
3180
|
titleScheduled = false;
|
|
3181
|
+
titleInput;
|
|
2493
3182
|
confirmHandler;
|
|
2494
3183
|
// Hook failures never block the turn (except an explicit PreToolUse block);
|
|
2495
3184
|
// their stderr surfaces through this callback (REPL system message / stderr
|
|
@@ -2497,6 +3186,15 @@ var AgentLoop = class {
|
|
|
2497
3186
|
onHookWarning;
|
|
2498
3187
|
constructor(opts) {
|
|
2499
3188
|
this.opts = opts;
|
|
3189
|
+
const depth = opts.subagentDepth ?? 0;
|
|
3190
|
+
if (depth > 0) {
|
|
3191
|
+
opts.registry = new ToolRegistry(new TodoStore());
|
|
3192
|
+
} else {
|
|
3193
|
+
setTodoPersistGuard(() => {
|
|
3194
|
+
const mode = this.opts.config.permissionMode;
|
|
3195
|
+
return mode !== "readonly" && mode !== "plan";
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
2500
3198
|
if (opts.system) {
|
|
2501
3199
|
this.messages.push({ role: "system", content: opts.system });
|
|
2502
3200
|
}
|
|
@@ -2505,7 +3203,6 @@ var AgentLoop = class {
|
|
|
2505
3203
|
}
|
|
2506
3204
|
opts.registry?.register(createSkillTool({ cwd: opts.cwd }));
|
|
2507
3205
|
opts.registry?.register(createRememberTool());
|
|
2508
|
-
const depth = opts.subagentDepth ?? 0;
|
|
2509
3206
|
if (opts.registry && depth < MAX_SUBAGENT_DEPTH) {
|
|
2510
3207
|
opts.registry.register(
|
|
2511
3208
|
createSubagentTool({
|
|
@@ -2524,10 +3221,14 @@ var AgentLoop = class {
|
|
|
2524
3221
|
}
|
|
2525
3222
|
// Swaps the persistence target (/new starts a fresh session mid-REPL).
|
|
2526
3223
|
// Title generation is re-armed so the new session gets one after its first
|
|
2527
|
-
// turn, and snapshot checkpoints now flow to the new store.
|
|
3224
|
+
// turn, and snapshot checkpoints now flow to the new store. The redo stack
|
|
3225
|
+
// dies with the old session context: its trees describe working-tree states
|
|
3226
|
+
// that no longer line up with the new session's conversation.
|
|
2528
3227
|
setSessionStore(store) {
|
|
2529
3228
|
this.opts.sessionStore = store;
|
|
2530
3229
|
this.titleScheduled = false;
|
|
3230
|
+
this.titleInput = void 0;
|
|
3231
|
+
this.redoStack = [];
|
|
2531
3232
|
if (store) this.bindSnapshotHooks(store);
|
|
2532
3233
|
}
|
|
2533
3234
|
bindSnapshotHooks(store) {
|
|
@@ -2540,7 +3241,9 @@ var AgentLoop = class {
|
|
|
2540
3241
|
existed: snapshot.existed,
|
|
2541
3242
|
toolName: snapshot.toolName,
|
|
2542
3243
|
turn: snapshot.turn,
|
|
2543
|
-
messageIndex: snapshot.messageIndex
|
|
3244
|
+
messageIndex: snapshot.messageIndex,
|
|
3245
|
+
owner: snapshot.owner,
|
|
3246
|
+
contentTooLarge: snapshot.contentTooLarge
|
|
2544
3247
|
},
|
|
2545
3248
|
snapshot.content
|
|
2546
3249
|
),
|
|
@@ -2550,37 +3253,92 @@ var AgentLoop = class {
|
|
|
2550
3253
|
async loadMessages(messages) {
|
|
2551
3254
|
this.messages = reconcileToolCalls(messages);
|
|
2552
3255
|
this.turnMarkers = [];
|
|
3256
|
+
this.redoStack = [];
|
|
2553
3257
|
}
|
|
2554
3258
|
// Read-only counterpart of retractLastTurn: how many messages /undo would
|
|
2555
|
-
// drop
|
|
2556
|
-
// no verified turn, so no file snapshots may be reverted)
|
|
2557
|
-
//
|
|
3259
|
+
// drop, which snapshot turn's file changes it would revert (undefined =
|
|
3260
|
+
// no verified turn, so no file snapshots may be reverted), and the turn's
|
|
3261
|
+
// whole-tree snapshot when one was captured. Mutates nothing, so the REPL
|
|
3262
|
+
// can show the preview before the user confirms.
|
|
2558
3263
|
previewLastTurnRetraction() {
|
|
2559
3264
|
const result = retractLastTurn([...this.messages]);
|
|
2560
3265
|
if (result.removed === 0) return { removed: 0 };
|
|
2561
3266
|
const userIndex = result.messages.length;
|
|
2562
3267
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2563
|
-
const
|
|
2564
|
-
return {
|
|
3268
|
+
const verified = last && last.userIndex === userIndex;
|
|
3269
|
+
return {
|
|
3270
|
+
removed: result.removed,
|
|
3271
|
+
turn: verified ? last.seq : void 0,
|
|
3272
|
+
tree: verified ? last.tree : void 0
|
|
3273
|
+
};
|
|
2565
3274
|
}
|
|
2566
3275
|
// Drops the final user message and everything after it, and persists the
|
|
2567
3276
|
// trimmed history so a later /resume does not bring the turn back.
|
|
2568
3277
|
// `turn` is the retracted turn's snapshot seq when it could be verified
|
|
2569
3278
|
// against the marker recorded at turn start — undefined means the caller
|
|
2570
|
-
// must not revert any file snapshots.
|
|
3279
|
+
// must not revert any file snapshots. `tree` is the turn's whole-tree
|
|
3280
|
+
// snapshot when git snapshots tracked it.
|
|
2571
3281
|
async retractLastTurn() {
|
|
2572
3282
|
const result = retractLastTurn([...this.messages]);
|
|
2573
3283
|
if (result.removed === 0) return { removed: 0 };
|
|
2574
3284
|
const userIndex = result.messages.length;
|
|
3285
|
+
this.lastUndoneLabel = messageText(this.messages[userIndex]).slice(0, REDO_LABEL_MAX);
|
|
2575
3286
|
this.messages = result.messages;
|
|
2576
3287
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2577
3288
|
const last = this.turnMarkers[this.turnMarkers.length - 1];
|
|
2578
3289
|
let turn;
|
|
3290
|
+
let tree;
|
|
2579
3291
|
if (last && last.userIndex === userIndex) {
|
|
2580
3292
|
turn = last.seq;
|
|
3293
|
+
tree = last.tree;
|
|
2581
3294
|
this.turnMarkers.pop();
|
|
2582
3295
|
}
|
|
2583
|
-
return { removed: result.removed, turn };
|
|
3296
|
+
return { removed: result.removed, turn, tree };
|
|
3297
|
+
}
|
|
3298
|
+
// The confirmed /undo: retract the last turn, then revert its file changes.
|
|
3299
|
+
// With a whole-tree snapshot the working tree is restored from git — which
|
|
3300
|
+
// also reverts changes no tool snapshot saw (bash edits, deleted files) —
|
|
3301
|
+
// and the turn's per-file snapshots are dropped instead of replayed (they
|
|
3302
|
+
// would double-restore). The pre-undo tree feeds the redo stack, so /redo
|
|
3303
|
+
// returns to the exact state before the undo. Any git failure falls back
|
|
3304
|
+
// to the per-file snapshot revert, which never pushes the redo stack.
|
|
3305
|
+
async undoLastTurn() {
|
|
3306
|
+
const { removed, turn, tree } = await this.retractLastTurn();
|
|
3307
|
+
if (removed === 0) return { removed, reverted: [] };
|
|
3308
|
+
if (turn !== void 0 && tree !== void 0) {
|
|
3309
|
+
const preUndoTree = await trackTree(this.opts.cwd);
|
|
3310
|
+
if (preUndoTree !== null && await restoreTree(this.opts.cwd, tree)) {
|
|
3311
|
+
this.redoStack.push({ tree: preUndoTree, label: this.lastUndoneLabel });
|
|
3312
|
+
const dropped = await dropTurnSnapshots(turn, true);
|
|
3313
|
+
return {
|
|
3314
|
+
removed,
|
|
3315
|
+
reverted: [
|
|
3316
|
+
`Restored the working tree from the git snapshot taken at the turn's start (${tree.slice(0, 8)}; covers changes from any tool, bash included; ${dropped} per-file snapshot(s) discarded).`
|
|
3317
|
+
],
|
|
3318
|
+
tree
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
const reverted = turn !== void 0 ? await undoTurnSnapshots(turn) : [];
|
|
3323
|
+
return { removed, reverted };
|
|
3324
|
+
}
|
|
3325
|
+
// The redo entry /redo would restore, without popping it (for the confirm
|
|
3326
|
+
// prompt); null when there is nothing to redo.
|
|
3327
|
+
peekRedo() {
|
|
3328
|
+
return this.redoStack[this.redoStack.length - 1] ?? null;
|
|
3329
|
+
}
|
|
3330
|
+
// Restores the working tree to its state just before the last git-path
|
|
3331
|
+
// /undo. Conversation messages are not re-added — redo is file-level only.
|
|
3332
|
+
// The entry stays on the stack when the restore fails, so it can be retried.
|
|
3333
|
+
async redoLastUndo() {
|
|
3334
|
+
const entry = this.redoStack[this.redoStack.length - 1];
|
|
3335
|
+
if (!entry) return null;
|
|
3336
|
+
const names = await diffTreeNames(this.opts.cwd, entry.tree);
|
|
3337
|
+
if (!await restoreTree(this.opts.cwd, entry.tree)) {
|
|
3338
|
+
return { ok: false, tree: entry.tree, label: entry.label, files: null };
|
|
3339
|
+
}
|
|
3340
|
+
this.redoStack.pop();
|
|
3341
|
+
return { ok: true, tree: entry.tree, label: entry.label, files: names?.length ?? null };
|
|
2584
3342
|
}
|
|
2585
3343
|
// Cut point for a rewind: messages[index] should be the user message that
|
|
2586
3344
|
// started the rewound turn, but compaction may have shifted indices, so
|
|
@@ -2626,13 +3384,18 @@ var AgentLoop = class {
|
|
|
2626
3384
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
2627
3385
|
return removed;
|
|
2628
3386
|
}
|
|
2629
|
-
//
|
|
2630
|
-
//
|
|
2631
|
-
//
|
|
2632
|
-
|
|
3387
|
+
// Background title generation runs once per loop, fed by the first real
|
|
3388
|
+
// user message of the session. It must NOT latch on the first assistant
|
|
3389
|
+
// step's text: that text is empty for a tool-call-only first step (the
|
|
3390
|
+
// common agentic case), which used to burn the one-shot flag on an empty
|
|
3391
|
+
// input and left every such session untitled. The store-level title check
|
|
3392
|
+
// makes resumed sessions that already have a title a no-op.
|
|
3393
|
+
maybeScheduleTitle() {
|
|
2633
3394
|
if (this.titleScheduled) return;
|
|
2634
3395
|
const store = this.opts.sessionStore;
|
|
2635
3396
|
if (!store) return;
|
|
3397
|
+
const input = this.titleInput;
|
|
3398
|
+
if (!input) return;
|
|
2636
3399
|
this.titleScheduled = true;
|
|
2637
3400
|
scheduleSessionTitle(store, input, this.opts.model);
|
|
2638
3401
|
}
|
|
@@ -2681,6 +3444,9 @@ var AgentLoop = class {
|
|
|
2681
3444
|
this.syncSystemMessage();
|
|
2682
3445
|
const inputText = typeof input === "string" ? input : input.text;
|
|
2683
3446
|
const images = typeof input === "string" ? [] : input.images;
|
|
3447
|
+
if (this.titleInput === void 0 && inputText.trim().length > 0) {
|
|
3448
|
+
this.titleInput = inputText;
|
|
3449
|
+
}
|
|
2684
3450
|
const userMessage = images.length > 0 ? {
|
|
2685
3451
|
role: "user",
|
|
2686
3452
|
content: [
|
|
@@ -2692,12 +3458,17 @@ var AgentLoop = class {
|
|
|
2692
3458
|
{ type: "text", text: inputText }
|
|
2693
3459
|
]
|
|
2694
3460
|
} : { role: "user", content: inputText };
|
|
3461
|
+
const isRoot = (this.opts.subagentDepth ?? 0) === 0;
|
|
3462
|
+
const tree = isRoot && this.opts.config.gitSnapshots ? await trackTree(this.opts.cwd) ?? void 0 : void 0;
|
|
2695
3463
|
this.messages.push(userMessage);
|
|
2696
|
-
const seq =
|
|
2697
|
-
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1 });
|
|
3464
|
+
const seq = isRoot ? beginTurn(this.messages.length - 1) : currentTurnSeq();
|
|
3465
|
+
this.turnMarkers.push({ seq, userIndex: this.messages.length - 1, tree });
|
|
3466
|
+
this.activeTurnSeq = seq;
|
|
3467
|
+
this.activeTurnUserIndex = this.messages.length - 1;
|
|
2698
3468
|
await this.persist(
|
|
2699
3469
|
opts?.persistAs !== void 0 ? { role: "user", content: opts.persistAs } : userMessage
|
|
2700
3470
|
);
|
|
3471
|
+
this.maybeScheduleTitle();
|
|
2701
3472
|
const { config, registry, cwd } = this.opts;
|
|
2702
3473
|
const aiTools = this.buildAiTools();
|
|
2703
3474
|
let autoContinues = 0;
|
|
@@ -2718,9 +3489,13 @@ ${finished.result}`
|
|
|
2718
3489
|
}
|
|
2719
3490
|
}
|
|
2720
3491
|
const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
|
|
2721
|
-
const compacted = compactMessages(
|
|
3492
|
+
const compacted = compactMessages(
|
|
3493
|
+
[...this.messages],
|
|
3494
|
+
resolveCompactThreshold(config, maxTokens)
|
|
3495
|
+
);
|
|
2722
3496
|
if (compacted.compacted) {
|
|
2723
|
-
this.messages = await this.applyCompactionSummary(compacted);
|
|
3497
|
+
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
3498
|
+
this.turnMarkers = [];
|
|
2724
3499
|
}
|
|
2725
3500
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
2726
3501
|
const baseDelay = this.opts.retryDelayMs ?? STREAM_RETRY_BASE_DELAY_MS;
|
|
@@ -2785,7 +3560,7 @@ ${finished.result}`
|
|
|
2785
3560
|
if (attempt >= maxRetries) break;
|
|
2786
3561
|
if (!failure && text.length === 0 && toolCalls.length === 0 && attempt >= 1 && (isFilteredEmptyFinish(lastFinishReason) || sawReasoning || lastUsage !== void 0 && lastUsage.completionTokens > 0))
|
|
2787
3562
|
break;
|
|
2788
|
-
const delayMs = computeRetryDelayMs(attempt, baseDelay, failure);
|
|
3563
|
+
const delayMs = computeRetryDelayMs(attempt, failure ? baseDelay : baseDelay * 2, failure);
|
|
2789
3564
|
yield {
|
|
2790
3565
|
type: "retry",
|
|
2791
3566
|
attempt: attempt + 2,
|
|
@@ -2793,7 +3568,10 @@ ${finished.result}`
|
|
|
2793
3568
|
delayMs,
|
|
2794
3569
|
reason: failure ? summarizeStreamError(failure) : `empty response (finish: ${lastFinishReason ?? "none"})`
|
|
2795
3570
|
};
|
|
2796
|
-
if (!await sleep(delayMs, signal))
|
|
3571
|
+
if (!await sleep(delayMs, signal)) {
|
|
3572
|
+
yield* this.persistInterrupted(text, toolCalls);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
2797
3575
|
}
|
|
2798
3576
|
if (failure) {
|
|
2799
3577
|
yield { type: "error", error: failure };
|
|
@@ -2835,7 +3613,6 @@ ${finished.result}`
|
|
|
2835
3613
|
};
|
|
2836
3614
|
this.messages.push(assistantMessage);
|
|
2837
3615
|
await this.persist(assistantMessage);
|
|
2838
|
-
this.maybeScheduleTitle(text);
|
|
2839
3616
|
if (toolCalls.length === 0) {
|
|
2840
3617
|
let reason = null;
|
|
2841
3618
|
let nudgeText = AUTO_CONTINUE_NUDGE;
|
|
@@ -2883,7 +3660,11 @@ ${finished.result}`
|
|
|
2883
3660
|
try {
|
|
2884
3661
|
for (const call of toolCalls) {
|
|
2885
3662
|
if (signal.aborted) break;
|
|
2886
|
-
const
|
|
3663
|
+
const refusal = this.doomLoopRefusal(call);
|
|
3664
|
+
if (refusal !== null) {
|
|
3665
|
+
yield { type: "notice", message: refusal };
|
|
3666
|
+
}
|
|
3667
|
+
const result = refusal !== null ? { content: refusal, isError: true } : await this.executeTool(call, signal);
|
|
2887
3668
|
const toolMessage = {
|
|
2888
3669
|
role: "tool",
|
|
2889
3670
|
content: [
|
|
@@ -2960,7 +3741,7 @@ ${finished.result}`
|
|
|
2960
3741
|
if (userMemory) parts.push(userMemory);
|
|
2961
3742
|
const skills = discoverSkills(this.opts.cwd);
|
|
2962
3743
|
if (skills.length > 0) parts.push(formatSkillsBlock(skills));
|
|
2963
|
-
const git =
|
|
3744
|
+
const git = getGitSummaryCached(this.opts.cwd);
|
|
2964
3745
|
if (git) parts.push(formatGitSummary(git));
|
|
2965
3746
|
if (parts.length === 0) return;
|
|
2966
3747
|
const content = parts.join("\n\n");
|
|
@@ -2971,7 +3752,7 @@ ${finished.result}`
|
|
|
2971
3752
|
this.messages.unshift({ role: "system", content });
|
|
2972
3753
|
}
|
|
2973
3754
|
}
|
|
2974
|
-
async applyCompactionSummary(compacted) {
|
|
3755
|
+
async applyCompactionSummary(compacted, signal) {
|
|
2975
3756
|
const { config, model } = this.opts;
|
|
2976
3757
|
if (config.contextCompaction !== "summary" || !model) {
|
|
2977
3758
|
return compacted.messages;
|
|
@@ -2979,7 +3760,7 @@ ${finished.result}`
|
|
|
2979
3760
|
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
2980
3761
|
const dropped = this.messages.slice(headCount, headCount + compacted.droppedCount);
|
|
2981
3762
|
try {
|
|
2982
|
-
const summary = await summarizeMessages(dropped, model);
|
|
3763
|
+
const summary = await summarizeMessages(dropped, model, signal);
|
|
2983
3764
|
const messages = compacted.messages.slice();
|
|
2984
3765
|
messages[headCount] = {
|
|
2985
3766
|
role: "user",
|
|
@@ -2992,15 +3773,23 @@ ${summary}`
|
|
|
2992
3773
|
}
|
|
2993
3774
|
}
|
|
2994
3775
|
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
2995
|
-
const { streamChat } = await import("./stream-
|
|
2996
|
-
|
|
3776
|
+
const { streamChat } = await import("./stream-H4BYBVAZ.js");
|
|
3777
|
+
for await (const event of streamChat({
|
|
2997
3778
|
model: this.opts.model,
|
|
2998
3779
|
messages: this.messages,
|
|
2999
3780
|
tools: aiTools,
|
|
3000
3781
|
abortSignal: signal,
|
|
3001
3782
|
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
|
|
3002
3783
|
firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
|
|
3003
|
-
})
|
|
3784
|
+
})) {
|
|
3785
|
+
if (event.type === "finish" && event.truncated) {
|
|
3786
|
+
yield {
|
|
3787
|
+
type: "notice",
|
|
3788
|
+
message: "Response was cut short by the stream idle timeout \u2014 the reply may be incomplete."
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
yield event;
|
|
3792
|
+
}
|
|
3004
3793
|
}
|
|
3005
3794
|
buildAiTools() {
|
|
3006
3795
|
const plan = this.opts.config.permissionMode === "plan";
|
|
@@ -3030,6 +3819,23 @@ ${summary}`
|
|
|
3030
3819
|
return empty;
|
|
3031
3820
|
}
|
|
3032
3821
|
}
|
|
3822
|
+
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3823
|
+
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3824
|
+
// times in a row; 0 disables. Any different signature resets the streak.
|
|
3825
|
+
// Subagents are separate loop instances, so each counts its own streak.
|
|
3826
|
+
doomLoopRefusal(call) {
|
|
3827
|
+
const threshold = this.opts.config.doomLoopThreshold;
|
|
3828
|
+
if (threshold <= 0) return null;
|
|
3829
|
+
const signature = `${call.name} ${stableStringify(call.args)}`;
|
|
3830
|
+
if (signature === this.lastToolSignature) {
|
|
3831
|
+
this.repeatedToolCalls += 1;
|
|
3832
|
+
} else {
|
|
3833
|
+
this.lastToolSignature = signature;
|
|
3834
|
+
this.repeatedToolCalls = 1;
|
|
3835
|
+
}
|
|
3836
|
+
if (this.repeatedToolCalls < threshold) return null;
|
|
3837
|
+
return `Refused: tool "${call.name}" was called ${this.repeatedToolCalls} times in a row with identical arguments. Stop retrying it \u2014 change your approach or report that you are blocked.`;
|
|
3838
|
+
}
|
|
3033
3839
|
async executeTool(call, signal) {
|
|
3034
3840
|
const { registry, config, cwd } = this.opts;
|
|
3035
3841
|
const tool = registry.get(call.name);
|
|
@@ -3074,7 +3880,15 @@ ${summary}`
|
|
|
3074
3880
|
}
|
|
3075
3881
|
let result;
|
|
3076
3882
|
try {
|
|
3077
|
-
result = await tool.execute(parsed.data, {
|
|
3883
|
+
result = await tool.execute(parsed.data, {
|
|
3884
|
+
cwd,
|
|
3885
|
+
abortSignal: signal,
|
|
3886
|
+
snapshotContext: {
|
|
3887
|
+
owner: (this.opts.subagentDepth ?? 0) > 0 ? "subagent" : "root",
|
|
3888
|
+
turn: this.activeTurnSeq,
|
|
3889
|
+
messageIndex: this.activeTurnUserIndex
|
|
3890
|
+
}
|
|
3891
|
+
});
|
|
3078
3892
|
} catch (error) {
|
|
3079
3893
|
if (signal.aborted) {
|
|
3080
3894
|
return { content: "Tool execution aborted.", isError: true };
|
|
@@ -3093,6 +3907,9 @@ ${summary}`
|
|
|
3093
3907
|
|
|
3094
3908
|
export {
|
|
3095
3909
|
defaultAgentTasks,
|
|
3910
|
+
formatAgentTaskUpdate,
|
|
3911
|
+
ConfigSchema,
|
|
3912
|
+
contextWindowTokens,
|
|
3096
3913
|
estimateTokens,
|
|
3097
3914
|
summarizeMessages,
|
|
3098
3915
|
compactMessages,
|
|
@@ -3105,34 +3922,34 @@ export {
|
|
|
3105
3922
|
MAX_IMAGE_DIMENSION,
|
|
3106
3923
|
probeImageDimensions,
|
|
3107
3924
|
reconcileToolCalls,
|
|
3925
|
+
isSensitivePath,
|
|
3108
3926
|
isAllowedByRules,
|
|
3109
3927
|
buildAllowRule,
|
|
3110
3928
|
isDangerousCommand,
|
|
3929
|
+
diffTreeNames,
|
|
3111
3930
|
listSnapshots,
|
|
3112
3931
|
clearSnapshots,
|
|
3113
3932
|
hydrateSnapshots,
|
|
3114
3933
|
listTurnSnapshots,
|
|
3115
3934
|
resolveSnapshotContent,
|
|
3116
|
-
undoTurnSnapshots,
|
|
3117
3935
|
rewindToSnapshot,
|
|
3118
|
-
TodoStore,
|
|
3119
|
-
formatTodos,
|
|
3120
|
-
parseTodoArgs,
|
|
3121
|
-
loadTodos,
|
|
3122
|
-
resetTodos,
|
|
3123
|
-
discoverSkills,
|
|
3124
3936
|
defaultTaskManager,
|
|
3125
3937
|
resolveShell,
|
|
3126
3938
|
bashTool,
|
|
3127
3939
|
SKIP_DIRS,
|
|
3128
3940
|
createIgnorePredicate,
|
|
3129
|
-
isSensitivePath2 as isSensitivePath,
|
|
3130
3941
|
formatTaskList,
|
|
3131
3942
|
formatTaskStarted,
|
|
3132
3943
|
formatTaskFinished,
|
|
3944
|
+
TodoStore,
|
|
3945
|
+
formatTodos,
|
|
3946
|
+
parseTodoArgs,
|
|
3947
|
+
loadTodos,
|
|
3948
|
+
resetTodos,
|
|
3949
|
+
discoverSkills,
|
|
3133
3950
|
createDefaultRegistry,
|
|
3134
3951
|
appendUserMemory,
|
|
3135
3952
|
PLAN_MODE_PROMPT,
|
|
3136
3953
|
AgentLoop
|
|
3137
3954
|
};
|
|
3138
|
-
//# sourceMappingURL=chunk-
|
|
3955
|
+
//# sourceMappingURL=chunk-K3R42LOX.js.map
|