@botlearn-course/daemon 0.0.1
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/agent-service-client.d.ts +18 -0
- package/dist/agent-service-client.js +108 -0
- package/dist/auth-store.d.ts +16 -0
- package/dist/auth-store.js +106 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +354 -0
- package/dist/course-client.d.ts +46 -0
- package/dist/course-client.js +143 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +85 -0
- package/dist/file-candidates.d.ts +34 -0
- package/dist/file-candidates.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/log.d.ts +20 -0
- package/dist/log.js +154 -0
- package/dist/path-env.d.ts +8 -0
- package/dist/path-env.js +42 -0
- package/dist/redaction.d.ts +24 -0
- package/dist/redaction.js +158 -0
- package/dist/run-dispatcher.d.ts +43 -0
- package/dist/run-dispatcher.js +294 -0
- package/dist/run-queue.d.ts +11 -0
- package/dist/run-queue.js +26 -0
- package/dist/runtime-capabilities.d.ts +3 -0
- package/dist/runtime-capabilities.js +42 -0
- package/dist/runtime-profile.d.ts +8 -0
- package/dist/runtime-profile.js +213 -0
- package/dist/runtimes/acp-stream.d.ts +96 -0
- package/dist/runtimes/acp-stream.js +488 -0
- package/dist/runtimes/claude-code.d.ts +41 -0
- package/dist/runtimes/claude-code.js +353 -0
- package/dist/runtimes/codex.d.ts +44 -0
- package/dist/runtimes/codex.js +332 -0
- package/dist/runtimes/deepseek-tui.d.ts +50 -0
- package/dist/runtimes/deepseek-tui.js +701 -0
- package/dist/runtimes/engine.d.ts +52 -0
- package/dist/runtimes/engine.js +127 -0
- package/dist/runtimes/fake.d.ts +13 -0
- package/dist/runtimes/fake.js +45 -0
- package/dist/runtimes/gemini.d.ts +39 -0
- package/dist/runtimes/gemini.js +251 -0
- package/dist/runtimes/hermes-agent.d.ts +61 -0
- package/dist/runtimes/hermes-agent.js +173 -0
- package/dist/runtimes/index.d.ts +15 -0
- package/dist/runtimes/index.js +74 -0
- package/dist/runtimes/kimi.d.ts +35 -0
- package/dist/runtimes/kimi.js +335 -0
- package/dist/runtimes/ndjson-stream.d.ts +51 -0
- package/dist/runtimes/ndjson-stream.js +207 -0
- package/dist/runtimes/openclaw-acp.d.ts +52 -0
- package/dist/runtimes/openclaw-acp.js +872 -0
- package/dist/runtimes/probe.d.ts +17 -0
- package/dist/runtimes/probe.js +54 -0
- package/dist/runtimes/runtime-errors.d.ts +20 -0
- package/dist/runtimes/runtime-errors.js +95 -0
- package/dist/runtimes/text-cap.d.ts +7 -0
- package/dist/runtimes/text-cap.js +25 -0
- package/dist/transcript.d.ts +13 -0
- package/dist/transcript.js +46 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.js +17 -0
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.js +54 -0
- package/package.json +40 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type EngineAdapter, type EngineRunOptions, type EngineRunResult, type RuntimeStatusEvent, type StreamBlock } from "./engine.js";
|
|
2
|
+
import type { Logger } from "../log.js";
|
|
3
|
+
/** 本客户端目标的 ACP 协议版本。 */
|
|
4
|
+
export declare const ACP_PROTOCOL_VERSION = 1;
|
|
5
|
+
export interface AcpInitializeResult {
|
|
6
|
+
protocolVersion?: number;
|
|
7
|
+
agentInfo?: {
|
|
8
|
+
name?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
};
|
|
11
|
+
agentCapabilities?: Record<string, unknown>;
|
|
12
|
+
authMethods?: Array<{
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}>;
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface AcpPermissionOption {
|
|
20
|
+
optionId: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
/**
|
|
23
|
+
* ACP option kind,常见值 allow_once / allow_always / reject_once /
|
|
24
|
+
* reject_always。基类视为不透明字符串,由子类检查决定结果。
|
|
25
|
+
*/
|
|
26
|
+
kind?: string;
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface AcpPermissionRequest {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
toolCall?: {
|
|
32
|
+
name?: string;
|
|
33
|
+
rawInput?: unknown;
|
|
34
|
+
[k: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
options: AcpPermissionOption[];
|
|
37
|
+
[k: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
export type AcpPermissionResponse = {
|
|
40
|
+
outcome: {
|
|
41
|
+
outcome: "selected";
|
|
42
|
+
optionId: string;
|
|
43
|
+
};
|
|
44
|
+
} | {
|
|
45
|
+
outcome: {
|
|
46
|
+
outcome: "cancelled";
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export interface AcpUpdateParams {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
update: {
|
|
52
|
+
sessionUpdate?: string;
|
|
53
|
+
[k: string]: unknown;
|
|
54
|
+
};
|
|
55
|
+
[k: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
/** 子类在一轮执行中响应入站流量的钩子。 */
|
|
58
|
+
export interface AcpTurnHooks {
|
|
59
|
+
onUpdate(params: AcpUpdateParams, ctx: AcpUpdateCtx): void;
|
|
60
|
+
onPermissionRequest(req: AcpPermissionRequest): Promise<AcpPermissionResponse>;
|
|
61
|
+
}
|
|
62
|
+
export interface AcpUpdateCtx {
|
|
63
|
+
/** 追加本轮流式 assistant 文本(尊重字节上限)。 */
|
|
64
|
+
appendAssistantText(text: string): void;
|
|
65
|
+
emitBlock(block: StreamBlock): void;
|
|
66
|
+
/** 转发 typing/thinking 状态事件(如 thought chunk、tool 进度)。 */
|
|
67
|
+
emitStatus(event: RuntimeStatusEvent): void;
|
|
68
|
+
/** 本轮 1-based 序号。 */
|
|
69
|
+
seq: number;
|
|
70
|
+
}
|
|
71
|
+
export declare abstract class AcpRuntimeAdapter implements EngineAdapter {
|
|
72
|
+
abstract readonly id: string;
|
|
73
|
+
protected readonly log: Logger;
|
|
74
|
+
constructor(logger?: Logger);
|
|
75
|
+
protected abstract resolveBinary(opts: EngineRunOptions): string;
|
|
76
|
+
/** argv 尾部(不含二进制)。ACP server 通常不需要参数。 */
|
|
77
|
+
protected buildArgs(_opts: EngineRunOptions): string[];
|
|
78
|
+
protected abstract spawnEnv(opts: EngineRunOptions): NodeJS.ProcessEnv;
|
|
79
|
+
/** 子类钩子:响应一条 session/update 通知。 */
|
|
80
|
+
protected abstract onUpdate(params: AcpUpdateParams, ctx: AcpUpdateCtx): void;
|
|
81
|
+
/** 子类钩子:应答 session/request_permission 请求。 */
|
|
82
|
+
protected abstract onPermissionRequest(req: AcpPermissionRequest, opts: EngineRunOptions): Promise<AcpPermissionResponse>;
|
|
83
|
+
/** initialize 时发送的 clientCapabilities。 */
|
|
84
|
+
protected clientCapabilities(): Record<string, unknown>;
|
|
85
|
+
/** initialize 时发送的 clientInfo。 */
|
|
86
|
+
protected clientInfo(): {
|
|
87
|
+
name: string;
|
|
88
|
+
version: string;
|
|
89
|
+
};
|
|
90
|
+
/** spawn 之前同步调用;子类可借此把 systemContext 落盘(如 <cwd>/AGENTS.md)。 */
|
|
91
|
+
protected prepareTurn(_opts: EngineRunOptions): void;
|
|
92
|
+
/** 传给 ACP session/new / session/load 的 cwd。 */
|
|
93
|
+
protected sessionCwd(opts: EngineRunOptions): string;
|
|
94
|
+
run(opts: EngineRunOptions): Promise<EngineRunResult>;
|
|
95
|
+
private withTimeout;
|
|
96
|
+
}
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { sanitizeRuntimeFailureText } from "../redaction.js";
|
|
3
|
+
import { sliceUtf8Bytes, utf8ByteLength } from "./text-cap.js";
|
|
4
|
+
import { consoleLogger, } from "./engine.js";
|
|
5
|
+
/**
|
|
6
|
+
* ACP(Agent Client Protocol)over stdio 的最小双向客户端基类
|
|
7
|
+
* (JSON-RPC 2.0,newline-delimited)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么不用 NdjsonStreamAdapter:ACP 是双向 RPC —— agent 除了发通知
|
|
10
|
+
* (session/update)还会发起 server→client 请求(session/request_permission),
|
|
11
|
+
* daemon 必须应答否则 agent 卡死;单向事件流基类无法正确驱动。
|
|
12
|
+
*/
|
|
13
|
+
/** 保留用于错误报告的 stderr 尾部长度。 */
|
|
14
|
+
const STDERR_TAIL_CAP = 8 * 1024;
|
|
15
|
+
/** 合成错误里包含的 stderr 末尾长度。 */
|
|
16
|
+
const STDERR_ERROR_SNIPPET = 500;
|
|
17
|
+
/** 单轮流式 assistant 文本字节上限 —— 防失控 runtime。 */
|
|
18
|
+
const ASSISTANT_TEXT_CAP = 1 * 1024 * 1024;
|
|
19
|
+
/** abort 时 SIGTERM → SIGKILL 的宽限期。 */
|
|
20
|
+
const KILL_GRACE_MS = 5_000;
|
|
21
|
+
/** initialize 握手的最长等待。 */
|
|
22
|
+
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
23
|
+
/** prompt RPC 出错后等待迟到 session/update 块的短暂 drain 窗口。 */
|
|
24
|
+
const PROMPT_ERROR_DRAIN_MS = 750;
|
|
25
|
+
/** 本客户端目标的 ACP 协议版本。 */
|
|
26
|
+
export const ACP_PROTOCOL_VERSION = 1;
|
|
27
|
+
/** 子进程 stdio 之上的最小 newline-JSON-RPC 分帧。 */
|
|
28
|
+
class AcpConnection {
|
|
29
|
+
child;
|
|
30
|
+
handlers;
|
|
31
|
+
logId;
|
|
32
|
+
log;
|
|
33
|
+
nextId = 1;
|
|
34
|
+
pending = new Map();
|
|
35
|
+
stdoutBuf = "";
|
|
36
|
+
closed = false;
|
|
37
|
+
closeReason = null;
|
|
38
|
+
constructor(child, handlers, logId, log) {
|
|
39
|
+
this.child = child;
|
|
40
|
+
this.handlers = handlers;
|
|
41
|
+
this.logId = logId;
|
|
42
|
+
this.log = log;
|
|
43
|
+
child.stdout.setEncoding("utf8");
|
|
44
|
+
child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
45
|
+
child.stdout.on("end", () => this.fail(new Error("stdout closed")));
|
|
46
|
+
child.on("close", (code) => this.fail(new Error(`process exited with code ${code ?? 0}`)));
|
|
47
|
+
child.on("error", (err) => this.fail(err));
|
|
48
|
+
}
|
|
49
|
+
onStdout(chunk) {
|
|
50
|
+
this.stdoutBuf += chunk;
|
|
51
|
+
let idx;
|
|
52
|
+
while ((idx = this.stdoutBuf.indexOf("\n")) !== -1) {
|
|
53
|
+
const line = this.stdoutBuf.slice(0, idx).trim();
|
|
54
|
+
this.stdoutBuf = this.stdoutBuf.slice(idx + 1);
|
|
55
|
+
if (!line)
|
|
56
|
+
continue;
|
|
57
|
+
this.dispatchLine(line);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
dispatchLine(line) {
|
|
61
|
+
let msg;
|
|
62
|
+
try {
|
|
63
|
+
msg = JSON.parse(line);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
this.log.warn(`${this.logId} non-json acp line`, { line: line.slice(0, 200) });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof msg !== "object" || msg === null)
|
|
70
|
+
return;
|
|
71
|
+
// 对客户端→服务端请求的响应
|
|
72
|
+
if (typeof msg.id === "number" && (msg.result !== undefined || msg.error !== undefined)) {
|
|
73
|
+
const pending = this.pending.get(msg.id);
|
|
74
|
+
if (!pending)
|
|
75
|
+
return;
|
|
76
|
+
this.pending.delete(msg.id);
|
|
77
|
+
if (msg.error) {
|
|
78
|
+
const err = new Error(`acp error ${msg.error.code ?? "?"}: ${formatRpcError(msg.error)}`);
|
|
79
|
+
pending.reject(err);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
pending.resolve(msg.result ?? null);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (typeof msg.method === "string") {
|
|
87
|
+
// 服务端→客户端请求(带 id)或通知(无 id)
|
|
88
|
+
if (msg.id !== undefined) {
|
|
89
|
+
void this.handleServerRequest(msg.id, msg.method, msg.params);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
try {
|
|
93
|
+
this.handlers.onNotification(msg.method, msg.params);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
this.log.warn(`${this.logId} notification handler threw`, {
|
|
97
|
+
method: msg.method,
|
|
98
|
+
err: String(err),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async handleServerRequest(id, method, params) {
|
|
105
|
+
let result;
|
|
106
|
+
let error = null;
|
|
107
|
+
try {
|
|
108
|
+
result = await this.handlers.onRequest(method, params);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
error = {
|
|
112
|
+
code: -32603,
|
|
113
|
+
message: err instanceof Error ? err.message : String(err),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const reply = error
|
|
117
|
+
? { jsonrpc: "2.0", id, error }
|
|
118
|
+
: { jsonrpc: "2.0", id, result: result ?? null };
|
|
119
|
+
this.writeMessage(reply);
|
|
120
|
+
}
|
|
121
|
+
writeMessage(obj) {
|
|
122
|
+
if (this.closed)
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
this.child.stdin.write(JSON.stringify(obj) + "\n");
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
this.fail(err instanceof Error ? err : new Error(String(err)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
request(method, params) {
|
|
132
|
+
if (this.closed) {
|
|
133
|
+
return Promise.reject(this.closeReason ?? new Error("acp closed"));
|
|
134
|
+
}
|
|
135
|
+
const id = this.nextId++;
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.pending.set(id, {
|
|
138
|
+
resolve: (v) => resolve(v),
|
|
139
|
+
reject,
|
|
140
|
+
});
|
|
141
|
+
this.writeMessage({ jsonrpc: "2.0", id, method, params });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
notify(method, params) {
|
|
145
|
+
this.writeMessage({ jsonrpc: "2.0", method, params });
|
|
146
|
+
}
|
|
147
|
+
fail(err) {
|
|
148
|
+
if (this.closed)
|
|
149
|
+
return;
|
|
150
|
+
this.closed = true;
|
|
151
|
+
this.closeReason = err;
|
|
152
|
+
for (const [, p] of this.pending)
|
|
153
|
+
p.reject(err);
|
|
154
|
+
this.pending.clear();
|
|
155
|
+
}
|
|
156
|
+
isClosed() {
|
|
157
|
+
return this.closed;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export class AcpRuntimeAdapter {
|
|
161
|
+
log;
|
|
162
|
+
constructor(logger) {
|
|
163
|
+
this.log = logger ?? consoleLogger;
|
|
164
|
+
}
|
|
165
|
+
/** argv 尾部(不含二进制)。ACP server 通常不需要参数。 */
|
|
166
|
+
buildArgs(_opts) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
/** initialize 时发送的 clientCapabilities。 */
|
|
170
|
+
clientCapabilities() {
|
|
171
|
+
return { fs: { readTextFile: false, writeTextFile: false } };
|
|
172
|
+
}
|
|
173
|
+
/** initialize 时发送的 clientInfo。 */
|
|
174
|
+
clientInfo() {
|
|
175
|
+
return { name: "botlearn-course-daemon", version: "0.1" };
|
|
176
|
+
}
|
|
177
|
+
/** spawn 之前同步调用;子类可借此把 systemContext 落盘(如 <cwd>/AGENTS.md)。 */
|
|
178
|
+
prepareTurn(_opts) {
|
|
179
|
+
/* default: noop */
|
|
180
|
+
}
|
|
181
|
+
/** 传给 ACP session/new / session/load 的 cwd。 */
|
|
182
|
+
sessionCwd(opts) {
|
|
183
|
+
return opts.cwd;
|
|
184
|
+
}
|
|
185
|
+
async run(opts) {
|
|
186
|
+
if (opts.signal.aborted) {
|
|
187
|
+
return {
|
|
188
|
+
text: "",
|
|
189
|
+
newSessionId: opts.sessionId ?? "",
|
|
190
|
+
error: `${this.id} aborted before spawn`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
this.prepareTurn(opts);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
this.log.warn(`${this.id} prepareTurn threw`, { err: String(err) });
|
|
198
|
+
}
|
|
199
|
+
const binary = this.resolveBinary(opts);
|
|
200
|
+
const args = this.buildArgs(opts);
|
|
201
|
+
this.log.debug(`${this.id} spawn`, {
|
|
202
|
+
cwd: opts.cwd,
|
|
203
|
+
sessionId: opts.sessionId,
|
|
204
|
+
argv: args,
|
|
205
|
+
});
|
|
206
|
+
const child = spawn(binary, args, {
|
|
207
|
+
cwd: opts.cwd,
|
|
208
|
+
env: this.spawnEnv(opts),
|
|
209
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
210
|
+
});
|
|
211
|
+
let killTimer = null;
|
|
212
|
+
const onAbort = () => {
|
|
213
|
+
if (child.killed)
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
child.stdin.end();
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* best-effort */
|
|
220
|
+
}
|
|
221
|
+
child.kill("SIGTERM");
|
|
222
|
+
killTimer = setTimeout(() => {
|
|
223
|
+
if (!child.killed) {
|
|
224
|
+
this.log.warn(`${this.id} did not exit after SIGTERM; sending SIGKILL`);
|
|
225
|
+
try {
|
|
226
|
+
child.kill("SIGKILL");
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
/* best-effort */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, KILL_GRACE_MS);
|
|
233
|
+
if (typeof killTimer.unref === "function")
|
|
234
|
+
killTimer.unref();
|
|
235
|
+
};
|
|
236
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
237
|
+
let stderrTail = "";
|
|
238
|
+
child.stderr.setEncoding("utf8");
|
|
239
|
+
child.stderr.on("data", (chunk) => {
|
|
240
|
+
stderrTail = sanitizeRuntimeFailureText(stderrTail + chunk, STDERR_TAIL_CAP);
|
|
241
|
+
});
|
|
242
|
+
const state = {
|
|
243
|
+
finalText: "",
|
|
244
|
+
assistantTextChunks: [],
|
|
245
|
+
assistantTextBytes: 0,
|
|
246
|
+
assistantTextCapped: false,
|
|
247
|
+
};
|
|
248
|
+
const appendAssistantText = (text) => {
|
|
249
|
+
if (!text || state.assistantTextCapped)
|
|
250
|
+
return;
|
|
251
|
+
const budget = ASSISTANT_TEXT_CAP - state.assistantTextBytes;
|
|
252
|
+
if (budget <= 0) {
|
|
253
|
+
state.assistantTextCapped = true;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const bytes = utf8ByteLength(text);
|
|
257
|
+
if (bytes > budget) {
|
|
258
|
+
const chunk = sliceUtf8Bytes(text, budget);
|
|
259
|
+
if (chunk) {
|
|
260
|
+
state.assistantTextChunks.push(chunk);
|
|
261
|
+
state.assistantTextBytes += utf8ByteLength(chunk);
|
|
262
|
+
}
|
|
263
|
+
state.assistantTextCapped = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
state.assistantTextChunks.push(text);
|
|
267
|
+
state.assistantTextBytes += bytes;
|
|
268
|
+
};
|
|
269
|
+
let seq = 0;
|
|
270
|
+
const conn = new AcpConnection(child, {
|
|
271
|
+
onNotification: (method, params) => {
|
|
272
|
+
if (method === "session/update") {
|
|
273
|
+
seq += 1;
|
|
274
|
+
this.onUpdate(params, {
|
|
275
|
+
appendAssistantText,
|
|
276
|
+
emitBlock: (b) => opts.onBlock?.(b),
|
|
277
|
+
emitStatus: (e) => {
|
|
278
|
+
try {
|
|
279
|
+
opts.onStatus?.(e);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.log.warn(`${this.id} onStatus threw`, { err: String(err) });
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
seq,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
onRequest: async (method, params) => {
|
|
290
|
+
if (method === "session/request_permission") {
|
|
291
|
+
return this.onPermissionRequest(params, opts);
|
|
292
|
+
}
|
|
293
|
+
// 未知的 server→client 请求:抛错经 AcpConnection 折叠成 JSON-RPC
|
|
294
|
+
// error 应答,让 server 自行决定。
|
|
295
|
+
const err = new Error(`unknown server request: ${method}`);
|
|
296
|
+
throw err;
|
|
297
|
+
},
|
|
298
|
+
}, this.id, this.log);
|
|
299
|
+
const childExit = new Promise((resolve) => {
|
|
300
|
+
child.on("close", (code) => {
|
|
301
|
+
resolve(code ?? 0);
|
|
302
|
+
});
|
|
303
|
+
child.on("error", () => {
|
|
304
|
+
/* close 仍会触发;error 本身不携带 exit code */
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
let newSessionId = opts.sessionId ?? "";
|
|
308
|
+
let promptStarted = false;
|
|
309
|
+
try {
|
|
310
|
+
// 1) initialize
|
|
311
|
+
await this.withTimeout(conn.request("initialize", {
|
|
312
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
313
|
+
clientCapabilities: this.clientCapabilities(),
|
|
314
|
+
clientInfo: this.clientInfo(),
|
|
315
|
+
}), INITIALIZE_TIMEOUT_MS, "initialize");
|
|
316
|
+
// 2) session/load(resume 时)→ 失败可恢复则回退 session/new
|
|
317
|
+
const cwd = this.sessionCwd(opts);
|
|
318
|
+
let sessionId = "";
|
|
319
|
+
if (opts.sessionId) {
|
|
320
|
+
try {
|
|
321
|
+
const loaded = (await conn.request("session/load", {
|
|
322
|
+
sessionId: opts.sessionId,
|
|
323
|
+
cwd,
|
|
324
|
+
mcpServers: [],
|
|
325
|
+
}));
|
|
326
|
+
if (loaded !== null && loaded !== undefined) {
|
|
327
|
+
// Hermes 的 load_session 不回传 session_id —— 复用请求的那个;
|
|
328
|
+
// 若未来的 server 回传了,则优先用回传值。
|
|
329
|
+
sessionId =
|
|
330
|
+
(loaded && typeof loaded.sessionId === "string" ? loaded.sessionId : "") ||
|
|
331
|
+
opts.sessionId;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
if (!isRecoverableSessionLoadError(err)) {
|
|
336
|
+
throw new Error(`session/load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
337
|
+
}
|
|
338
|
+
this.log.warn(`${this.id} session/load failed; falling back to new`, {
|
|
339
|
+
err: err instanceof Error ? err.message : String(err),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!sessionId) {
|
|
344
|
+
const created = await conn.request("session/new", {
|
|
345
|
+
cwd,
|
|
346
|
+
mcpServers: [],
|
|
347
|
+
});
|
|
348
|
+
sessionId = created?.sessionId ?? "";
|
|
349
|
+
}
|
|
350
|
+
if (!sessionId) {
|
|
351
|
+
throw new Error("acp server did not return a sessionId");
|
|
352
|
+
}
|
|
353
|
+
newSessionId = sessionId;
|
|
354
|
+
// 3) session/prompt
|
|
355
|
+
promptStarted = true;
|
|
356
|
+
const promptResult = (await conn.request("session/prompt", {
|
|
357
|
+
sessionId,
|
|
358
|
+
prompt: [{ type: "text", text: opts.text }],
|
|
359
|
+
}));
|
|
360
|
+
const stopReason = promptResult?.stopReason ?? "end_turn";
|
|
361
|
+
if (stopReason === "refusal" || stopReason === "error") {
|
|
362
|
+
const tail = stderrTail.slice(-STDERR_ERROR_SNIPPET).trim();
|
|
363
|
+
state.errorText =
|
|
364
|
+
state.errorText ??
|
|
365
|
+
(tail ? `prompt stopped: ${stopReason}; stderr: ${tail}` : `prompt stopped: ${stopReason}`);
|
|
366
|
+
}
|
|
367
|
+
// 提前告知 dispatcher 推理循环已结束 —— 对没有产生
|
|
368
|
+
// agent_message_chunk 的轮次(纯工具副作用、refusal、error)尤为重要;
|
|
369
|
+
// 子进程退出可能还要等数秒。
|
|
370
|
+
try {
|
|
371
|
+
opts.onStatus?.({ kind: "thinking", phase: "stopped" });
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
this.log.warn(`${this.id} onStatus(prompt-done) threw`, { err: String(err) });
|
|
375
|
+
}
|
|
376
|
+
// 礼貌地关 stdin 让 server 可以退出;不退出则由 abort 信号 SIGTERM。
|
|
377
|
+
try {
|
|
378
|
+
child.stdin.end();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
/* best-effort */
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
const baseMsg = err instanceof Error ? err.message : String(err);
|
|
386
|
+
const tail = stderrTail.slice(-STDERR_ERROR_SNIPPET).trim();
|
|
387
|
+
state.errorText = state.errorText ?? (tail ? `${baseMsg}; stderr: ${tail}` : baseMsg);
|
|
388
|
+
if (promptStarted && !opts.signal.aborted) {
|
|
389
|
+
await sleepUnlessAborted(PROMPT_ERROR_DRAIN_MS, opts.signal);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
child.stdin.end();
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
/* best-effort */
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
let code = 0;
|
|
399
|
+
try {
|
|
400
|
+
code = await childExit;
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
404
|
+
if (killTimer)
|
|
405
|
+
clearTimeout(killTimer);
|
|
406
|
+
}
|
|
407
|
+
if (code !== 0 && !state.errorText) {
|
|
408
|
+
state.errorText = `${this.id} exited with code ${code}: ${stderrTail.slice(-STDERR_ERROR_SNIPPET)}`;
|
|
409
|
+
}
|
|
410
|
+
const rawText = state.finalText || state.assistantTextChunks.join("").trim();
|
|
411
|
+
const text = utf8ByteLength(rawText) > ASSISTANT_TEXT_CAP
|
|
412
|
+
? sliceUtf8Bytes(rawText, ASSISTANT_TEXT_CAP)
|
|
413
|
+
: rawText;
|
|
414
|
+
return {
|
|
415
|
+
text,
|
|
416
|
+
newSessionId,
|
|
417
|
+
...(state.errorText ? { error: state.errorText } : {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
withTimeout(p, ms, label) {
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const t = setTimeout(() => reject(new Error(`${this.id} ${label} timed out after ${ms}ms`)), ms);
|
|
423
|
+
if (typeof t.unref === "function")
|
|
424
|
+
t.unref();
|
|
425
|
+
p.then((v) => {
|
|
426
|
+
clearTimeout(t);
|
|
427
|
+
resolve(v);
|
|
428
|
+
}, (e) => {
|
|
429
|
+
clearTimeout(t);
|
|
430
|
+
reject(e);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function formatRpcError(error) {
|
|
436
|
+
if (!error || typeof error !== "object")
|
|
437
|
+
return "(no message)";
|
|
438
|
+
const e = error;
|
|
439
|
+
const message = typeof e.message === "string" && e.message ? e.message : "(no message)";
|
|
440
|
+
const data = e.data;
|
|
441
|
+
if (data === undefined || data === null)
|
|
442
|
+
return message;
|
|
443
|
+
let detail = "";
|
|
444
|
+
if (typeof data === "string") {
|
|
445
|
+
detail = data;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const dataObj = typeof data === "object" ? data : null;
|
|
449
|
+
const details = dataObj && typeof dataObj.details === "string" ? dataObj.details : "";
|
|
450
|
+
if (details) {
|
|
451
|
+
detail = details;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
try {
|
|
455
|
+
detail = JSON.stringify(data);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
detail = String(data);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return detail ? `${message}: ${detail}` : message;
|
|
463
|
+
}
|
|
464
|
+
function isRecoverableSessionLoadError(err) {
|
|
465
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
466
|
+
return (isSessionNotFoundError(err) ||
|
|
467
|
+
/\bnot\s+found\b/i.test(msg) ||
|
|
468
|
+
/method\s+not\s+found|unknown\s+method|not\s+implemented/i.test(msg));
|
|
469
|
+
}
|
|
470
|
+
function isSessionNotFoundError(err) {
|
|
471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
+
return /session(?:\s+[\w-]+)?\s+not\s+found|no\s+session\s+found|unknown\s+session/i.test(msg);
|
|
473
|
+
}
|
|
474
|
+
function sleepUnlessAborted(ms, signal) {
|
|
475
|
+
if (signal.aborted)
|
|
476
|
+
return Promise.resolve();
|
|
477
|
+
return new Promise((resolve) => {
|
|
478
|
+
const t = setTimeout(done, ms);
|
|
479
|
+
if (typeof t.unref === "function")
|
|
480
|
+
t.unref();
|
|
481
|
+
function done() {
|
|
482
|
+
signal.removeEventListener("abort", done);
|
|
483
|
+
clearTimeout(t);
|
|
484
|
+
resolve();
|
|
485
|
+
}
|
|
486
|
+
signal.addEventListener("abort", done, { once: true });
|
|
487
|
+
});
|
|
488
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
|
|
2
|
+
import { type EngineRunOptions } from "./engine.js";
|
|
3
|
+
import { type ProbeDeps } from "./probe.js";
|
|
4
|
+
import type { Logger } from "../log.js";
|
|
5
|
+
import type { RuntimeAuthProbe, RuntimeModule, RuntimeProbe } from "../types.js";
|
|
6
|
+
export declare function scrubClaudeCodeAuthEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
7
|
+
export declare function sanitizeClaudeExtraArgs(extraArgs: string[] | undefined): string[];
|
|
8
|
+
/** 通过 PATH 或 macOS 桌面 bundle 兜底解析 Claude Code CLI。 */
|
|
9
|
+
export declare function resolveClaudeCommand(deps?: ProbeDeps): string | null;
|
|
10
|
+
/** 探测 Claude Code CLI 是否安装并读取版本。 */
|
|
11
|
+
export declare function probeClaude(deps?: ProbeDeps): RuntimeProbe;
|
|
12
|
+
export interface ClaudeAuthProbeResult extends RuntimeAuthProbe {
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
/** 用 `claude -p ping` 探测登录态;Claude Code 会把认证失败伪装成 success result。 */
|
|
16
|
+
export declare function probeClaudeAuth(deps?: ProbeDeps): ClaudeAuthProbeResult;
|
|
17
|
+
/**
|
|
18
|
+
* Claude Code adapter — spawn `claude -p "<text>" --output-format stream-json`
|
|
19
|
+
* (有会话时加 `--resume <sid>`)并解析 ndjson 流。
|
|
20
|
+
*
|
|
21
|
+
* stream-json 形状(节选):
|
|
22
|
+
* {type:"system", subtype:"init", session_id:"...", ...}
|
|
23
|
+
* {type:"assistant", message:{content:[{type:"text", text:"..."} | {type:"tool_use", ...}]}}
|
|
24
|
+
* {type:"user", message:{content:[{type:"tool_result", ...}]}}
|
|
25
|
+
* {type:"result", subtype:"success", session_id:"...", total_cost_usd: 0.01, result:"final text"}
|
|
26
|
+
*/
|
|
27
|
+
export declare class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
28
|
+
readonly id: "claude-code";
|
|
29
|
+
private readonly explicitBinary;
|
|
30
|
+
private resolvedBinary;
|
|
31
|
+
constructor(opts?: {
|
|
32
|
+
binary?: string;
|
|
33
|
+
logger?: Logger;
|
|
34
|
+
});
|
|
35
|
+
run(opts: EngineRunOptions): Promise<import("./engine.js").EngineRunResult>;
|
|
36
|
+
protected resolveBinary(): string;
|
|
37
|
+
protected buildArgs(opts: EngineRunOptions): string[];
|
|
38
|
+
protected spawnEnv(opts: EngineRunOptions): NodeJS.ProcessEnv;
|
|
39
|
+
protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
|
|
40
|
+
}
|
|
41
|
+
export declare const claudeCodeModule: RuntimeModule;
|