linkshell-cli 0.2.126 → 0.3.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/dist/cli/src/index.js +0 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +9 -9
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -1
- package/dist/cli/src/runtime/bridge-session.js +30 -95
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
- package/dist/cli/src/runtime/screen-fallback.js +4 -4
- package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.d.ts +1 -1
- package/dist/cli/src/runtime/screen-share.js +7 -7
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +4034 -5909
- package/dist/shared-protocol/src/index.js +2 -83
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +12 -12
- package/src/runtime/acp/agent-workspace.ts +10 -10
- package/src/runtime/bridge-session.ts +29 -102
- package/src/runtime/screen-fallback.ts +5 -5
- package/src/runtime/screen-share.ts +8 -8
- package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
- package/dist/cli/src/runtime/acp/agent-session.js +0 -1075
- package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
- package/src/runtime/acp/agent-session.ts +0 -1180
|
@@ -1,1180 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createEnvelope,
|
|
3
|
-
parseTypedPayload,
|
|
4
|
-
type Envelope,
|
|
5
|
-
} from "@linkshell/protocol";
|
|
6
|
-
import { AcpClient } from "./acp-client.js";
|
|
7
|
-
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
8
|
-
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
9
|
-
import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
|
|
10
|
-
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
11
|
-
|
|
12
|
-
type AgentStatus = "unavailable" | "idle" | "running" | "waiting_permission" | "error";
|
|
13
|
-
|
|
14
|
-
function protocolSupportsImages(protocol: AgentProtocol | undefined): boolean {
|
|
15
|
-
return protocol === "codex-app-server" ||
|
|
16
|
-
protocol === "claude-agent-sdk" ||
|
|
17
|
-
protocol === "claude-stream-json";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface AgentMessage {
|
|
21
|
-
id: string;
|
|
22
|
-
role: "user" | "assistant" | "system";
|
|
23
|
-
content: string;
|
|
24
|
-
createdAt: number;
|
|
25
|
-
isStreaming?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface AgentToolCall {
|
|
29
|
-
id: string;
|
|
30
|
-
name: string;
|
|
31
|
-
input?: string;
|
|
32
|
-
output?: string;
|
|
33
|
-
createdAt?: number;
|
|
34
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface AgentPlanStep {
|
|
38
|
-
id: string;
|
|
39
|
-
text: string;
|
|
40
|
-
status: "pending" | "in_progress" | "completed";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface AgentPermission {
|
|
44
|
-
requestId: string;
|
|
45
|
-
toolName?: string;
|
|
46
|
-
toolInput?: string;
|
|
47
|
-
context?: string;
|
|
48
|
-
options: { id: string; label: string; kind: "allow" | "deny" | "other" }[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface PendingPermissionWaiter {
|
|
52
|
-
resolve: (value: unknown) => void;
|
|
53
|
-
timer: ReturnType<typeof setTimeout>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
57
|
-
|
|
58
|
-
function id(prefix: string): string {
|
|
59
|
-
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stringify(value: unknown): string {
|
|
63
|
-
if (typeof value === "string") return value;
|
|
64
|
-
try {
|
|
65
|
-
return JSON.stringify(value, null, 2);
|
|
66
|
-
} catch {
|
|
67
|
-
return String(value);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function firstString(value: Record<string, unknown>, keys: string[]): string | undefined {
|
|
72
|
-
for (const key of keys) {
|
|
73
|
-
const next = value[key];
|
|
74
|
-
if (typeof next === "string" && next.length > 0) return next;
|
|
75
|
-
}
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
80
|
-
return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function extractItem(value: unknown): Record<string, unknown> | undefined {
|
|
84
|
-
const raw = asRecord(value);
|
|
85
|
-
if (!raw) return undefined;
|
|
86
|
-
return asRecord(raw.item) ?? raw;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function stringifyDefined(value: unknown): string | undefined {
|
|
90
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
91
|
-
return stringify(value);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function appendCapped(current: string | undefined, delta: string, maxLength: number): string {
|
|
95
|
-
const next = `${current ?? ""}${delta}`;
|
|
96
|
-
if (next.length <= maxLength) return next;
|
|
97
|
-
return next.slice(next.length - maxLength);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function decodeBase64(value: string | undefined): string | undefined {
|
|
101
|
-
if (!value) return undefined;
|
|
102
|
-
try {
|
|
103
|
-
return Buffer.from(value, "base64").toString("utf8");
|
|
104
|
-
} catch {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeToolStatus(value: unknown, completedFallback = false): AgentToolCall["status"] {
|
|
110
|
-
if (value === "completed" || value === "succeeded" || value === "success" || value === "applied") {
|
|
111
|
-
return "completed";
|
|
112
|
-
}
|
|
113
|
-
if (value === "failed" || value === "error" || value === "declined" || value === "cancelled") {
|
|
114
|
-
return "failed";
|
|
115
|
-
}
|
|
116
|
-
if (value === "pending" || value === "queued") return "pending";
|
|
117
|
-
if (value === "running" || value === "inProgress" || value === "executing") return "running";
|
|
118
|
-
return completedFallback ? "completed" : "running";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
|
|
122
|
-
if (value === "completed" || value === "done") return "completed";
|
|
123
|
-
if (value === "inProgress" || value === "running" || value === "active") return "in_progress";
|
|
124
|
-
return "pending";
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function planStepFromItem(item: Record<string, unknown>): AgentPlanStep | undefined {
|
|
128
|
-
const text = firstString(item, ["text", "title", "description", "message"]);
|
|
129
|
-
if (!text) return undefined;
|
|
130
|
-
return {
|
|
131
|
-
id: firstString(item, ["id", "itemId"]) ?? id("plan"),
|
|
132
|
-
text,
|
|
133
|
-
status: normalizePlanStatus(item.status),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function nameFromToolMethod(method: string): string {
|
|
138
|
-
if (method.includes("commandExecution")) return "命令";
|
|
139
|
-
if (method.includes("fileChange")) return "文件修改";
|
|
140
|
-
if (method.includes("mcpToolCall")) return "MCP 工具";
|
|
141
|
-
return "工具";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function isToolItemType(itemType: string | undefined): boolean {
|
|
145
|
-
return (
|
|
146
|
-
itemType === "commandExecution" ||
|
|
147
|
-
itemType === "fileChange" ||
|
|
148
|
-
itemType === "mcpToolCall" ||
|
|
149
|
-
itemType === "dynamicToolCall"
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function toolNameFromItem(item: Record<string, unknown>): string | undefined {
|
|
154
|
-
const itemType = firstString(item, ["type"]);
|
|
155
|
-
if (itemType === "commandExecution") return "命令";
|
|
156
|
-
if (itemType === "fileChange") return "文件修改";
|
|
157
|
-
if (itemType === "mcpToolCall") {
|
|
158
|
-
const server = firstString(item, ["server"]);
|
|
159
|
-
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
160
|
-
return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
|
|
161
|
-
}
|
|
162
|
-
if (itemType === "dynamicToolCall") {
|
|
163
|
-
const namespace = firstString(item, ["namespace"]);
|
|
164
|
-
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
165
|
-
return [namespace, tool].filter(Boolean).join(" · ") || "工具";
|
|
166
|
-
}
|
|
167
|
-
return firstString(item, ["toolName", "tool", "name", "title"]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function toolInputFromItem(item: Record<string, unknown>): string | undefined {
|
|
171
|
-
const itemType = firstString(item, ["type"]);
|
|
172
|
-
if (itemType === "commandExecution") {
|
|
173
|
-
const command = firstString(item, ["command"]);
|
|
174
|
-
const cwd = firstString(item, ["cwd"]);
|
|
175
|
-
if (command && cwd) return `${command}\n\ncwd: ${cwd}`;
|
|
176
|
-
return command ?? cwd;
|
|
177
|
-
}
|
|
178
|
-
if (itemType === "fileChange") {
|
|
179
|
-
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
180
|
-
return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
181
|
-
}
|
|
182
|
-
return stringifyDefined(item.arguments ?? item.input ?? item.toolInput);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function looksLikeDiff(text: string): boolean {
|
|
186
|
-
const value = text.trim();
|
|
187
|
-
return (
|
|
188
|
-
value.startsWith("diff --git ") ||
|
|
189
|
-
value.startsWith("@@ ") ||
|
|
190
|
-
value.includes("\n@@ ") ||
|
|
191
|
-
(value.includes("\n--- ") && value.includes("\n+++ "))
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function collectDiffStrings(value: unknown, depth = 0): string[] {
|
|
196
|
-
if (depth > 6 || value === undefined || value === null) return [];
|
|
197
|
-
if (typeof value === "string") return looksLikeDiff(value) ? [value] : [];
|
|
198
|
-
if (Array.isArray(value)) return value.flatMap((entry) => collectDiffStrings(entry, depth + 1));
|
|
199
|
-
const raw = asRecord(value);
|
|
200
|
-
if (!raw) return [];
|
|
201
|
-
const direct: string[] = [];
|
|
202
|
-
const nested: string[] = [];
|
|
203
|
-
for (const [key, entry] of Object.entries(raw)) {
|
|
204
|
-
const lowerKey = key.toLowerCase();
|
|
205
|
-
const isDiffField =
|
|
206
|
-
lowerKey.includes("diff") ||
|
|
207
|
-
lowerKey.includes("patch") ||
|
|
208
|
-
lowerKey.includes("unified");
|
|
209
|
-
if (typeof entry === "string" && isDiffField && entry.trim()) {
|
|
210
|
-
direct.push(entry);
|
|
211
|
-
} else if (typeof entry === "object" && entry) {
|
|
212
|
-
nested.push(...collectDiffStrings(entry, depth + 1));
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return [...direct, ...nested].filter((entry) => looksLikeDiff(entry));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function extractDiffText(value: unknown): string | undefined {
|
|
219
|
-
const diffs = collectDiffStrings(value)
|
|
220
|
-
.map((entry) => entry.trim())
|
|
221
|
-
.filter(Boolean);
|
|
222
|
-
if (diffs.length === 0) return undefined;
|
|
223
|
-
return diffs
|
|
224
|
-
.filter((entry, index, array) => array.indexOf(entry) === index)
|
|
225
|
-
.join("\n\n")
|
|
226
|
-
.slice(0, 24_000);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
230
|
-
const lines = changes
|
|
231
|
-
.map((change) => {
|
|
232
|
-
const raw = asRecord(change);
|
|
233
|
-
if (!raw) return undefined;
|
|
234
|
-
const path =
|
|
235
|
-
firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
|
|
236
|
-
firstString(asRecord(raw.update) ?? {}, ["path", "file", "filePath"]);
|
|
237
|
-
const kind = firstString(raw, ["kind", "type", "operation", "action"]);
|
|
238
|
-
return [kind, path].filter(Boolean).join(" ") || path;
|
|
239
|
-
})
|
|
240
|
-
.filter((line): line is string => Boolean(line));
|
|
241
|
-
return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export class AgentSessionProxy {
|
|
245
|
-
private client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined;
|
|
246
|
-
private activeProvider: AgentProvider | undefined;
|
|
247
|
-
private activeProtocol: AgentProtocol | undefined;
|
|
248
|
-
private agentSessionId: string | undefined;
|
|
249
|
-
private status: AgentStatus = "unavailable";
|
|
250
|
-
private error: string | undefined;
|
|
251
|
-
private initialized = false;
|
|
252
|
-
private currentTurnId: string | undefined;
|
|
253
|
-
private messages: AgentMessage[] = [];
|
|
254
|
-
private toolCalls = new Map<string, AgentToolCall>();
|
|
255
|
-
private toolOutputBuffers = new Map<string, string>();
|
|
256
|
-
private plan: AgentPlanStep[] = [];
|
|
257
|
-
private planDeltaBuffers = new Map<string, string>();
|
|
258
|
-
private pendingPermissions = new Map<string, AgentPermission>();
|
|
259
|
-
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
260
|
-
private permissionSources = new Map<string, string>();
|
|
261
|
-
|
|
262
|
-
constructor(
|
|
263
|
-
private readonly input: {
|
|
264
|
-
sessionId: string;
|
|
265
|
-
cwd: string;
|
|
266
|
-
availableProviders: AgentProvider[];
|
|
267
|
-
command?: string;
|
|
268
|
-
send: (envelope: Envelope) => void;
|
|
269
|
-
verbose?: boolean;
|
|
270
|
-
},
|
|
271
|
-
) {}
|
|
272
|
-
|
|
273
|
-
async handleEnvelope(envelope: Envelope): Promise<void> {
|
|
274
|
-
switch (envelope.type) {
|
|
275
|
-
case "agent.initialize":
|
|
276
|
-
await this.initialize();
|
|
277
|
-
this.sendSnapshot();
|
|
278
|
-
break;
|
|
279
|
-
case "agent.session.new": {
|
|
280
|
-
const payload = parseTypedPayload("agent.session.new", envelope.payload);
|
|
281
|
-
await this.ensureSession(payload.cwd ?? this.input.cwd, payload.mcpServers);
|
|
282
|
-
this.sendSnapshot();
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
case "agent.session.load": {
|
|
286
|
-
const payload = parseTypedPayload("agent.session.load", envelope.payload);
|
|
287
|
-
await this.ensureClient();
|
|
288
|
-
if (!this.client) return;
|
|
289
|
-
const result = await this.client.loadSession({
|
|
290
|
-
sessionId: payload.agentSessionId,
|
|
291
|
-
cwd: payload.cwd ?? this.input.cwd,
|
|
292
|
-
});
|
|
293
|
-
this.agentSessionId = this.extractSessionId(result) ?? payload.agentSessionId;
|
|
294
|
-
this.status = "idle";
|
|
295
|
-
this.error = undefined;
|
|
296
|
-
this.sendSnapshot();
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
case "agent.session.list":
|
|
300
|
-
await this.sendSessionList();
|
|
301
|
-
break;
|
|
302
|
-
case "agent.prompt": {
|
|
303
|
-
const payload = parseTypedPayload("agent.prompt", envelope.payload);
|
|
304
|
-
await this.sendPrompt(payload);
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case "agent.cancel": {
|
|
308
|
-
const payload = parseTypedPayload("agent.cancel", envelope.payload);
|
|
309
|
-
this.cancelPendingPermissions();
|
|
310
|
-
this.client?.cancel({
|
|
311
|
-
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
312
|
-
turnId: this.currentTurnId,
|
|
313
|
-
});
|
|
314
|
-
this.currentTurnId = undefined;
|
|
315
|
-
this.status = "idle";
|
|
316
|
-
this.sendUpdate({ kind: "status", status: "idle" });
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
case "agent.permission.response": {
|
|
320
|
-
const payload = parseTypedPayload("agent.permission.response", envelope.payload);
|
|
321
|
-
const permission = this.pendingPermissions.get(payload.requestId);
|
|
322
|
-
this.pendingPermissions.delete(payload.requestId);
|
|
323
|
-
const selectedOptionId =
|
|
324
|
-
payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
325
|
-
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
326
|
-
if (waiter) {
|
|
327
|
-
clearTimeout(waiter.timer);
|
|
328
|
-
this.permissionWaiters.delete(payload.requestId);
|
|
329
|
-
waiter.resolve(formatPermissionResponse(
|
|
330
|
-
this.permissionSources.get(payload.requestId),
|
|
331
|
-
payload.outcome,
|
|
332
|
-
selectedOptionId,
|
|
333
|
-
));
|
|
334
|
-
this.permissionSources.delete(payload.requestId);
|
|
335
|
-
} else {
|
|
336
|
-
this.client?.respondPermission({
|
|
337
|
-
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
338
|
-
requestId: payload.requestId,
|
|
339
|
-
outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
|
|
340
|
-
optionId: selectedOptionId,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
this.status = "running";
|
|
344
|
-
this.sendSnapshot();
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
stop(): void {
|
|
351
|
-
this.client?.stop();
|
|
352
|
-
this.client = undefined;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
private async initialize(): Promise<void> {
|
|
356
|
-
if (this.initialized) {
|
|
357
|
-
this.sendCapabilities();
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
await this.tryStartFirstAvailable();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private async tryStartFirstAvailable(): Promise<void> {
|
|
364
|
-
if (this.client) return;
|
|
365
|
-
|
|
366
|
-
for (const provider of this.input.availableProviders) {
|
|
367
|
-
const resolved = resolveAgentCommand({
|
|
368
|
-
provider,
|
|
369
|
-
command: this.input.command,
|
|
370
|
-
});
|
|
371
|
-
if (!resolved) continue;
|
|
372
|
-
|
|
373
|
-
const tryCreateClient = async (config: typeof resolved): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient> => {
|
|
374
|
-
const isClaudeSdk = config.protocol === "claude-agent-sdk";
|
|
375
|
-
const isClaudeStreamJson = config.protocol === "claude-stream-json";
|
|
376
|
-
const client = isClaudeSdk
|
|
377
|
-
? new ClaudeSdkClient({
|
|
378
|
-
command: config.command,
|
|
379
|
-
protocol: config.protocol,
|
|
380
|
-
framing: config.framing,
|
|
381
|
-
cwd: this.input.cwd,
|
|
382
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
383
|
-
onRequest: (method, params) => this.handleRequest(method, params),
|
|
384
|
-
onExit: (message) => this.handleExit(message),
|
|
385
|
-
})
|
|
386
|
-
: isClaudeStreamJson
|
|
387
|
-
? new ClaudeStreamJsonClient({
|
|
388
|
-
command: config.command,
|
|
389
|
-
protocol: config.protocol,
|
|
390
|
-
framing: config.framing,
|
|
391
|
-
cwd: this.input.cwd,
|
|
392
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
393
|
-
onRequest: (method, params) => this.handleRequest(method, params),
|
|
394
|
-
onExit: (message) => this.handleExit(message),
|
|
395
|
-
})
|
|
396
|
-
: new AcpClient({
|
|
397
|
-
command: config.command,
|
|
398
|
-
protocol: config.protocol,
|
|
399
|
-
framing: config.framing,
|
|
400
|
-
cwd: this.input.cwd,
|
|
401
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
402
|
-
onRequest: (method, params) => this.handleRequest(method, params),
|
|
403
|
-
onExit: (message) => this.handleExit(message),
|
|
404
|
-
});
|
|
405
|
-
await client.initialize();
|
|
406
|
-
return client;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
this.client = await tryCreateClient(resolved);
|
|
411
|
-
this.activeProvider = provider;
|
|
412
|
-
this.activeProtocol = resolved.protocol;
|
|
413
|
-
this.initialized = true;
|
|
414
|
-
this.status = "idle";
|
|
415
|
-
this.error = undefined;
|
|
416
|
-
this.sendCapabilities();
|
|
417
|
-
return;
|
|
418
|
-
} catch (error) {
|
|
419
|
-
if (provider === "claude" && resolved.protocol === "claude-agent-sdk") {
|
|
420
|
-
if (this.input.verbose) {
|
|
421
|
-
process.stderr.write(`[agent] Claude SDK failed, falling back to stream-json: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
const fallback = {
|
|
425
|
-
provider,
|
|
426
|
-
command: "claude --print --output-format stream-json --input-format stream-json --verbose --permission-mode bypassPermissions",
|
|
427
|
-
protocol: "claude-stream-json" as const,
|
|
428
|
-
framing: "newline" as const,
|
|
429
|
-
};
|
|
430
|
-
this.client = await tryCreateClient(fallback);
|
|
431
|
-
this.activeProvider = provider;
|
|
432
|
-
this.activeProtocol = fallback.protocol;
|
|
433
|
-
this.initialized = true;
|
|
434
|
-
this.status = "idle";
|
|
435
|
-
this.error = undefined;
|
|
436
|
-
this.sendCapabilities();
|
|
437
|
-
return;
|
|
438
|
-
} catch (fallbackError) {
|
|
439
|
-
this.client?.stop();
|
|
440
|
-
this.client = undefined;
|
|
441
|
-
this.activeProtocol = undefined;
|
|
442
|
-
if (this.input.verbose) {
|
|
443
|
-
process.stderr.write(`[agent] Claude stream-json fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}\n`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
this.client?.stop();
|
|
448
|
-
this.client = undefined;
|
|
449
|
-
this.activeProtocol = undefined;
|
|
450
|
-
if (this.input.verbose) {
|
|
451
|
-
process.stderr.write(`[agent] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
this.status = "unavailable";
|
|
457
|
-
this.error = "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。";
|
|
458
|
-
this.sendCapabilities();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private async ensureClient(): Promise<void> {
|
|
462
|
-
if (this.client) return;
|
|
463
|
-
await this.tryStartFirstAvailable();
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
private async ensureSession(
|
|
467
|
-
cwd: string,
|
|
468
|
-
mcpServers?: Record<string, unknown>,
|
|
469
|
-
): Promise<void> {
|
|
470
|
-
await this.ensureClient();
|
|
471
|
-
if (!this.client || this.agentSessionId) return;
|
|
472
|
-
try {
|
|
473
|
-
const result = await this.client.newSession({ cwd, mcpServers });
|
|
474
|
-
this.agentSessionId = this.extractSessionId(result) ?? id("agent-session");
|
|
475
|
-
this.status = "idle";
|
|
476
|
-
this.error = undefined;
|
|
477
|
-
this.sendUpdate({ kind: "status", status: "idle" });
|
|
478
|
-
} catch (error) {
|
|
479
|
-
this.status = "error";
|
|
480
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
481
|
-
this.sendUpdate({ kind: "error", error: this.error, status: "error" });
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private async sendPrompt(payload: {
|
|
486
|
-
agentSessionId?: string;
|
|
487
|
-
clientMessageId: string;
|
|
488
|
-
contentBlocks: { type: "text" | "image"; text?: string; data?: string; mimeType?: string }[];
|
|
489
|
-
model?: string;
|
|
490
|
-
reasoningEffort?: string;
|
|
491
|
-
permissionMode?: "read_only" | "workspace_write" | "full_access";
|
|
492
|
-
}): Promise<void> {
|
|
493
|
-
await this.ensureSession(this.input.cwd);
|
|
494
|
-
if (!this.client || !this.agentSessionId) return;
|
|
495
|
-
|
|
496
|
-
const content = payload.contentBlocks
|
|
497
|
-
.map((block) => (block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`))
|
|
498
|
-
.filter(Boolean)
|
|
499
|
-
.join("\n");
|
|
500
|
-
const userMessage: AgentMessage = {
|
|
501
|
-
id: payload.clientMessageId,
|
|
502
|
-
role: "user",
|
|
503
|
-
content,
|
|
504
|
-
createdAt: Date.now(),
|
|
505
|
-
};
|
|
506
|
-
this.messages.push(userMessage);
|
|
507
|
-
this.status = "running";
|
|
508
|
-
this.sendUpdate({ kind: "message", message: userMessage, status: "running" });
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const result = await this.client.prompt({
|
|
512
|
-
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
513
|
-
content: payload.contentBlocks,
|
|
514
|
-
clientMessageId: payload.clientMessageId,
|
|
515
|
-
model: payload.model,
|
|
516
|
-
reasoningEffort: payload.reasoningEffort,
|
|
517
|
-
permissionMode: payload.permissionMode,
|
|
518
|
-
cwd: this.input.cwd,
|
|
519
|
-
});
|
|
520
|
-
this.currentTurnId = this.extractTurnId(result) ?? this.currentTurnId;
|
|
521
|
-
if (this.status === "running") {
|
|
522
|
-
this.status = "idle";
|
|
523
|
-
this.sendUpdate({ kind: "status", status: "idle" });
|
|
524
|
-
}
|
|
525
|
-
} catch (error) {
|
|
526
|
-
this.status = "error";
|
|
527
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
528
|
-
this.sendUpdate({ kind: "error", error: this.error, status: "error" });
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
533
|
-
if (isPermissionRequestMethod(method)) {
|
|
534
|
-
return this.handlePermission(params, true, method);
|
|
535
|
-
}
|
|
536
|
-
if (this.input.verbose) {
|
|
537
|
-
process.stderr.write(`[agent:request] unsupported ${method}\n`);
|
|
538
|
-
}
|
|
539
|
-
return {};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private async sendSessionList(): Promise<void> {
|
|
543
|
-
await this.ensureClient();
|
|
544
|
-
if (!this.client) return;
|
|
545
|
-
try {
|
|
546
|
-
const result = await this.client.listSessions();
|
|
547
|
-
this.sendUpdate({
|
|
548
|
-
kind: "status",
|
|
549
|
-
status: "idle",
|
|
550
|
-
delta: stringify(result).slice(0, 4000),
|
|
551
|
-
});
|
|
552
|
-
} catch (error) {
|
|
553
|
-
this.sendUpdate({
|
|
554
|
-
kind: "error",
|
|
555
|
-
error: error instanceof Error ? error.message : String(error),
|
|
556
|
-
status: "error",
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private handleNotification(method: string, params: unknown): void {
|
|
562
|
-
if (this.input.verbose) {
|
|
563
|
-
process.stderr.write(`[agent] ${method} ${stringify(params).slice(0, 500)}\n`);
|
|
564
|
-
}
|
|
565
|
-
if (
|
|
566
|
-
method === "initialized" ||
|
|
567
|
-
method.startsWith("account/") ||
|
|
568
|
-
method.startsWith("mcpServer/startupStatus/") ||
|
|
569
|
-
method === "thread/status/changed" ||
|
|
570
|
-
method === "thread/tokenUsage/updated" ||
|
|
571
|
-
method === "serverRequest/resolved" ||
|
|
572
|
-
method === "mcpServer/oauthLogin/completed"
|
|
573
|
-
) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (method === "thread/started") {
|
|
577
|
-
this.agentSessionId = this.extractSessionId(params) ?? this.agentSessionId;
|
|
578
|
-
this.status = "idle";
|
|
579
|
-
this.sendUpdate({ kind: "status", status: "idle" });
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (method === "turn/started") {
|
|
583
|
-
this.currentTurnId = this.extractTurnId(params) ?? this.currentTurnId;
|
|
584
|
-
this.status = "running";
|
|
585
|
-
this.sendUpdate({ kind: "status", status: "running" });
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (method === "turn/completed") {
|
|
589
|
-
this.currentTurnId = undefined;
|
|
590
|
-
this.status = "idle";
|
|
591
|
-
this.sendUpdate({ kind: "status", status: "idle" });
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (isPermissionRequestMethod(method)) {
|
|
595
|
-
this.handlePermission(params, false, method);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
switch (method) {
|
|
600
|
-
case "item/agentMessage/delta":
|
|
601
|
-
this.handleAgentMessageDelta(params);
|
|
602
|
-
return;
|
|
603
|
-
case "turn/plan/updated":
|
|
604
|
-
this.handlePlanUpdated(params);
|
|
605
|
-
return;
|
|
606
|
-
case "item/plan/delta":
|
|
607
|
-
this.handlePlanDelta(params);
|
|
608
|
-
return;
|
|
609
|
-
case "item/started":
|
|
610
|
-
this.handleItemStarted(params);
|
|
611
|
-
return;
|
|
612
|
-
case "item/completed":
|
|
613
|
-
this.handleItemCompleted(params);
|
|
614
|
-
return;
|
|
615
|
-
case "item/commandExecution/outputDelta":
|
|
616
|
-
case "item/fileChange/outputDelta":
|
|
617
|
-
case "item/mcpToolCall/progress":
|
|
618
|
-
this.handleToolDelta(method, params);
|
|
619
|
-
return;
|
|
620
|
-
case "item/fileChange/patchUpdated":
|
|
621
|
-
this.handleFilePatchUpdated(params);
|
|
622
|
-
return;
|
|
623
|
-
case "turn/diff/updated":
|
|
624
|
-
this.handleTurnDiffUpdated(params);
|
|
625
|
-
return;
|
|
626
|
-
case "command/exec/outputDelta":
|
|
627
|
-
this.handleCommandExecDelta(params);
|
|
628
|
-
return;
|
|
629
|
-
case "item/autoApprovalReview/started":
|
|
630
|
-
case "item/autoApprovalReview/completed":
|
|
631
|
-
case "item/commandExecution/terminalInteraction":
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if (method === "session/update") {
|
|
636
|
-
this.handleUpdate(params);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (this.input.verbose) {
|
|
641
|
-
process.stderr.write(`[agent] ignored ${method}\n`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
private handlePermission(
|
|
646
|
-
params: unknown,
|
|
647
|
-
waitForResponse: boolean,
|
|
648
|
-
source?: string,
|
|
649
|
-
): Promise<unknown> | void {
|
|
650
|
-
const raw = typeof params === "object" && params ? params as Record<string, unknown> : {};
|
|
651
|
-
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
652
|
-
const rawToolCall = typeof raw.toolCall === "object" && raw.toolCall
|
|
653
|
-
? raw.toolCall as Record<string, unknown>
|
|
654
|
-
: raw;
|
|
655
|
-
const permission: AgentPermission = {
|
|
656
|
-
requestId,
|
|
657
|
-
toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]),
|
|
658
|
-
toolInput: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
659
|
-
context: firstString(raw, ["context", "description", "message", "title"]),
|
|
660
|
-
options: parsePermissionOptions(raw.options),
|
|
661
|
-
};
|
|
662
|
-
this.pendingPermissions.set(requestId, permission);
|
|
663
|
-
if (source) this.permissionSources.set(requestId, source);
|
|
664
|
-
this.status = "waiting_permission";
|
|
665
|
-
this.input.send(createEnvelope({
|
|
666
|
-
type: "agent.permission.request",
|
|
667
|
-
sessionId: this.input.sessionId,
|
|
668
|
-
payload: { agentSessionId: this.agentSessionId, ...permission },
|
|
669
|
-
}));
|
|
670
|
-
|
|
671
|
-
if (!waitForResponse) return;
|
|
672
|
-
|
|
673
|
-
return new Promise((resolve) => {
|
|
674
|
-
const timer = setTimeout(() => {
|
|
675
|
-
this.pendingPermissions.delete(requestId);
|
|
676
|
-
this.permissionWaiters.delete(requestId);
|
|
677
|
-
this.permissionSources.delete(requestId);
|
|
678
|
-
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
679
|
-
this.sendSnapshot();
|
|
680
|
-
}, PERMISSION_TIMEOUT_MS);
|
|
681
|
-
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
private handleAgentMessageDelta(params: unknown): void {
|
|
686
|
-
const raw = asRecord(params);
|
|
687
|
-
if (!raw) return;
|
|
688
|
-
const itemId = firstString(raw, ["itemId", "id", "messageId"]) ?? id("msg");
|
|
689
|
-
const delta = firstString(raw, ["delta", "text", "content"]);
|
|
690
|
-
if (!delta) return;
|
|
691
|
-
const current = this.messages.find((message) => message.id === itemId);
|
|
692
|
-
const message: AgentMessage = {
|
|
693
|
-
id: itemId,
|
|
694
|
-
role: "assistant",
|
|
695
|
-
content: `${current?.content ?? ""}${delta}`,
|
|
696
|
-
createdAt: current?.createdAt ?? Date.now(),
|
|
697
|
-
isStreaming: true,
|
|
698
|
-
};
|
|
699
|
-
this.upsertMessage(message);
|
|
700
|
-
this.status = "running";
|
|
701
|
-
this.sendUpdate({ kind: "message_delta", message, delta, status: "running" });
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
private handlePlanUpdated(params: unknown): void {
|
|
705
|
-
const raw = asRecord(params);
|
|
706
|
-
const plan = Array.isArray(raw?.plan) ? raw.plan : [];
|
|
707
|
-
this.plan = plan
|
|
708
|
-
.map((entry, index) => {
|
|
709
|
-
const step = asRecord(entry);
|
|
710
|
-
const text = firstString(step ?? {}, ["text", "title", "description", "message"]);
|
|
711
|
-
if (!text) return undefined;
|
|
712
|
-
return {
|
|
713
|
-
id: firstString(step ?? {}, ["id"]) ?? `plan-${index + 1}`,
|
|
714
|
-
text,
|
|
715
|
-
status: normalizePlanStatus(step?.status),
|
|
716
|
-
} satisfies AgentPlanStep;
|
|
717
|
-
})
|
|
718
|
-
.filter((step): step is AgentPlanStep => Boolean(step));
|
|
719
|
-
if (this.plan.length > 0) {
|
|
720
|
-
this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
private handlePlanDelta(params: unknown): void {
|
|
725
|
-
const raw = asRecord(params);
|
|
726
|
-
if (!raw) return;
|
|
727
|
-
const itemId = firstString(raw, ["itemId", "id"]) ?? id("plan");
|
|
728
|
-
const delta = firstString(raw, ["delta", "text"]);
|
|
729
|
-
if (!delta) return;
|
|
730
|
-
const text = `${this.planDeltaBuffers.get(itemId) ?? ""}${delta}`;
|
|
731
|
-
this.planDeltaBuffers.set(itemId, text);
|
|
732
|
-
const existing = this.plan.findIndex((step) => step.id === itemId);
|
|
733
|
-
const step: AgentPlanStep = { id: itemId, text, status: "in_progress" };
|
|
734
|
-
if (existing >= 0) this.plan[existing] = step;
|
|
735
|
-
else this.plan.push(step);
|
|
736
|
-
this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private handleItemStarted(params: unknown): void {
|
|
740
|
-
const item = extractItem(params);
|
|
741
|
-
if (!item) return;
|
|
742
|
-
const itemType = firstString(item, ["type"]);
|
|
743
|
-
|
|
744
|
-
if (itemType === "agentMessage" || itemType === "assistantMessage") {
|
|
745
|
-
this.handleCompletedMessageItem(item, true);
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (itemType === "plan") {
|
|
750
|
-
const planStep = planStepFromItem(item);
|
|
751
|
-
if (planStep) {
|
|
752
|
-
const existing = this.plan.findIndex((step) => step.id === planStep.id);
|
|
753
|
-
if (existing >= 0) this.plan[existing] = planStep;
|
|
754
|
-
else this.plan.push(planStep);
|
|
755
|
-
this.sendUpdate({ kind: "plan", plan: this.plan, status: "running" });
|
|
756
|
-
}
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const toolCall = this.withToolCreatedAt(this.toolCallFromItem(item, "running"));
|
|
761
|
-
if (!toolCall) return;
|
|
762
|
-
this.toolCalls.set(toolCall.id, toolCall);
|
|
763
|
-
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
private handleItemCompleted(params: unknown): void {
|
|
767
|
-
const item = extractItem(params);
|
|
768
|
-
if (!item) return;
|
|
769
|
-
const itemType = firstString(item, ["type"]);
|
|
770
|
-
|
|
771
|
-
if (itemType === "agentMessage" || itemType === "assistantMessage") {
|
|
772
|
-
this.handleCompletedMessageItem(item, false);
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (itemType === "plan") {
|
|
777
|
-
const planStep = planStepFromItem(item);
|
|
778
|
-
if (planStep) {
|
|
779
|
-
const existing = this.plan.findIndex((step) => step.id === planStep.id);
|
|
780
|
-
const completed = { ...planStep, status: "completed" as const };
|
|
781
|
-
if (existing >= 0) this.plan[existing] = completed;
|
|
782
|
-
else this.plan.push(completed);
|
|
783
|
-
this.sendUpdate({ kind: "plan", plan: this.plan, status: this.status === "running" ? "running" : "idle" });
|
|
784
|
-
}
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const toolCall = this.withToolCreatedAt(
|
|
789
|
-
this.toolCallFromItem(item, normalizeToolStatus(item.status, true)),
|
|
790
|
-
);
|
|
791
|
-
if (!toolCall) return;
|
|
792
|
-
const bufferedOutput = this.toolOutputBuffers.get(toolCall.id);
|
|
793
|
-
if (bufferedOutput && !toolCall.output) toolCall.output = bufferedOutput;
|
|
794
|
-
this.toolCalls.set(toolCall.id, toolCall);
|
|
795
|
-
this.sendUpdate({ kind: "tool_result", toolCall, status: this.status === "running" ? "running" : "idle" });
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
private handleToolDelta(method: string, params: unknown): void {
|
|
799
|
-
const raw = asRecord(params);
|
|
800
|
-
if (!raw) return;
|
|
801
|
-
const itemId = firstString(raw, ["itemId", "id", "toolCallId"]) ?? id("tool");
|
|
802
|
-
const delta = firstString(raw, ["delta", "message", "text"]);
|
|
803
|
-
if (!delta) return;
|
|
804
|
-
const output = appendCapped(this.toolOutputBuffers.get(itemId), delta, 6000);
|
|
805
|
-
this.toolOutputBuffers.set(itemId, output);
|
|
806
|
-
const existing = this.toolCalls.get(itemId);
|
|
807
|
-
const toolCall: AgentToolCall = {
|
|
808
|
-
id: itemId,
|
|
809
|
-
name: existing?.name ?? nameFromToolMethod(method),
|
|
810
|
-
input: existing?.input,
|
|
811
|
-
output,
|
|
812
|
-
createdAt: existing?.createdAt ?? Date.now(),
|
|
813
|
-
status: "running",
|
|
814
|
-
};
|
|
815
|
-
this.toolCalls.set(itemId, toolCall);
|
|
816
|
-
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
private handleFilePatchUpdated(params: unknown): void {
|
|
820
|
-
const raw = asRecord(params);
|
|
821
|
-
if (!raw) return;
|
|
822
|
-
const itemId = firstString(raw, ["itemId", "id"]) ?? id("file");
|
|
823
|
-
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
824
|
-
const output = extractDiffText(raw) ?? summarizeFileChanges(changes);
|
|
825
|
-
const existing = this.toolCalls.get(itemId);
|
|
826
|
-
const toolCall = this.withToolCreatedAt({
|
|
827
|
-
id: itemId,
|
|
828
|
-
name: existing?.name ?? "文件修改",
|
|
829
|
-
input: existing?.input,
|
|
830
|
-
output: output || existing?.output,
|
|
831
|
-
createdAt: existing?.createdAt ?? Date.now(),
|
|
832
|
-
status: existing?.status ?? "running",
|
|
833
|
-
});
|
|
834
|
-
if (!toolCall) return;
|
|
835
|
-
if (toolCall.id !== itemId) this.toolCalls.delete(itemId);
|
|
836
|
-
this.toolCalls.set(toolCall.id, toolCall);
|
|
837
|
-
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
private handleTurnDiffUpdated(params: unknown): void {
|
|
841
|
-
const raw = asRecord(params);
|
|
842
|
-
if (!raw) return;
|
|
843
|
-
const diff = extractDiffText(raw);
|
|
844
|
-
if (!diff) return;
|
|
845
|
-
const itemId = firstString(raw, ["itemId", "id", "turnId"]) ?? "workspace-diff";
|
|
846
|
-
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
847
|
-
const existing = this.toolCalls.get(itemId);
|
|
848
|
-
const toolCall = this.withToolCreatedAt({
|
|
849
|
-
id: itemId,
|
|
850
|
-
name: existing?.name ?? "文件修改",
|
|
851
|
-
input: existing?.input ?? summarizeFileChanges(changes),
|
|
852
|
-
output: diff,
|
|
853
|
-
createdAt: existing?.createdAt ?? Date.now(),
|
|
854
|
-
status: existing?.status ?? "running",
|
|
855
|
-
});
|
|
856
|
-
if (!toolCall) return;
|
|
857
|
-
if (toolCall.id !== itemId) this.toolCalls.delete(itemId);
|
|
858
|
-
this.toolCalls.set(toolCall.id, toolCall);
|
|
859
|
-
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
private handleCommandExecDelta(params: unknown): void {
|
|
863
|
-
const raw = asRecord(params);
|
|
864
|
-
if (!raw) return;
|
|
865
|
-
const processId = firstString(raw, ["processId", "id"]) ?? id("exec");
|
|
866
|
-
const delta =
|
|
867
|
-
firstString(raw, ["delta", "text"]) ??
|
|
868
|
-
decodeBase64(firstString(raw, ["deltaBase64"]));
|
|
869
|
-
if (!delta) return;
|
|
870
|
-
const output = appendCapped(this.toolOutputBuffers.get(processId), delta, 6000);
|
|
871
|
-
this.toolOutputBuffers.set(processId, output);
|
|
872
|
-
const existing = this.toolCalls.get(processId);
|
|
873
|
-
const toolCall: AgentToolCall = {
|
|
874
|
-
id: processId,
|
|
875
|
-
name: existing?.name ?? "命令输出",
|
|
876
|
-
input: existing?.input,
|
|
877
|
-
output,
|
|
878
|
-
createdAt: existing?.createdAt ?? Date.now(),
|
|
879
|
-
status: "running",
|
|
880
|
-
};
|
|
881
|
-
this.toolCalls.set(processId, toolCall);
|
|
882
|
-
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
private handleCompletedMessageItem(item: Record<string, unknown>, streaming: boolean): void {
|
|
886
|
-
const itemId = firstString(item, ["id"]) ?? id("msg");
|
|
887
|
-
const existing = this.messages.find((message) => message.id === itemId);
|
|
888
|
-
const content = firstString(item, ["text", "content", "message"]) ?? existing?.content;
|
|
889
|
-
if (!content) return;
|
|
890
|
-
const message: AgentMessage = {
|
|
891
|
-
id: itemId,
|
|
892
|
-
role: "assistant",
|
|
893
|
-
content,
|
|
894
|
-
createdAt: existing?.createdAt ?? Date.now(),
|
|
895
|
-
isStreaming: streaming,
|
|
896
|
-
};
|
|
897
|
-
this.upsertMessage(message);
|
|
898
|
-
this.sendUpdate({
|
|
899
|
-
kind: streaming ? "message_delta" : "message",
|
|
900
|
-
message,
|
|
901
|
-
status: this.status === "running" ? "running" : "idle",
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
private toolCallFromItem(
|
|
906
|
-
item: Record<string, unknown>,
|
|
907
|
-
fallbackStatus: AgentToolCall["status"],
|
|
908
|
-
): AgentToolCall | undefined {
|
|
909
|
-
const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
|
|
910
|
-
if (!itemId) return undefined;
|
|
911
|
-
const itemType = firstString(item, ["type"]);
|
|
912
|
-
const name = toolNameFromItem(item);
|
|
913
|
-
if (!name && !isToolItemType(itemType)) return undefined;
|
|
914
|
-
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
915
|
-
const rawOutput =
|
|
916
|
-
firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
917
|
-
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
918
|
-
const output = itemType === "fileChange"
|
|
919
|
-
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
920
|
-
: rawOutput ?? bufferedOutput;
|
|
921
|
-
return {
|
|
922
|
-
id: itemId,
|
|
923
|
-
name: name ?? "工具",
|
|
924
|
-
input: toolInputFromItem(item),
|
|
925
|
-
output,
|
|
926
|
-
createdAt: Date.now(),
|
|
927
|
-
status: normalizeToolStatus(item.status, fallbackStatus === "completed"),
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
private handleUpdate(params: unknown): void {
|
|
932
|
-
const raw = typeof params === "object" && params ? params as Record<string, unknown> : {};
|
|
933
|
-
const nested = typeof raw.params === "object" && raw.params ? raw.params as Record<string, unknown> : {};
|
|
934
|
-
const text =
|
|
935
|
-
firstString(raw, ["delta", "text", "content", "message"]) ??
|
|
936
|
-
firstString(nested, ["delta", "text", "content", "message"]) ??
|
|
937
|
-
undefined;
|
|
938
|
-
if (!text) return;
|
|
939
|
-
const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
|
|
940
|
-
|
|
941
|
-
if (firstString(raw, ["toolName", "tool", "name"])) {
|
|
942
|
-
const toolCall: AgentToolCall = {
|
|
943
|
-
id: firstString(raw, ["toolCallId", "callId", "id"]) ?? id("tool"),
|
|
944
|
-
name: firstString(raw, ["toolName", "tool", "name"]) ?? "tool",
|
|
945
|
-
input: stringify(raw.input ?? raw.toolInput ?? ""),
|
|
946
|
-
output: stringify(raw.output ?? raw.result ?? ""),
|
|
947
|
-
createdAt: Date.now(),
|
|
948
|
-
status: raw.status === "completed" || raw.status === "failed" || raw.status === "running"
|
|
949
|
-
? raw.status
|
|
950
|
-
: "running",
|
|
951
|
-
};
|
|
952
|
-
const nextToolCall = this.withToolCreatedAt(toolCall);
|
|
953
|
-
if (!nextToolCall) return;
|
|
954
|
-
this.toolCalls.set(nextToolCall.id, nextToolCall);
|
|
955
|
-
this.sendUpdate({ kind: "tool_call", toolCall: nextToolCall, status: "running" });
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const message: AgentMessage = {
|
|
960
|
-
id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
|
|
961
|
-
role,
|
|
962
|
-
content: text,
|
|
963
|
-
createdAt: Date.now(),
|
|
964
|
-
isStreaming: raw.done === false || raw.isStreaming === true,
|
|
965
|
-
};
|
|
966
|
-
this.upsertMessage(message);
|
|
967
|
-
this.status = raw.done === true ? "idle" : "running";
|
|
968
|
-
this.sendUpdate({
|
|
969
|
-
kind: "message",
|
|
970
|
-
message,
|
|
971
|
-
status: this.status === "running" ? "running" : "idle",
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private handleExit(message: string): void {
|
|
976
|
-
this.cancelPendingPermissions();
|
|
977
|
-
this.status = "error";
|
|
978
|
-
this.error = message;
|
|
979
|
-
this.client = undefined;
|
|
980
|
-
this.activeProtocol = undefined;
|
|
981
|
-
this.sendUpdate({ kind: "error", error: message, status: "error" });
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private cancelPendingPermissions(): void {
|
|
985
|
-
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
986
|
-
clearTimeout(waiter.timer);
|
|
987
|
-
waiter.resolve(formatPermissionResponse(
|
|
988
|
-
this.permissionSources.get(requestId),
|
|
989
|
-
"cancelled",
|
|
990
|
-
"cancelled",
|
|
991
|
-
));
|
|
992
|
-
this.pendingPermissions.delete(requestId);
|
|
993
|
-
this.permissionSources.delete(requestId);
|
|
994
|
-
}
|
|
995
|
-
this.permissionWaiters.clear();
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
private upsertMessage(message: AgentMessage): void {
|
|
999
|
-
const existing = this.messages.findIndex((entry) => entry.id === message.id);
|
|
1000
|
-
if (existing >= 0) this.messages[existing] = message;
|
|
1001
|
-
else this.messages.push(message);
|
|
1002
|
-
if (this.messages.length > 100) this.messages.shift();
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
private withToolCreatedAt(toolCall: AgentToolCall | undefined): AgentToolCall | undefined {
|
|
1006
|
-
if (!toolCall) return undefined;
|
|
1007
|
-
const duplicate = this.findDuplicateFileTool(toolCall);
|
|
1008
|
-
const existing = duplicate ?? this.toolCalls.get(toolCall.id);
|
|
1009
|
-
return {
|
|
1010
|
-
...existing,
|
|
1011
|
-
...toolCall,
|
|
1012
|
-
id: existing?.id ?? toolCall.id,
|
|
1013
|
-
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
private findDuplicateFileTool(toolCall: AgentToolCall): AgentToolCall | undefined {
|
|
1018
|
-
const output = toolCall.output?.trim();
|
|
1019
|
-
if (!toolCall.name.includes("文件") || !output) return undefined;
|
|
1020
|
-
return [...this.toolCalls.values()].find((existing) =>
|
|
1021
|
-
existing.id !== toolCall.id &&
|
|
1022
|
-
existing.name.includes("文件") &&
|
|
1023
|
-
existing.output?.trim() === output
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private sendCapabilities(): void {
|
|
1028
|
-
const enabled = Boolean(this.client && this.initialized && !this.error);
|
|
1029
|
-
const activeProvider = this.activeProvider ?? this.input.availableProviders[0];
|
|
1030
|
-
const supportsPermission = enabled && this.activeProtocol !== "claude-stream-json";
|
|
1031
|
-
this.input.send(createEnvelope({
|
|
1032
|
-
type: "agent.capabilities",
|
|
1033
|
-
sessionId: this.input.sessionId,
|
|
1034
|
-
payload: {
|
|
1035
|
-
enabled,
|
|
1036
|
-
provider: activeProvider ?? "codex",
|
|
1037
|
-
protocolVersion: 1,
|
|
1038
|
-
error: enabled ? undefined : this.error,
|
|
1039
|
-
supportsSessionList: enabled,
|
|
1040
|
-
supportsSessionLoad: enabled,
|
|
1041
|
-
supportsImages: enabled && protocolSupportsImages(this.activeProtocol),
|
|
1042
|
-
supportsAudio: false,
|
|
1043
|
-
supportsPermission,
|
|
1044
|
-
supportsPlan: enabled,
|
|
1045
|
-
supportsCancel: enabled,
|
|
1046
|
-
},
|
|
1047
|
-
}));
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
private sendSnapshot(): void {
|
|
1051
|
-
this.input.send(createEnvelope({
|
|
1052
|
-
type: "agent.snapshot",
|
|
1053
|
-
sessionId: this.input.sessionId,
|
|
1054
|
-
payload: {
|
|
1055
|
-
agentSessionId: this.agentSessionId,
|
|
1056
|
-
messages: this.messages,
|
|
1057
|
-
toolCalls: [...this.toolCalls.values()],
|
|
1058
|
-
pendingPermissions: [...this.pendingPermissions.values()],
|
|
1059
|
-
status: this.status,
|
|
1060
|
-
error: this.error,
|
|
1061
|
-
},
|
|
1062
|
-
}));
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
private sendUpdate(payload: {
|
|
1066
|
-
kind: "message" | "message_delta" | "tool_call" | "tool_result" | "plan" | "status" | "error";
|
|
1067
|
-
message?: AgentMessage;
|
|
1068
|
-
delta?: string;
|
|
1069
|
-
toolCall?: AgentToolCall;
|
|
1070
|
-
plan?: AgentPlanStep[];
|
|
1071
|
-
status?: "idle" | "running" | "waiting_permission" | "error";
|
|
1072
|
-
error?: string;
|
|
1073
|
-
}): void {
|
|
1074
|
-
this.input.send(createEnvelope({
|
|
1075
|
-
type: "agent.update",
|
|
1076
|
-
sessionId: this.input.sessionId,
|
|
1077
|
-
payload: { agentSessionId: this.agentSessionId, ...payload },
|
|
1078
|
-
}));
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
private extractSessionId(value: unknown): string | undefined {
|
|
1082
|
-
if (!value || typeof value !== "object") return undefined;
|
|
1083
|
-
const raw = value as Record<string, unknown>;
|
|
1084
|
-
if (raw.thread && typeof raw.thread === "object") {
|
|
1085
|
-
const threadId = firstString(raw.thread as Record<string, unknown>, ["id", "threadId"]);
|
|
1086
|
-
if (threadId) return threadId;
|
|
1087
|
-
}
|
|
1088
|
-
return firstString(raw, ["sessionId", "id", "agentSessionId"]);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
private extractTurnId(value: unknown): string | undefined {
|
|
1092
|
-
if (!value || typeof value !== "object") return undefined;
|
|
1093
|
-
const raw = value as Record<string, unknown>;
|
|
1094
|
-
if (raw.turn && typeof raw.turn === "object") {
|
|
1095
|
-
const turnId = firstString(raw.turn as Record<string, unknown>, ["id", "turnId"]);
|
|
1096
|
-
if (turnId) return turnId;
|
|
1097
|
-
}
|
|
1098
|
-
return firstString(raw, ["turnId", "id"]);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function parsePermissionOptions(value: unknown): AgentPermission["options"] {
|
|
1103
|
-
if (!Array.isArray(value)) {
|
|
1104
|
-
return [
|
|
1105
|
-
{ id: "allow", label: "允许", kind: "allow" },
|
|
1106
|
-
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
1107
|
-
];
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
const options = value
|
|
1111
|
-
.map((entry, index) => {
|
|
1112
|
-
const raw = typeof entry === "object" && entry ? entry as Record<string, unknown> : {};
|
|
1113
|
-
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? `option-${index + 1}`;
|
|
1114
|
-
const labelValue = raw.name ?? raw.label ?? raw.kind ?? String(idValue);
|
|
1115
|
-
const id = String(idValue);
|
|
1116
|
-
const label = String(labelValue);
|
|
1117
|
-
const normalized = `${id} ${label}`.toLowerCase();
|
|
1118
|
-
const kind: AgentPermission["options"][number]["kind"] = normalized.includes("reject") || normalized.includes("deny")
|
|
1119
|
-
? "deny"
|
|
1120
|
-
: normalized.includes("allow")
|
|
1121
|
-
? "allow"
|
|
1122
|
-
: "other";
|
|
1123
|
-
return { id, label, kind };
|
|
1124
|
-
})
|
|
1125
|
-
.filter((option) => option.id.length > 0 && option.label.length > 0);
|
|
1126
|
-
|
|
1127
|
-
return options.length > 0
|
|
1128
|
-
? options
|
|
1129
|
-
: [
|
|
1130
|
-
{ id: "allow", label: "允许", kind: "allow" },
|
|
1131
|
-
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
1132
|
-
];
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
function selectPermissionOption(
|
|
1136
|
-
permission: AgentPermission | undefined,
|
|
1137
|
-
outcome: "allow" | "deny" | "cancelled",
|
|
1138
|
-
): string {
|
|
1139
|
-
if (outcome === "cancelled") return "cancelled";
|
|
1140
|
-
const option = permission?.options.find((item) => item.kind === outcome);
|
|
1141
|
-
return option?.id ?? outcome;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function isPermissionRequestMethod(method: string): boolean {
|
|
1145
|
-
return (
|
|
1146
|
-
method === "session/request_permission" ||
|
|
1147
|
-
method.endsWith("/requestApproval") ||
|
|
1148
|
-
method === "mcpServer/elicitation/request" ||
|
|
1149
|
-
method === "item/tool/requestUserInput" ||
|
|
1150
|
-
method === "claude/requestApproval"
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function formatPermissionResponse(
|
|
1155
|
-
source: string | undefined,
|
|
1156
|
-
outcome: "allow" | "deny" | "cancelled",
|
|
1157
|
-
optionId: string,
|
|
1158
|
-
): unknown {
|
|
1159
|
-
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
1160
|
-
return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
|
|
1161
|
-
}
|
|
1162
|
-
if (source === "item/permissions/requestApproval") {
|
|
1163
|
-
if (outcome === "allow") {
|
|
1164
|
-
return {
|
|
1165
|
-
permissions: { type: "managed", network: { enabled: true }, fileSystem: { type: "fullAccess" } },
|
|
1166
|
-
scope: optionId.includes("session") ? "session" : "turn",
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
|
|
1170
|
-
}
|
|
1171
|
-
if (source === "claude/requestApproval") {
|
|
1172
|
-
return { behavior: outcome === "allow" ? "allow" : "deny" };
|
|
1173
|
-
}
|
|
1174
|
-
return {
|
|
1175
|
-
outcome:
|
|
1176
|
-
outcome === "cancelled"
|
|
1177
|
-
? { outcome: "cancelled" }
|
|
1178
|
-
: { outcome: "selected", optionId },
|
|
1179
|
-
};
|
|
1180
|
-
}
|