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.
- package/dist/core/agent.js +4 -0
- package/dist/core/config.js +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +1 -1
- package/dist/core/loop.js +9 -0
- package/dist/core/prompt-registry.d.ts +2 -2
- package/dist/core/prompt-registry.js +42 -3
- package/dist/core/slash.js +102 -6
- package/dist/core/types.d.ts +10 -1
- package/dist/core/workflow.d.ts +1 -1
- package/dist/core/workflow.js +56 -5
- package/dist/core/workflows/discovery.d.ts +23 -2
- package/dist/core/workflows/discovery.js +38 -2
- package/dist/core/workflows/index.d.ts +3 -2
- package/dist/core/workflows/index.js +2 -2
- package/dist/core/workflows/registry.d.ts +8 -1
- package/dist/core/workflows/registry.js +21 -1
- package/dist/core/workflows/runtime.d.ts +3 -0
- package/dist/core/workflows/runtime.js +2 -2
- package/dist/project/resolve.d.ts +75 -0
- package/dist/project/resolve.js +253 -1
- package/dist/server/app.js +3 -0
- package/dist/server/context.js +2 -1
- package/dist/server/dev.js +3 -1
- package/dist/server/routes/chat.js +12 -0
- package/dist/server/routes/commands.js +1 -0
- package/dist/server/routes/files.js +252 -4
- package/dist/server/routes/terminal.js +2 -1
- package/dist/server/routes/todo.d.ts +4 -0
- package/dist/server/routes/todo.js +107 -0
- package/dist/server/routes/workflows.js +83 -10
- package/dist/server/server.d.ts +8 -1
- package/dist/server/server.js +113 -23
- package/dist/server/terminal/pty-manager.d.ts +14 -0
- package/dist/server/terminal/pty-manager.js +105 -7
- package/dist/shared/types/agent.d.ts +9 -0
- package/dist/shared/types/config.d.ts +6 -0
- package/dist/shared/types/tool.d.ts +13 -0
- package/dist/tools/builtin/todo.d.ts +67 -0
- package/dist/tools/builtin/todo.js +517 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/types.d.ts +32 -1
- package/package.json +2 -1
package/dist/server/server.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
/**
|
|
57
|
-
|
|
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(
|
|
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
|
-
//
|
|
87
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
173
|
-
//
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|