c0de-agent 1.0.0 → 1.1.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/cli/deps.d.ts +16 -5
- package/dist/cli/deps.js +18 -5
- package/dist/cli/index.js +8 -2
- package/dist/core/agent.js +5 -0
- package/dist/core/config.js +1 -0
- package/dist/core/loop.d.ts +10 -0
- package/dist/core/loop.js +559 -295
- package/dist/core/slash.js +3 -4
- package/dist/core/title.js +3 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/core/worktree.js +6 -4
- package/dist/dap/session.js +3 -3
- package/dist/llm/provider.js +5 -1
- package/dist/plugins/loader.js +3 -2
- package/dist/server/agent-manager.d.ts +2 -0
- package/dist/server/agent-manager.js +8 -0
- package/dist/server/app.js +3 -0
- package/dist/server/context.js +2 -0
- package/dist/server/dev.d.ts +4 -1
- package/dist/server/dev.js +92 -27
- package/dist/server/permission/store.d.ts +2 -0
- package/dist/server/permission/store.js +9 -0
- package/dist/server/routes/chat.js +37 -1
- package/dist/server/routes/session.js +75 -1
- package/dist/server/routes/terminal.d.ts +5 -0
- package/dist/server/routes/terminal.js +66 -0
- package/dist/server/server.d.ts +14 -1
- package/dist/server/server.js +100 -28
- package/dist/server/terminal/pty-manager.d.ts +53 -0
- package/dist/server/terminal/pty-manager.js +160 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/session/archive.d.ts +1 -1
- package/dist/session/compaction.d.ts +8 -2
- package/dist/session/compaction.js +85 -16
- package/dist/session/shake.d.ts +66 -0
- package/dist/session/shake.js +304 -0
- package/dist/session/types.d.ts +1 -1
- package/dist/shared/types/agent.d.ts +27 -0
- package/dist/shared/types/config.d.ts +10 -0
- package/dist/shared/types/llm.d.ts +1 -0
- package/dist/shared/types/tool.d.ts +3 -0
- package/package.json +11 -3
package/dist/core/slash.js
CHANGED
|
@@ -40,10 +40,9 @@ const compactCommand = {
|
|
|
40
40
|
name: 'compact',
|
|
41
41
|
description: 'Manually trigger context compaction',
|
|
42
42
|
execute: async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
43
|
+
// 仅声明意图:真正的压缩由消费方(loop.compactContext / chat 路由)执行,
|
|
44
|
+
// 复用 createSummarizer + runCompaction,且不把 /compact 当作 user 消息发给 LLM。
|
|
45
|
+
return { _tag: 'compact' };
|
|
47
46
|
},
|
|
48
47
|
};
|
|
49
48
|
const modelCommand = {
|
package/dist/core/title.js
CHANGED
|
@@ -108,8 +108,9 @@ async function generateSessionTitle(deps, sessionId, firstMessage, chatProvider,
|
|
|
108
108
|
return;
|
|
109
109
|
await updateSessionTitle(db, sessionId, title);
|
|
110
110
|
}
|
|
111
|
-
catch {
|
|
112
|
-
//
|
|
111
|
+
catch (e) {
|
|
112
|
+
// 标题生成是尽力而为的辅助功能:失败不阻塞主对话流,但记录以便排查。
|
|
113
|
+
console.warn('[title] generateSessionTitle failed:', e instanceof Error ? e.message : String(e));
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
export { DEFAULT_SESSION_TITLE, generateSessionTitle, isDefaultTitle };
|
package/dist/core/types.d.ts
CHANGED
package/dist/core/worktree.js
CHANGED
|
@@ -48,8 +48,9 @@ async function applyPatchToParent(repoRoot, patch, commitMessage) {
|
|
|
48
48
|
try {
|
|
49
49
|
gitWithInput(repoRoot, ['apply'], patch);
|
|
50
50
|
}
|
|
51
|
-
catch {
|
|
52
|
-
// apply
|
|
51
|
+
catch (e) {
|
|
52
|
+
// apply 失败:记录后尝试 3-way 合并
|
|
53
|
+
console.warn('[worktree] git apply failed, retrying with --3way:', e instanceof Error ? e.message : String(e));
|
|
53
54
|
gitWithInput(repoRoot, ['apply', '--3way'], patch);
|
|
54
55
|
}
|
|
55
56
|
git(repoRoot, ['add', '-A']);
|
|
@@ -61,8 +62,9 @@ function removeWorktree(repoRoot, worktreeDir) {
|
|
|
61
62
|
try {
|
|
62
63
|
git(repoRoot, ['worktree', 'remove', '--force', worktreeDir]);
|
|
63
64
|
}
|
|
64
|
-
catch {
|
|
65
|
-
//
|
|
65
|
+
catch (e) {
|
|
66
|
+
// 清理失败忽略(可能已删),但记录以便排查残留 worktree
|
|
67
|
+
console.warn('[worktree] worktree cleanup failed:', e instanceof Error ? e.message : String(e));
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
export { applyPatchToParent, captureBaseline, captureDeltaPatch, createWorktree, removeWorktree };
|
package/dist/dap/session.js
CHANGED
|
@@ -112,12 +112,12 @@ function createDebugSessionManager() {
|
|
|
112
112
|
if (!s)
|
|
113
113
|
return;
|
|
114
114
|
s.session.state = 'stopped';
|
|
115
|
-
// disconnect
|
|
115
|
+
// disconnect 适配器可能已退出;吞错但记录,便于排查非正常退出。
|
|
116
116
|
try {
|
|
117
117
|
await s.client.request('disconnect', {});
|
|
118
118
|
}
|
|
119
|
-
catch {
|
|
120
|
-
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn('[dap] disconnect failed:', e instanceof Error ? e.message : String(e));
|
|
121
121
|
}
|
|
122
122
|
s.client.dispose();
|
|
123
123
|
sessions.delete(sessionId);
|
package/dist/llm/provider.js
CHANGED
|
@@ -93,7 +93,11 @@ const toStreamChunk = (event) => {
|
|
|
93
93
|
case 'provider-error':
|
|
94
94
|
return {
|
|
95
95
|
_tag: 'error',
|
|
96
|
-
error: {
|
|
96
|
+
error: {
|
|
97
|
+
message: event.message,
|
|
98
|
+
retryable: event.retryable ?? false,
|
|
99
|
+
...(event.classification !== undefined ? { classification: event.classification } : {}),
|
|
100
|
+
},
|
|
97
101
|
};
|
|
98
102
|
default:
|
|
99
103
|
return null;
|
package/dist/plugins/loader.js
CHANGED
|
@@ -59,8 +59,9 @@ async function discoverPlugins(projectDir) {
|
|
|
59
59
|
const plugin = await loadPlugin(join(entry.path, 'index.js'));
|
|
60
60
|
results.push({ name: entry.name, path: entry.path, plugin });
|
|
61
61
|
}
|
|
62
|
-
catch {
|
|
63
|
-
//
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Plugin failed to load — skip but warn (otherwise silently inactive)
|
|
64
|
+
console.warn(`[plugin] failed to load "${entry.name}" from ${entry.path}:`, e instanceof Error ? e.message : String(e));
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
return results;
|
|
@@ -26,6 +26,8 @@ type AgentManager = {
|
|
|
26
26
|
children(parentSessionId: string): ActiveRun[];
|
|
27
27
|
/** 查询所有后台任务(jobId 非空的 run)。 */
|
|
28
28
|
backgroundJobs(): ActiveRun[];
|
|
29
|
+
/** 中止所有活跃 run 并清空(dev 热重载重建前调用)。 */
|
|
30
|
+
dispose(): void;
|
|
29
31
|
};
|
|
30
32
|
declare function createAgentManager(): AgentManager;
|
|
31
33
|
export type { ActiveRun, AgentManager };
|
|
@@ -49,6 +49,14 @@ function createAgentManager() {
|
|
|
49
49
|
backgroundJobs() {
|
|
50
50
|
return Array.from(runs.values()).filter((r) => r.jobId !== undefined);
|
|
51
51
|
},
|
|
52
|
+
dispose() {
|
|
53
|
+
// dev 热重载重建前调用:中止所有活跃 run(loop 在 turn/流边界检测 signal
|
|
54
|
+
// → unwind → 调用方 finally 持久化 + unregister),然后清空 Map。
|
|
55
|
+
for (const run of runs.values()) {
|
|
56
|
+
abortAgent(run.state);
|
|
57
|
+
}
|
|
58
|
+
runs.clear();
|
|
59
|
+
},
|
|
52
60
|
};
|
|
53
61
|
}
|
|
54
62
|
export { createAgentManager };
|
package/dist/server/app.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createPermissionsRoute } from './routes/permissions.js';
|
|
|
18
18
|
import { createProjectRoute } from './routes/project.js';
|
|
19
19
|
import { createProviderRoute } from './routes/provider.js';
|
|
20
20
|
import { createSessionRoute } from './routes/session.js';
|
|
21
|
+
import { createTerminalRoute } from './routes/terminal.js';
|
|
21
22
|
import { createToolRoute } from './routes/tool.js';
|
|
22
23
|
import { createUpdateRoute } from './routes/update.js';
|
|
23
24
|
/** 创建完整的 Hono 应用,挂载所有路由 + 中间件。 */
|
|
@@ -44,6 +45,7 @@ function createApp(ctx) {
|
|
|
44
45
|
app.route('/api/config', createConfigRoute(ctx));
|
|
45
46
|
app.route('/api/permissions', createPermissionsRoute(ctx));
|
|
46
47
|
app.route('/api/files', createFilesRoute(ctx));
|
|
48
|
+
app.route('/api/terminal', createTerminalRoute(ctx));
|
|
47
49
|
// 根路径
|
|
48
50
|
app.get('/', (c) => c.json({
|
|
49
51
|
name: 'c0de-agent',
|
|
@@ -63,6 +65,7 @@ function createApp(ctx) {
|
|
|
63
65
|
'/api/config',
|
|
64
66
|
'/api/permissions',
|
|
65
67
|
'/api/files',
|
|
68
|
+
'/api/terminal',
|
|
66
69
|
],
|
|
67
70
|
}));
|
|
68
71
|
// 静态文件服务(生产环境 dist-web/ 存在时启用)
|
package/dist/server/context.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.
|
|
|
6
6
|
import { createUpdateScheduler } from '../update/index.js';
|
|
7
7
|
import { createAgentManager } from './agent-manager.js';
|
|
8
8
|
import { createPermissionStore } from './permission/store.js';
|
|
9
|
+
import { PTYManager } from './terminal/pty-manager.js';
|
|
9
10
|
function createServerContext(opts) {
|
|
10
11
|
// 测试/dev 工厂:补足 urlRegistry + hookRunner + pluginRegistry(空壳,不激活插件),
|
|
11
12
|
// 生产启动走 bootstrapServerContext → initPlugins(激活 builtin + 发现外部插件)。
|
|
@@ -41,6 +42,7 @@ function createServerContext(opts) {
|
|
|
41
42
|
}),
|
|
42
43
|
}),
|
|
43
44
|
cwd: opts.cwd ?? process.cwd(),
|
|
45
|
+
ptyManager: new PTYManager(),
|
|
44
46
|
...(opts.chatStream ? { chatStream: opts.chatStream } : {}),
|
|
45
47
|
};
|
|
46
48
|
}
|
package/dist/server/dev.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import type { Hono } from 'hono';
|
|
3
|
+
import type { ServerContext } from './types.js';
|
|
3
4
|
declare function getDevApp(): Promise<Hono>;
|
|
5
|
+
/** 获取当前 dev ctx(WebSocket 升级等需要直接访问 ptyManager 的场景)。 */
|
|
6
|
+
declare function getDevCtx(): Promise<ServerContext>;
|
|
4
7
|
/** vite dev server 关闭时调用,确保 PGLite WASM 正常 close 并刷写 WAL。 */
|
|
5
8
|
declare function closeDevApp(): Promise<void>;
|
|
6
9
|
/**
|
|
@@ -8,4 +11,4 @@ declare function closeDevApp(): Promise<void>;
|
|
|
8
11
|
* 用 Readable.fromWeb 管道转发 Response body,原生处理背压与分块。
|
|
9
12
|
*/
|
|
10
13
|
declare function handleApiRequest(app: Hono, req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
11
|
-
export { closeDevApp, getDevApp, handleApiRequest };
|
|
14
|
+
export { closeDevApp, getDevApp, getDevCtx, handleApiRequest };
|
package/dist/server/dev.js
CHANGED
|
@@ -1,35 +1,92 @@
|
|
|
1
1
|
// src/server/dev.ts
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
import { createApp } from './app.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildServerContext, createDevDb } from './server.js';
|
|
5
5
|
/**
|
|
6
6
|
* 开发环境入口:初始化并返回 Hono app。
|
|
7
7
|
* 供 vite dev server 中间件复用,使前后端共享同一端口。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* ## 全量重建(B 方案)
|
|
10
|
+
*
|
|
11
|
+
* 目标:编辑任意 server/core 代码后热重载,新代码对**新请求**全部生效——与进程重启同语义,
|
|
12
|
+
* 只是不开新进程。
|
|
13
|
+
*
|
|
14
|
+
* 缓存策略分两层:
|
|
15
|
+
*
|
|
16
|
+
* 1. **PGLite DB handle** → globalThis(唯一跨重载存活物)。
|
|
17
|
+
* PGLite 是单写者 WASM DB,同一 dataDir 不能开第二个连接(WAL 冲突 / Aborted()),
|
|
18
|
+
* 所以 db 必须跨重载复用。其余一切重建。
|
|
19
|
+
*
|
|
20
|
+
* 2. **ctx + app** → 模块级变量。
|
|
21
|
+
* Vite 重载本模块时归 null,下次请求触发 rebuild:
|
|
22
|
+
* dispose 旧 ctx(abort 活跃 run + settle pending permission + stop scheduler + close handoff)
|
|
23
|
+
* → 围绕复用的 db 调 buildServerContext(重建 agentManager/permissionStore/registries/plugins/app)
|
|
24
|
+
* → 全部用最新代码。
|
|
25
|
+
*
|
|
26
|
+
* ## 与热升级的关系
|
|
27
|
+
*
|
|
28
|
+
* 和 `performHotUpdate` 是同一原理,差只在 spawn 新进程 vs 进程内重建。两者都:
|
|
29
|
+
* - 保留 durable 状态(DB rows)
|
|
30
|
+
* - 丢弃非 durable 状态(in-flight 工具执行、pending Promise resolver、活跃 run 内存态)
|
|
31
|
+
* - 活跃 run 从最后持久化消息重启
|
|
32
|
+
*
|
|
33
|
+
* 活跃 run 被 abort 后,loop 在 turn/流边界检测 signal → unwind → 调用方 finally 持久化
|
|
34
|
+
* 状态(标记 interrupted/completed)。前端再发消息时从 DB 历史起新 run,全用新代码。
|
|
13
35
|
*/
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
const DEV_DB_KEY = '__c0de_dev_db__';
|
|
37
|
+
/** 模块级:随本模块重载而重置。 */
|
|
38
|
+
let ctx = null;
|
|
39
|
+
let app = null;
|
|
40
|
+
let disposeCtx = null;
|
|
41
|
+
async function rebuild() {
|
|
42
|
+
// 1. dispose 旧 ctx(不 close db)
|
|
43
|
+
if (disposeCtx) {
|
|
44
|
+
await disposeCtx();
|
|
45
|
+
disposeCtx = null;
|
|
46
|
+
ctx = null;
|
|
47
|
+
app = null;
|
|
48
|
+
}
|
|
49
|
+
// 2. 取/建 devDb(globalThis,PGLite 单写者,只建一次)
|
|
17
50
|
const g = globalThis;
|
|
18
|
-
if (!g[
|
|
19
|
-
|
|
20
|
-
g[DEV_APP_KEY] = createApp(ctx);
|
|
21
|
-
g[DEV_CLOSE_KEY] = close;
|
|
51
|
+
if (!g[DEV_DB_KEY]) {
|
|
52
|
+
g[DEV_DB_KEY] = await createDevDb(process.cwd());
|
|
22
53
|
}
|
|
23
|
-
|
|
54
|
+
const db = g[DEV_DB_KEY];
|
|
55
|
+
// 3. 围绕复用的 db 重建 ctx(skipHandoff:dev 不跑热升级 handoff)
|
|
56
|
+
const built = await buildServerContext(db, { cwd: process.cwd(), skipHandoff: true });
|
|
57
|
+
ctx = built.ctx;
|
|
58
|
+
disposeCtx = built.dispose;
|
|
59
|
+
app = createApp(ctx);
|
|
60
|
+
}
|
|
61
|
+
async function getDevApp() {
|
|
62
|
+
if (app)
|
|
63
|
+
return app;
|
|
64
|
+
await rebuild();
|
|
65
|
+
if (!app)
|
|
66
|
+
throw new Error('dev app failed to initialize');
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
/** 获取当前 dev ctx(WebSocket 升级等需要直接访问 ptyManager 的场景)。 */
|
|
70
|
+
async function getDevCtx() {
|
|
71
|
+
if (!ctx)
|
|
72
|
+
await getDevApp();
|
|
73
|
+
if (!ctx)
|
|
74
|
+
throw new Error('dev ctx not initialized');
|
|
75
|
+
return ctx;
|
|
24
76
|
}
|
|
25
77
|
/** vite dev server 关闭时调用,确保 PGLite WASM 正常 close 并刷写 WAL。 */
|
|
26
78
|
async function closeDevApp() {
|
|
79
|
+
if (disposeCtx) {
|
|
80
|
+
await disposeCtx();
|
|
81
|
+
disposeCtx = null;
|
|
82
|
+
ctx = null;
|
|
83
|
+
app = null;
|
|
84
|
+
}
|
|
27
85
|
const g = globalThis;
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
await close();
|
|
31
|
-
delete g[
|
|
32
|
-
delete g[DEV_CLOSE_KEY];
|
|
86
|
+
const db = g[DEV_DB_KEY];
|
|
87
|
+
if (db) {
|
|
88
|
+
await db.close();
|
|
89
|
+
delete g[DEV_DB_KEY];
|
|
33
90
|
}
|
|
34
91
|
}
|
|
35
92
|
/**
|
|
@@ -49,16 +106,20 @@ async function handleApiRequest(app, req, res) {
|
|
|
49
106
|
}
|
|
50
107
|
const headers = new Headers();
|
|
51
108
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
52
|
-
if (value
|
|
53
|
-
headers.set(key,
|
|
109
|
+
if (typeof value === 'string') {
|
|
110
|
+
headers.set(key, value);
|
|
111
|
+
}
|
|
112
|
+
else if (Array.isArray(value)) {
|
|
113
|
+
for (const v of value)
|
|
114
|
+
headers.append(key, v);
|
|
54
115
|
}
|
|
55
116
|
}
|
|
56
117
|
const init = {
|
|
57
118
|
method: req.method,
|
|
58
119
|
headers,
|
|
59
|
-
body: body ?? undefined,
|
|
60
120
|
};
|
|
61
121
|
if (body !== undefined) {
|
|
122
|
+
init.body = body;
|
|
62
123
|
init.duplex = 'half';
|
|
63
124
|
}
|
|
64
125
|
const request = new Request(url.toString(), init);
|
|
@@ -69,15 +130,19 @@ async function handleApiRequest(app, req, res) {
|
|
|
69
130
|
});
|
|
70
131
|
const responseBody = response.body;
|
|
71
132
|
if (responseBody) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
// Readable.fromWeb 需要 web stream;Hono Response.body 已是 ReadableStream。
|
|
134
|
+
// native pipeline handles backpressure + chunked encoding for SSE.
|
|
135
|
+
Readable.fromWeb(responseBody)
|
|
136
|
+
.pipe(res)
|
|
137
|
+
.on('error', (err) => {
|
|
138
|
+
if (!res.headersSent)
|
|
139
|
+
res.statusCode = 500;
|
|
140
|
+
res.end();
|
|
141
|
+
console.error('[c0de-hono-api] response stream error:', err);
|
|
77
142
|
});
|
|
78
143
|
}
|
|
79
144
|
else {
|
|
80
145
|
res.end();
|
|
81
146
|
}
|
|
82
147
|
}
|
|
83
|
-
export { closeDevApp, getDevApp, handleApiRequest };
|
|
148
|
+
export { closeDevApp, getDevApp, getDevCtx, handleApiRequest };
|
|
@@ -24,6 +24,8 @@ type PermissionStore = {
|
|
|
24
24
|
resolve(toolCallId: string, approved: boolean): boolean;
|
|
25
25
|
has(toolCallId: string): boolean;
|
|
26
26
|
size(): number;
|
|
27
|
+
/** settle 所有 pending 为 deny 并清空(dev 热重载重建前调用)。 */
|
|
28
|
+
dispose(): void;
|
|
27
29
|
};
|
|
28
30
|
declare function createPermissionStore(opts?: PermissionStoreOptions): PermissionStore;
|
|
29
31
|
export type { PendingPermission, PermissionRequest, PermissionStore, PermissionStoreOptions };
|
|
@@ -42,6 +42,15 @@ function createPermissionStore(opts = {}) {
|
|
|
42
42
|
size() {
|
|
43
43
|
return pending.size;
|
|
44
44
|
},
|
|
45
|
+
dispose() {
|
|
46
|
+
// dev 热重载重建前调用:所有 pending settle 为 deny(避免悬空 Promise +
|
|
47
|
+
// timer 泄漏),clearTimeout 后清空 Map。
|
|
48
|
+
for (const [id, p] of pending) {
|
|
49
|
+
clearTimeout(p.timer);
|
|
50
|
+
p.resolve({ _tag: 'deny', reason: 'Server context disposed (hot reload)' });
|
|
51
|
+
pending.delete(id);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
export { createPermissionStore, DEFAULT_PERMISSION_TIMEOUT_MS };
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { streamSSE } from 'hono/streaming';
|
|
4
4
|
import { createAgent, runAgent } from '../../core/agent.js';
|
|
5
|
+
import { compactContext } from '../../core/loop.js';
|
|
5
6
|
import { createSlashRegistry, parseSlashInput } from '../../core/slash.js';
|
|
6
7
|
import { getProject } from '../../project/project.js';
|
|
7
8
|
import { getLLMSegments, getSession, updateSessionLastRun } from '../../session/session.js';
|
|
@@ -63,7 +64,42 @@ function createChatRoute(ctx) {
|
|
|
63
64
|
return streamSSE(c, async (stream) => {
|
|
64
65
|
try {
|
|
65
66
|
const result = await cmd.execute(parsed.args, commandCtx);
|
|
66
|
-
if (result._tag === '
|
|
67
|
+
if (result._tag === 'compact') {
|
|
68
|
+
// /compact:手动触发上下文压缩。复用 loop.compactContext(createSummarizer +
|
|
69
|
+
// runCompaction),不创建主 agent、不进入 LLM turn 循环(不把 /compact 当作
|
|
70
|
+
// user 消息发给模型)。
|
|
71
|
+
const provider = body.provider ?? ctx.config.defaultProvider;
|
|
72
|
+
const model = body.model ?? ctx.config.defaultModel;
|
|
73
|
+
const agentConfig = {
|
|
74
|
+
provider,
|
|
75
|
+
model,
|
|
76
|
+
tools: [],
|
|
77
|
+
plugins: ctx.config.plugins.enabled,
|
|
78
|
+
agentName: 'default',
|
|
79
|
+
};
|
|
80
|
+
const compactState = await createAgent(session, agentConfig, commandCtx.deps);
|
|
81
|
+
try {
|
|
82
|
+
for await (const event of compactContext(compactState, commandCtx.deps)) {
|
|
83
|
+
await stream.writeSSE({
|
|
84
|
+
event: event._tag,
|
|
85
|
+
data: JSON.stringify(event),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
await stream.writeSSE({
|
|
91
|
+
event: 'error',
|
|
92
|
+
data: JSON.stringify({
|
|
93
|
+
_tag: 'error',
|
|
94
|
+
error: {
|
|
95
|
+
_tag: 'unexpected',
|
|
96
|
+
message: e instanceof Error ? e.message : String(e),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (result._tag === 'error') {
|
|
67
103
|
await stream.writeSSE({
|
|
68
104
|
event: 'error',
|
|
69
105
|
data: JSON.stringify({
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { createSummarizer, runCompaction } from '../../core/compact.js';
|
|
3
3
|
import { fromDirectory } from '../../project/index.js';
|
|
4
|
+
import { archiveOriginalEntries } from '../../session/archive.js';
|
|
4
5
|
import { forkSession, getBranches, getTree } from '../../session/branch.js';
|
|
5
|
-
import { getMessages } from '../../session/message.js';
|
|
6
|
+
import { deleteEntriesByIds, getMessages, insertEntry } from '../../session/message.js';
|
|
6
7
|
import { createSession, deleteSession, getLLMSegments, getSession, listSessions, listSessionsByProject, } from '../../session/session.js';
|
|
8
|
+
import { applyShakeRegions, collectShakeRegions, DEFAULT_SHAKE_CONFIG, toRegionViews, } from '../../session/shake.js';
|
|
9
|
+
import { estimateMessageTokens } from '../../session/token.js';
|
|
10
|
+
import { generateId } from '../../shared/index.js';
|
|
7
11
|
import { apiError } from '../middleware/error.js';
|
|
8
12
|
function createSessionRoute(ctx) {
|
|
9
13
|
const app = new Hono();
|
|
@@ -120,6 +124,76 @@ function createSessionRoute(ctx) {
|
|
|
120
124
|
}
|
|
121
125
|
return c.json({ _tag: 'idle' });
|
|
122
126
|
});
|
|
127
|
+
// shake preview:返回可 shake 的区域列表
|
|
128
|
+
app.post('/:id/shake/preview', async (c) => {
|
|
129
|
+
const id = c.req.param('id');
|
|
130
|
+
let session;
|
|
131
|
+
try {
|
|
132
|
+
session = await getSession(ctx.db, id);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
136
|
+
}
|
|
137
|
+
if (!session)
|
|
138
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
139
|
+
const messages = await getMessages(ctx.db, id);
|
|
140
|
+
// Manual shake: show ALL candidates (protectTokens=0, minSavings=0).
|
|
141
|
+
// toRegionViews still marks isAfterProtectWindow using the real config.
|
|
142
|
+
const manualConfig = { ...DEFAULT_SHAKE_CONFIG, protectTokens: 0, minSavings: 0 };
|
|
143
|
+
const regions = collectShakeRegions(messages, manualConfig);
|
|
144
|
+
const views = toRegionViews(regions, DEFAULT_SHAKE_CONFIG, messages);
|
|
145
|
+
return c.json({ regions: views });
|
|
146
|
+
});
|
|
147
|
+
// shake apply:归档原始内容 + 原位替换
|
|
148
|
+
app.post('/:id/shake/apply', async (c) => {
|
|
149
|
+
const id = c.req.param('id');
|
|
150
|
+
let session;
|
|
151
|
+
try {
|
|
152
|
+
session = await getSession(ctx.db, id);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
156
|
+
}
|
|
157
|
+
if (!session)
|
|
158
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
159
|
+
const body = await c.req.json().catch(() => ({}));
|
|
160
|
+
const regionIds = body.regionIds ?? [];
|
|
161
|
+
const messages = await getMessages(ctx.db, id);
|
|
162
|
+
const manualConfig = { ...DEFAULT_SHAKE_CONFIG, protectTokens: 0, minSavings: 0 };
|
|
163
|
+
const regions = collectShakeRegions(messages, manualConfig);
|
|
164
|
+
// 校验:所有 regionIds 必须命中当前 preview 结果(原子性)
|
|
165
|
+
const availableIds = new Set(regions.map((r) => r.id));
|
|
166
|
+
const unknownIds = regionIds.filter((rid) => !availableIds.has(rid));
|
|
167
|
+
if (unknownIds.length > 0) {
|
|
168
|
+
return apiError(c, 400, 'INVALID_REGIONS', '消息已变化,请重新预览');
|
|
169
|
+
}
|
|
170
|
+
const selectedSet = new Set(regionIds);
|
|
171
|
+
const selected = regions.filter((r) => selectedSet.has(r.id));
|
|
172
|
+
if (selected.length === 0) {
|
|
173
|
+
return c.json({ shaken: 0, archiveId: '' });
|
|
174
|
+
}
|
|
175
|
+
const affectedIds = [...new Set(selected.map((r) => r.messageId))];
|
|
176
|
+
const originalMessages = messages.filter((m) => affectedIds.includes(m.id));
|
|
177
|
+
const archiveId = generateId();
|
|
178
|
+
const totalTokens = selected.reduce((sum, r) => sum + r.tokens, 0);
|
|
179
|
+
await archiveOriginalEntries(ctx.db, id, originalMessages, 'shake', `Shaken ${selected.length} regions, saved ${totalTokens} tokens`, archiveId);
|
|
180
|
+
const shakenMessages = applyShakeRegions(messages, selected);
|
|
181
|
+
await deleteEntriesByIds(ctx.db, affectedIds);
|
|
182
|
+
for (const msg of shakenMessages) {
|
|
183
|
+
if (!affectedIds.includes(msg.id))
|
|
184
|
+
continue;
|
|
185
|
+
await insertEntry(ctx.db, {
|
|
186
|
+
id: msg.id,
|
|
187
|
+
sessionId: id,
|
|
188
|
+
tag: 'message',
|
|
189
|
+
role: msg.role,
|
|
190
|
+
content: msg.content,
|
|
191
|
+
tokenCount: estimateMessageTokens(msg.content),
|
|
192
|
+
createdAt: new Date(msg.createdAt),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return c.json({ shaken: selected.length, archiveId });
|
|
196
|
+
});
|
|
123
197
|
// 获取分支
|
|
124
198
|
app.get('/:id/branches', async (c) => {
|
|
125
199
|
const branches = await getBranches(ctx.db, c.req.param('id'));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/server/routes/terminal.ts
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { apiError } from '../middleware/error.js';
|
|
4
|
+
/** 创建终端路由(REST 部分;WebSocket 升级在 server.ts 中处理)。 */
|
|
5
|
+
function createTerminalRoute(ctx) {
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
const mgr = ctx.ptyManager;
|
|
8
|
+
// 列出所有活跃 PTY
|
|
9
|
+
app.get('/', (c) => {
|
|
10
|
+
return c.json({ terminals: mgr.list() });
|
|
11
|
+
});
|
|
12
|
+
// 创建新 PTY 会话
|
|
13
|
+
app.post('/', async (c) => {
|
|
14
|
+
const body = await c.req.json().catch(() => ({}));
|
|
15
|
+
const cwd = typeof body.cwd === 'string' && body.cwd.length > 0 ? body.cwd : ctx.cwd;
|
|
16
|
+
const cols = Number.isFinite(body.cols) ? Number(body.cols) : undefined;
|
|
17
|
+
const rows = Number.isFinite(body.rows) ? Number(body.rows) : undefined;
|
|
18
|
+
const title = typeof body.title === 'string' ? body.title : undefined;
|
|
19
|
+
const shell = typeof body.shell === 'string' && body.shell.length > 0 ? body.shell : undefined;
|
|
20
|
+
try {
|
|
21
|
+
const info = mgr.create({ cwd, cols, rows, title, shell });
|
|
22
|
+
return c.json(info, 201);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : 'Failed to create terminal';
|
|
26
|
+
return apiError(c, 500, 'PTY_CREATE_FAILED', message);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
// 获取单个 PTY 信息
|
|
30
|
+
app.get('/:id', (c) => {
|
|
31
|
+
const info = mgr.get(c.req.param('id'));
|
|
32
|
+
if (!info)
|
|
33
|
+
return apiError(c, 404, 'PTY_NOT_FOUND', 'Terminal not found');
|
|
34
|
+
return c.json(info);
|
|
35
|
+
});
|
|
36
|
+
// 调整尺寸 / 更新标题
|
|
37
|
+
app.put('/:id', async (c) => {
|
|
38
|
+
const id = c.req.param('id');
|
|
39
|
+
const info = mgr.get(id);
|
|
40
|
+
if (!info)
|
|
41
|
+
return apiError(c, 404, 'PTY_NOT_FOUND', 'Terminal not found');
|
|
42
|
+
const body = await c.req.json().catch(() => ({}));
|
|
43
|
+
if (body.cols != null && body.rows != null) {
|
|
44
|
+
const cols = Number(body.cols);
|
|
45
|
+
const rows = Number(body.rows);
|
|
46
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) {
|
|
47
|
+
return apiError(c, 400, 'INVALID_SIZE', 'cols and rows must be numbers');
|
|
48
|
+
}
|
|
49
|
+
mgr.resize(id, cols, rows);
|
|
50
|
+
}
|
|
51
|
+
if (typeof body.title === 'string') {
|
|
52
|
+
mgr.setTitle(id, body.title);
|
|
53
|
+
}
|
|
54
|
+
return c.json(mgr.get(id));
|
|
55
|
+
});
|
|
56
|
+
// 终止 PTY
|
|
57
|
+
app.delete('/:id', (c) => {
|
|
58
|
+
const id = c.req.param('id');
|
|
59
|
+
if (!mgr.get(id))
|
|
60
|
+
return apiError(c, 404, 'PTY_NOT_FOUND', 'Terminal not found');
|
|
61
|
+
mgr.kill(id);
|
|
62
|
+
return c.json({ ok: true });
|
|
63
|
+
});
|
|
64
|
+
return app;
|
|
65
|
+
}
|
|
66
|
+
export { createTerminalRoute };
|
package/dist/server/server.d.ts
CHANGED
|
@@ -36,8 +36,21 @@ declare function buildRegistryFromConfig(config: Config): Registry;
|
|
|
36
36
|
* 使运行中的 ServerContext 立即生效,无需重启。
|
|
37
37
|
*/
|
|
38
38
|
declare function syncRegistryFromConfig(registry: Registry, config: Config): void;
|
|
39
|
+
/**
|
|
40
|
+
* 围绕已有 DB handle 组装 ServerContext(不建/不关闭 DB)。
|
|
41
|
+
*
|
|
42
|
+
* dev 热重载重建复用此函数:PGLite 单写者约束下 DB handle 必须跨重载存活,
|
|
43
|
+
* 但 ctx 其余资源(agentManager/permissionStore/registries/plugins)全部重建为新实例。
|
|
44
|
+
* 返回的 dispose 清理 ctx 资源但**不 close db**——db 由调用方持有。
|
|
45
|
+
*/
|
|
46
|
+
declare function buildServerContext(db: DB, opts?: StartServerOptions): Promise<{
|
|
47
|
+
ctx: ServerContext;
|
|
48
|
+
dispose: () => Promise<void>;
|
|
49
|
+
}>;
|
|
50
|
+
/** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
|
|
51
|
+
declare function createDevDb(cwd: string): Promise<DB>;
|
|
39
52
|
/** 初始化 DB + 配置 + 注册表,返回 ServerContext + 清理函数(dev 与独立后端共用)。 */
|
|
40
53
|
declare function bootstrapServerContext(opts?: StartServerOptions): Promise<BootstrappedServer>;
|
|
41
54
|
declare function startServer(opts?: StartServerOptions): Promise<RunningServer>;
|
|
42
55
|
export type { BootstrappedServer, RunningServer, StartServerOptions };
|
|
43
|
-
export { bootstrapServerContext, buildRegistryFromConfig, startServer, syncRegistryFromConfig };
|
|
56
|
+
export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, startServer, syncRegistryFromConfig, };
|