linkshell-cli 0.2.65 → 0.2.67
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/README.md +3 -0
- package/dist/cli/src/index.js +42 -2
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +41 -0
- package/dist/cli/src/runtime/acp/acp-client.js +102 -0
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-session.d.ts +42 -0
- package/dist/cli/src/runtime/acp/agent-session.js +492 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +21 -0
- package/dist/cli/src/runtime/acp/json-rpc.js +150 -0
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +13 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js +22 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +7 -0
- package/dist/cli/src/runtime/bridge-session.js +61 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +6 -0
- package/dist/cli/src/utils/daemon.js +22 -0
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/src/utils/keep-awake.d.ts +6 -0
- package/dist/cli/src/utils/keep-awake.js +48 -0
- package/dist/cli/src/utils/keep-awake.js.map +1 -0
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1076 -28
- package/dist/shared-protocol/src/index.js +108 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +49 -2
- package/src/runtime/acp/acp-client.ts +133 -0
- package/src/runtime/acp/agent-session.ts +589 -0
- package/src/runtime/acp/json-rpc.ts +177 -0
- package/src/runtime/acp/provider-resolver.ts +37 -0
- package/src/runtime/bridge-session.ts +72 -0
- package/src/utils/daemon.ts +28 -0
- package/src/utils/keep-awake.ts +61 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEnvelope,
|
|
3
|
+
parseTypedPayload,
|
|
4
|
+
type Envelope,
|
|
5
|
+
} from "@linkshell/protocol";
|
|
6
|
+
import { AcpClient } from "./acp-client.js";
|
|
7
|
+
import type { AgentProvider } from "./provider-resolver.js";
|
|
8
|
+
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
9
|
+
|
|
10
|
+
type AgentStatus = "unavailable" | "idle" | "running" | "waiting_permission" | "error";
|
|
11
|
+
|
|
12
|
+
interface AgentMessage {
|
|
13
|
+
id: string;
|
|
14
|
+
role: "user" | "assistant" | "system";
|
|
15
|
+
content: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
isStreaming?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AgentToolCall {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
input?: string;
|
|
24
|
+
output?: string;
|
|
25
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AgentPermission {
|
|
29
|
+
requestId: string;
|
|
30
|
+
toolName?: string;
|
|
31
|
+
toolInput?: string;
|
|
32
|
+
context?: string;
|
|
33
|
+
options: { id: string; label: string; kind: "allow" | "deny" | "other" }[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PendingPermissionWaiter {
|
|
37
|
+
resolve: (value: unknown) => void;
|
|
38
|
+
timer: ReturnType<typeof setTimeout>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
42
|
+
|
|
43
|
+
function id(prefix: string): string {
|
|
44
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stringify(value: unknown): string {
|
|
48
|
+
if (typeof value === "string") return value;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(value, null, 2);
|
|
51
|
+
} catch {
|
|
52
|
+
return String(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function firstString(value: Record<string, unknown>, keys: string[]): string | undefined {
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const next = value[key];
|
|
59
|
+
if (typeof next === "string" && next.length > 0) return next;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class AgentSessionProxy {
|
|
65
|
+
private client: AcpClient | undefined;
|
|
66
|
+
private agentSessionId: string | undefined;
|
|
67
|
+
private status: AgentStatus = "unavailable";
|
|
68
|
+
private error: string | undefined;
|
|
69
|
+
private initialized = false;
|
|
70
|
+
private currentTurnId: string | undefined;
|
|
71
|
+
private messages: AgentMessage[] = [];
|
|
72
|
+
private toolCalls = new Map<string, AgentToolCall>();
|
|
73
|
+
private pendingPermissions = new Map<string, AgentPermission>();
|
|
74
|
+
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
75
|
+
private permissionSources = new Map<string, string>();
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private readonly input: {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
cwd: string;
|
|
81
|
+
provider: AgentProvider;
|
|
82
|
+
command?: string;
|
|
83
|
+
send: (envelope: Envelope) => void;
|
|
84
|
+
verbose?: boolean;
|
|
85
|
+
},
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async handleEnvelope(envelope: Envelope): Promise<void> {
|
|
89
|
+
switch (envelope.type) {
|
|
90
|
+
case "agent.initialize":
|
|
91
|
+
await this.initialize();
|
|
92
|
+
this.sendSnapshot();
|
|
93
|
+
break;
|
|
94
|
+
case "agent.session.new": {
|
|
95
|
+
const payload = parseTypedPayload("agent.session.new", envelope.payload);
|
|
96
|
+
await this.ensureSession(payload.cwd ?? this.input.cwd, payload.mcpServers);
|
|
97
|
+
this.sendSnapshot();
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "agent.session.load": {
|
|
101
|
+
const payload = parseTypedPayload("agent.session.load", envelope.payload);
|
|
102
|
+
await this.ensureClient();
|
|
103
|
+
if (!this.client) return;
|
|
104
|
+
const result = await this.client.loadSession({
|
|
105
|
+
sessionId: payload.agentSessionId,
|
|
106
|
+
cwd: payload.cwd ?? this.input.cwd,
|
|
107
|
+
});
|
|
108
|
+
this.agentSessionId = this.extractSessionId(result) ?? payload.agentSessionId;
|
|
109
|
+
this.status = "idle";
|
|
110
|
+
this.error = undefined;
|
|
111
|
+
this.sendSnapshot();
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "agent.session.list":
|
|
115
|
+
await this.sendSessionList();
|
|
116
|
+
break;
|
|
117
|
+
case "agent.prompt": {
|
|
118
|
+
const payload = parseTypedPayload("agent.prompt", envelope.payload);
|
|
119
|
+
await this.sendPrompt(payload);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "agent.cancel": {
|
|
123
|
+
const payload = parseTypedPayload("agent.cancel", envelope.payload);
|
|
124
|
+
this.cancelPendingPermissions();
|
|
125
|
+
this.client?.cancel({
|
|
126
|
+
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
127
|
+
turnId: this.currentTurnId,
|
|
128
|
+
});
|
|
129
|
+
this.currentTurnId = undefined;
|
|
130
|
+
this.status = "idle";
|
|
131
|
+
this.sendUpdate({ kind: "status", status: "idle" });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "agent.permission.response": {
|
|
135
|
+
const payload = parseTypedPayload("agent.permission.response", envelope.payload);
|
|
136
|
+
const permission = this.pendingPermissions.get(payload.requestId);
|
|
137
|
+
this.pendingPermissions.delete(payload.requestId);
|
|
138
|
+
const selectedOptionId =
|
|
139
|
+
payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
140
|
+
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
141
|
+
if (waiter) {
|
|
142
|
+
clearTimeout(waiter.timer);
|
|
143
|
+
this.permissionWaiters.delete(payload.requestId);
|
|
144
|
+
waiter.resolve(formatPermissionResponse(
|
|
145
|
+
this.permissionSources.get(payload.requestId),
|
|
146
|
+
payload.outcome,
|
|
147
|
+
selectedOptionId,
|
|
148
|
+
));
|
|
149
|
+
this.permissionSources.delete(payload.requestId);
|
|
150
|
+
} else {
|
|
151
|
+
this.client?.respondPermission({
|
|
152
|
+
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
153
|
+
requestId: payload.requestId,
|
|
154
|
+
outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
|
|
155
|
+
optionId: selectedOptionId,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
this.status = "running";
|
|
159
|
+
this.sendSnapshot();
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
stop(): void {
|
|
166
|
+
this.client?.stop();
|
|
167
|
+
this.client = undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async initialize(): Promise<void> {
|
|
171
|
+
if (this.initialized) {
|
|
172
|
+
this.sendCapabilities();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await this.ensureClient();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async ensureClient(): Promise<void> {
|
|
179
|
+
if (this.client) return;
|
|
180
|
+
|
|
181
|
+
const resolved = resolveAgentCommand({
|
|
182
|
+
provider: this.input.provider,
|
|
183
|
+
command: this.input.command,
|
|
184
|
+
});
|
|
185
|
+
if (!resolved) {
|
|
186
|
+
this.status = "unavailable";
|
|
187
|
+
this.error = `Agent GUI requires --agent-command for ${this.input.provider}`;
|
|
188
|
+
this.sendCapabilities();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
this.client = new AcpClient({
|
|
194
|
+
command: resolved.command,
|
|
195
|
+
protocol: resolved.protocol,
|
|
196
|
+
framing: resolved.framing,
|
|
197
|
+
cwd: this.input.cwd,
|
|
198
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
199
|
+
onRequest: (method, params) => this.handleRequest(method, params),
|
|
200
|
+
onExit: (message) => this.handleExit(message),
|
|
201
|
+
});
|
|
202
|
+
await this.client.initialize();
|
|
203
|
+
this.initialized = true;
|
|
204
|
+
this.status = "idle";
|
|
205
|
+
this.error = undefined;
|
|
206
|
+
this.sendCapabilities();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.client?.stop();
|
|
209
|
+
this.client = undefined;
|
|
210
|
+
this.status = "error";
|
|
211
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
212
|
+
this.sendCapabilities();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async ensureSession(
|
|
217
|
+
cwd: string,
|
|
218
|
+
mcpServers?: Record<string, unknown>,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
await this.ensureClient();
|
|
221
|
+
if (!this.client || this.agentSessionId) return;
|
|
222
|
+
try {
|
|
223
|
+
const result = await this.client.newSession({ cwd, mcpServers });
|
|
224
|
+
this.agentSessionId = this.extractSessionId(result) ?? id("agent-session");
|
|
225
|
+
this.status = "idle";
|
|
226
|
+
this.error = undefined;
|
|
227
|
+
this.sendUpdate({ kind: "status", status: "idle" });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.status = "error";
|
|
230
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
231
|
+
this.sendUpdate({ kind: "error", error: this.error, status: "error" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async sendPrompt(payload: {
|
|
236
|
+
agentSessionId?: string;
|
|
237
|
+
clientMessageId: string;
|
|
238
|
+
contentBlocks: { type: "text" | "image"; text?: string; data?: string; mimeType?: string }[];
|
|
239
|
+
}): Promise<void> {
|
|
240
|
+
await this.ensureSession(this.input.cwd);
|
|
241
|
+
if (!this.client || !this.agentSessionId) return;
|
|
242
|
+
|
|
243
|
+
const content = payload.contentBlocks
|
|
244
|
+
.map((block) => (block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`))
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join("\n");
|
|
247
|
+
const userMessage: AgentMessage = {
|
|
248
|
+
id: payload.clientMessageId,
|
|
249
|
+
role: "user",
|
|
250
|
+
content,
|
|
251
|
+
createdAt: Date.now(),
|
|
252
|
+
};
|
|
253
|
+
this.messages.push(userMessage);
|
|
254
|
+
this.status = "running";
|
|
255
|
+
this.sendUpdate({ kind: "message", message: userMessage, status: "running" });
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const result = await this.client.prompt({
|
|
259
|
+
sessionId: payload.agentSessionId ?? this.agentSessionId,
|
|
260
|
+
content: payload.contentBlocks,
|
|
261
|
+
clientMessageId: payload.clientMessageId,
|
|
262
|
+
});
|
|
263
|
+
this.currentTurnId = this.extractTurnId(result) ?? this.currentTurnId;
|
|
264
|
+
if (this.status === "running") {
|
|
265
|
+
this.status = "idle";
|
|
266
|
+
this.sendUpdate({ kind: "status", status: "idle" });
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
this.status = "error";
|
|
270
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
271
|
+
this.sendUpdate({ kind: "error", error: this.error, status: "error" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
276
|
+
if (
|
|
277
|
+
method === "session/request_permission" ||
|
|
278
|
+
method.endsWith("/requestApproval") ||
|
|
279
|
+
method === "mcpServer/elicitation/request" ||
|
|
280
|
+
method === "item/tool/requestUserInput"
|
|
281
|
+
) {
|
|
282
|
+
return this.handlePermission(params, true, method);
|
|
283
|
+
}
|
|
284
|
+
if (this.input.verbose) {
|
|
285
|
+
process.stderr.write(`[agent:request] unsupported ${method}\n`);
|
|
286
|
+
}
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async sendSessionList(): Promise<void> {
|
|
291
|
+
await this.ensureClient();
|
|
292
|
+
if (!this.client) return;
|
|
293
|
+
try {
|
|
294
|
+
const result = await this.client.listSessions();
|
|
295
|
+
this.sendUpdate({
|
|
296
|
+
kind: "status",
|
|
297
|
+
status: "idle",
|
|
298
|
+
delta: stringify(result).slice(0, 4000),
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
this.sendUpdate({
|
|
302
|
+
kind: "error",
|
|
303
|
+
error: error instanceof Error ? error.message : String(error),
|
|
304
|
+
status: "error",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private handleNotification(method: string, params: unknown): void {
|
|
310
|
+
if (this.input.verbose) {
|
|
311
|
+
process.stderr.write(`[agent] ${method} ${stringify(params).slice(0, 500)}\n`);
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
method === "initialized" ||
|
|
315
|
+
method.startsWith("account/") ||
|
|
316
|
+
method.startsWith("mcpServer/startupStatus/")
|
|
317
|
+
) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (method === "thread/started") {
|
|
321
|
+
this.agentSessionId = this.extractSessionId(params) ?? this.agentSessionId;
|
|
322
|
+
this.status = "idle";
|
|
323
|
+
this.sendSnapshot();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (method === "turn/started") {
|
|
327
|
+
this.currentTurnId = this.extractTurnId(params) ?? this.currentTurnId;
|
|
328
|
+
this.status = "running";
|
|
329
|
+
this.sendUpdate({ kind: "status", status: "running" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (method === "turn/completed") {
|
|
333
|
+
this.currentTurnId = undefined;
|
|
334
|
+
this.status = "idle";
|
|
335
|
+
this.sendUpdate({ kind: "status", status: "idle" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (method === "session/request_permission") {
|
|
339
|
+
this.handlePermission(params, false, method);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (method === "session/update" || method.startsWith("item/")) {
|
|
343
|
+
this.handleUpdate(params);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.handleUpdate({ method, params });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private handlePermission(
|
|
350
|
+
params: unknown,
|
|
351
|
+
waitForResponse: boolean,
|
|
352
|
+
source?: string,
|
|
353
|
+
): Promise<unknown> | void {
|
|
354
|
+
const raw = typeof params === "object" && params ? params as Record<string, unknown> : {};
|
|
355
|
+
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
356
|
+
const rawToolCall = typeof raw.toolCall === "object" && raw.toolCall
|
|
357
|
+
? raw.toolCall as Record<string, unknown>
|
|
358
|
+
: raw;
|
|
359
|
+
const permission: AgentPermission = {
|
|
360
|
+
requestId,
|
|
361
|
+
toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]),
|
|
362
|
+
toolInput: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
363
|
+
context: firstString(raw, ["context", "description", "message", "title"]),
|
|
364
|
+
options: parsePermissionOptions(raw.options),
|
|
365
|
+
};
|
|
366
|
+
this.pendingPermissions.set(requestId, permission);
|
|
367
|
+
if (source) this.permissionSources.set(requestId, source);
|
|
368
|
+
this.status = "waiting_permission";
|
|
369
|
+
this.input.send(createEnvelope({
|
|
370
|
+
type: "agent.permission.request",
|
|
371
|
+
sessionId: this.input.sessionId,
|
|
372
|
+
payload: { agentSessionId: this.agentSessionId, ...permission },
|
|
373
|
+
}));
|
|
374
|
+
|
|
375
|
+
if (!waitForResponse) return;
|
|
376
|
+
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const timer = setTimeout(() => {
|
|
379
|
+
this.pendingPermissions.delete(requestId);
|
|
380
|
+
this.permissionWaiters.delete(requestId);
|
|
381
|
+
this.permissionSources.delete(requestId);
|
|
382
|
+
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
383
|
+
this.sendSnapshot();
|
|
384
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
385
|
+
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private handleUpdate(params: unknown): void {
|
|
390
|
+
const raw = typeof params === "object" && params ? params as Record<string, unknown> : {};
|
|
391
|
+
const nested = typeof raw.params === "object" && raw.params ? raw.params as Record<string, unknown> : {};
|
|
392
|
+
const text =
|
|
393
|
+
firstString(raw, ["delta", "text", "content", "message"]) ??
|
|
394
|
+
firstString(nested, ["delta", "text", "content", "message"]) ??
|
|
395
|
+
stringify(raw.update ?? raw.params ?? params);
|
|
396
|
+
const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
|
|
397
|
+
|
|
398
|
+
if (firstString(raw, ["toolName", "tool", "name"])) {
|
|
399
|
+
const toolCall: AgentToolCall = {
|
|
400
|
+
id: firstString(raw, ["toolCallId", "callId", "id"]) ?? id("tool"),
|
|
401
|
+
name: firstString(raw, ["toolName", "tool", "name"]) ?? "tool",
|
|
402
|
+
input: stringify(raw.input ?? raw.toolInput ?? ""),
|
|
403
|
+
output: stringify(raw.output ?? raw.result ?? ""),
|
|
404
|
+
status: raw.status === "completed" || raw.status === "failed" || raw.status === "running"
|
|
405
|
+
? raw.status
|
|
406
|
+
: "running",
|
|
407
|
+
};
|
|
408
|
+
this.toolCalls.set(toolCall.id, toolCall);
|
|
409
|
+
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const message: AgentMessage = {
|
|
414
|
+
id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
|
|
415
|
+
role,
|
|
416
|
+
content: text,
|
|
417
|
+
createdAt: Date.now(),
|
|
418
|
+
isStreaming: raw.done === false || raw.isStreaming === true,
|
|
419
|
+
};
|
|
420
|
+
this.messages.push(message);
|
|
421
|
+
if (this.messages.length > 100) this.messages.shift();
|
|
422
|
+
this.status = raw.done === true ? "idle" : "running";
|
|
423
|
+
this.sendUpdate({
|
|
424
|
+
kind: "message",
|
|
425
|
+
message,
|
|
426
|
+
status: this.status === "running" ? "running" : "idle",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private handleExit(message: string): void {
|
|
431
|
+
this.cancelPendingPermissions();
|
|
432
|
+
this.status = "error";
|
|
433
|
+
this.error = message;
|
|
434
|
+
this.client = undefined;
|
|
435
|
+
this.sendUpdate({ kind: "error", error: message, status: "error" });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private cancelPendingPermissions(): void {
|
|
439
|
+
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
440
|
+
clearTimeout(waiter.timer);
|
|
441
|
+
waiter.resolve(formatPermissionResponse(
|
|
442
|
+
this.permissionSources.get(requestId),
|
|
443
|
+
"cancelled",
|
|
444
|
+
"cancelled",
|
|
445
|
+
));
|
|
446
|
+
this.pendingPermissions.delete(requestId);
|
|
447
|
+
this.permissionSources.delete(requestId);
|
|
448
|
+
}
|
|
449
|
+
this.permissionWaiters.clear();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private sendCapabilities(): void {
|
|
453
|
+
const enabled = Boolean(this.client && this.initialized && !this.error);
|
|
454
|
+
this.input.send(createEnvelope({
|
|
455
|
+
type: "agent.capabilities",
|
|
456
|
+
sessionId: this.input.sessionId,
|
|
457
|
+
payload: {
|
|
458
|
+
enabled,
|
|
459
|
+
provider: this.input.provider,
|
|
460
|
+
protocolVersion: 1,
|
|
461
|
+
error: enabled ? undefined : this.error,
|
|
462
|
+
supportsSessionList: enabled,
|
|
463
|
+
supportsSessionLoad: enabled,
|
|
464
|
+
supportsImages: false,
|
|
465
|
+
supportsAudio: false,
|
|
466
|
+
supportsPermission: enabled,
|
|
467
|
+
supportsPlan: false,
|
|
468
|
+
supportsCancel: enabled,
|
|
469
|
+
},
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private sendSnapshot(): void {
|
|
474
|
+
this.input.send(createEnvelope({
|
|
475
|
+
type: "agent.snapshot",
|
|
476
|
+
sessionId: this.input.sessionId,
|
|
477
|
+
payload: {
|
|
478
|
+
agentSessionId: this.agentSessionId,
|
|
479
|
+
messages: this.messages,
|
|
480
|
+
toolCalls: [...this.toolCalls.values()],
|
|
481
|
+
pendingPermissions: [...this.pendingPermissions.values()],
|
|
482
|
+
status: this.status,
|
|
483
|
+
error: this.error,
|
|
484
|
+
},
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private sendUpdate(payload: {
|
|
489
|
+
kind: "message" | "message_delta" | "tool_call" | "tool_result" | "plan" | "status" | "error";
|
|
490
|
+
message?: AgentMessage;
|
|
491
|
+
delta?: string;
|
|
492
|
+
toolCall?: AgentToolCall;
|
|
493
|
+
status?: "idle" | "running" | "waiting_permission" | "error";
|
|
494
|
+
error?: string;
|
|
495
|
+
}): void {
|
|
496
|
+
this.input.send(createEnvelope({
|
|
497
|
+
type: "agent.update",
|
|
498
|
+
sessionId: this.input.sessionId,
|
|
499
|
+
payload: { agentSessionId: this.agentSessionId, ...payload },
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private extractSessionId(value: unknown): string | undefined {
|
|
504
|
+
if (!value || typeof value !== "object") return undefined;
|
|
505
|
+
const raw = value as Record<string, unknown>;
|
|
506
|
+
if (raw.thread && typeof raw.thread === "object") {
|
|
507
|
+
const threadId = firstString(raw.thread as Record<string, unknown>, ["id", "threadId"]);
|
|
508
|
+
if (threadId) return threadId;
|
|
509
|
+
}
|
|
510
|
+
return firstString(raw, ["sessionId", "id", "agentSessionId"]);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private extractTurnId(value: unknown): string | undefined {
|
|
514
|
+
if (!value || typeof value !== "object") return undefined;
|
|
515
|
+
const raw = value as Record<string, unknown>;
|
|
516
|
+
if (raw.turn && typeof raw.turn === "object") {
|
|
517
|
+
const turnId = firstString(raw.turn as Record<string, unknown>, ["id", "turnId"]);
|
|
518
|
+
if (turnId) return turnId;
|
|
519
|
+
}
|
|
520
|
+
return firstString(raw, ["turnId", "id"]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function parsePermissionOptions(value: unknown): AgentPermission["options"] {
|
|
525
|
+
if (!Array.isArray(value)) {
|
|
526
|
+
return [
|
|
527
|
+
{ id: "allow", label: "允许", kind: "allow" },
|
|
528
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const options = value
|
|
533
|
+
.map((entry, index) => {
|
|
534
|
+
const raw = typeof entry === "object" && entry ? entry as Record<string, unknown> : {};
|
|
535
|
+
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? `option-${index + 1}`;
|
|
536
|
+
const labelValue = raw.name ?? raw.label ?? raw.kind ?? String(idValue);
|
|
537
|
+
const id = String(idValue);
|
|
538
|
+
const label = String(labelValue);
|
|
539
|
+
const normalized = `${id} ${label}`.toLowerCase();
|
|
540
|
+
const kind: AgentPermission["options"][number]["kind"] = normalized.includes("reject") || normalized.includes("deny")
|
|
541
|
+
? "deny"
|
|
542
|
+
: normalized.includes("allow")
|
|
543
|
+
? "allow"
|
|
544
|
+
: "other";
|
|
545
|
+
return { id, label, kind };
|
|
546
|
+
})
|
|
547
|
+
.filter((option) => option.id.length > 0 && option.label.length > 0);
|
|
548
|
+
|
|
549
|
+
return options.length > 0
|
|
550
|
+
? options
|
|
551
|
+
: [
|
|
552
|
+
{ id: "allow", label: "允许", kind: "allow" },
|
|
553
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function selectPermissionOption(
|
|
558
|
+
permission: AgentPermission | undefined,
|
|
559
|
+
outcome: "allow" | "deny" | "cancelled",
|
|
560
|
+
): string {
|
|
561
|
+
if (outcome === "cancelled") return "cancelled";
|
|
562
|
+
const option = permission?.options.find((item) => item.kind === outcome);
|
|
563
|
+
return option?.id ?? outcome;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function formatPermissionResponse(
|
|
567
|
+
source: string | undefined,
|
|
568
|
+
outcome: "allow" | "deny" | "cancelled",
|
|
569
|
+
optionId: string,
|
|
570
|
+
): unknown {
|
|
571
|
+
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
572
|
+
return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
|
|
573
|
+
}
|
|
574
|
+
if (source === "item/permissions/requestApproval") {
|
|
575
|
+
if (outcome === "allow") {
|
|
576
|
+
return {
|
|
577
|
+
permissions: { type: "managed", network: { enabled: true }, fileSystem: { type: "fullAccess" } },
|
|
578
|
+
scope: optionId.includes("session") ? "session" : "turn",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
outcome:
|
|
585
|
+
outcome === "cancelled"
|
|
586
|
+
? { outcome: "cancelled" }
|
|
587
|
+
: { outcome: "selected", optionId },
|
|
588
|
+
};
|
|
589
|
+
}
|