linkshell-cli 0.2.88 → 0.2.89
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/runtime/acp/agent-workspace.d.ts +7 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +499 -16
- package/dist/cli/src/runtime/acp/agent-workspace.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/runtime/acp/agent-workspace.ts +610 -16
|
@@ -15,6 +15,8 @@ export declare class AgentWorkspaceProxy {
|
|
|
15
15
|
private pendingPermissions;
|
|
16
16
|
private permissionWaiters;
|
|
17
17
|
private permissionSources;
|
|
18
|
+
private pendingStructuredInputs;
|
|
19
|
+
private structuredInputWaiters;
|
|
18
20
|
private toolConversationIds;
|
|
19
21
|
private agentProtocol;
|
|
20
22
|
constructor(input: {
|
|
@@ -46,9 +48,14 @@ export declare class AgentWorkspaceProxy {
|
|
|
46
48
|
private handleCommandExecDelta;
|
|
47
49
|
private handleCompletedMessageItem;
|
|
48
50
|
private handleSessionUpdate;
|
|
51
|
+
private handleSemanticSystemItem;
|
|
52
|
+
private handleSubagentItem;
|
|
53
|
+
private handleStructuredInput;
|
|
49
54
|
private toolCallFromItem;
|
|
50
55
|
private handlePermission;
|
|
51
56
|
private respondPermission;
|
|
57
|
+
private respondStructuredInput;
|
|
58
|
+
private markStructuredInput;
|
|
52
59
|
private addItem;
|
|
53
60
|
private upsertItem;
|
|
54
61
|
private upsertTool;
|
|
@@ -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
|
}
|
|
@@ -229,6 +512,8 @@ export class AgentWorkspaceProxy {
|
|
|
229
512
|
pendingPermissions = new Map();
|
|
230
513
|
permissionWaiters = new Map();
|
|
231
514
|
permissionSources = new Map();
|
|
515
|
+
pendingStructuredInputs = new Map();
|
|
516
|
+
structuredInputWaiters = new Map();
|
|
232
517
|
toolConversationIds = new Map();
|
|
233
518
|
agentProtocol;
|
|
234
519
|
constructor(input) {
|
|
@@ -283,6 +568,11 @@ export class AgentWorkspaceProxy {
|
|
|
283
568
|
this.respondPermission(payload);
|
|
284
569
|
break;
|
|
285
570
|
}
|
|
571
|
+
case "agent.v2.structured_input.respond": {
|
|
572
|
+
const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
|
|
573
|
+
this.respondStructuredInput(payload);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
286
576
|
}
|
|
287
577
|
}
|
|
288
578
|
stop() {
|
|
@@ -534,6 +824,9 @@ export class AgentWorkspaceProxy {
|
|
|
534
824
|
}
|
|
535
825
|
}
|
|
536
826
|
handleRequest(method, params) {
|
|
827
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
828
|
+
return this.handleStructuredInput(params, true);
|
|
829
|
+
}
|
|
537
830
|
if (isPermissionRequestMethod(method)) {
|
|
538
831
|
return this.handlePermission(params, true, method);
|
|
539
832
|
}
|
|
@@ -556,6 +849,10 @@ export class AgentWorkspaceProxy {
|
|
|
556
849
|
return;
|
|
557
850
|
}
|
|
558
851
|
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
852
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
853
|
+
this.handleStructuredInput(params);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
559
856
|
if (isPermissionRequestMethod(method)) {
|
|
560
857
|
this.handlePermission(params, false, method);
|
|
561
858
|
return;
|
|
@@ -712,14 +1009,21 @@ export class AgentWorkspaceProxy {
|
|
|
712
1009
|
if (!item)
|
|
713
1010
|
return;
|
|
714
1011
|
const itemType = firstString(item, ["type"]);
|
|
715
|
-
|
|
1012
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1013
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
716
1014
|
this.handleCompletedMessageItem(item, true);
|
|
717
1015
|
return;
|
|
718
1016
|
}
|
|
719
|
-
if (
|
|
1017
|
+
if (normalizedItemType === "plan") {
|
|
720
1018
|
this.handlePlanUpdated({ plan: [item] });
|
|
721
1019
|
return;
|
|
722
1020
|
}
|
|
1021
|
+
if (isSubagentItemType(itemType)) {
|
|
1022
|
+
this.handleSubagentItem(item, "running", true);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.handleSemanticSystemItem(item, "running", true))
|
|
1026
|
+
return;
|
|
723
1027
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
724
1028
|
const toolCall = this.toolCallFromItem(item, "running");
|
|
725
1029
|
if (!conversationId || !toolCall)
|
|
@@ -732,10 +1036,21 @@ export class AgentWorkspaceProxy {
|
|
|
732
1036
|
if (!item)
|
|
733
1037
|
return;
|
|
734
1038
|
const itemType = firstString(item, ["type"]);
|
|
735
|
-
|
|
1039
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1040
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
736
1041
|
this.handleCompletedMessageItem(item, false);
|
|
737
1042
|
return;
|
|
738
1043
|
}
|
|
1044
|
+
if (normalizedItemType === "plan") {
|
|
1045
|
+
this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (isSubagentItemType(itemType)) {
|
|
1049
|
+
this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false))
|
|
1053
|
+
return;
|
|
739
1054
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
740
1055
|
const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
|
|
741
1056
|
if (!conversationId || !toolCall)
|
|
@@ -900,18 +1215,131 @@ export class AgentWorkspaceProxy {
|
|
|
900
1215
|
});
|
|
901
1216
|
this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
|
|
902
1217
|
}
|
|
1218
|
+
handleSemanticSystemItem(item, status, streaming) {
|
|
1219
|
+
const itemType = firstString(item, ["type"]);
|
|
1220
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1221
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1222
|
+
if (!conversationId)
|
|
1223
|
+
return false;
|
|
1224
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
|
|
1225
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1226
|
+
const base = {
|
|
1227
|
+
id: itemId,
|
|
1228
|
+
conversationId,
|
|
1229
|
+
type: "status",
|
|
1230
|
+
role: "system",
|
|
1231
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1232
|
+
itemId,
|
|
1233
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1234
|
+
updatedAt: Date.now(),
|
|
1235
|
+
isStreaming: streaming,
|
|
1236
|
+
};
|
|
1237
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1238
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1239
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1240
|
+
this.upsertItem(conversationId, {
|
|
1241
|
+
...base,
|
|
1242
|
+
kind: "thinking",
|
|
1243
|
+
text: text ?? (streaming ? "正在思考" : "完成思考"),
|
|
1244
|
+
});
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
if (normalized === "enteredreviewmode") {
|
|
1248
|
+
const target = firstString(item, ["review", "target", "label"]) ?? "changes";
|
|
1249
|
+
this.upsertItem(conversationId, {
|
|
1250
|
+
...base,
|
|
1251
|
+
kind: "review",
|
|
1252
|
+
text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
|
|
1253
|
+
});
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
if (normalized === "contextcompaction") {
|
|
1257
|
+
this.upsertItem(conversationId, {
|
|
1258
|
+
...base,
|
|
1259
|
+
kind: "context_compaction",
|
|
1260
|
+
text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
|
|
1261
|
+
});
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
handleSubagentItem(item, status, streaming) {
|
|
1267
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1268
|
+
if (!conversationId)
|
|
1269
|
+
return;
|
|
1270
|
+
const subagent = decodeSubagentAction(item, status);
|
|
1271
|
+
if (!subagent)
|
|
1272
|
+
return;
|
|
1273
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
|
|
1274
|
+
const text = summarizeSubagentAction(subagent);
|
|
1275
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1276
|
+
this.upsertItem(conversationId, {
|
|
1277
|
+
id: itemId,
|
|
1278
|
+
conversationId,
|
|
1279
|
+
type: "status",
|
|
1280
|
+
kind: "subagent_action",
|
|
1281
|
+
role: "system",
|
|
1282
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1283
|
+
itemId,
|
|
1284
|
+
text,
|
|
1285
|
+
subagent,
|
|
1286
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1287
|
+
updatedAt: Date.now(),
|
|
1288
|
+
isStreaming: streaming,
|
|
1289
|
+
});
|
|
1290
|
+
this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
|
|
1291
|
+
}
|
|
1292
|
+
handleStructuredInput(params, waitForResponse = false) {
|
|
1293
|
+
const raw = asRecord(params) ?? {};
|
|
1294
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1295
|
+
if (!conversationId)
|
|
1296
|
+
return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1297
|
+
const structuredInput = decodeStructuredInput(raw);
|
|
1298
|
+
if (!structuredInput)
|
|
1299
|
+
return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1300
|
+
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1301
|
+
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1302
|
+
this.upsertItem(conversationId, {
|
|
1303
|
+
id: `input:${structuredInput.requestId}`,
|
|
1304
|
+
conversationId,
|
|
1305
|
+
type: "status",
|
|
1306
|
+
kind: "user_input_prompt",
|
|
1307
|
+
role: "system",
|
|
1308
|
+
text,
|
|
1309
|
+
structuredInput,
|
|
1310
|
+
metadata: { inputPending: true },
|
|
1311
|
+
createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
|
|
1312
|
+
updatedAt: Date.now(),
|
|
1313
|
+
});
|
|
1314
|
+
this.updateConversationPreview(conversationId, "需要用户输入", "running");
|
|
1315
|
+
if (!waitForResponse)
|
|
1316
|
+
return;
|
|
1317
|
+
return new Promise((resolve) => {
|
|
1318
|
+
const timer = setTimeout(() => {
|
|
1319
|
+
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1320
|
+
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1321
|
+
resolve(formatStructuredInputResponse({}));
|
|
1322
|
+
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1323
|
+
inputPending: false,
|
|
1324
|
+
inputError: "等待用户输入超时",
|
|
1325
|
+
});
|
|
1326
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1327
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
903
1330
|
toolCallFromItem(item, fallbackStatus) {
|
|
904
1331
|
const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
|
|
905
1332
|
if (!itemId)
|
|
906
1333
|
return undefined;
|
|
907
1334
|
const itemType = firstString(item, ["type"]);
|
|
1335
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
908
1336
|
const name = toolNameFromItem(item);
|
|
909
1337
|
if (!name && !isToolItemType(itemType))
|
|
910
1338
|
return undefined;
|
|
911
1339
|
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
912
1340
|
const rawOutput = firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
913
1341
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
914
|
-
const output =
|
|
1342
|
+
const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
|
|
915
1343
|
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
916
1344
|
: rawOutput ?? bufferedOutput;
|
|
917
1345
|
return {
|
|
@@ -989,6 +1417,32 @@ export class AgentWorkspaceProxy {
|
|
|
989
1417
|
}
|
|
990
1418
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
991
1419
|
}
|
|
1420
|
+
respondStructuredInput(payload) {
|
|
1421
|
+
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
1422
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
1423
|
+
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
1424
|
+
if (waiter) {
|
|
1425
|
+
clearTimeout(waiter.timer);
|
|
1426
|
+
this.structuredInputWaiters.delete(payload.requestId);
|
|
1427
|
+
waiter.resolve(formatStructuredInputResponse(payload.answers));
|
|
1428
|
+
}
|
|
1429
|
+
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1430
|
+
inputPending: false,
|
|
1431
|
+
inputSubmitted: true,
|
|
1432
|
+
answers: payload.answers,
|
|
1433
|
+
});
|
|
1434
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1435
|
+
}
|
|
1436
|
+
markStructuredInput(conversationId, requestId, metadata) {
|
|
1437
|
+
const item = this.findItem(conversationId, `input:${requestId}`);
|
|
1438
|
+
if (!item)
|
|
1439
|
+
return;
|
|
1440
|
+
this.upsertItem(conversationId, {
|
|
1441
|
+
...item,
|
|
1442
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
1443
|
+
updatedAt: Date.now(),
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
992
1446
|
addItem(conversationId, item) {
|
|
993
1447
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
994
1448
|
timeline.push(item);
|
|
@@ -1028,11 +1482,20 @@ export class AgentWorkspaceProxy {
|
|
|
1028
1482
|
};
|
|
1029
1483
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
1030
1484
|
this.toolConversationIds.set(nextToolCall.id, conversationId);
|
|
1485
|
+
const kind = nextToolCall.name.includes("文件")
|
|
1486
|
+
? "file_change"
|
|
1487
|
+
: nextToolCall.name.includes("命令")
|
|
1488
|
+
? "command_execution"
|
|
1489
|
+
: "tool_activity";
|
|
1031
1490
|
this.upsertItem(conversationId, {
|
|
1032
1491
|
id: `tool:${nextToolCall.id}`,
|
|
1033
1492
|
conversationId,
|
|
1034
1493
|
type: "tool_call",
|
|
1494
|
+
kind,
|
|
1495
|
+
itemId: nextToolCall.id,
|
|
1035
1496
|
toolCall: nextToolCall,
|
|
1497
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
|
|
1498
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
|
|
1036
1499
|
createdAt: nextToolCall.createdAt ?? Date.now(),
|
|
1037
1500
|
updatedAt: Date.now(),
|
|
1038
1501
|
});
|
|
@@ -1205,6 +1668,19 @@ export class AgentWorkspaceProxy {
|
|
|
1205
1668
|
this.permissionSources.delete(requestId);
|
|
1206
1669
|
}
|
|
1207
1670
|
this.permissionWaiters.clear();
|
|
1671
|
+
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
1672
|
+
clearTimeout(waiter.timer);
|
|
1673
|
+
waiter.resolve(formatStructuredInputResponse({}));
|
|
1674
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
1675
|
+
if (pending) {
|
|
1676
|
+
this.markStructuredInput(pending.conversationId, requestId, {
|
|
1677
|
+
inputPending: false,
|
|
1678
|
+
inputError: "已停止",
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
this.pendingStructuredInputs.delete(requestId);
|
|
1682
|
+
}
|
|
1683
|
+
this.structuredInputWaiters.clear();
|
|
1208
1684
|
if (conversationId)
|
|
1209
1685
|
this.updateConversationStatus(conversationId, "idle");
|
|
1210
1686
|
}
|
|
@@ -1272,8 +1748,15 @@ function selectPermissionOption(permission, outcome) {
|
|
|
1272
1748
|
function isPermissionRequestMethod(method) {
|
|
1273
1749
|
return (method === "session/request_permission" ||
|
|
1274
1750
|
method.endsWith("/requestApproval") ||
|
|
1275
|
-
method === "mcpServer/elicitation/request"
|
|
1276
|
-
|
|
1751
|
+
method === "mcpServer/elicitation/request");
|
|
1752
|
+
}
|
|
1753
|
+
function formatStructuredInputResponse(answers) {
|
|
1754
|
+
return {
|
|
1755
|
+
answers: Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
|
|
1756
|
+
questionId,
|
|
1757
|
+
{ answers: values.map((value) => value.trim()).filter(Boolean) },
|
|
1758
|
+
])),
|
|
1759
|
+
};
|
|
1277
1760
|
}
|
|
1278
1761
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
1279
1762
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|