linkshell-cli 0.2.88 → 0.2.90
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 +1 -2
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +3 -1
- package/dist/cli/src/runtime/acp/agent-session.js +44 -34
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +14 -5
- package/dist/cli/src/runtime/acp/agent-workspace.js +595 -100
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js +27 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +1 -1
- package/dist/cli/src/runtime/bridge-session.js +18 -10
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +4420 -75
- package/dist/shared-protocol/src/index.js +85 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -2
- package/src/runtime/acp/agent-session.ts +45 -35
- package/src/runtime/acp/agent-workspace.ts +707 -100
- package/src/runtime/acp/provider-resolver.ts +29 -0
- package/src/runtime/bridge-session.ts +17 -12
|
@@ -30,6 +30,32 @@ function firstString(value, keys) {
|
|
|
30
30
|
}
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
|
+
function normalizedIdentifier(value) {
|
|
34
|
+
return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
|
|
35
|
+
}
|
|
36
|
+
function firstNumber(value, keys) {
|
|
37
|
+
if (!value)
|
|
38
|
+
return undefined;
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const next = value[key];
|
|
41
|
+
if (typeof next === "number" && Number.isFinite(next))
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function stringArray(value) {
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
return [];
|
|
49
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
50
|
+
}
|
|
51
|
+
function arrayFromKeys(value, keys) {
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
const next = value[key];
|
|
54
|
+
if (Array.isArray(next))
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
33
59
|
function extractItem(value) {
|
|
34
60
|
const raw = asRecord(value);
|
|
35
61
|
if (!raw)
|
|
@@ -87,23 +113,29 @@ function nameFromToolMethod(method) {
|
|
|
87
113
|
return "工具";
|
|
88
114
|
}
|
|
89
115
|
function isToolItemType(itemType) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
const normalized = normalizedIdentifier(itemType);
|
|
117
|
+
return (normalized === "commandexecution" ||
|
|
118
|
+
normalized === "filechange" ||
|
|
119
|
+
normalized === "diff" ||
|
|
120
|
+
normalized === "toolcall" ||
|
|
121
|
+
normalized === "mcptoolcall" ||
|
|
122
|
+
normalized === "dynamictoolcall");
|
|
94
123
|
}
|
|
95
124
|
function toolNameFromItem(item) {
|
|
96
125
|
const itemType = firstString(item, ["type"]);
|
|
97
|
-
|
|
126
|
+
const normalized = normalizedIdentifier(itemType);
|
|
127
|
+
if (normalized === "commandexecution")
|
|
98
128
|
return "命令";
|
|
99
|
-
if (
|
|
129
|
+
if (normalized === "filechange" || normalized === "diff")
|
|
100
130
|
return "文件修改";
|
|
101
|
-
if (
|
|
131
|
+
if (normalized === "toolcall")
|
|
132
|
+
return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
|
|
133
|
+
if (normalized === "mcptoolcall") {
|
|
102
134
|
const server = firstString(item, ["server"]);
|
|
103
135
|
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
104
136
|
return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
|
|
105
137
|
}
|
|
106
|
-
if (
|
|
138
|
+
if (normalized === "dynamictoolcall") {
|
|
107
139
|
const namespace = firstString(item, ["namespace"]);
|
|
108
140
|
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
109
141
|
return [namespace, tool].filter(Boolean).join(" · ") || "工具";
|
|
@@ -124,6 +156,92 @@ function summarizeFileChanges(changes) {
|
|
|
124
156
|
.filter((line) => Boolean(line));
|
|
125
157
|
return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
|
|
126
158
|
}
|
|
159
|
+
function fileChangeEntriesFromItem(item) {
|
|
160
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const change of changes) {
|
|
163
|
+
const raw = asRecord(change);
|
|
164
|
+
if (!raw)
|
|
165
|
+
continue;
|
|
166
|
+
const path = firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
|
|
167
|
+
firstString(asRecord(raw.update), ["path", "file", "filePath"]);
|
|
168
|
+
if (!path)
|
|
169
|
+
continue;
|
|
170
|
+
const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
|
|
171
|
+
const entry = { path };
|
|
172
|
+
const kind = firstString(raw, ["kind", "type", "operation", "action"]);
|
|
173
|
+
const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
|
|
174
|
+
const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
|
|
175
|
+
if (kind)
|
|
176
|
+
entry.kind = kind;
|
|
177
|
+
if (added !== undefined)
|
|
178
|
+
entry.added = added;
|
|
179
|
+
if (removed !== undefined)
|
|
180
|
+
entry.removed = removed;
|
|
181
|
+
entries.push(entry);
|
|
182
|
+
}
|
|
183
|
+
const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
184
|
+
if (entries.length === 0 && directPath) {
|
|
185
|
+
const entry = { path: directPath };
|
|
186
|
+
const kind = firstString(item, ["kind", "type", "operation", "action"]);
|
|
187
|
+
if (kind)
|
|
188
|
+
entry.kind = kind;
|
|
189
|
+
return [entry];
|
|
190
|
+
}
|
|
191
|
+
return entries;
|
|
192
|
+
}
|
|
193
|
+
function commandExecutionFromItem(item, status, output) {
|
|
194
|
+
const command = firstString(item, ["command"]);
|
|
195
|
+
const cwd = firstString(item, ["cwd"]);
|
|
196
|
+
const exitCode = firstNumber(item, ["exitCode", "code"]);
|
|
197
|
+
if (!command && !cwd && !output && exitCode === undefined)
|
|
198
|
+
return undefined;
|
|
199
|
+
return { command, cwd, output, exitCode: exitCode ?? undefined, status };
|
|
200
|
+
}
|
|
201
|
+
function fileChangeFromItem(item, status, diff) {
|
|
202
|
+
const entries = fileChangeEntriesFromItem(item);
|
|
203
|
+
const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
|
|
204
|
+
const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
|
|
205
|
+
if (entries.length === 0 && !diff && !summary && !changeSetId)
|
|
206
|
+
return undefined;
|
|
207
|
+
return { entries, diff, summary, changeSetId, status };
|
|
208
|
+
}
|
|
209
|
+
function commandExecutionFromTool(toolCall) {
|
|
210
|
+
const input = toolCall.input?.trim();
|
|
211
|
+
if (!input && !toolCall.output)
|
|
212
|
+
return undefined;
|
|
213
|
+
const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
|
|
214
|
+
return {
|
|
215
|
+
command: commandPart || input,
|
|
216
|
+
cwd: cwdPart,
|
|
217
|
+
output: toolCall.output,
|
|
218
|
+
status: toolCall.status,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function fileChangeFromTool(toolCall) {
|
|
222
|
+
const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
|
|
223
|
+
const entries = (toolCall.input ?? "")
|
|
224
|
+
.split("\n")
|
|
225
|
+
.map((line) => line.trim())
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.map((line) => {
|
|
228
|
+
const [kind, ...rest] = line.split(/\s+/);
|
|
229
|
+
const path = rest.length > 0 ? rest.join(" ") : kind;
|
|
230
|
+
const entry = { path: path ?? line };
|
|
231
|
+
if (rest.length > 0 && kind)
|
|
232
|
+
entry.kind = kind;
|
|
233
|
+
return entry;
|
|
234
|
+
})
|
|
235
|
+
.filter((entry) => entry.path.length > 0);
|
|
236
|
+
if (entries.length === 0 && !diff && !toolCall.output)
|
|
237
|
+
return undefined;
|
|
238
|
+
return {
|
|
239
|
+
entries,
|
|
240
|
+
diff,
|
|
241
|
+
summary: diff ? undefined : toolCall.output,
|
|
242
|
+
status: toolCall.status,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
127
245
|
function looksLikeDiff(text) {
|
|
128
246
|
const value = text.trim();
|
|
129
247
|
return (value.startsWith("diff --git ") ||
|
|
@@ -170,14 +288,15 @@ function extractDiffText(value) {
|
|
|
170
288
|
}
|
|
171
289
|
function toolInputFromItem(item) {
|
|
172
290
|
const itemType = firstString(item, ["type"]);
|
|
173
|
-
|
|
291
|
+
const normalized = normalizedIdentifier(itemType);
|
|
292
|
+
if (normalized === "commandexecution") {
|
|
174
293
|
const command = firstString(item, ["command"]);
|
|
175
294
|
const cwd = firstString(item, ["cwd"]);
|
|
176
295
|
if (command && cwd)
|
|
177
296
|
return `${command}\n\ncwd: ${cwd}`;
|
|
178
297
|
return command ?? cwd;
|
|
179
298
|
}
|
|
180
|
-
if (
|
|
299
|
+
if (normalized === "filechange" || normalized === "diff") {
|
|
181
300
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
182
301
|
return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
183
302
|
}
|
|
@@ -192,6 +311,170 @@ function textFromBlocks(blocks) {
|
|
|
192
311
|
.filter(Boolean)
|
|
193
312
|
.join("\n");
|
|
194
313
|
}
|
|
314
|
+
function isSubagentItemType(itemType) {
|
|
315
|
+
const normalized = normalizedIdentifier(itemType);
|
|
316
|
+
return (normalized === "collabagenttoolcall" ||
|
|
317
|
+
normalized === "collabtoolcall" ||
|
|
318
|
+
normalized.startsWith("collabagentspawn") ||
|
|
319
|
+
normalized.startsWith("collabwaiting") ||
|
|
320
|
+
normalized.startsWith("collabclose") ||
|
|
321
|
+
normalized.startsWith("collabresume") ||
|
|
322
|
+
normalized.startsWith("collabagentinteraction"));
|
|
323
|
+
}
|
|
324
|
+
function parseSubagentRef(value) {
|
|
325
|
+
const raw = asRecord(value);
|
|
326
|
+
if (!raw)
|
|
327
|
+
return undefined;
|
|
328
|
+
const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
|
|
329
|
+
if (!threadId)
|
|
330
|
+
return undefined;
|
|
331
|
+
return {
|
|
332
|
+
threadId,
|
|
333
|
+
agentId: firstString(raw, ["agentId", "agentID"]),
|
|
334
|
+
nickname: firstString(raw, ["nickname", "name", "label"]),
|
|
335
|
+
role: firstString(raw, ["role", "kind"]),
|
|
336
|
+
model: firstString(raw, ["model", "modelName"]),
|
|
337
|
+
prompt: firstString(raw, ["prompt", "instructions", "message"]),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function parseSubagentStates(value) {
|
|
341
|
+
const result = {};
|
|
342
|
+
if (Array.isArray(value)) {
|
|
343
|
+
for (const entry of value) {
|
|
344
|
+
const raw = asRecord(entry);
|
|
345
|
+
const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
|
|
346
|
+
const status = firstString(raw, ["status", "state", "phase"]);
|
|
347
|
+
if (!threadId || !status)
|
|
348
|
+
continue;
|
|
349
|
+
result[threadId] = {
|
|
350
|
+
threadId,
|
|
351
|
+
status,
|
|
352
|
+
message: firstString(raw, ["message", "summary", "text"]),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
const raw = asRecord(value);
|
|
358
|
+
if (!raw)
|
|
359
|
+
return result;
|
|
360
|
+
for (const [threadId, entry] of Object.entries(raw)) {
|
|
361
|
+
const state = asRecord(entry);
|
|
362
|
+
if (state) {
|
|
363
|
+
result[threadId] = {
|
|
364
|
+
threadId,
|
|
365
|
+
status: firstString(state, ["status", "state", "phase"]) ?? "running",
|
|
366
|
+
message: firstString(state, ["message", "summary", "text"]),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
else if (typeof entry === "string") {
|
|
370
|
+
result[threadId] = { threadId, status: entry };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
function parseStructuredInputOption(value, index) {
|
|
376
|
+
const raw = asRecord(value);
|
|
377
|
+
if (!raw) {
|
|
378
|
+
if (typeof value === "string" && value.trim()) {
|
|
379
|
+
return { id: `option-${index + 1}`, label: value.trim() };
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const label = firstString(raw, ["label", "title", "text", "value"]);
|
|
384
|
+
if (!label)
|
|
385
|
+
return undefined;
|
|
386
|
+
return {
|
|
387
|
+
id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
|
|
388
|
+
label,
|
|
389
|
+
description: firstString(raw, ["description", "detail", "subtitle"]),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function parseStructuredInputQuestion(value, index) {
|
|
393
|
+
const raw = asRecord(value);
|
|
394
|
+
if (!raw)
|
|
395
|
+
return undefined;
|
|
396
|
+
const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
|
|
397
|
+
if (!question)
|
|
398
|
+
return undefined;
|
|
399
|
+
const options = arrayFromKeys(raw, ["options", "choices", "items"])
|
|
400
|
+
.map(parseStructuredInputOption)
|
|
401
|
+
.filter((option) => Boolean(option));
|
|
402
|
+
return {
|
|
403
|
+
id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
|
|
404
|
+
header: firstString(raw, ["header", "title"]),
|
|
405
|
+
question,
|
|
406
|
+
isOther: raw.isOther === true,
|
|
407
|
+
isSecret: raw.isSecret === true || raw.secret === true,
|
|
408
|
+
selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
|
|
409
|
+
options: options.length > 0 ? options : undefined,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function decodeStructuredInput(value) {
|
|
413
|
+
const raw = asRecord(value) ?? {};
|
|
414
|
+
const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
|
|
415
|
+
.map(parseStructuredInputQuestion)
|
|
416
|
+
.filter((question) => Boolean(question));
|
|
417
|
+
if (questions.length === 0) {
|
|
418
|
+
const single = parseStructuredInputQuestion(raw, 0);
|
|
419
|
+
if (single)
|
|
420
|
+
questions.push(single);
|
|
421
|
+
}
|
|
422
|
+
if (questions.length === 0)
|
|
423
|
+
return undefined;
|
|
424
|
+
return {
|
|
425
|
+
requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
|
|
426
|
+
questions,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function decodeSubagentAction(item, status) {
|
|
430
|
+
const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
|
|
431
|
+
const receiverAgents = [
|
|
432
|
+
...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
|
|
433
|
+
...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
|
|
434
|
+
].filter((entry) => Boolean(entry));
|
|
435
|
+
const receiverThreadIds = [
|
|
436
|
+
...stringArray(item.receiverThreadIds),
|
|
437
|
+
...stringArray(item.threadIds),
|
|
438
|
+
...stringArray(item.childThreadIds),
|
|
439
|
+
...stringArray(item.agentThreadIds),
|
|
440
|
+
...stringArray(nested.receiverThreadIds),
|
|
441
|
+
...stringArray(nested.threadIds),
|
|
442
|
+
...receiverAgents.map((agent) => agent.threadId),
|
|
443
|
+
].filter((threadId, index, array) => array.indexOf(threadId) === index);
|
|
444
|
+
const agentStates = {
|
|
445
|
+
...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
|
|
446
|
+
...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
|
|
447
|
+
};
|
|
448
|
+
if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0)
|
|
449
|
+
return undefined;
|
|
450
|
+
return {
|
|
451
|
+
tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
|
|
452
|
+
firstString(nested, ["tool", "toolName", "name", "type"]) ??
|
|
453
|
+
"subagent",
|
|
454
|
+
status,
|
|
455
|
+
prompt: firstString(item, ["prompt", "instructions", "message"]) ??
|
|
456
|
+
firstString(nested, ["prompt", "instructions", "message"]),
|
|
457
|
+
model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
|
|
458
|
+
receiverThreadIds,
|
|
459
|
+
receiverAgents,
|
|
460
|
+
agentStates,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function summarizeSubagentAction(action) {
|
|
464
|
+
const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
|
|
465
|
+
const normalized = normalizedIdentifier(action.tool);
|
|
466
|
+
if (normalized.includes("spawn"))
|
|
467
|
+
return `启动 ${count} 个子 Agent`;
|
|
468
|
+
if (normalized.includes("wait"))
|
|
469
|
+
return `等待 ${count} 个子 Agent`;
|
|
470
|
+
if (normalized.includes("resume"))
|
|
471
|
+
return `恢复 ${count} 个子 Agent`;
|
|
472
|
+
if (normalized.includes("close"))
|
|
473
|
+
return `关闭 ${count} 个子 Agent`;
|
|
474
|
+
if (normalized.includes("sendinput"))
|
|
475
|
+
return `更新 ${count} 个子 Agent`;
|
|
476
|
+
return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
|
|
477
|
+
}
|
|
195
478
|
function previewText(text) {
|
|
196
479
|
return text.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
197
480
|
}
|
|
@@ -202,21 +485,10 @@ function providerLabel(provider) {
|
|
|
202
485
|
return "Claude";
|
|
203
486
|
return "Custom";
|
|
204
487
|
}
|
|
205
|
-
function providerSetupReason(provider, activeProvider, error) {
|
|
206
|
-
if (provider === activeProvider) {
|
|
207
|
-
return error ?? `${providerLabel(provider)} Agent 正在初始化或不可用。`;
|
|
208
|
-
}
|
|
209
|
-
if (provider === "codex") {
|
|
210
|
-
return `当前 CLI 启用的是 ${providerLabel(activeProvider)} Agent。`;
|
|
211
|
-
}
|
|
212
|
-
if (provider === "claude") {
|
|
213
|
-
return "Claude ACP adapter 尚未启用,请用 --agent-provider claude --agent-command 配置。";
|
|
214
|
-
}
|
|
215
|
-
return "Custom Agent 需要用 --agent-provider custom --agent-command 配置后才能使用。";
|
|
216
|
-
}
|
|
217
488
|
export class AgentWorkspaceProxy {
|
|
218
489
|
input;
|
|
219
|
-
|
|
490
|
+
clients = new Map();
|
|
491
|
+
agentProtocols = new Map();
|
|
220
492
|
initialized = false;
|
|
221
493
|
status = "unavailable";
|
|
222
494
|
error;
|
|
@@ -229,8 +501,9 @@ export class AgentWorkspaceProxy {
|
|
|
229
501
|
pendingPermissions = new Map();
|
|
230
502
|
permissionWaiters = new Map();
|
|
231
503
|
permissionSources = new Map();
|
|
504
|
+
pendingStructuredInputs = new Map();
|
|
505
|
+
structuredInputWaiters = new Map();
|
|
232
506
|
toolConversationIds = new Map();
|
|
233
|
-
agentProtocol;
|
|
234
507
|
constructor(input) {
|
|
235
508
|
this.input = input;
|
|
236
509
|
}
|
|
@@ -269,7 +542,8 @@ export class AgentWorkspaceProxy {
|
|
|
269
542
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
270
543
|
const conversation = this.conversations.get(payload.conversationId);
|
|
271
544
|
this.cancelPendingPermissions(payload.conversationId);
|
|
272
|
-
this.
|
|
545
|
+
const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
546
|
+
cancelClient?.cancel({
|
|
273
547
|
sessionId: conversation?.agentSessionId,
|
|
274
548
|
turnId: this.currentTurnId,
|
|
275
549
|
});
|
|
@@ -283,102 +557,122 @@ export class AgentWorkspaceProxy {
|
|
|
283
557
|
this.respondPermission(payload);
|
|
284
558
|
break;
|
|
285
559
|
}
|
|
560
|
+
case "agent.v2.structured_input.respond": {
|
|
561
|
+
const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
|
|
562
|
+
this.respondStructuredInput(payload);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
286
565
|
}
|
|
287
566
|
}
|
|
288
567
|
stop() {
|
|
289
|
-
this.
|
|
290
|
-
|
|
568
|
+
for (const client of this.clients.values()) {
|
|
569
|
+
client.stop();
|
|
570
|
+
}
|
|
571
|
+
this.clients.clear();
|
|
572
|
+
}
|
|
573
|
+
clientForProvider(provider) {
|
|
574
|
+
return this.clients.get(provider);
|
|
575
|
+
}
|
|
576
|
+
protocolForProvider(provider) {
|
|
577
|
+
return this.agentProtocols.get(provider);
|
|
291
578
|
}
|
|
292
579
|
async initialize() {
|
|
293
580
|
if (this.initialized)
|
|
294
581
|
return;
|
|
295
|
-
|
|
582
|
+
// trigger capability report immediately, lazy-start providers on first use
|
|
583
|
+
this.initialized = true;
|
|
584
|
+
this.status = "idle";
|
|
585
|
+
this.error = undefined;
|
|
586
|
+
this.sendCapabilities();
|
|
296
587
|
}
|
|
297
|
-
async
|
|
298
|
-
|
|
299
|
-
|
|
588
|
+
async ensureProviderClient(provider) {
|
|
589
|
+
const existing = this.clients.get(provider);
|
|
590
|
+
if (existing)
|
|
591
|
+
return existing;
|
|
300
592
|
const resolved = resolveAgentCommand({
|
|
301
|
-
provider
|
|
593
|
+
provider,
|
|
302
594
|
command: this.input.command,
|
|
303
595
|
});
|
|
304
596
|
if (!resolved) {
|
|
305
|
-
this.
|
|
306
|
-
|
|
307
|
-
|
|
597
|
+
if (this.input.verbose) {
|
|
598
|
+
process.stderr.write(`[agent:v2] no command for provider ${provider}\n`);
|
|
599
|
+
}
|
|
600
|
+
return undefined;
|
|
308
601
|
}
|
|
309
602
|
try {
|
|
310
|
-
this.
|
|
311
|
-
|
|
603
|
+
this.agentProtocols.set(provider, resolved.protocol);
|
|
604
|
+
const client = new AcpClient({
|
|
312
605
|
command: resolved.command,
|
|
313
606
|
protocol: resolved.protocol,
|
|
314
607
|
framing: resolved.framing,
|
|
315
608
|
cwd: this.input.cwd,
|
|
316
609
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
317
610
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
318
|
-
onExit: (message) => this.
|
|
611
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
319
612
|
});
|
|
320
|
-
await
|
|
321
|
-
this.
|
|
613
|
+
await client.initialize();
|
|
614
|
+
this.clients.set(provider, client);
|
|
322
615
|
this.status = "idle";
|
|
323
616
|
this.error = undefined;
|
|
617
|
+
this.sendCapabilities();
|
|
618
|
+
return client;
|
|
324
619
|
}
|
|
325
620
|
catch (error) {
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
621
|
+
if (this.input.verbose) {
|
|
622
|
+
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
623
|
+
}
|
|
624
|
+
return undefined;
|
|
330
625
|
}
|
|
331
626
|
}
|
|
332
627
|
sendCapabilities() {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
628
|
+
const providers = this.input.availableProviders.map((provider) => {
|
|
629
|
+
const client = this.clients.get(provider);
|
|
630
|
+
const protocol = this.agentProtocols.get(provider);
|
|
631
|
+
const enabled = Boolean(client);
|
|
632
|
+
const supportsImages = enabled && protocol === "codex-app-server";
|
|
633
|
+
return {
|
|
634
|
+
id: provider,
|
|
635
|
+
label: providerLabel(provider),
|
|
636
|
+
enabled,
|
|
637
|
+
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
638
|
+
supportsImages,
|
|
639
|
+
supportsPermission: enabled,
|
|
640
|
+
supportsPlan: enabled,
|
|
641
|
+
supportsCancel: enabled,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
const anyEnabled = providers.some((p) => p.enabled);
|
|
339
645
|
this.input.send(createEnvelope({
|
|
340
646
|
type: "agent.v2.capabilities",
|
|
341
647
|
sessionId: this.input.sessionId,
|
|
342
648
|
payload: {
|
|
343
|
-
enabled,
|
|
344
|
-
provider:
|
|
345
|
-
providers
|
|
346
|
-
const isActive = provider === activeProvider;
|
|
347
|
-
const canUse = isActive && enabled;
|
|
348
|
-
return {
|
|
349
|
-
id: provider,
|
|
350
|
-
label: providerLabel(provider),
|
|
351
|
-
enabled: canUse,
|
|
352
|
-
reason: canUse
|
|
353
|
-
? undefined
|
|
354
|
-
: providerSetupReason(provider, activeProvider, isActive ? this.error : undefined),
|
|
355
|
-
supportsImages: canUse && supportsImages,
|
|
356
|
-
supportsPermission: canUse,
|
|
357
|
-
supportsPlan: canUse,
|
|
358
|
-
supportsCancel: canUse,
|
|
359
|
-
};
|
|
360
|
-
}),
|
|
649
|
+
enabled: anyEnabled,
|
|
650
|
+
provider: this.input.availableProviders[0] ?? "codex",
|
|
651
|
+
providers,
|
|
361
652
|
protocolVersion: 1,
|
|
362
653
|
workspaceProtocolVersion: 2,
|
|
363
|
-
error:
|
|
364
|
-
supportsSessionList:
|
|
365
|
-
supportsSessionLoad:
|
|
366
|
-
supportsImages,
|
|
654
|
+
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
655
|
+
supportsSessionList: anyEnabled,
|
|
656
|
+
supportsSessionLoad: anyEnabled,
|
|
657
|
+
supportsImages: providers.some((p) => p.supportsImages),
|
|
367
658
|
supportsAudio: false,
|
|
368
|
-
supportsPermission:
|
|
369
|
-
supportsPlan:
|
|
370
|
-
supportsCancel:
|
|
659
|
+
supportsPermission: anyEnabled,
|
|
660
|
+
supportsPlan: anyEnabled,
|
|
661
|
+
supportsCancel: anyEnabled,
|
|
371
662
|
},
|
|
372
663
|
}));
|
|
373
664
|
}
|
|
374
665
|
async openConversation(payload) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
666
|
+
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
667
|
+
if (!provider) {
|
|
668
|
+
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
669
|
+
}
|
|
670
|
+
if (!this.input.availableProviders.includes(provider)) {
|
|
671
|
+
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
379
672
|
}
|
|
380
|
-
|
|
381
|
-
|
|
673
|
+
const client = await this.ensureProviderClient(provider);
|
|
674
|
+
if (!client) {
|
|
675
|
+
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
382
676
|
}
|
|
383
677
|
const cwd = payload.cwd ?? this.input.cwd;
|
|
384
678
|
let agentSessionId = payload.agentSessionId;
|
|
@@ -401,8 +695,8 @@ export class AgentWorkspaceProxy {
|
|
|
401
695
|
}
|
|
402
696
|
try {
|
|
403
697
|
const result = agentSessionId
|
|
404
|
-
? await
|
|
405
|
-
: await
|
|
698
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
699
|
+
: await client.newSession({ cwd });
|
|
406
700
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
407
701
|
const now = Date.now();
|
|
408
702
|
const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
|
|
@@ -410,7 +704,7 @@ export class AgentWorkspaceProxy {
|
|
|
410
704
|
...existingConversation,
|
|
411
705
|
id: conversationId,
|
|
412
706
|
agentSessionId,
|
|
413
|
-
provider
|
|
707
|
+
provider,
|
|
414
708
|
cwd,
|
|
415
709
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
416
710
|
model: payload.model ?? existingConversation?.model,
|
|
@@ -443,7 +737,7 @@ export class AgentWorkspaceProxy {
|
|
|
443
737
|
const now = Date.now();
|
|
444
738
|
const conversation = {
|
|
445
739
|
id: fallbackId,
|
|
446
|
-
provider: payload.provider ?? this.input.
|
|
740
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
447
741
|
cwd,
|
|
448
742
|
title: payload.title ?? titleFromCwd(cwd),
|
|
449
743
|
model: payload.model,
|
|
@@ -474,9 +768,13 @@ export class AgentWorkspaceProxy {
|
|
|
474
768
|
async sendPrompt(payload) {
|
|
475
769
|
const conversation = this.conversations.get(payload.conversationId) ??
|
|
476
770
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
477
|
-
if (!conversation || !
|
|
771
|
+
if (!conversation || !conversation.agentSessionId)
|
|
478
772
|
return;
|
|
479
|
-
|
|
773
|
+
const client = this.clientForProvider(conversation.provider);
|
|
774
|
+
if (!client)
|
|
775
|
+
return;
|
|
776
|
+
const protocol = this.protocolForProvider(conversation.provider);
|
|
777
|
+
if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
|
|
480
778
|
conversation.status = "idle";
|
|
481
779
|
conversation.lastActivityAt = Date.now();
|
|
482
780
|
this.emitConversation(conversation);
|
|
@@ -507,7 +805,7 @@ export class AgentWorkspaceProxy {
|
|
|
507
805
|
});
|
|
508
806
|
this.emitConversation(conversation);
|
|
509
807
|
try {
|
|
510
|
-
const result = await
|
|
808
|
+
const result = await client.prompt({
|
|
511
809
|
sessionId: conversation.agentSessionId,
|
|
512
810
|
content: payload.contentBlocks,
|
|
513
811
|
clientMessageId: payload.clientMessageId,
|
|
@@ -534,6 +832,9 @@ export class AgentWorkspaceProxy {
|
|
|
534
832
|
}
|
|
535
833
|
}
|
|
536
834
|
handleRequest(method, params) {
|
|
835
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
836
|
+
return this.handleStructuredInput(params, true);
|
|
837
|
+
}
|
|
537
838
|
if (isPermissionRequestMethod(method)) {
|
|
538
839
|
return this.handlePermission(params, true, method);
|
|
539
840
|
}
|
|
@@ -556,6 +857,10 @@ export class AgentWorkspaceProxy {
|
|
|
556
857
|
return;
|
|
557
858
|
}
|
|
558
859
|
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
860
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
861
|
+
this.handleStructuredInput(params);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
559
864
|
if (isPermissionRequestMethod(method)) {
|
|
560
865
|
this.handlePermission(params, false, method);
|
|
561
866
|
return;
|
|
@@ -712,14 +1017,21 @@ export class AgentWorkspaceProxy {
|
|
|
712
1017
|
if (!item)
|
|
713
1018
|
return;
|
|
714
1019
|
const itemType = firstString(item, ["type"]);
|
|
715
|
-
|
|
1020
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1021
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
716
1022
|
this.handleCompletedMessageItem(item, true);
|
|
717
1023
|
return;
|
|
718
1024
|
}
|
|
719
|
-
if (
|
|
1025
|
+
if (normalizedItemType === "plan") {
|
|
720
1026
|
this.handlePlanUpdated({ plan: [item] });
|
|
721
1027
|
return;
|
|
722
1028
|
}
|
|
1029
|
+
if (isSubagentItemType(itemType)) {
|
|
1030
|
+
this.handleSubagentItem(item, "running", true);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (this.handleSemanticSystemItem(item, "running", true))
|
|
1034
|
+
return;
|
|
723
1035
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
724
1036
|
const toolCall = this.toolCallFromItem(item, "running");
|
|
725
1037
|
if (!conversationId || !toolCall)
|
|
@@ -732,10 +1044,21 @@ export class AgentWorkspaceProxy {
|
|
|
732
1044
|
if (!item)
|
|
733
1045
|
return;
|
|
734
1046
|
const itemType = firstString(item, ["type"]);
|
|
735
|
-
|
|
1047
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1048
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
736
1049
|
this.handleCompletedMessageItem(item, false);
|
|
737
1050
|
return;
|
|
738
1051
|
}
|
|
1052
|
+
if (normalizedItemType === "plan") {
|
|
1053
|
+
this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (isSubagentItemType(itemType)) {
|
|
1057
|
+
this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false))
|
|
1061
|
+
return;
|
|
739
1062
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
740
1063
|
const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
|
|
741
1064
|
if (!conversationId || !toolCall)
|
|
@@ -900,18 +1223,131 @@ export class AgentWorkspaceProxy {
|
|
|
900
1223
|
});
|
|
901
1224
|
this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
|
|
902
1225
|
}
|
|
1226
|
+
handleSemanticSystemItem(item, status, streaming) {
|
|
1227
|
+
const itemType = firstString(item, ["type"]);
|
|
1228
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1229
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1230
|
+
if (!conversationId)
|
|
1231
|
+
return false;
|
|
1232
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
|
|
1233
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1234
|
+
const base = {
|
|
1235
|
+
id: itemId,
|
|
1236
|
+
conversationId,
|
|
1237
|
+
type: "status",
|
|
1238
|
+
role: "system",
|
|
1239
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1240
|
+
itemId,
|
|
1241
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1242
|
+
updatedAt: Date.now(),
|
|
1243
|
+
isStreaming: streaming,
|
|
1244
|
+
};
|
|
1245
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1246
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1247
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1248
|
+
this.upsertItem(conversationId, {
|
|
1249
|
+
...base,
|
|
1250
|
+
kind: "thinking",
|
|
1251
|
+
text: text ?? (streaming ? "正在思考" : "完成思考"),
|
|
1252
|
+
});
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
if (normalized === "enteredreviewmode") {
|
|
1256
|
+
const target = firstString(item, ["review", "target", "label"]) ?? "changes";
|
|
1257
|
+
this.upsertItem(conversationId, {
|
|
1258
|
+
...base,
|
|
1259
|
+
kind: "review",
|
|
1260
|
+
text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
|
|
1261
|
+
});
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
if (normalized === "contextcompaction") {
|
|
1265
|
+
this.upsertItem(conversationId, {
|
|
1266
|
+
...base,
|
|
1267
|
+
kind: "context_compaction",
|
|
1268
|
+
text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
|
|
1269
|
+
});
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
handleSubagentItem(item, status, streaming) {
|
|
1275
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1276
|
+
if (!conversationId)
|
|
1277
|
+
return;
|
|
1278
|
+
const subagent = decodeSubagentAction(item, status);
|
|
1279
|
+
if (!subagent)
|
|
1280
|
+
return;
|
|
1281
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
|
|
1282
|
+
const text = summarizeSubagentAction(subagent);
|
|
1283
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1284
|
+
this.upsertItem(conversationId, {
|
|
1285
|
+
id: itemId,
|
|
1286
|
+
conversationId,
|
|
1287
|
+
type: "status",
|
|
1288
|
+
kind: "subagent_action",
|
|
1289
|
+
role: "system",
|
|
1290
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1291
|
+
itemId,
|
|
1292
|
+
text,
|
|
1293
|
+
subagent,
|
|
1294
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1295
|
+
updatedAt: Date.now(),
|
|
1296
|
+
isStreaming: streaming,
|
|
1297
|
+
});
|
|
1298
|
+
this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
|
|
1299
|
+
}
|
|
1300
|
+
handleStructuredInput(params, waitForResponse = false) {
|
|
1301
|
+
const raw = asRecord(params) ?? {};
|
|
1302
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1303
|
+
if (!conversationId)
|
|
1304
|
+
return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1305
|
+
const structuredInput = decodeStructuredInput(raw);
|
|
1306
|
+
if (!structuredInput)
|
|
1307
|
+
return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1308
|
+
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1309
|
+
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1310
|
+
this.upsertItem(conversationId, {
|
|
1311
|
+
id: `input:${structuredInput.requestId}`,
|
|
1312
|
+
conversationId,
|
|
1313
|
+
type: "status",
|
|
1314
|
+
kind: "user_input_prompt",
|
|
1315
|
+
role: "system",
|
|
1316
|
+
text,
|
|
1317
|
+
structuredInput,
|
|
1318
|
+
metadata: { inputPending: true },
|
|
1319
|
+
createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
|
|
1320
|
+
updatedAt: Date.now(),
|
|
1321
|
+
});
|
|
1322
|
+
this.updateConversationPreview(conversationId, "需要用户输入", "running");
|
|
1323
|
+
if (!waitForResponse)
|
|
1324
|
+
return;
|
|
1325
|
+
return new Promise((resolve) => {
|
|
1326
|
+
const timer = setTimeout(() => {
|
|
1327
|
+
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1328
|
+
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1329
|
+
resolve(formatStructuredInputResponse({}));
|
|
1330
|
+
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1331
|
+
inputPending: false,
|
|
1332
|
+
inputError: "等待用户输入超时",
|
|
1333
|
+
});
|
|
1334
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1335
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
903
1338
|
toolCallFromItem(item, fallbackStatus) {
|
|
904
1339
|
const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
|
|
905
1340
|
if (!itemId)
|
|
906
1341
|
return undefined;
|
|
907
1342
|
const itemType = firstString(item, ["type"]);
|
|
1343
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
908
1344
|
const name = toolNameFromItem(item);
|
|
909
1345
|
if (!name && !isToolItemType(itemType))
|
|
910
1346
|
return undefined;
|
|
911
1347
|
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
912
1348
|
const rawOutput = firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
913
1349
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
914
|
-
const output =
|
|
1350
|
+
const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
|
|
915
1351
|
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
916
1352
|
: rawOutput ?? bufferedOutput;
|
|
917
1353
|
return {
|
|
@@ -980,8 +1416,10 @@ export class AgentWorkspaceProxy {
|
|
|
980
1416
|
this.permissionSources.delete(payload.requestId);
|
|
981
1417
|
}
|
|
982
1418
|
else {
|
|
983
|
-
this.
|
|
984
|
-
|
|
1419
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
1420
|
+
const respondClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
1421
|
+
respondClient?.respondPermission({
|
|
1422
|
+
sessionId: conversation?.agentSessionId,
|
|
985
1423
|
requestId: payload.requestId,
|
|
986
1424
|
outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
|
|
987
1425
|
optionId: selectedOptionId,
|
|
@@ -989,6 +1427,32 @@ export class AgentWorkspaceProxy {
|
|
|
989
1427
|
}
|
|
990
1428
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
991
1429
|
}
|
|
1430
|
+
respondStructuredInput(payload) {
|
|
1431
|
+
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
1432
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
1433
|
+
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
1434
|
+
if (waiter) {
|
|
1435
|
+
clearTimeout(waiter.timer);
|
|
1436
|
+
this.structuredInputWaiters.delete(payload.requestId);
|
|
1437
|
+
waiter.resolve(formatStructuredInputResponse(payload.answers));
|
|
1438
|
+
}
|
|
1439
|
+
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1440
|
+
inputPending: false,
|
|
1441
|
+
inputSubmitted: true,
|
|
1442
|
+
answers: payload.answers,
|
|
1443
|
+
});
|
|
1444
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1445
|
+
}
|
|
1446
|
+
markStructuredInput(conversationId, requestId, metadata) {
|
|
1447
|
+
const item = this.findItem(conversationId, `input:${requestId}`);
|
|
1448
|
+
if (!item)
|
|
1449
|
+
return;
|
|
1450
|
+
this.upsertItem(conversationId, {
|
|
1451
|
+
...item,
|
|
1452
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
1453
|
+
updatedAt: Date.now(),
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
992
1456
|
addItem(conversationId, item) {
|
|
993
1457
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
994
1458
|
timeline.push(item);
|
|
@@ -1028,11 +1492,20 @@ export class AgentWorkspaceProxy {
|
|
|
1028
1492
|
};
|
|
1029
1493
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
1030
1494
|
this.toolConversationIds.set(nextToolCall.id, conversationId);
|
|
1495
|
+
const kind = nextToolCall.name.includes("文件")
|
|
1496
|
+
? "file_change"
|
|
1497
|
+
: nextToolCall.name.includes("命令")
|
|
1498
|
+
? "command_execution"
|
|
1499
|
+
: "tool_activity";
|
|
1031
1500
|
this.upsertItem(conversationId, {
|
|
1032
1501
|
id: `tool:${nextToolCall.id}`,
|
|
1033
1502
|
conversationId,
|
|
1034
1503
|
type: "tool_call",
|
|
1504
|
+
kind,
|
|
1505
|
+
itemId: nextToolCall.id,
|
|
1035
1506
|
toolCall: nextToolCall,
|
|
1507
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
|
|
1508
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
|
|
1036
1509
|
createdAt: nextToolCall.createdAt ?? Date.now(),
|
|
1037
1510
|
updatedAt: Date.now(),
|
|
1038
1511
|
});
|
|
@@ -1178,12 +1651,13 @@ export class AgentWorkspaceProxy {
|
|
|
1178
1651
|
return this.conversationByAgentSessionId.get(threadId);
|
|
1179
1652
|
return undefined;
|
|
1180
1653
|
}
|
|
1181
|
-
|
|
1654
|
+
handleProviderExit(provider, message) {
|
|
1655
|
+
this.clients.delete(provider);
|
|
1656
|
+
this.agentProtocols.delete(provider);
|
|
1182
1657
|
this.cancelPendingPermissions();
|
|
1183
|
-
this.status = "error";
|
|
1184
|
-
this.error = message;
|
|
1185
|
-
this.client = undefined;
|
|
1186
1658
|
for (const conversation of this.conversations.values()) {
|
|
1659
|
+
if (conversation.provider !== provider)
|
|
1660
|
+
continue;
|
|
1187
1661
|
conversation.status = "error";
|
|
1188
1662
|
conversation.lastMessagePreview = message;
|
|
1189
1663
|
conversation.lastActivityAt = Date.now();
|
|
@@ -1196,6 +1670,7 @@ export class AgentWorkspaceProxy {
|
|
|
1196
1670
|
createdAt: Date.now(),
|
|
1197
1671
|
});
|
|
1198
1672
|
}
|
|
1673
|
+
this.sendCapabilities();
|
|
1199
1674
|
}
|
|
1200
1675
|
cancelPendingPermissions(conversationId) {
|
|
1201
1676
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
@@ -1205,6 +1680,19 @@ export class AgentWorkspaceProxy {
|
|
|
1205
1680
|
this.permissionSources.delete(requestId);
|
|
1206
1681
|
}
|
|
1207
1682
|
this.permissionWaiters.clear();
|
|
1683
|
+
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
1684
|
+
clearTimeout(waiter.timer);
|
|
1685
|
+
waiter.resolve(formatStructuredInputResponse({}));
|
|
1686
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
1687
|
+
if (pending) {
|
|
1688
|
+
this.markStructuredInput(pending.conversationId, requestId, {
|
|
1689
|
+
inputPending: false,
|
|
1690
|
+
inputError: "已停止",
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
this.pendingStructuredInputs.delete(requestId);
|
|
1694
|
+
}
|
|
1695
|
+
this.structuredInputWaiters.clear();
|
|
1208
1696
|
if (conversationId)
|
|
1209
1697
|
this.updateConversationStatus(conversationId, "idle");
|
|
1210
1698
|
}
|
|
@@ -1272,8 +1760,15 @@ function selectPermissionOption(permission, outcome) {
|
|
|
1272
1760
|
function isPermissionRequestMethod(method) {
|
|
1273
1761
|
return (method === "session/request_permission" ||
|
|
1274
1762
|
method.endsWith("/requestApproval") ||
|
|
1275
|
-
method === "mcpServer/elicitation/request"
|
|
1276
|
-
|
|
1763
|
+
method === "mcpServer/elicitation/request");
|
|
1764
|
+
}
|
|
1765
|
+
function formatStructuredInputResponse(answers) {
|
|
1766
|
+
return {
|
|
1767
|
+
answers: Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
|
|
1768
|
+
questionId,
|
|
1769
|
+
{ answers: values.map((value) => value.trim()).filter(Boolean) },
|
|
1770
|
+
])),
|
|
1771
|
+
};
|
|
1277
1772
|
}
|
|
1278
1773
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
1279
1774
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|