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