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
|
@@ -41,6 +41,87 @@ interface AgentPermission {
|
|
|
41
41
|
options: { id: string; label: string; kind: "allow" | "deny" | "other" }[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
type AgentTimelineKind =
|
|
45
|
+
| "chat"
|
|
46
|
+
| "thinking"
|
|
47
|
+
| "tool_activity"
|
|
48
|
+
| "command_execution"
|
|
49
|
+
| "file_change"
|
|
50
|
+
| "subagent_action"
|
|
51
|
+
| "plan"
|
|
52
|
+
| "user_input_prompt"
|
|
53
|
+
| "review"
|
|
54
|
+
| "context_compaction";
|
|
55
|
+
|
|
56
|
+
interface AgentFileChangeEntry {
|
|
57
|
+
path: string;
|
|
58
|
+
kind?: string;
|
|
59
|
+
added?: number;
|
|
60
|
+
removed?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface AgentFileChange {
|
|
64
|
+
entries: AgentFileChangeEntry[];
|
|
65
|
+
diff?: string;
|
|
66
|
+
summary?: string;
|
|
67
|
+
changeSetId?: string;
|
|
68
|
+
status?: AgentToolCall["status"];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AgentCommandExecution {
|
|
72
|
+
command?: string;
|
|
73
|
+
cwd?: string;
|
|
74
|
+
output?: string;
|
|
75
|
+
exitCode?: number | null;
|
|
76
|
+
status?: AgentToolCall["status"];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AgentStructuredInputOption {
|
|
80
|
+
id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface AgentStructuredInputQuestion {
|
|
86
|
+
id: string;
|
|
87
|
+
header?: string;
|
|
88
|
+
question: string;
|
|
89
|
+
isOther?: boolean;
|
|
90
|
+
isSecret?: boolean;
|
|
91
|
+
selectionLimit?: number;
|
|
92
|
+
options?: AgentStructuredInputOption[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface AgentStructuredInput {
|
|
96
|
+
requestId: string;
|
|
97
|
+
questions: AgentStructuredInputQuestion[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface AgentSubagentRef {
|
|
101
|
+
threadId: string;
|
|
102
|
+
agentId?: string;
|
|
103
|
+
nickname?: string;
|
|
104
|
+
role?: string;
|
|
105
|
+
model?: string;
|
|
106
|
+
prompt?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface AgentSubagentState {
|
|
110
|
+
threadId: string;
|
|
111
|
+
status: string;
|
|
112
|
+
message?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface AgentSubagentAction {
|
|
116
|
+
tool: string;
|
|
117
|
+
status: string;
|
|
118
|
+
prompt?: string;
|
|
119
|
+
model?: string;
|
|
120
|
+
receiverThreadIds: string[];
|
|
121
|
+
receiverAgents: AgentSubagentRef[];
|
|
122
|
+
agentStates: Record<string, AgentSubagentState>;
|
|
123
|
+
}
|
|
124
|
+
|
|
44
125
|
interface AgentConversation {
|
|
45
126
|
id: string;
|
|
46
127
|
agentSessionId?: string;
|
|
@@ -61,10 +142,17 @@ interface AgentTimelineItem {
|
|
|
61
142
|
id: string;
|
|
62
143
|
conversationId: string;
|
|
63
144
|
type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
|
|
145
|
+
kind?: AgentTimelineKind;
|
|
146
|
+
turnId?: string;
|
|
147
|
+
itemId?: string;
|
|
64
148
|
role?: "user" | "assistant" | "system";
|
|
65
149
|
content?: AgentContentBlock[];
|
|
66
150
|
text?: string;
|
|
67
151
|
toolCall?: AgentToolCall;
|
|
152
|
+
commandExecution?: AgentCommandExecution;
|
|
153
|
+
fileChange?: AgentFileChange;
|
|
154
|
+
subagent?: AgentSubagentAction;
|
|
155
|
+
structuredInput?: AgentStructuredInput;
|
|
68
156
|
plan?: AgentPlanStep[];
|
|
69
157
|
permission?: AgentPermission;
|
|
70
158
|
status?: AgentStatus;
|
|
@@ -80,6 +168,11 @@ interface PendingPermissionWaiter {
|
|
|
80
168
|
timer: ReturnType<typeof setTimeout>;
|
|
81
169
|
}
|
|
82
170
|
|
|
171
|
+
interface PendingStructuredInputWaiter {
|
|
172
|
+
resolve: (value: unknown) => void;
|
|
173
|
+
timer: ReturnType<typeof setTimeout>;
|
|
174
|
+
}
|
|
175
|
+
|
|
83
176
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
84
177
|
const MAX_TIMELINE_ITEMS = 200;
|
|
85
178
|
|
|
@@ -109,6 +202,32 @@ function firstString(value: Record<string, unknown> | undefined, keys: string[])
|
|
|
109
202
|
return undefined;
|
|
110
203
|
}
|
|
111
204
|
|
|
205
|
+
function normalizedIdentifier(value: string | undefined): string {
|
|
206
|
+
return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function firstNumber(value: Record<string, unknown> | undefined, keys: string[]): number | undefined {
|
|
210
|
+
if (!value) return undefined;
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
const next = value[key];
|
|
213
|
+
if (typeof next === "number" && Number.isFinite(next)) return next;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function stringArray(value: unknown): string[] {
|
|
219
|
+
if (!Array.isArray(value)) return [];
|
|
220
|
+
return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function arrayFromKeys(value: Record<string, unknown>, keys: string[]): unknown[] {
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
const next = value[key];
|
|
226
|
+
if (Array.isArray(next)) return next;
|
|
227
|
+
}
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
112
231
|
function extractItem(value: unknown): Record<string, unknown> | undefined {
|
|
113
232
|
const raw = asRecord(value);
|
|
114
233
|
if (!raw) return undefined;
|
|
@@ -161,24 +280,29 @@ function nameFromToolMethod(method: string): string {
|
|
|
161
280
|
}
|
|
162
281
|
|
|
163
282
|
function isToolItemType(itemType: string | undefined): boolean {
|
|
283
|
+
const normalized = normalizedIdentifier(itemType);
|
|
164
284
|
return (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
285
|
+
normalized === "commandexecution" ||
|
|
286
|
+
normalized === "filechange" ||
|
|
287
|
+
normalized === "diff" ||
|
|
288
|
+
normalized === "toolcall" ||
|
|
289
|
+
normalized === "mcptoolcall" ||
|
|
290
|
+
normalized === "dynamictoolcall"
|
|
169
291
|
);
|
|
170
292
|
}
|
|
171
293
|
|
|
172
294
|
function toolNameFromItem(item: Record<string, unknown>): string | undefined {
|
|
173
295
|
const itemType = firstString(item, ["type"]);
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
if (
|
|
296
|
+
const normalized = normalizedIdentifier(itemType);
|
|
297
|
+
if (normalized === "commandexecution") return "命令";
|
|
298
|
+
if (normalized === "filechange" || normalized === "diff") return "文件修改";
|
|
299
|
+
if (normalized === "toolcall") return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
|
|
300
|
+
if (normalized === "mcptoolcall") {
|
|
177
301
|
const server = firstString(item, ["server"]);
|
|
178
302
|
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
179
303
|
return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
|
|
180
304
|
}
|
|
181
|
-
if (
|
|
305
|
+
if (normalized === "dynamictoolcall") {
|
|
182
306
|
const namespace = firstString(item, ["namespace"]);
|
|
183
307
|
const tool = firstString(item, ["tool", "toolName", "name"]);
|
|
184
308
|
return [namespace, tool].filter(Boolean).join(" · ") || "工具";
|
|
@@ -201,6 +325,95 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
|
201
325
|
return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
|
|
202
326
|
}
|
|
203
327
|
|
|
328
|
+
function fileChangeEntriesFromItem(item: Record<string, unknown>): AgentFileChangeEntry[] {
|
|
329
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
330
|
+
const entries: AgentFileChangeEntry[] = [];
|
|
331
|
+
for (const change of changes) {
|
|
332
|
+
const raw = asRecord(change);
|
|
333
|
+
if (!raw) continue;
|
|
334
|
+
const path =
|
|
335
|
+
firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
|
|
336
|
+
firstString(asRecord(raw.update), ["path", "file", "filePath"]);
|
|
337
|
+
if (!path) continue;
|
|
338
|
+
const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
|
|
339
|
+
const entry: AgentFileChangeEntry = { path };
|
|
340
|
+
const kind = firstString(raw, ["kind", "type", "operation", "action"]);
|
|
341
|
+
const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
|
|
342
|
+
const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
|
|
343
|
+
if (kind) entry.kind = kind;
|
|
344
|
+
if (added !== undefined) entry.added = added;
|
|
345
|
+
if (removed !== undefined) entry.removed = removed;
|
|
346
|
+
entries.push(entry);
|
|
347
|
+
}
|
|
348
|
+
const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
349
|
+
if (entries.length === 0 && directPath) {
|
|
350
|
+
const entry: AgentFileChangeEntry = { path: directPath };
|
|
351
|
+
const kind = firstString(item, ["kind", "type", "operation", "action"]);
|
|
352
|
+
if (kind) entry.kind = kind;
|
|
353
|
+
return [entry];
|
|
354
|
+
}
|
|
355
|
+
return entries;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function commandExecutionFromItem(
|
|
359
|
+
item: Record<string, unknown>,
|
|
360
|
+
status: AgentToolCall["status"],
|
|
361
|
+
output?: string,
|
|
362
|
+
): AgentCommandExecution | undefined {
|
|
363
|
+
const command = firstString(item, ["command"]);
|
|
364
|
+
const cwd = firstString(item, ["cwd"]);
|
|
365
|
+
const exitCode = firstNumber(item, ["exitCode", "code"]);
|
|
366
|
+
if (!command && !cwd && !output && exitCode === undefined) return undefined;
|
|
367
|
+
return { command, cwd, output, exitCode: exitCode ?? undefined, status };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function fileChangeFromItem(
|
|
371
|
+
item: Record<string, unknown>,
|
|
372
|
+
status: AgentToolCall["status"],
|
|
373
|
+
diff?: string,
|
|
374
|
+
): AgentFileChange | undefined {
|
|
375
|
+
const entries = fileChangeEntriesFromItem(item);
|
|
376
|
+
const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
|
|
377
|
+
const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
|
|
378
|
+
if (entries.length === 0 && !diff && !summary && !changeSetId) return undefined;
|
|
379
|
+
return { entries, diff, summary, changeSetId, status };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function commandExecutionFromTool(toolCall: AgentToolCall): AgentCommandExecution | undefined {
|
|
383
|
+
const input = toolCall.input?.trim();
|
|
384
|
+
if (!input && !toolCall.output) return undefined;
|
|
385
|
+
const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
|
|
386
|
+
return {
|
|
387
|
+
command: commandPart || input,
|
|
388
|
+
cwd: cwdPart,
|
|
389
|
+
output: toolCall.output,
|
|
390
|
+
status: toolCall.status,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
|
|
395
|
+
const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
|
|
396
|
+
const entries: AgentFileChangeEntry[] = (toolCall.input ?? "")
|
|
397
|
+
.split("\n")
|
|
398
|
+
.map((line) => line.trim())
|
|
399
|
+
.filter(Boolean)
|
|
400
|
+
.map((line) => {
|
|
401
|
+
const [kind, ...rest] = line.split(/\s+/);
|
|
402
|
+
const path = rest.length > 0 ? rest.join(" ") : kind;
|
|
403
|
+
const entry: AgentFileChangeEntry = { path: path ?? line };
|
|
404
|
+
if (rest.length > 0 && kind) entry.kind = kind;
|
|
405
|
+
return entry;
|
|
406
|
+
})
|
|
407
|
+
.filter((entry) => entry.path.length > 0);
|
|
408
|
+
if (entries.length === 0 && !diff && !toolCall.output) return undefined;
|
|
409
|
+
return {
|
|
410
|
+
entries,
|
|
411
|
+
diff,
|
|
412
|
+
summary: diff ? undefined : toolCall.output,
|
|
413
|
+
status: toolCall.status,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
204
417
|
function looksLikeDiff(text: string): boolean {
|
|
205
418
|
const value = text.trim();
|
|
206
419
|
return (
|
|
@@ -247,13 +460,14 @@ function extractDiffText(value: unknown): string | undefined {
|
|
|
247
460
|
|
|
248
461
|
function toolInputFromItem(item: Record<string, unknown>): string | undefined {
|
|
249
462
|
const itemType = firstString(item, ["type"]);
|
|
250
|
-
|
|
463
|
+
const normalized = normalizedIdentifier(itemType);
|
|
464
|
+
if (normalized === "commandexecution") {
|
|
251
465
|
const command = firstString(item, ["command"]);
|
|
252
466
|
const cwd = firstString(item, ["cwd"]);
|
|
253
467
|
if (command && cwd) return `${command}\n\ncwd: ${cwd}`;
|
|
254
468
|
return command ?? cwd;
|
|
255
469
|
}
|
|
256
|
-
if (
|
|
470
|
+
if (normalized === "filechange" || normalized === "diff") {
|
|
257
471
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
258
472
|
return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
259
473
|
}
|
|
@@ -271,6 +485,167 @@ function textFromBlocks(blocks: AgentContentBlock[]): string {
|
|
|
271
485
|
.join("\n");
|
|
272
486
|
}
|
|
273
487
|
|
|
488
|
+
function isSubagentItemType(itemType: string | undefined): boolean {
|
|
489
|
+
const normalized = normalizedIdentifier(itemType);
|
|
490
|
+
return (
|
|
491
|
+
normalized === "collabagenttoolcall" ||
|
|
492
|
+
normalized === "collabtoolcall" ||
|
|
493
|
+
normalized.startsWith("collabagentspawn") ||
|
|
494
|
+
normalized.startsWith("collabwaiting") ||
|
|
495
|
+
normalized.startsWith("collabclose") ||
|
|
496
|
+
normalized.startsWith("collabresume") ||
|
|
497
|
+
normalized.startsWith("collabagentinteraction")
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parseSubagentRef(value: unknown): AgentSubagentRef | undefined {
|
|
502
|
+
const raw = asRecord(value);
|
|
503
|
+
if (!raw) return undefined;
|
|
504
|
+
const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
|
|
505
|
+
if (!threadId) return undefined;
|
|
506
|
+
return {
|
|
507
|
+
threadId,
|
|
508
|
+
agentId: firstString(raw, ["agentId", "agentID"]),
|
|
509
|
+
nickname: firstString(raw, ["nickname", "name", "label"]),
|
|
510
|
+
role: firstString(raw, ["role", "kind"]),
|
|
511
|
+
model: firstString(raw, ["model", "modelName"]),
|
|
512
|
+
prompt: firstString(raw, ["prompt", "instructions", "message"]),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function parseSubagentStates(value: unknown): Record<string, AgentSubagentState> {
|
|
517
|
+
const result: Record<string, AgentSubagentState> = {};
|
|
518
|
+
if (Array.isArray(value)) {
|
|
519
|
+
for (const entry of value) {
|
|
520
|
+
const raw = asRecord(entry);
|
|
521
|
+
const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
|
|
522
|
+
const status = firstString(raw, ["status", "state", "phase"]);
|
|
523
|
+
if (!threadId || !status) continue;
|
|
524
|
+
result[threadId] = {
|
|
525
|
+
threadId,
|
|
526
|
+
status,
|
|
527
|
+
message: firstString(raw, ["message", "summary", "text"]),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
const raw = asRecord(value);
|
|
533
|
+
if (!raw) return result;
|
|
534
|
+
for (const [threadId, entry] of Object.entries(raw)) {
|
|
535
|
+
const state = asRecord(entry);
|
|
536
|
+
if (state) {
|
|
537
|
+
result[threadId] = {
|
|
538
|
+
threadId,
|
|
539
|
+
status: firstString(state, ["status", "state", "phase"]) ?? "running",
|
|
540
|
+
message: firstString(state, ["message", "summary", "text"]),
|
|
541
|
+
};
|
|
542
|
+
} else if (typeof entry === "string") {
|
|
543
|
+
result[threadId] = { threadId, status: entry };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function parseStructuredInputOption(value: unknown, index: number): AgentStructuredInputOption | undefined {
|
|
550
|
+
const raw = asRecord(value);
|
|
551
|
+
if (!raw) {
|
|
552
|
+
if (typeof value === "string" && value.trim()) {
|
|
553
|
+
return { id: `option-${index + 1}`, label: value.trim() };
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
const label = firstString(raw, ["label", "title", "text", "value"]);
|
|
558
|
+
if (!label) return undefined;
|
|
559
|
+
return {
|
|
560
|
+
id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
|
|
561
|
+
label,
|
|
562
|
+
description: firstString(raw, ["description", "detail", "subtitle"]),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function parseStructuredInputQuestion(value: unknown, index: number): AgentStructuredInputQuestion | undefined {
|
|
567
|
+
const raw = asRecord(value);
|
|
568
|
+
if (!raw) return undefined;
|
|
569
|
+
const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
|
|
570
|
+
if (!question) return undefined;
|
|
571
|
+
const options = arrayFromKeys(raw, ["options", "choices", "items"])
|
|
572
|
+
.map(parseStructuredInputOption)
|
|
573
|
+
.filter((option): option is AgentStructuredInputOption => Boolean(option));
|
|
574
|
+
return {
|
|
575
|
+
id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
|
|
576
|
+
header: firstString(raw, ["header", "title"]),
|
|
577
|
+
question,
|
|
578
|
+
isOther: raw.isOther === true,
|
|
579
|
+
isSecret: raw.isSecret === true || raw.secret === true,
|
|
580
|
+
selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
|
|
581
|
+
options: options.length > 0 ? options : undefined,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function decodeStructuredInput(value: unknown): AgentStructuredInput | undefined {
|
|
586
|
+
const raw = asRecord(value) ?? {};
|
|
587
|
+
const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
|
|
588
|
+
.map(parseStructuredInputQuestion)
|
|
589
|
+
.filter((question): question is AgentStructuredInputQuestion => Boolean(question));
|
|
590
|
+
if (questions.length === 0) {
|
|
591
|
+
const single = parseStructuredInputQuestion(raw, 0);
|
|
592
|
+
if (single) questions.push(single);
|
|
593
|
+
}
|
|
594
|
+
if (questions.length === 0) return undefined;
|
|
595
|
+
return {
|
|
596
|
+
requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
|
|
597
|
+
questions,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function decodeSubagentAction(
|
|
602
|
+
item: Record<string, unknown>,
|
|
603
|
+
status: "running" | "completed" | "failed" | "pending",
|
|
604
|
+
): AgentSubagentAction | undefined {
|
|
605
|
+
const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
|
|
606
|
+
const receiverAgents = [
|
|
607
|
+
...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
|
|
608
|
+
...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
|
|
609
|
+
].filter((entry): entry is AgentSubagentRef => Boolean(entry));
|
|
610
|
+
const receiverThreadIds = [
|
|
611
|
+
...stringArray(item.receiverThreadIds),
|
|
612
|
+
...stringArray(item.threadIds),
|
|
613
|
+
...stringArray(item.childThreadIds),
|
|
614
|
+
...stringArray(item.agentThreadIds),
|
|
615
|
+
...stringArray(nested.receiverThreadIds),
|
|
616
|
+
...stringArray(nested.threadIds),
|
|
617
|
+
...receiverAgents.map((agent) => agent.threadId),
|
|
618
|
+
].filter((threadId, index, array) => array.indexOf(threadId) === index);
|
|
619
|
+
const agentStates = {
|
|
620
|
+
...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
|
|
621
|
+
...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
|
|
622
|
+
};
|
|
623
|
+
if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0) return undefined;
|
|
624
|
+
return {
|
|
625
|
+
tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
|
|
626
|
+
firstString(nested, ["tool", "toolName", "name", "type"]) ??
|
|
627
|
+
"subagent",
|
|
628
|
+
status,
|
|
629
|
+
prompt: firstString(item, ["prompt", "instructions", "message"]) ??
|
|
630
|
+
firstString(nested, ["prompt", "instructions", "message"]),
|
|
631
|
+
model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
|
|
632
|
+
receiverThreadIds,
|
|
633
|
+
receiverAgents,
|
|
634
|
+
agentStates,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function summarizeSubagentAction(action: AgentSubagentAction): string {
|
|
639
|
+
const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
|
|
640
|
+
const normalized = normalizedIdentifier(action.tool);
|
|
641
|
+
if (normalized.includes("spawn")) return `启动 ${count} 个子 Agent`;
|
|
642
|
+
if (normalized.includes("wait")) return `等待 ${count} 个子 Agent`;
|
|
643
|
+
if (normalized.includes("resume")) return `恢复 ${count} 个子 Agent`;
|
|
644
|
+
if (normalized.includes("close")) return `关闭 ${count} 个子 Agent`;
|
|
645
|
+
if (normalized.includes("sendinput")) return `更新 ${count} 个子 Agent`;
|
|
646
|
+
return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
|
|
647
|
+
}
|
|
648
|
+
|
|
274
649
|
function previewText(text: string): string {
|
|
275
650
|
return text.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
276
651
|
}
|
|
@@ -308,6 +683,8 @@ export class AgentWorkspaceProxy {
|
|
|
308
683
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
309
684
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
310
685
|
private permissionSources = new Map<string, string>();
|
|
686
|
+
private pendingStructuredInputs = new Map<string, { conversationId: string; input: AgentStructuredInput }>();
|
|
687
|
+
private structuredInputWaiters = new Map<string, PendingStructuredInputWaiter>();
|
|
311
688
|
private toolConversationIds = new Map<string, string>();
|
|
312
689
|
private agentProtocol: AgentProtocol | undefined;
|
|
313
690
|
|
|
@@ -373,6 +750,11 @@ export class AgentWorkspaceProxy {
|
|
|
373
750
|
this.respondPermission(payload);
|
|
374
751
|
break;
|
|
375
752
|
}
|
|
753
|
+
case "agent.v2.structured_input.respond": {
|
|
754
|
+
const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
|
|
755
|
+
this.respondStructuredInput(payload);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
376
758
|
}
|
|
377
759
|
}
|
|
378
760
|
|
|
@@ -670,6 +1052,9 @@ export class AgentWorkspaceProxy {
|
|
|
670
1052
|
}
|
|
671
1053
|
|
|
672
1054
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
1055
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1056
|
+
return this.handleStructuredInput(params, true);
|
|
1057
|
+
}
|
|
673
1058
|
if (isPermissionRequestMethod(method)) {
|
|
674
1059
|
return this.handlePermission(params, true, method);
|
|
675
1060
|
}
|
|
@@ -696,6 +1081,10 @@ export class AgentWorkspaceProxy {
|
|
|
696
1081
|
}
|
|
697
1082
|
|
|
698
1083
|
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
1084
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1085
|
+
this.handleStructuredInput(params);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
699
1088
|
if (isPermissionRequestMethod(method)) {
|
|
700
1089
|
this.handlePermission(params, false, method);
|
|
701
1090
|
return;
|
|
@@ -845,14 +1234,20 @@ export class AgentWorkspaceProxy {
|
|
|
845
1234
|
const item = extractItem(params);
|
|
846
1235
|
if (!item) return;
|
|
847
1236
|
const itemType = firstString(item, ["type"]);
|
|
848
|
-
|
|
1237
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1238
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
849
1239
|
this.handleCompletedMessageItem(item, true);
|
|
850
1240
|
return;
|
|
851
1241
|
}
|
|
852
|
-
if (
|
|
1242
|
+
if (normalizedItemType === "plan") {
|
|
853
1243
|
this.handlePlanUpdated({ plan: [item] });
|
|
854
1244
|
return;
|
|
855
1245
|
}
|
|
1246
|
+
if (isSubagentItemType(itemType)) {
|
|
1247
|
+
this.handleSubagentItem(item, "running", true);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (this.handleSemanticSystemItem(item, "running", true)) return;
|
|
856
1251
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
857
1252
|
const toolCall = this.toolCallFromItem(item, "running");
|
|
858
1253
|
if (!conversationId || !toolCall) return;
|
|
@@ -864,10 +1259,20 @@ export class AgentWorkspaceProxy {
|
|
|
864
1259
|
const item = extractItem(params);
|
|
865
1260
|
if (!item) return;
|
|
866
1261
|
const itemType = firstString(item, ["type"]);
|
|
867
|
-
|
|
1262
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1263
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
868
1264
|
this.handleCompletedMessageItem(item, false);
|
|
869
1265
|
return;
|
|
870
1266
|
}
|
|
1267
|
+
if (normalizedItemType === "plan") {
|
|
1268
|
+
this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (isSubagentItemType(itemType)) {
|
|
1272
|
+
this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false)) return;
|
|
871
1276
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
872
1277
|
const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
|
|
873
1278
|
if (!conversationId || !toolCall) return;
|
|
@@ -1029,6 +1434,127 @@ export class AgentWorkspaceProxy {
|
|
|
1029
1434
|
this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
|
|
1030
1435
|
}
|
|
1031
1436
|
|
|
1437
|
+
private handleSemanticSystemItem(
|
|
1438
|
+
item: Record<string, unknown>,
|
|
1439
|
+
status: AgentToolCall["status"],
|
|
1440
|
+
streaming: boolean,
|
|
1441
|
+
): boolean {
|
|
1442
|
+
const itemType = firstString(item, ["type"]);
|
|
1443
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1444
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1445
|
+
if (!conversationId) return false;
|
|
1446
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
|
|
1447
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1448
|
+
const base = {
|
|
1449
|
+
id: itemId,
|
|
1450
|
+
conversationId,
|
|
1451
|
+
type: "status" as const,
|
|
1452
|
+
role: "system" as const,
|
|
1453
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1454
|
+
itemId,
|
|
1455
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1456
|
+
updatedAt: Date.now(),
|
|
1457
|
+
isStreaming: streaming,
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1461
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1462
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1463
|
+
this.upsertItem(conversationId, {
|
|
1464
|
+
...base,
|
|
1465
|
+
kind: "thinking",
|
|
1466
|
+
text: text ?? (streaming ? "正在思考" : "完成思考"),
|
|
1467
|
+
});
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (normalized === "enteredreviewmode") {
|
|
1472
|
+
const target = firstString(item, ["review", "target", "label"]) ?? "changes";
|
|
1473
|
+
this.upsertItem(conversationId, {
|
|
1474
|
+
...base,
|
|
1475
|
+
kind: "review",
|
|
1476
|
+
text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
|
|
1477
|
+
});
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (normalized === "contextcompaction") {
|
|
1482
|
+
this.upsertItem(conversationId, {
|
|
1483
|
+
...base,
|
|
1484
|
+
kind: "context_compaction",
|
|
1485
|
+
text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
|
|
1486
|
+
});
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
private handleSubagentItem(
|
|
1494
|
+
item: Record<string, unknown>,
|
|
1495
|
+
status: AgentToolCall["status"],
|
|
1496
|
+
streaming: boolean,
|
|
1497
|
+
): void {
|
|
1498
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1499
|
+
if (!conversationId) return;
|
|
1500
|
+
const subagent = decodeSubagentAction(item, status);
|
|
1501
|
+
if (!subagent) return;
|
|
1502
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
|
|
1503
|
+
const text = summarizeSubagentAction(subagent);
|
|
1504
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1505
|
+
this.upsertItem(conversationId, {
|
|
1506
|
+
id: itemId,
|
|
1507
|
+
conversationId,
|
|
1508
|
+
type: "status",
|
|
1509
|
+
kind: "subagent_action",
|
|
1510
|
+
role: "system",
|
|
1511
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1512
|
+
itemId,
|
|
1513
|
+
text,
|
|
1514
|
+
subagent,
|
|
1515
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1516
|
+
updatedAt: Date.now(),
|
|
1517
|
+
isStreaming: streaming,
|
|
1518
|
+
});
|
|
1519
|
+
this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
|
|
1523
|
+
const raw = asRecord(params) ?? {};
|
|
1524
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1525
|
+
if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1526
|
+
const structuredInput = decodeStructuredInput(raw);
|
|
1527
|
+
if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1528
|
+
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1529
|
+
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1530
|
+
this.upsertItem(conversationId, {
|
|
1531
|
+
id: `input:${structuredInput.requestId}`,
|
|
1532
|
+
conversationId,
|
|
1533
|
+
type: "status",
|
|
1534
|
+
kind: "user_input_prompt",
|
|
1535
|
+
role: "system",
|
|
1536
|
+
text,
|
|
1537
|
+
structuredInput,
|
|
1538
|
+
metadata: { inputPending: true },
|
|
1539
|
+
createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
|
|
1540
|
+
updatedAt: Date.now(),
|
|
1541
|
+
});
|
|
1542
|
+
this.updateConversationPreview(conversationId, "需要用户输入", "running");
|
|
1543
|
+
if (!waitForResponse) return;
|
|
1544
|
+
return new Promise((resolve) => {
|
|
1545
|
+
const timer = setTimeout(() => {
|
|
1546
|
+
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1547
|
+
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1548
|
+
resolve(formatStructuredInputResponse({}));
|
|
1549
|
+
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1550
|
+
inputPending: false,
|
|
1551
|
+
inputError: "等待用户输入超时",
|
|
1552
|
+
});
|
|
1553
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1554
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1032
1558
|
private toolCallFromItem(
|
|
1033
1559
|
item: Record<string, unknown>,
|
|
1034
1560
|
fallbackStatus: AgentToolCall["status"],
|
|
@@ -1036,13 +1562,14 @@ export class AgentWorkspaceProxy {
|
|
|
1036
1562
|
const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
|
|
1037
1563
|
if (!itemId) return undefined;
|
|
1038
1564
|
const itemType = firstString(item, ["type"]);
|
|
1565
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1039
1566
|
const name = toolNameFromItem(item);
|
|
1040
1567
|
if (!name && !isToolItemType(itemType)) return undefined;
|
|
1041
1568
|
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
1042
1569
|
const rawOutput =
|
|
1043
1570
|
firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
1044
1571
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
1045
|
-
const output =
|
|
1572
|
+
const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
|
|
1046
1573
|
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
1047
1574
|
: rawOutput ?? bufferedOutput;
|
|
1048
1575
|
return {
|
|
@@ -1134,6 +1661,41 @@ export class AgentWorkspaceProxy {
|
|
|
1134
1661
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
1135
1662
|
}
|
|
1136
1663
|
|
|
1664
|
+
private respondStructuredInput(payload: {
|
|
1665
|
+
conversationId: string;
|
|
1666
|
+
requestId: string;
|
|
1667
|
+
answers: Record<string, string[]>;
|
|
1668
|
+
}): void {
|
|
1669
|
+
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
1670
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
1671
|
+
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
1672
|
+
if (waiter) {
|
|
1673
|
+
clearTimeout(waiter.timer);
|
|
1674
|
+
this.structuredInputWaiters.delete(payload.requestId);
|
|
1675
|
+
waiter.resolve(formatStructuredInputResponse(payload.answers));
|
|
1676
|
+
}
|
|
1677
|
+
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1678
|
+
inputPending: false,
|
|
1679
|
+
inputSubmitted: true,
|
|
1680
|
+
answers: payload.answers,
|
|
1681
|
+
});
|
|
1682
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
private markStructuredInput(
|
|
1686
|
+
conversationId: string,
|
|
1687
|
+
requestId: string,
|
|
1688
|
+
metadata: Record<string, unknown>,
|
|
1689
|
+
): void {
|
|
1690
|
+
const item = this.findItem(conversationId, `input:${requestId}`);
|
|
1691
|
+
if (!item) return;
|
|
1692
|
+
this.upsertItem(conversationId, {
|
|
1693
|
+
...item,
|
|
1694
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
1695
|
+
updatedAt: Date.now(),
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1137
1699
|
private addItem(conversationId: string, item: AgentTimelineItem): void {
|
|
1138
1700
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
1139
1701
|
timeline.push(item);
|
|
@@ -1173,11 +1735,20 @@ export class AgentWorkspaceProxy {
|
|
|
1173
1735
|
};
|
|
1174
1736
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
1175
1737
|
this.toolConversationIds.set(nextToolCall.id, conversationId);
|
|
1738
|
+
const kind: AgentTimelineKind = nextToolCall.name.includes("文件")
|
|
1739
|
+
? "file_change"
|
|
1740
|
+
: nextToolCall.name.includes("命令")
|
|
1741
|
+
? "command_execution"
|
|
1742
|
+
: "tool_activity";
|
|
1176
1743
|
this.upsertItem(conversationId, {
|
|
1177
1744
|
id: `tool:${nextToolCall.id}`,
|
|
1178
1745
|
conversationId,
|
|
1179
1746
|
type: "tool_call",
|
|
1747
|
+
kind,
|
|
1748
|
+
itemId: nextToolCall.id,
|
|
1180
1749
|
toolCall: nextToolCall,
|
|
1750
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
|
|
1751
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
|
|
1181
1752
|
createdAt: nextToolCall.createdAt ?? Date.now(),
|
|
1182
1753
|
updatedAt: Date.now(),
|
|
1183
1754
|
});
|
|
@@ -1380,6 +1951,19 @@ export class AgentWorkspaceProxy {
|
|
|
1380
1951
|
this.permissionSources.delete(requestId);
|
|
1381
1952
|
}
|
|
1382
1953
|
this.permissionWaiters.clear();
|
|
1954
|
+
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
1955
|
+
clearTimeout(waiter.timer);
|
|
1956
|
+
waiter.resolve(formatStructuredInputResponse({}));
|
|
1957
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
1958
|
+
if (pending) {
|
|
1959
|
+
this.markStructuredInput(pending.conversationId, requestId, {
|
|
1960
|
+
inputPending: false,
|
|
1961
|
+
inputError: "已停止",
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
this.pendingStructuredInputs.delete(requestId);
|
|
1965
|
+
}
|
|
1966
|
+
this.structuredInputWaiters.clear();
|
|
1383
1967
|
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
1384
1968
|
}
|
|
1385
1969
|
|
|
@@ -1452,11 +2036,21 @@ function isPermissionRequestMethod(method: string): boolean {
|
|
|
1452
2036
|
return (
|
|
1453
2037
|
method === "session/request_permission" ||
|
|
1454
2038
|
method.endsWith("/requestApproval") ||
|
|
1455
|
-
method === "mcpServer/elicitation/request"
|
|
1456
|
-
method === "item/tool/requestUserInput"
|
|
2039
|
+
method === "mcpServer/elicitation/request"
|
|
1457
2040
|
);
|
|
1458
2041
|
}
|
|
1459
2042
|
|
|
2043
|
+
function formatStructuredInputResponse(answers: Record<string, string[]>): unknown {
|
|
2044
|
+
return {
|
|
2045
|
+
answers: Object.fromEntries(
|
|
2046
|
+
Object.entries(answers).map(([questionId, values]) => [
|
|
2047
|
+
questionId,
|
|
2048
|
+
{ answers: values.map((value) => value.trim()).filter(Boolean) },
|
|
2049
|
+
]),
|
|
2050
|
+
),
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1460
2054
|
function formatPermissionResponse(
|
|
1461
2055
|
source: string | undefined,
|
|
1462
2056
|
outcome: "allow" | "deny" | "cancelled",
|