@cryer/star-cli 0.3.3 → 0.3.5

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 CHANGED
@@ -52,6 +52,13 @@ Config file: `~/.star-cli/config.toml` (project-level override: `.star/config.to
52
52
  defaultModel = "gpt"
53
53
  permissionMode = "ask" # ask | auto | readonly | yolo | plan
54
54
  contextMaxTokens = 100000
55
+ # Max model round-trips ("steps") per turn between progress checkpoints — one
56
+ # step is a reply plus the tool calls it asked for; retries and auto-continue
57
+ # nudges don't count. Reaching the cap while tools still execute resets it
58
+ # with a notice instead of stopping the turn; 0 disables the cap entirely.
59
+ maxSteps = 100
60
+ # compactThresholdTokens = 80000 # optional auto-compaction trigger in tokens; defaults to the
61
+ # context window. /compact always compacts, regardless of this.
55
62
  contextCompaction = "summary" # summary | truncate — how over-budget history is compacted
56
63
  # Seconds with no stream output before a stalled response is ended gracefully
57
64
  # (some relays never close the stream). The first token gets a fixed 120s allowance.
@@ -148,7 +155,7 @@ star --clear-sessions all delete every stored session
148
155
  | `/connect` | interactive provider onboarding wizard: pick a preset or custom endpoint, paste the API key (masked), name a model — appends `[[providers]]`/`[[models]]` to the config, stores the key in `~/.star-cli/.env` (never in `config.toml`), and can set the new model as default + switch to it immediately |
149
156
  | `/plan` | toggle plan mode: read-only research, then approve the generated plan before it executes (session-only) |
150
157
  | `/memory [add <text>]` | show the long-term memory file (`~/.star-cli/MEMORY.md`, injected into every session's system prompt), or append a line to it — see [Long-term memory](#long-term-memory) |
151
- | `/compact` | compact conversation history to free up context |
158
+ | `/compact` | compact conversation history now — always runs, even below the auto-compact threshold |
152
159
  | `/export [path]` | export the current session to a Markdown file |
153
160
  | `/undo` | undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages — earlier turns are never touched (previews the message count and per-file revert diffs, then asks for confirmation) |
154
161
  | `/rewind [n]` | list file-change checkpoints, or rewind to just before checkpoint `n`: restore every file changed since then and retract the matching conversation messages (asks for confirmation first) |
package/README.zh-CN.md CHANGED
@@ -52,6 +52,12 @@ star -p "hi" # non-interactive print mode
52
52
  defaultModel = "gpt"
53
53
  permissionMode = "ask" # ask | auto | readonly | yolo | plan
54
54
  contextMaxTokens = 100000
55
+ # 每回合在进度检查点之间允许的最大模型往返("step")数——一个 step 是
56
+ # 一次回复加上它请求的所有工具调用;重试和 auto-continue nudge 不占 step。
57
+ # 到达上限时若工具仍在执行,会发提示并重置额度而不是终止回合;0 表示完全不限。
58
+ maxSteps = 100
59
+ # compactThresholdTokens = 80000 # optional auto-compaction trigger in tokens; defaults to the
60
+ # context window. /compact always compacts, regardless of this.
55
61
  contextCompaction = "summary" # summary | truncate — how over-budget history is compacted
56
62
  # Seconds with no stream output before a stalled response is ended gracefully
57
63
  # (some relays never close the stream). The first token gets a fixed 120s allowance.
@@ -148,7 +154,7 @@ star --clear-sessions all delete every stored session
148
154
  | `/connect` | 交互式服务商接入向导:选择预设或自定义端点,粘贴 API 密钥(掩码显示),命名模型——把 `[[providers]]`/`[[models]]` 追加到配置,密钥存入 `~/.star-cli/.env`(绝不写入 `config.toml`),还可将新模型设为默认并立即切换 |
149
155
  | `/plan` | 切换计划模式:先只读调研,再批准生成的计划后才执行(仅会话内有效) |
150
156
  | `/memory [add <text>]` | 查看长期记忆文件(`~/.star-cli/MEMORY.md`,注入每个会话的系统提示词),或向其中追加一行——见[长期记忆](#长期记忆) |
151
- | `/compact` | 压缩对话历史以释放上下文 |
157
+ | `/compact` | 立即压缩对话历史——低于自动压缩阈值时也会执行 |
152
158
  | `/export [path]` | 将当前会话导出为 Markdown 文件 |
153
159
  | `/undo` | 撤销上一轮对话:还原其文件改动(write_file/edit_file)并撤回其消息——更早的回合不受影响(先预览消息数量与逐文件还原 diff,再要求确认) |
154
160
  | `/rewind [n]` | 列出文件改动检查点,或回滚到检查点 `n` 之前:还原此后改动的所有文件并撤回对应的对话消息(先要求确认) |
@@ -12,6 +12,129 @@ import {
12
12
  // src/agent/loop.ts
13
13
  import { tool as aiTool, generateText as generateText3 } from "ai";
14
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
+ // Cap on model round-trips ("steps") per turn between progress checkpoints.
72
+ // One step is a streamed reply plus every tool call it asked for — stream
73
+ // retries and auto-continue nudges do not consume steps. Reaching the cap
74
+ // while tools still execute resets the budget with a notice; the turn stops
75
+ // only when no tool ran since the last checkpoint (the doom-loop
76
+ // signature). 0 disables the cap entirely (opencode/codex have none).
77
+ maxSteps: z.number().int().min(0).default(100),
78
+ contextMaxTokens: z.number().int().positive().default(1e5),
79
+ // Token threshold at which the agent loop auto-compacts the history.
80
+ // Defaults to the effective context window (compact only when full); set a
81
+ // lower value to compact earlier and keep headroom. Manual /compact ignores
82
+ // this — it always compacts.
83
+ compactThresholdTokens: z.number().int().positive().optional(),
84
+ // Seconds without any stream part before a stalled response is ended
85
+ // gracefully (some relays never send the terminal chunks). Applies once
86
+ // streaming has started; the first part gets a longer, fixed allowance.
87
+ streamIdleTimeoutSec: z.number().positive().default(20),
88
+ // Seconds to wait for the very first stream part before giving up; slow
89
+ // thinking-model relays can buffer for minutes before sending anything.
90
+ streamFirstChunkTimeoutSec: z.number().positive().default(300),
91
+ // Extra attempts per model request when a stream fails transiently
92
+ // (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
93
+ // relay hiccup does not silently end a half-finished turn. 0 disables.
94
+ // The wait between attempts honors Retry-After headers and otherwise backs
95
+ // off exponentially with jitter (llm/retry.ts); retries after a
96
+ // timeout/empty failure also scale both stream timeouts up (attempt number
97
+ // ×, capped at 3x) since the relay is likely overloaded. Default 5 matches
98
+ // opencode/Codex retry budgets and rides out flaky relays (中转站) better
99
+ // than a minimal budget.
100
+ streamMaxRetries: z.number().int().min(0).default(5),
101
+ // How many consecutive unproductive replies may be nudged before the turn
102
+ // is handed back: a text-only reply that announces pending work ("我将…",
103
+ // "I will…") or answers a nudge with more words, or an empty reply that
104
+ // survived the stream retry budget (steered with a nudge — a changed
105
+ // request — since identical resends already failed). Tool calls reset the
106
+ // count, so a productive task keeps going; exhaustion surfaces a notice
107
+ // instead of stopping silently. Never fires in plan mode. 0 disables.
108
+ maxAutoContinues: z.number().int().min(0).default(2),
109
+ contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
110
+ // Optional per-session dollar budget; when set, the REPL stops the session
111
+ // once the estimated cost for the session reaches this amount.
112
+ sessionBudgetUsd: z.number().positive().optional(),
113
+ // Terminal bell (REPL only): ring when a turn takes longer than
114
+ // notifyBellThresholdSec, and when a background task finishes.
115
+ // STAR_NO_NOTIFY=1 disables without touching the config.
116
+ notifyBell: z.boolean().default(true),
117
+ notifyBellThresholdSec: z.number().positive().default(10),
118
+ permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
119
+ hooks: z.array(HookConfigSchema).default([]),
120
+ // Consecutive identical tool calls (same tool + same arguments) allowed
121
+ // before the loop refuses to execute the repeat and tells the model to
122
+ // change approach — a guard against agents stuck retrying a failing call
123
+ // (opencode's doom_loop). 0 disables.
124
+ doomLoopThreshold: z.number().int().min(0).default(3),
125
+ // Track the whole working tree with an internal git repo (outside the
126
+ // user's .git) at every turn boundary, so /undo and /redo can restore
127
+ // changes made by ANY tool (bash included), not just write_file/edit_file.
128
+ // Falls back silently to per-file snapshots when git is unavailable.
129
+ gitSnapshots: z.boolean().default(true)
130
+ });
131
+ function contextWindowTokens(config, modelName) {
132
+ return config.models.find((m) => m.name === modelName)?.contextMaxTokens ?? config.contextMaxTokens;
133
+ }
134
+ function resolveCompactThreshold(config, windowTokens) {
135
+ return Math.min(config.compactThresholdTokens ?? windowTokens, windowTokens);
136
+ }
137
+
15
138
  // src/context/compaction.ts
16
139
  import { generateText } from "ai";
17
140
 
@@ -106,8 +229,9 @@ ${transcript}`,
106
229
  });
107
230
  return text.trim();
108
231
  }
109
- function compactMessages(messages, maxTokens) {
110
- if (estimateTokens(messages) <= maxTokens) {
232
+ function compactMessages(messages, maxTokens, opts = {}) {
233
+ const limit = opts.force ? 0 : maxTokens;
234
+ if (estimateTokens(messages) <= limit) {
111
235
  return { messages, compacted: false, droppedCount: 0 };
112
236
  }
113
237
  const first = messages[0];
@@ -133,7 +257,7 @@ function compactMessages(messages, maxTokens) {
133
257
  droppedCount += turn.length;
134
258
  turnIndex += 1;
135
259
  const candidate = [...head, placeholderMessage(droppedCount), ...rest.slice(droppedCount)];
136
- if (estimateTokens(candidate) <= maxTokens) {
260
+ if (estimateTokens(candidate) <= limit) {
137
261
  break;
138
262
  }
139
263
  }
@@ -1091,7 +1215,7 @@ async function rewindToSnapshot(id, includeSubagent = false) {
1091
1215
  import { spawn as spawn3 } from "child_process";
1092
1216
  import { existsSync as existsSync2 } from "fs";
1093
1217
  import path4 from "path";
1094
- import { z } from "zod";
1218
+ import { z as z2 } from "zod";
1095
1219
 
1096
1220
  // src/tasks/manager.ts
1097
1221
  import { spawn as spawn2 } from "child_process";
@@ -1270,11 +1394,11 @@ function resolveShell() {
1270
1394
  }
1271
1395
  return cachedSpec;
1272
1396
  }
1273
- var schema = z.object({
1274
- command: z.string().describe("Shell command to execute"),
1275
- timeout: z.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1276
- description: z.string().optional().describe("Short description of what the command does"),
1277
- run_in_background: z.boolean().optional().describe(
1397
+ var schema = z2.object({
1398
+ command: z2.string().describe("Shell command to execute"),
1399
+ timeout: z2.number().positive().max(MAX_TIMEOUT).optional().describe("Timeout in seconds (default 120, max 600)"),
1400
+ description: z2.string().optional().describe("Short description of what the command does"),
1401
+ run_in_background: z2.boolean().optional().describe(
1278
1402
  "Run the command in the background and return a task id immediately (default false). Use task_output/task_kill to inspect or stop it."
1279
1403
  )
1280
1404
  });
@@ -1408,12 +1532,12 @@ Command timed out after ${timeoutSeconds}s`,
1408
1532
  // src/tools/fs/edit.ts
1409
1533
  import { readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1410
1534
  import path5 from "path";
1411
- import { z as z2 } from "zod";
1412
- var schema2 = z2.object({
1413
- path: z2.string().describe("File path, absolute or relative to the working directory"),
1414
- old_string: z2.string().min(1).describe("Exact text to replace"),
1415
- new_string: z2.string().describe("Replacement text"),
1416
- replace_all: z2.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1535
+ import { z as z3 } from "zod";
1536
+ var schema2 = z3.object({
1537
+ path: z3.string().describe("File path, absolute or relative to the working directory"),
1538
+ old_string: z3.string().min(1).describe("Exact text to replace"),
1539
+ new_string: z3.string().describe("Replacement text"),
1540
+ replace_all: z3.boolean().optional().describe("Replace every occurrence instead of requiring a unique match")
1417
1541
  });
1418
1542
  var editFileTool = {
1419
1543
  name: "edit_file",
@@ -1477,7 +1601,7 @@ var editFileTool = {
1477
1601
 
1478
1602
  // src/tools/fs/glob.ts
1479
1603
  import path7 from "path";
1480
- import { z as z3 } from "zod";
1604
+ import { z as z4 } from "zod";
1481
1605
 
1482
1606
  // src/tools/fs/util.ts
1483
1607
  import { readFileSync, statSync } from "fs";
@@ -1708,11 +1832,11 @@ function literalDirPrefix(pattern) {
1708
1832
  }
1709
1833
  return out;
1710
1834
  }
1711
- var schema3 = z3.object({
1712
- pattern: z3.string().describe(
1835
+ var schema3 = z4.object({
1836
+ pattern: z4.string().describe(
1713
1837
  "Glob pattern, e.g. 'src/**/*.ts'. Supports **, *, ?, {a,b} braces and [abc] character classes. A bare pattern like '*.ts' matches basenames recursively."
1714
1838
  ),
1715
- path: z3.string().optional().describe("Directory to search, defaults to the working directory")
1839
+ path: z4.string().optional().describe("Directory to search, defaults to the working directory")
1716
1840
  });
1717
1841
  var globTool = {
1718
1842
  name: "glob",
@@ -1736,7 +1860,7 @@ var globTool = {
1736
1860
  // src/tools/fs/grep.ts
1737
1861
  import { readFile as readFile3, stat as stat4 } from "fs/promises";
1738
1862
  import path8 from "path";
1739
- import { z as z4 } from "zod";
1863
+ import { z as z5 } from "zod";
1740
1864
  var MAX_MATCHES = 250;
1741
1865
  var MAX_LINE_CHARS = 500;
1742
1866
  var MAX_PATTERN_CHARS = 200;
@@ -1744,11 +1868,11 @@ var MAX_FILE_BYTES = 5 * 1024 * 1024;
1744
1868
  function isTooComplexPattern(pattern) {
1745
1869
  return pattern.length > MAX_PATTERN_CHARS || /\)[+*]/.test(pattern) || /\)\{\d+(,\d*)?\}/.test(pattern);
1746
1870
  }
1747
- var schema4 = z4.object({
1748
- pattern: z4.string().describe("Regular expression to search for"),
1749
- path: z4.string().optional().describe("File or directory to search, defaults to the working directory"),
1750
- glob: z4.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1751
- ignoreCase: z4.boolean().optional().describe("Case-insensitive matching")
1871
+ var schema4 = z5.object({
1872
+ pattern: z5.string().describe("Regular expression to search for"),
1873
+ path: z5.string().optional().describe("File or directory to search, defaults to the working directory"),
1874
+ glob: z5.string().optional().describe("Glob pattern to filter which files are searched, e.g. '*.ts'"),
1875
+ ignoreCase: z5.boolean().optional().describe("Case-insensitive matching")
1752
1876
  });
1753
1877
  var grepTool = {
1754
1878
  name: "grep",
@@ -1847,7 +1971,7 @@ var grepTool = {
1847
1971
  // src/tools/fs/read.ts
1848
1972
  import { open, stat as stat5 } from "fs/promises";
1849
1973
  import path9 from "path";
1850
- import { z as z5 } from "zod";
1974
+ import { z as z6 } from "zod";
1851
1975
  var DEFAULT_LIMIT = 2e3;
1852
1976
  var MAX_CHARS = 100 * 1024;
1853
1977
  var MAX_READ_BYTES = 1024 * 1024;
@@ -1873,10 +1997,10 @@ function decodeTextBuffer(buf) {
1873
1997
  }
1874
1998
  return { text: buf.toString("utf8") };
1875
1999
  }
1876
- var schema5 = z5.object({
1877
- path: z5.string().describe("File path, absolute or relative to the working directory"),
1878
- offset: z5.number().int().positive().optional().describe("1-based line number to start reading from"),
1879
- limit: z5.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
2000
+ var schema5 = z6.object({
2001
+ path: z6.string().describe("File path, absolute or relative to the working directory"),
2002
+ offset: z6.number().int().positive().optional().describe("1-based line number to start reading from"),
2003
+ limit: z6.number().int().positive().optional().describe("Maximum number of lines to read (default 2000)")
1880
2004
  });
1881
2005
  var readFileTool = {
1882
2006
  name: "read_file",
@@ -1949,10 +2073,10 @@ var readFileTool = {
1949
2073
  // src/tools/fs/write.ts
1950
2074
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1951
2075
  import path10 from "path";
1952
- import { z as z6 } from "zod";
1953
- var schema6 = z6.object({
1954
- path: z6.string().describe("File path, absolute or relative to the working directory"),
1955
- content: z6.string().describe("Full content to write to the file")
2076
+ import { z as z7 } from "zod";
2077
+ var schema6 = z7.object({
2078
+ path: z7.string().describe("File path, absolute or relative to the working directory"),
2079
+ content: z7.string().describe("Full content to write to the file")
1956
2080
  });
1957
2081
  var writeFileTool = {
1958
2082
  name: "write_file",
@@ -1974,7 +2098,7 @@ var writeFileTool = {
1974
2098
  };
1975
2099
 
1976
2100
  // src/tools/tasks.ts
1977
- import { z as z7 } from "zod";
2101
+ import { z as z8 } from "zod";
1978
2102
 
1979
2103
  // src/agent/agent-tasks.ts
1980
2104
  import { EventEmitter as EventEmitter2 } from "events";
@@ -2086,6 +2210,20 @@ var AgentTaskManager = class extends EventEmitter2 {
2086
2210
  }
2087
2211
  };
2088
2212
  var defaultAgentTasks = new AgentTaskManager();
2213
+ function formatAgentTaskUpdate(task) {
2214
+ const label = task.description ? `: ${task.description}` : "";
2215
+ if (task.status === "running") {
2216
+ return `Background subagent ${task.id} started${label}`;
2217
+ }
2218
+ let text = `Background subagent ${task.id} ${task.status}${label}`;
2219
+ if (task.status === "failed") {
2220
+ const reason = (task.result.split("\n")[0] ?? "").replace(/^Subagent failed:\s*/, "");
2221
+ if (reason) {
2222
+ text += ` \u2014 ${reason.length > 140 ? `${reason.slice(0, 140)}\u2026` : reason}`;
2223
+ }
2224
+ }
2225
+ return text;
2226
+ }
2089
2227
 
2090
2228
  // src/tasks/format.ts
2091
2229
  function formatDuration(startedAt, endedAt) {
@@ -2131,8 +2269,8 @@ function formatTaskFinished(task) {
2131
2269
  }
2132
2270
 
2133
2271
  // src/tools/tasks.ts
2134
- var idSchema = z7.object({
2135
- id: z7.string().describe("Task id, e.g. task-1 or agent-1")
2272
+ var idSchema = z8.object({
2273
+ id: z8.string().describe("Task id, e.g. task-1 or agent-1")
2136
2274
  });
2137
2275
  function truncateOutput(s) {
2138
2276
  if (s.length <= MAX_OUTPUT) return s;
@@ -2149,7 +2287,7 @@ var taskListTool = {
2149
2287
  name: "task_list",
2150
2288
  description: "List background tasks (shell commands and subagents) with status and runtime.",
2151
2289
  permission: "read",
2152
- parameters: z7.object({}),
2290
+ parameters: z8.object({}),
2153
2291
  execute() {
2154
2292
  const shell = formatTaskList(defaultTaskManager.list());
2155
2293
  const agents = defaultAgentTasks.list();
@@ -2210,16 +2348,16 @@ var taskKillTool = {
2210
2348
  // src/tools/todo.ts
2211
2349
  import { mkdir as mkdir4, readFile as readFile4, rename, writeFile as writeFile4 } from "fs/promises";
2212
2350
  import path11 from "path";
2213
- import { z as z8 } from "zod";
2214
- var todoItemSchema = z8.object({
2215
- id: z8.number().int(),
2216
- title: z8.string(),
2217
- status: z8.enum(["pending", "in_progress", "done"])
2351
+ import { z as z9 } from "zod";
2352
+ var todoItemSchema = z9.object({
2353
+ id: z9.number().int(),
2354
+ title: z9.string(),
2355
+ status: z9.enum(["pending", "in_progress", "done"])
2218
2356
  });
2219
- var writeSchema = z8.object({
2220
- todos: z8.array(todoItemSchema).describe("The full todo list, replacing the current one")
2357
+ var writeSchema = z9.object({
2358
+ todos: z9.array(todoItemSchema).describe("The full todo list, replacing the current one")
2221
2359
  });
2222
- var readSchema = z8.object({});
2360
+ var readSchema = z9.object({});
2223
2361
  function fileFor(cwd) {
2224
2362
  return path11.join(cwd, ".star", "todos.json");
2225
2363
  }
@@ -2335,13 +2473,13 @@ function createTodoTools(store = defaultStore) {
2335
2473
  }
2336
2474
 
2337
2475
  // src/tools/web/fetch.ts
2338
- import { z as z9 } from "zod";
2476
+ import { z as z10 } from "zod";
2339
2477
  var DEFAULT_MAX_CHARS = 2e4;
2340
2478
  var MAX_BYTES = 2 * 1024 * 1024;
2341
2479
  var TIMEOUT_MS = 3e4;
2342
- var schema7 = z9.object({
2343
- url: z9.string().describe("The http(s) URL to fetch"),
2344
- maxChars: z9.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
2480
+ var schema7 = z10.object({
2481
+ url: z10.string().describe("The http(s) URL to fetch"),
2482
+ maxChars: z10.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
2345
2483
  });
2346
2484
  function safeCodePoint(n, raw) {
2347
2485
  if (!Number.isInteger(n) || n < 0 || n > 1114111) {
@@ -2475,15 +2613,15 @@ var webFetchTool = {
2475
2613
  };
2476
2614
 
2477
2615
  // src/tools/web/search.ts
2478
- import { z as z10 } from "zod";
2616
+ import { z as z11 } from "zod";
2479
2617
  var TIMEOUT_MS2 = 15e3;
2480
2618
  var DEFAULT_MAX_RESULTS = 5;
2481
2619
  var MAX_RESULTS_LIMIT = 10;
2482
2620
  var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
2483
2621
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
2484
- var schema8 = z10.object({
2485
- query: z10.string().min(1).describe("The search query"),
2486
- maxResults: z10.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
2622
+ var schema8 = z11.object({
2623
+ query: z11.string().min(1).describe("The search query"),
2624
+ maxResults: z11.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
2487
2625
  `Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
2488
2626
  )
2489
2627
  });
@@ -2664,7 +2802,7 @@ ${body}`;
2664
2802
  // src/agent/skills.ts
2665
2803
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "fs";
2666
2804
  import path13 from "path";
2667
- import { z as z11 } from "zod";
2805
+ import { z as z12 } from "zod";
2668
2806
  var SKILL_FILE = "SKILL.md";
2669
2807
  var SKILL_MAX_CHARS = 32 * 1024;
2670
2808
  var NAME_PATTERN = /^[a-z0-9-]+$/;
@@ -2749,8 +2887,8 @@ function createSkillTool(deps) {
2749
2887
  name: "skill",
2750
2888
  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.",
2751
2889
  permission: "read",
2752
- parameters: z11.object({
2753
- name: z11.string().describe("Name of the skill to load, as listed in the system prompt.")
2890
+ parameters: z12.object({
2891
+ name: z12.string().describe("Name of the skill to load, as listed in the system prompt.")
2754
2892
  }),
2755
2893
  async execute(args) {
2756
2894
  const skills = discoverSkills(deps.cwd, deps.home);
@@ -2780,7 +2918,7 @@ function createSkillTool(deps) {
2780
2918
  }
2781
2919
 
2782
2920
  // src/agent/subagent.ts
2783
- import { z as z12 } from "zod";
2921
+ import { z as z13 } from "zod";
2784
2922
 
2785
2923
  // src/tools/index.ts
2786
2924
  function createDefaultRegistry(todoStore) {
@@ -2792,7 +2930,7 @@ var MAX_SUBAGENT_DEPTH = 1;
2792
2930
  var MAX_REPORT_CHARS = 2e4;
2793
2931
  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.";
2794
2932
  async function runSubagent(deps, args, cwd, signal) {
2795
- const { AgentLoop: AgentLoop2 } = await import("./loop-TQ7GFTTE.js");
2933
+ const { AgentLoop: AgentLoop2 } = await import("./loop-H5RYTO2O.js");
2796
2934
  const child = new AgentLoop2({
2797
2935
  model: deps.model,
2798
2936
  registry: createDefaultRegistry(),
@@ -2850,10 +2988,10 @@ function createSubagentTool(deps) {
2850
2988
  name: "subagent",
2851
2989
  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).",
2852
2990
  permission: "exec",
2853
- parameters: z12.object({
2854
- prompt: z12.string().describe("Complete, self-contained instructions for the subagent."),
2855
- description: z12.string().optional().describe("Short label for the subtask, shown in the result header."),
2856
- run_in_background: z12.boolean().optional().describe(
2991
+ parameters: z13.object({
2992
+ prompt: z13.string().describe("Complete, self-contained instructions for the subagent."),
2993
+ description: z13.string().optional().describe("Short label for the subtask, shown in the result header."),
2994
+ run_in_background: z13.boolean().optional().describe(
2857
2995
  "Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
2858
2996
  )
2859
2997
  }),
@@ -2881,7 +3019,7 @@ function createSubagentTool(deps) {
2881
3019
  // src/agent/user-memory.ts
2882
3020
  import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
2883
3021
  import path14 from "path";
2884
- import { z as z13 } from "zod";
3022
+ import { z as z14 } from "zod";
2885
3023
  var USER_MEMORY_MAX_CHARS = 32 * 1024;
2886
3024
  var REMEMBER_TEXT_MAX_CHARS = 500;
2887
3025
  var cache3 = /* @__PURE__ */ new Map();
@@ -2926,8 +3064,8 @@ function createRememberTool(deps = {}) {
2926
3064
  name: "remember",
2927
3065
  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.`,
2928
3066
  permission: "write",
2929
- parameters: z13.object({
2930
- text: z13.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
3067
+ parameters: z14.object({
3068
+ text: z14.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
2931
3069
  }),
2932
3070
  async execute(args) {
2933
3071
  appendUserMemory(args.text, deps.home);
@@ -3343,7 +3481,27 @@ var AgentLoop = class {
3343
3481
  let lastWasNudge = false;
3344
3482
  let usedToolsThisTurn = false;
3345
3483
  let openTodos = [];
3346
- for (let step = 0; step < config.maxSteps; step++) {
3484
+ const maxSteps = config.maxSteps;
3485
+ let step = 0;
3486
+ let toolExecutedSinceCheckpoint = false;
3487
+ for (; ; ) {
3488
+ if (maxSteps > 0 && step >= maxSteps) {
3489
+ if (!toolExecutedSinceCheckpoint) {
3490
+ yield {
3491
+ type: "error",
3492
+ error: new Error(
3493
+ `Max steps (${maxSteps}) reached with no tool execution since the last checkpoint, stopping.`
3494
+ )
3495
+ };
3496
+ return;
3497
+ }
3498
+ toolExecutedSinceCheckpoint = false;
3499
+ step = 0;
3500
+ yield {
3501
+ type: "notice",
3502
+ message: `maxSteps (${maxSteps}) reached and the turn is still making progress; continuing. Press Esc to interrupt.`
3503
+ };
3504
+ }
3347
3505
  if ((this.opts.subagentDepth ?? 0) === 0) {
3348
3506
  for (const finished of defaultAgentTasks.drainNotifications()) {
3349
3507
  const label = finished.description ? ` "${finished.description}"` : "";
@@ -3357,7 +3515,10 @@ ${finished.result}`
3357
3515
  }
3358
3516
  }
3359
3517
  const maxTokens = this.opts.contextMaxTokens ?? config.contextMaxTokens;
3360
- const compacted = compactMessages([...this.messages], maxTokens);
3518
+ const compacted = compactMessages(
3519
+ [...this.messages],
3520
+ resolveCompactThreshold(config, maxTokens)
3521
+ );
3361
3522
  if (compacted.compacted) {
3362
3523
  this.messages = await this.applyCompactionSummary(compacted, signal);
3363
3524
  this.turnMarkers = [];
@@ -3425,7 +3586,7 @@ ${finished.result}`
3425
3586
  if (attempt >= maxRetries) break;
3426
3587
  if (!failure && text.length === 0 && toolCalls.length === 0 && attempt >= 1 && (isFilteredEmptyFinish(lastFinishReason) || sawReasoning || lastUsage !== void 0 && lastUsage.completionTokens > 0))
3427
3588
  break;
3428
- const delayMs = computeRetryDelayMs(attempt, baseDelay, failure);
3589
+ const delayMs = computeRetryDelayMs(attempt, failure ? baseDelay : baseDelay * 2, failure);
3429
3590
  yield {
3430
3591
  type: "retry",
3431
3592
  attempt: attempt + 2,
@@ -3528,6 +3689,8 @@ ${finished.result}`
3528
3689
  const refusal = this.doomLoopRefusal(call);
3529
3690
  if (refusal !== null) {
3530
3691
  yield { type: "notice", message: refusal };
3692
+ } else {
3693
+ toolExecutedSinceCheckpoint = true;
3531
3694
  }
3532
3695
  const result = refusal !== null ? { content: refusal, isError: true } : await this.executeTool(call, signal);
3533
3696
  const toolMessage = {
@@ -3577,11 +3740,8 @@ ${finished.result}`
3577
3740
  }
3578
3741
  }
3579
3742
  if (signal.aborted) return;
3743
+ step++;
3580
3744
  }
3581
- yield {
3582
- type: "error",
3583
- error: new Error(`Max steps (${config.maxSteps}) reached, stopping.`)
3584
- };
3585
3745
  }
3586
3746
  // The permission mode can change at runtime, so the system message is
3587
3747
  // recomputed at the start of every turn instead of being frozen by the
@@ -3772,6 +3932,9 @@ ${summary}`
3772
3932
 
3773
3933
  export {
3774
3934
  defaultAgentTasks,
3935
+ formatAgentTaskUpdate,
3936
+ ConfigSchema,
3937
+ contextWindowTokens,
3775
3938
  estimateTokens,
3776
3939
  summarizeMessages,
3777
3940
  compactMessages,
@@ -3814,4 +3977,4 @@ export {
3814
3977
  PLAN_MODE_PROMPT,
3815
3978
  AgentLoop
3816
3979
  };
3817
- //# sourceMappingURL=chunk-3L6QKJM4.js.map
3980
+ //# sourceMappingURL=chunk-OIDB2E5J.js.map