c0de-agent 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/core/agent.js +4 -0
  2. package/dist/core/config.js +1 -1
  3. package/dist/core/index.d.ts +3 -3
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/loop.js +9 -0
  6. package/dist/core/prompt-registry.d.ts +2 -2
  7. package/dist/core/prompt-registry.js +42 -3
  8. package/dist/core/slash.js +102 -6
  9. package/dist/core/types.d.ts +10 -1
  10. package/dist/core/workflow.d.ts +1 -1
  11. package/dist/core/workflow.js +56 -5
  12. package/dist/core/workflows/discovery.d.ts +23 -2
  13. package/dist/core/workflows/discovery.js +38 -2
  14. package/dist/core/workflows/index.d.ts +3 -2
  15. package/dist/core/workflows/index.js +2 -2
  16. package/dist/core/workflows/registry.d.ts +8 -1
  17. package/dist/core/workflows/registry.js +21 -1
  18. package/dist/core/workflows/runtime.d.ts +3 -0
  19. package/dist/core/workflows/runtime.js +2 -2
  20. package/dist/project/resolve.d.ts +75 -0
  21. package/dist/project/resolve.js +253 -1
  22. package/dist/server/app.js +3 -0
  23. package/dist/server/context.js +2 -1
  24. package/dist/server/dev.js +3 -1
  25. package/dist/server/routes/chat.js +12 -0
  26. package/dist/server/routes/commands.js +1 -0
  27. package/dist/server/routes/files.js +252 -4
  28. package/dist/server/routes/terminal.js +2 -1
  29. package/dist/server/routes/todo.d.ts +4 -0
  30. package/dist/server/routes/todo.js +107 -0
  31. package/dist/server/routes/workflows.js +83 -10
  32. package/dist/server/server.d.ts +8 -1
  33. package/dist/server/server.js +113 -23
  34. package/dist/server/terminal/pty-manager.d.ts +14 -0
  35. package/dist/server/terminal/pty-manager.js +105 -7
  36. package/dist/shared/types/agent.d.ts +9 -0
  37. package/dist/shared/types/config.d.ts +6 -0
  38. package/dist/shared/types/tool.d.ts +13 -0
  39. package/dist/tools/builtin/todo.d.ts +67 -0
  40. package/dist/tools/builtin/todo.js +517 -0
  41. package/dist/tools/index.d.ts +2 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/types.d.ts +32 -1
  44. package/package.json +2 -1
@@ -1,13 +1,14 @@
1
1
  // src/server/server.ts
2
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { connect as tcpConnect } from 'node:net';
4
- import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
5
6
  import { serve } from '@hono/node-server';
6
7
  import { WebSocketServer } from 'ws';
7
8
  import { BUILTIN_AGENTS, createAgentRegistry } from '../core/agents/index.js';
8
9
  import { loadConfig } from '../core/config.js';
9
10
  import { decryptSecret } from '../core/secret.js';
10
- import { BUILTIN_WORKFLOWS, createWorkflowRegistry } from '../core/workflows/index.js';
11
+ import { createAndPopulateRegistry } from '../core/workflows/index.js';
11
12
  import { createDB, migrateDB } from '../db/index.js';
12
13
  import { createRegistry, overrideToCapabilities, registerProvider, } from '../llm/registry.js';
13
14
  import { initPlugins } from '../plugins/index.js';
@@ -53,12 +54,53 @@ function syncRegistryFromConfig(registry, config) {
53
54
  registerProviderFromConfig(registry, p);
54
55
  }
55
56
  }
56
- /** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则 <cwd>/.c0de/pglite */
57
- function resolveDbDir(cwd) {
57
+ /** 全局数据根目录:XDG_DATA_HOME 优先,否则 ~/.local/share/c0de。
58
+ * 与 opencode (~/.local/share/opencode/)、oh-my-pi (~/.omp/agent/) 同约定——
59
+ * 数据库等运行时数据放全局,项目 .c0de/ 只留配置和扩展。 */
60
+ function resolveGlobalDataRoot() {
61
+ const xdg = process.env.XDG_DATA_HOME;
62
+ if (xdg && xdg.trim() !== '')
63
+ return join(xdg, 'c0de');
64
+ return join(homedir(), '.local', 'share', 'c0de');
65
+ }
66
+ /** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则全局数据根下 pglite 子目录。
67
+ *
68
+ * 历史上数据库放在 <cwd>/.c0de/pglite(项目级),但项目 DB schema 通过 projectId
69
+ * 区分项目,设计本意是全局共享单库。旧路径已在 migrateLegacyPglite 中自动迁移。 */
70
+ function resolveDbDir() {
58
71
  const envDir = process.env.C0DE_DB_DIR;
59
72
  if (envDir && envDir.trim() !== '')
60
73
  return envDir;
61
- return join(cwd, '.c0de', 'pglite');
74
+ return join(resolveGlobalDataRoot(), 'pglite');
75
+ }
76
+ /** 一次性迁移:<cwd>/.c0de/pglite → 全局路径。仅当目标不存在时执行。
77
+ *
78
+ * 多个项目各有 .c0de/pglite 时只迁移第一个遇到的(首次启动即触发);
79
+ * 其余项目的旧数据保留在原地(已被 .gitignore 覆盖,不影响 git,但不再读取)。
80
+ * 用户可用 C0DE_DB_DIR 手动指向旧路径访问遗留数据。 */
81
+ function migrateLegacyPglite(cwd, targetDir) {
82
+ const legacy = join(cwd, '.c0de', 'pglite');
83
+ if (!existsSync(legacy))
84
+ return;
85
+ if (existsSync(targetDir))
86
+ return; // 全局目录已有数据,不覆盖
87
+ const parent = dirname(targetDir);
88
+ if (!existsSync(parent))
89
+ mkdirSync(parent, { recursive: true });
90
+ try {
91
+ renameSync(legacy, targetDir);
92
+ return;
93
+ }
94
+ catch {
95
+ // 跨文件系统 rename 失败,fallback 到 copy
96
+ }
97
+ try {
98
+ cpSync(legacy, targetDir, { recursive: true });
99
+ rmSync(legacy, { recursive: true, force: true });
100
+ }
101
+ catch {
102
+ // 迁移失败:旧数据留在项目目录(已 gitignore,无害),用户可手动迁移
103
+ }
62
104
  }
63
105
  /**
64
106
  * 围绕已有 DB handle 组装 ServerContext(不建/不关闭 DB)。
@@ -83,8 +125,9 @@ async function buildServerContext(db, opts = {}) {
83
125
  toolRegistry,
84
126
  llmRegistry,
85
127
  });
86
- // workflowRegistry 惰性初始化 backing field(见下方 ctx getter)。
87
- let _workflowRegistry;
128
+ // 工作流注册表:三级发现(builtin global project),eager 初始化。
129
+ // 此前是惰性 getter 只注册 builtin,导致项目级 .c0de/workflows/*.js 永远不可见。
130
+ const workflowRegistry = await createAndPopulateRegistry(cwd);
88
131
  const ctx = {
89
132
  db,
90
133
  config,
@@ -103,16 +146,7 @@ async function buildServerContext(db, opts = {}) {
103
146
  reg.register(def);
104
147
  return reg;
105
148
  })(),
106
- // 工作流注册表:惰性初始化,只含内置(项目级 discovery 由 bootstrap 或 API 触发热加载)。
107
- // 与 context.ts 的 createServerContext 保持一致的模式。
108
- get workflowRegistry() {
109
- if (!_workflowRegistry) {
110
- _workflowRegistry = createWorkflowRegistry();
111
- for (const wf of BUILTIN_WORKFLOWS)
112
- _workflowRegistry.register(wf);
113
- }
114
- return _workflowRegistry;
115
- },
149
+ workflowRegistry,
116
150
  // spec §18.1 后台版本检查调度器;config.update.enabled 控制是否启动。
117
151
  updateScheduler: createUpdateScheduler({
118
152
  checkFn: opts.checkForUpdateFn ?? checkForUpdate,
@@ -149,18 +183,74 @@ async function buildServerContext(db, opts = {}) {
149
183
  },
150
184
  };
151
185
  }
186
+ const DEV_LOCK_FILE = '.dev.lock';
187
+ /**
188
+ * Cross-process guard for PGLite dataDir.
189
+ *
190
+ * PGLite is single-writer WASM Postgres — two processes on the same dataDir
191
+ * always abort (`RuntimeError: Aborted()`). This lock prevents silent
192
+ * corruption when multiple dev servers (e.g. different ports) target the
193
+ * same project.
194
+ *
195
+ * - Live PID in lock → throw clear error instead of cryptic WASM abort.
196
+ * - Dead PID in lock → stale: remove `.dev.lock` + `postmaster.pid`, proceed.
197
+ */
198
+ function acquireDevDbLock(dataDir) {
199
+ const lockPath = join(dataDir, DEV_LOCK_FILE);
200
+ if (existsSync(lockPath)) {
201
+ const oldPid = parseInt(readFileSync(lockPath, 'utf8').trim(), 10);
202
+ let stale = false;
203
+ if (oldPid && oldPid !== process.pid) {
204
+ try {
205
+ process.kill(oldPid, 0); // signal 0 = liveness check
206
+ }
207
+ catch (err) {
208
+ // ESRCH = process dead → stale lock, fall through to cleanup.
209
+ // Any other error (EPERM etc.) re-throws — don't clobber a live lock.
210
+ if (err.code !== 'ESRCH')
211
+ throw err;
212
+ stale = true;
213
+ }
214
+ if (!stale) {
215
+ throw new Error(`Database is locked by another c0de process (PID ${oldPid}).\n` +
216
+ `PGLite is single-writer — only one process may use:\n ${dataDir}\n` +
217
+ `Kill the other process and retry:\n kill ${oldPid}`);
218
+ }
219
+ }
220
+ // Stale lock cleanup
221
+ try {
222
+ unlinkSync(lockPath);
223
+ }
224
+ catch { /* best-effort */ }
225
+ try {
226
+ unlinkSync(join(dataDir, 'postmaster.pid'));
227
+ }
228
+ catch { /* best-effort */ }
229
+ }
230
+ writeFileSync(lockPath, String(process.pid));
231
+ }
232
+ /** Release the dev DB lock (best-effort, stale-detection covers missed calls). */
233
+ function releaseDevDbLock(dataDir) {
234
+ try {
235
+ unlinkSync(join(dataDir, DEV_LOCK_FILE));
236
+ }
237
+ catch { /* best-effort */ }
238
+ }
152
239
  /** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
153
240
  async function createDevDb(cwd) {
154
- const dataDir = resolveDbDir(cwd);
241
+ const dataDir = resolveDbDir();
242
+ migrateLegacyPglite(cwd, dataDir);
155
243
  if (!existsSync(dataDir))
156
244
  mkdirSync(dataDir, { recursive: true });
245
+ acquireDevDbLock(dataDir);
157
246
  const db = await createDB({ driver: 'pglite', dataDir });
158
- // migrateDB 失败必须 close,否则 WASM 实例泄漏锁住 dataDir。
247
+ // migrateDB 失败必须 close + release lock,否则 WASM 实例泄漏锁住 dataDir。
159
248
  try {
160
249
  await migrateDB(db);
161
250
  }
162
251
  catch (err) {
163
252
  await db.close().catch(() => { });
253
+ releaseDevDbLock(dataDir);
164
254
  throw err;
165
255
  }
166
256
  return db;
@@ -169,8 +259,8 @@ async function createDevDb(cwd) {
169
259
  async function bootstrapServerContext(opts = {}) {
170
260
  const cwd = opts.cwd ?? process.cwd();
171
261
  const ownsDb = !opts.db;
172
- // 持久化 PGLite 数据:默认 <cwd>/.c0de/pglite(与 .c0de/cache、.c0de/config.json 同约定),
173
- // 可用 C0DE_DB_DIR 覆盖。此前默认 in-memory,进程重启即丢全部会话/消息/调用详情。
262
+ // 持久化 PGLite 数据:全局路径 ~/.local/share/c0de/pglite(可用 C0DE_DB_DIR 覆盖)。
263
+ // 跨项目共享单库,通过 projects/sessions 表的 projectId 区分。
174
264
  // 测试注入 opts.db 时跳过(保持 in-memory 隔离)。
175
265
  let db;
176
266
  if (opts.db) {
@@ -282,4 +372,4 @@ async function startServer(opts = {}) {
282
372
  };
283
373
  return { app, port, close };
284
374
  }
285
- export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, startServer, syncRegistryFromConfig, };
375
+ export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveDbDir, startServer, syncRegistryFromConfig, };
@@ -9,6 +9,8 @@ export interface PTYInfo {
9
9
  cwd: string;
10
10
  /** shell 程序路径。 */
11
11
  shell: string;
12
+ /** 所属项目 id(未归属时为 undefined)。 */
13
+ projectId?: string;
12
14
  }
13
15
  export interface CreatePTYOptions {
14
16
  cwd: string;
@@ -17,7 +19,19 @@ export interface CreatePTYOptions {
17
19
  title?: string;
18
20
  /** 覆盖默认 shell;不传则自动检测。 */
19
21
  shell?: string;
22
+ /** 所属项目 id。 */
23
+ projectId?: string;
20
24
  }
25
+ /**
26
+ * 检测当前平台默认 shell。
27
+ *
28
+ * 优先级:process.env.SHELL → os.userInfo().shell(/etc/passwd 登录 shell)→ /bin/bash。
29
+ * 仅依赖 process.env.SHELL 是不可靠的:当 server 经 npm 脚本(sh -c 包装)、
30
+ * 热更新重启或 IDE 启动器等链路启动时,SHELL 往往未被 export 到环境,
31
+ * 导致 node 进程读不到而错误回退到 /bin/bash。userInfo().shell 直接读取
32
+ * /etc/passwd(getpwuid),是用户真实登录 shell 的可靠来源。
33
+ */
34
+ export declare function detectShell(): string;
21
35
  /**
22
36
  * PTY 生命周期管理器。
23
37
  *
@@ -1,16 +1,94 @@
1
1
  // src/server/terminal/pty-manager.ts
2
- import { spawn } from 'node-pty';
3
2
  import { randomUUID } from 'node:crypto';
4
- /** 检测当前平台默认 shell。 */
5
- function detectShell() {
3
+ import { mkdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir, userInfo } from 'node:os';
5
+ import { basename, join } from 'node:path';
6
+ import { spawn } from 'node-pty';
7
+ /**
8
+ * 为指定 shell 创建 OSC 133 prompt 标记注入脚本。
9
+ *
10
+ * shell 在每次显示 prompt 前发送 \x1b]133;A\x07(OSC 133;A),
11
+ * 前端 xterm.js parser 捕获后精确记录命令块起始行。
12
+ * 标记嵌入 PTY 输出流 → scrollback 回放时自动重建,无需正则猜测。
13
+ *
14
+ * 支持的 shell:bash(--init-file)、zsh(ZDOTDIR)。
15
+ * 其他 shell 不注入,退化到前端正则启发式。
16
+ */
17
+ function setupShellIntegration(shell) {
18
+ const base = basename(shell);
19
+ // printf 的 \x1b / \x07 由 shell 自行解释,JS 中双反斜杠确保写入字面量
20
+ const mark = "printf '\\x1b]133;A\\x07'";
21
+ if (base === 'bash') {
22
+ const script = join(tmpdir(), `c0de-bash-${randomUUID()}.sh`);
23
+ writeFileSync(script, [
24
+ '[ -f /etc/bash.bashrc ] && source /etc/bash.bashrc',
25
+ '[ -f ~/.bashrc ] && source ~/.bashrc',
26
+ `__c0de_prompt_mark() { ${mark}; }`,
27
+ // 追加到已有 PROMPT_COMMAND 而非覆盖
28
+ 'if [ -n "$PROMPT_COMMAND" ]; then',
29
+ ' PROMPT_COMMAND=\'__c0de_prompt_mark;\'"$PROMPT_COMMAND"',
30
+ 'else',
31
+ ' PROMPT_COMMAND=__c0de_prompt_mark',
32
+ 'fi',
33
+ ].join('\n'));
34
+ return {
35
+ spawnArgs: ['--init-file', script],
36
+ env: {},
37
+ cleanup: () => {
38
+ try {
39
+ unlinkSync(script);
40
+ }
41
+ catch {
42
+ // 文件可能已被清理
43
+ }
44
+ },
45
+ };
46
+ }
47
+ if (base === 'zsh') {
48
+ const dir = join(tmpdir(), `c0de-zsh-${randomUUID()}`);
49
+ mkdirSync(dir, { recursive: true });
50
+ writeFileSync(join(dir, '.zshrc'), [
51
+ '[ -f ~/.zshrc ] && source ~/.zshrc',
52
+ `__c0de_prompt_mark() { ${mark}; }`,
53
+ 'autoload -Uz add-zsh-hook',
54
+ 'add-zsh-hook precmd __c0de_prompt_mark',
55
+ ].join('\n'));
56
+ return {
57
+ spawnArgs: [],
58
+ env: { ZDOTDIR: dir },
59
+ cleanup: () => {
60
+ try {
61
+ rmSync(dir, { recursive: true });
62
+ }
63
+ catch {
64
+ // 目录可能已被清理
65
+ }
66
+ },
67
+ };
68
+ }
69
+ // fish / 其他 shell 暂不支持,退化到前端正则
70
+ return { spawnArgs: [], env: {}, cleanup: () => { } };
71
+ }
72
+ /**
73
+ * 检测当前平台默认 shell。
74
+ *
75
+ * 优先级:process.env.SHELL → os.userInfo().shell(/etc/passwd 登录 shell)→ /bin/bash。
76
+ * 仅依赖 process.env.SHELL 是不可靠的:当 server 经 npm 脚本(sh -c 包装)、
77
+ * 热更新重启或 IDE 启动器等链路启动时,SHELL 往往未被 export 到环境,
78
+ * 导致 node 进程读不到而错误回退到 /bin/bash。userInfo().shell 直接读取
79
+ * /etc/passwd(getpwuid),是用户真实登录 shell 的可靠来源。
80
+ */
81
+ export function detectShell() {
6
82
  if (process.platform === 'win32') {
7
83
  return process.env.COMSPEC ?? 'cmd.exe';
8
84
  }
9
- return process.env.SHELL ?? '/bin/bash';
85
+ return process.env.SHELL ?? userInfo().shell ?? '/bin/bash';
10
86
  }
11
87
  const DEFAULT_COLS = 80;
12
88
  const DEFAULT_ROWS = 24;
13
89
  const MAX_TITLE_LEN = 100;
90
+ /** scrollback 环形缓冲最大字节数(约 50KB)。 */
91
+ const SCROLLBACK_MAX = 50_000;
14
92
  function truncateTitle(title) {
15
93
  const clean = title.replace(/[\r\n]/g, ' ').trim();
16
94
  return clean.length > MAX_TITLE_LEN ? `${clean.slice(0, MAX_TITLE_LEN)}…` : clean;
@@ -30,13 +108,15 @@ export class PTYManager {
30
108
  const cols = opts.cols ?? DEFAULT_COLS;
31
109
  const rows = opts.rows ?? DEFAULT_ROWS;
32
110
  const shell = opts.shell ?? detectShell();
33
- const pty = spawn(shell, [], {
111
+ const integration = setupShellIntegration(shell);
112
+ const pty = spawn(shell, integration.spawnArgs, {
34
113
  name: 'xterm-256color',
35
114
  cols,
36
115
  rows,
37
116
  cwd: opts.cwd,
38
117
  env: {
39
118
  ...process.env,
119
+ ...integration.env,
40
120
  TERM: 'xterm-256color',
41
121
  COLORTERM: 'truecolor',
42
122
  },
@@ -49,10 +129,21 @@ export class PTYManager {
49
129
  rows,
50
130
  cwd: opts.cwd,
51
131
  shell,
132
+ projectId: opts.projectId,
133
+ };
134
+ const entry = {
135
+ pty,
136
+ info,
137
+ sockets: new Set(),
138
+ scrollback: '',
139
+ integrationCleanup: integration.cleanup,
52
140
  };
53
- const entry = { pty, info, sockets: new Set() };
54
- // PTY 输出 → 广播到所有挂载的 WebSocket
141
+ // PTY 输出 广播到所有挂载的 WebSocket + 追加 scrollback
55
142
  pty.onData((data) => {
143
+ entry.scrollback += data;
144
+ if (entry.scrollback.length > SCROLLBACK_MAX) {
145
+ entry.scrollback = entry.scrollback.slice(-SCROLLBACK_MAX);
146
+ }
56
147
  for (const ws of entry.sockets) {
57
148
  if (ws.readyState === ws.OPEN) {
58
149
  ws.send(data);
@@ -113,6 +204,7 @@ export class PTYManager {
113
204
  catch {
114
205
  // 进程可能已退出
115
206
  }
207
+ entry.integrationCleanup?.();
116
208
  this.entries.delete(id);
117
209
  }
118
210
  /** 获取 PTY 信息。 */
@@ -134,6 +226,12 @@ export class PTYManager {
134
226
  if (!entry)
135
227
  return false;
136
228
  entry.sockets.add(ws);
229
+ // 先回放 scrollback,让前端恢复终端历史画面。
230
+ // 先 add 再 send 避免漏数据:add 之后 onData 的新输出会广播到此 WS,
231
+ // 而 scrollback 覆盖 add 之前的所有历史输出。Node 单线程保证无竞态。
232
+ if (entry.scrollback) {
233
+ ws.send(entry.scrollback);
234
+ }
137
235
  ws.on('message', (data) => {
138
236
  const text = typeof data === 'string' ? data : data.toString('utf8');
139
237
  try {
@@ -206,6 +206,15 @@ type AgentState = {
206
206
  tokenBudget: TokenBudget;
207
207
  /** estimateTokens 的校准系数(由 calibrateEstimate 按真实 usage EMA 更新,默认 1.0)。 */
208
208
  calibrationFactor: number;
209
+ /** 分阶段任务列表(todo 工具状态)。in-memory,通过 tool result metadata 持久化。
210
+ * createAgent 时从历史消息恢复;每次 todo 工具调用通过 todoState hook 更新。 */
211
+ todoPhases: {
212
+ name: string;
213
+ tasks: {
214
+ content: string;
215
+ status: string;
216
+ }[];
217
+ }[];
209
218
  compactionModel?: {
210
219
  provider: string;
211
220
  model: string;
@@ -91,6 +91,12 @@ type Config = {
91
91
  retryDelay: number;
92
92
  };
93
93
  compaction: CompactionConfig;
94
+ /** 一键提交使用的独立模型。未设置时回退到 defaultProvider/defaultModel。
95
+ * commit message 生成对推理能力要求低,可指定便宜/快速模型以降低成本。 */
96
+ commitModel?: {
97
+ provider: string;
98
+ model: string;
99
+ };
94
100
  tools: {
95
101
  enabled: string[];
96
102
  disabled: string[];
@@ -1,4 +1,11 @@
1
1
  import type { JSONSchema, SessionRef } from './base.js';
2
+ export type TodoPhaseLike = {
3
+ name: string;
4
+ tasks: {
5
+ content: string;
6
+ status: string;
7
+ }[];
8
+ };
2
9
  /** Permission level for tool execution. */
3
10
  type ToolPermission = 'auto' | 'ask' | 'deny';
4
11
  /** Result of a tool execution. Discriminated by `_tag`. */
@@ -66,6 +73,12 @@ type ToolContext = {
66
73
  debugSpawn?: (config: unknown) => DebugTransport;
67
74
  /** 子 agent 专用:yield 工具调用时收集结构化结果(runSubAgent 注入)。 */
68
75
  collectYield?: (data: unknown) => void;
76
+ /** Todo state accessor (dependency-reversal for the `todo` tool).
77
+ * Host (agent loop) injects get/set backed by AgentState.todoPhases. */
78
+ todoState?: {
79
+ get: () => TodoPhaseLike[];
80
+ set: (phases: TodoPhaseLike[]) => void;
81
+ };
69
82
  };
70
83
  /** 单个并行子任务项(批量模式)。 */
71
84
  type TaskItem = {
@@ -0,0 +1,67 @@
1
+ import type { ToolDef } from '../../shared/types/tool.js';
2
+ type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'abandoned';
3
+ type TodoItem = {
4
+ content: string;
5
+ status: TodoStatus;
6
+ };
7
+ type TodoPhase = {
8
+ name: string;
9
+ tasks: TodoItem[];
10
+ };
11
+ /** A single todo operation entry (the tool's input params). */
12
+ type TodoInput = {
13
+ op: 'init';
14
+ list?: {
15
+ phase: string;
16
+ items: string[];
17
+ }[];
18
+ phase?: string;
19
+ items?: string[];
20
+ } | {
21
+ op: 'start';
22
+ task: string;
23
+ } | {
24
+ op: 'done';
25
+ task?: string;
26
+ phase?: string;
27
+ } | {
28
+ op: 'drop';
29
+ task?: string;
30
+ phase?: string;
31
+ } | {
32
+ op: 'rm';
33
+ task?: string;
34
+ phase?: string;
35
+ } | {
36
+ op: 'append';
37
+ phase: string;
38
+ items: string[];
39
+ } | {
40
+ op: 'view';
41
+ };
42
+ /** Deep-clone phases (mutation-safe). */
43
+ export declare function clonePhases(phases: TodoPhase[]): TodoPhase[];
44
+ /** Return the active todo task, preferring in_progress over the first pending. */
45
+ export declare function nextActionableTask(phases: readonly TodoPhase[]): TodoItem | undefined;
46
+ /** Report whether `content` likely names the same work as any entry in
47
+ * `descriptions`. Normalize-then-equal first, with a substring fallback
48
+ * in either direction (≥6 char overlap on the contained side). */
49
+ export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
50
+ /** Render todo phases as a Markdown checklist suitable for editing/copying. */
51
+ export declare function phasesToMarkdown(phases: TodoPhase[]): string;
52
+ /** Parse a Markdown checklist back into todo phases. */
53
+ export declare function markdownToPhases(md: string): {
54
+ phases: TodoPhase[];
55
+ errors: string[];
56
+ };
57
+ /** Extract the latest todo phases from stored messages (tool results).
58
+ * Scans backwards for the most recent `todo` tool result with phases metadata. */
59
+ export declare function getLatestTodoPhasesFromMessages(messages: {
60
+ role: string;
61
+ content: unknown[];
62
+ }[]): TodoPhase[];
63
+ /** todo tool: phased task tracking with 7 operations.
64
+ * Permission: auto (no side effects beyond session state).
65
+ * State is held in-memory via ctx.todoState hook (dependency-reversal). */
66
+ export declare const todoTool: ToolDef;
67
+ export type { TodoInput, TodoItem, TodoPhase, TodoStatus };