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
|
@@ -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
|
}
|
|
@@ -281,21 +656,9 @@ function providerLabel(provider: AgentProvider): string {
|
|
|
281
656
|
return "Custom";
|
|
282
657
|
}
|
|
283
658
|
|
|
284
|
-
function providerSetupReason(provider: AgentProvider, activeProvider: AgentProvider, error?: string): string {
|
|
285
|
-
if (provider === activeProvider) {
|
|
286
|
-
return error ?? `${providerLabel(provider)} Agent 正在初始化或不可用。`;
|
|
287
|
-
}
|
|
288
|
-
if (provider === "codex") {
|
|
289
|
-
return `当前 CLI 启用的是 ${providerLabel(activeProvider)} Agent。`;
|
|
290
|
-
}
|
|
291
|
-
if (provider === "claude") {
|
|
292
|
-
return "Claude ACP adapter 尚未启用,请用 --agent-provider claude --agent-command 配置。";
|
|
293
|
-
}
|
|
294
|
-
return "Custom Agent 需要用 --agent-provider custom --agent-command 配置后才能使用。";
|
|
295
|
-
}
|
|
296
|
-
|
|
297
659
|
export class AgentWorkspaceProxy {
|
|
298
|
-
private
|
|
660
|
+
private clients = new Map<AgentProvider, AcpClient>();
|
|
661
|
+
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
299
662
|
private initialized = false;
|
|
300
663
|
private status: AgentStatus = "unavailable";
|
|
301
664
|
private error: string | undefined;
|
|
@@ -308,14 +671,15 @@ export class AgentWorkspaceProxy {
|
|
|
308
671
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
309
672
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
310
673
|
private permissionSources = new Map<string, string>();
|
|
674
|
+
private pendingStructuredInputs = new Map<string, { conversationId: string; input: AgentStructuredInput }>();
|
|
675
|
+
private structuredInputWaiters = new Map<string, PendingStructuredInputWaiter>();
|
|
311
676
|
private toolConversationIds = new Map<string, string>();
|
|
312
|
-
private agentProtocol: AgentProtocol | undefined;
|
|
313
677
|
|
|
314
678
|
constructor(
|
|
315
679
|
private readonly input: {
|
|
316
680
|
sessionId: string;
|
|
317
681
|
cwd: string;
|
|
318
|
-
|
|
682
|
+
availableProviders: AgentProvider[];
|
|
319
683
|
command?: string;
|
|
320
684
|
send: (envelope: Envelope) => void;
|
|
321
685
|
verbose?: boolean;
|
|
@@ -359,7 +723,8 @@ export class AgentWorkspaceProxy {
|
|
|
359
723
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
360
724
|
const conversation = this.conversations.get(payload.conversationId);
|
|
361
725
|
this.cancelPendingPermissions(payload.conversationId);
|
|
362
|
-
this.
|
|
726
|
+
const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
727
|
+
cancelClient?.cancel({
|
|
363
728
|
sessionId: conversation?.agentSessionId,
|
|
364
729
|
turnId: this.currentTurnId,
|
|
365
730
|
});
|
|
@@ -373,93 +738,113 @@ export class AgentWorkspaceProxy {
|
|
|
373
738
|
this.respondPermission(payload);
|
|
374
739
|
break;
|
|
375
740
|
}
|
|
741
|
+
case "agent.v2.structured_input.respond": {
|
|
742
|
+
const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
|
|
743
|
+
this.respondStructuredInput(payload);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
376
746
|
}
|
|
377
747
|
}
|
|
378
748
|
|
|
379
749
|
stop(): void {
|
|
380
|
-
this.
|
|
381
|
-
|
|
750
|
+
for (const client of this.clients.values()) {
|
|
751
|
+
client.stop();
|
|
752
|
+
}
|
|
753
|
+
this.clients.clear();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private clientForProvider(provider: AgentProvider): AcpClient | undefined {
|
|
757
|
+
return this.clients.get(provider);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
|
|
761
|
+
return this.agentProtocols.get(provider);
|
|
382
762
|
}
|
|
383
763
|
|
|
384
764
|
private async initialize(): Promise<void> {
|
|
385
765
|
if (this.initialized) return;
|
|
386
|
-
|
|
766
|
+
// trigger capability report immediately, lazy-start providers on first use
|
|
767
|
+
this.initialized = true;
|
|
768
|
+
this.status = "idle";
|
|
769
|
+
this.error = undefined;
|
|
770
|
+
this.sendCapabilities();
|
|
387
771
|
}
|
|
388
772
|
|
|
389
|
-
private async
|
|
390
|
-
|
|
773
|
+
private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | undefined> {
|
|
774
|
+
const existing = this.clients.get(provider);
|
|
775
|
+
if (existing) return existing;
|
|
391
776
|
|
|
392
777
|
const resolved = resolveAgentCommand({
|
|
393
|
-
provider
|
|
778
|
+
provider,
|
|
394
779
|
command: this.input.command,
|
|
395
780
|
});
|
|
396
781
|
if (!resolved) {
|
|
397
|
-
this.
|
|
398
|
-
|
|
399
|
-
|
|
782
|
+
if (this.input.verbose) {
|
|
783
|
+
process.stderr.write(`[agent:v2] no command for provider ${provider}\n`);
|
|
784
|
+
}
|
|
785
|
+
return undefined;
|
|
400
786
|
}
|
|
401
787
|
|
|
402
788
|
try {
|
|
403
|
-
this.
|
|
404
|
-
|
|
789
|
+
this.agentProtocols.set(provider, resolved.protocol);
|
|
790
|
+
const client = new AcpClient({
|
|
405
791
|
command: resolved.command,
|
|
406
792
|
protocol: resolved.protocol,
|
|
407
793
|
framing: resolved.framing,
|
|
408
794
|
cwd: this.input.cwd,
|
|
409
795
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
410
796
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
411
|
-
onExit: (message) => this.
|
|
797
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
412
798
|
});
|
|
413
|
-
await
|
|
414
|
-
this.
|
|
799
|
+
await client.initialize();
|
|
800
|
+
this.clients.set(provider, client);
|
|
415
801
|
this.status = "idle";
|
|
416
802
|
this.error = undefined;
|
|
803
|
+
this.sendCapabilities();
|
|
804
|
+
return client;
|
|
417
805
|
} catch (error) {
|
|
418
|
-
this.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
806
|
+
if (this.input.verbose) {
|
|
807
|
+
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
422
810
|
}
|
|
423
811
|
}
|
|
424
812
|
|
|
425
813
|
private sendCapabilities(): void {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
814
|
+
const providers = this.input.availableProviders.map((provider) => {
|
|
815
|
+
const client = this.clients.get(provider);
|
|
816
|
+
const protocol = this.agentProtocols.get(provider);
|
|
817
|
+
const enabled = Boolean(client);
|
|
818
|
+
const supportsImages = enabled && protocol === "codex-app-server";
|
|
819
|
+
return {
|
|
820
|
+
id: provider,
|
|
821
|
+
label: providerLabel(provider),
|
|
822
|
+
enabled,
|
|
823
|
+
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
824
|
+
supportsImages,
|
|
825
|
+
supportsPermission: enabled,
|
|
826
|
+
supportsPlan: enabled,
|
|
827
|
+
supportsCancel: enabled,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
const anyEnabled = providers.some((p) => p.enabled);
|
|
431
831
|
this.input.send(createEnvelope({
|
|
432
832
|
type: "agent.v2.capabilities",
|
|
433
833
|
sessionId: this.input.sessionId,
|
|
434
834
|
payload: {
|
|
435
|
-
enabled,
|
|
436
|
-
provider:
|
|
437
|
-
providers
|
|
438
|
-
const isActive = provider === activeProvider;
|
|
439
|
-
const canUse = isActive && enabled;
|
|
440
|
-
return {
|
|
441
|
-
id: provider,
|
|
442
|
-
label: providerLabel(provider),
|
|
443
|
-
enabled: canUse,
|
|
444
|
-
reason: canUse
|
|
445
|
-
? undefined
|
|
446
|
-
: providerSetupReason(provider, activeProvider, isActive ? this.error : undefined),
|
|
447
|
-
supportsImages: canUse && supportsImages,
|
|
448
|
-
supportsPermission: canUse,
|
|
449
|
-
supportsPlan: canUse,
|
|
450
|
-
supportsCancel: canUse,
|
|
451
|
-
};
|
|
452
|
-
}),
|
|
835
|
+
enabled: anyEnabled,
|
|
836
|
+
provider: this.input.availableProviders[0] ?? "codex",
|
|
837
|
+
providers,
|
|
453
838
|
protocolVersion: 1,
|
|
454
839
|
workspaceProtocolVersion: 2,
|
|
455
|
-
error:
|
|
456
|
-
supportsSessionList:
|
|
457
|
-
supportsSessionLoad:
|
|
458
|
-
supportsImages,
|
|
840
|
+
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
841
|
+
supportsSessionList: anyEnabled,
|
|
842
|
+
supportsSessionLoad: anyEnabled,
|
|
843
|
+
supportsImages: providers.some((p) => p.supportsImages),
|
|
459
844
|
supportsAudio: false,
|
|
460
|
-
supportsPermission:
|
|
461
|
-
supportsPlan:
|
|
462
|
-
supportsCancel:
|
|
845
|
+
supportsPermission: anyEnabled,
|
|
846
|
+
supportsPlan: anyEnabled,
|
|
847
|
+
supportsCancel: anyEnabled,
|
|
463
848
|
},
|
|
464
849
|
}));
|
|
465
850
|
}
|
|
@@ -474,18 +859,22 @@ export class AgentWorkspaceProxy {
|
|
|
474
859
|
permissionMode?: AgentPermissionMode;
|
|
475
860
|
title?: string;
|
|
476
861
|
}): Promise<AgentConversation | undefined> {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
862
|
+
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
863
|
+
if (!provider) {
|
|
864
|
+
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
865
|
+
}
|
|
866
|
+
if (!this.input.availableProviders.includes(provider)) {
|
|
480
867
|
return this.openFailure(
|
|
481
868
|
payload,
|
|
482
|
-
|
|
869
|
+
`${providerLabel(provider)} 未安装或不可用。`,
|
|
483
870
|
);
|
|
484
871
|
}
|
|
485
|
-
|
|
872
|
+
|
|
873
|
+
const client = await this.ensureProviderClient(provider);
|
|
874
|
+
if (!client) {
|
|
486
875
|
return this.openFailure(
|
|
487
876
|
payload,
|
|
488
|
-
|
|
877
|
+
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
489
878
|
);
|
|
490
879
|
}
|
|
491
880
|
|
|
@@ -513,8 +902,8 @@ export class AgentWorkspaceProxy {
|
|
|
513
902
|
|
|
514
903
|
try {
|
|
515
904
|
const result = agentSessionId
|
|
516
|
-
? await
|
|
517
|
-
: await
|
|
905
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
906
|
+
: await client.newSession({ cwd });
|
|
518
907
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
519
908
|
const now = Date.now();
|
|
520
909
|
const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
|
|
@@ -522,7 +911,7 @@ export class AgentWorkspaceProxy {
|
|
|
522
911
|
...existingConversation,
|
|
523
912
|
id: conversationId,
|
|
524
913
|
agentSessionId,
|
|
525
|
-
provider
|
|
914
|
+
provider,
|
|
526
915
|
cwd,
|
|
527
916
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
528
917
|
model: payload.model ?? existingConversation?.model,
|
|
@@ -567,7 +956,7 @@ export class AgentWorkspaceProxy {
|
|
|
567
956
|
const now = Date.now();
|
|
568
957
|
const conversation: AgentConversation = {
|
|
569
958
|
id: fallbackId,
|
|
570
|
-
provider: payload.provider ?? this.input.
|
|
959
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
571
960
|
cwd,
|
|
572
961
|
title: payload.title ?? titleFromCwd(cwd),
|
|
573
962
|
model: payload.model,
|
|
@@ -607,9 +996,12 @@ export class AgentWorkspaceProxy {
|
|
|
607
996
|
const conversation =
|
|
608
997
|
this.conversations.get(payload.conversationId) ??
|
|
609
998
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
610
|
-
if (!conversation || !
|
|
999
|
+
if (!conversation || !conversation.agentSessionId) return;
|
|
1000
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1001
|
+
if (!client) return;
|
|
611
1002
|
|
|
612
|
-
|
|
1003
|
+
const protocol = this.protocolForProvider(conversation.provider);
|
|
1004
|
+
if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
|
|
613
1005
|
conversation.status = "idle";
|
|
614
1006
|
conversation.lastActivityAt = Date.now();
|
|
615
1007
|
this.emitConversation(conversation);
|
|
@@ -643,7 +1035,7 @@ export class AgentWorkspaceProxy {
|
|
|
643
1035
|
this.emitConversation(conversation);
|
|
644
1036
|
|
|
645
1037
|
try {
|
|
646
|
-
const result = await
|
|
1038
|
+
const result = await client.prompt({
|
|
647
1039
|
sessionId: conversation.agentSessionId,
|
|
648
1040
|
content: payload.contentBlocks,
|
|
649
1041
|
clientMessageId: payload.clientMessageId,
|
|
@@ -670,6 +1062,9 @@ export class AgentWorkspaceProxy {
|
|
|
670
1062
|
}
|
|
671
1063
|
|
|
672
1064
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
1065
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1066
|
+
return this.handleStructuredInput(params, true);
|
|
1067
|
+
}
|
|
673
1068
|
if (isPermissionRequestMethod(method)) {
|
|
674
1069
|
return this.handlePermission(params, true, method);
|
|
675
1070
|
}
|
|
@@ -696,6 +1091,10 @@ export class AgentWorkspaceProxy {
|
|
|
696
1091
|
}
|
|
697
1092
|
|
|
698
1093
|
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
1094
|
+
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1095
|
+
this.handleStructuredInput(params);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
699
1098
|
if (isPermissionRequestMethod(method)) {
|
|
700
1099
|
this.handlePermission(params, false, method);
|
|
701
1100
|
return;
|
|
@@ -845,14 +1244,20 @@ export class AgentWorkspaceProxy {
|
|
|
845
1244
|
const item = extractItem(params);
|
|
846
1245
|
if (!item) return;
|
|
847
1246
|
const itemType = firstString(item, ["type"]);
|
|
848
|
-
|
|
1247
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1248
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
849
1249
|
this.handleCompletedMessageItem(item, true);
|
|
850
1250
|
return;
|
|
851
1251
|
}
|
|
852
|
-
if (
|
|
1252
|
+
if (normalizedItemType === "plan") {
|
|
853
1253
|
this.handlePlanUpdated({ plan: [item] });
|
|
854
1254
|
return;
|
|
855
1255
|
}
|
|
1256
|
+
if (isSubagentItemType(itemType)) {
|
|
1257
|
+
this.handleSubagentItem(item, "running", true);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (this.handleSemanticSystemItem(item, "running", true)) return;
|
|
856
1261
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
857
1262
|
const toolCall = this.toolCallFromItem(item, "running");
|
|
858
1263
|
if (!conversationId || !toolCall) return;
|
|
@@ -864,10 +1269,20 @@ export class AgentWorkspaceProxy {
|
|
|
864
1269
|
const item = extractItem(params);
|
|
865
1270
|
if (!item) return;
|
|
866
1271
|
const itemType = firstString(item, ["type"]);
|
|
867
|
-
|
|
1272
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1273
|
+
if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
|
|
868
1274
|
this.handleCompletedMessageItem(item, false);
|
|
869
1275
|
return;
|
|
870
1276
|
}
|
|
1277
|
+
if (normalizedItemType === "plan") {
|
|
1278
|
+
this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (isSubagentItemType(itemType)) {
|
|
1282
|
+
this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false)) return;
|
|
871
1286
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
872
1287
|
const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
|
|
873
1288
|
if (!conversationId || !toolCall) return;
|
|
@@ -1029,6 +1444,127 @@ export class AgentWorkspaceProxy {
|
|
|
1029
1444
|
this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
|
|
1030
1445
|
}
|
|
1031
1446
|
|
|
1447
|
+
private handleSemanticSystemItem(
|
|
1448
|
+
item: Record<string, unknown>,
|
|
1449
|
+
status: AgentToolCall["status"],
|
|
1450
|
+
streaming: boolean,
|
|
1451
|
+
): boolean {
|
|
1452
|
+
const itemType = firstString(item, ["type"]);
|
|
1453
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1454
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1455
|
+
if (!conversationId) return false;
|
|
1456
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
|
|
1457
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1458
|
+
const base = {
|
|
1459
|
+
id: itemId,
|
|
1460
|
+
conversationId,
|
|
1461
|
+
type: "status" as const,
|
|
1462
|
+
role: "system" as const,
|
|
1463
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1464
|
+
itemId,
|
|
1465
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1466
|
+
updatedAt: Date.now(),
|
|
1467
|
+
isStreaming: streaming,
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1471
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1472
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1473
|
+
this.upsertItem(conversationId, {
|
|
1474
|
+
...base,
|
|
1475
|
+
kind: "thinking",
|
|
1476
|
+
text: text ?? (streaming ? "正在思考" : "完成思考"),
|
|
1477
|
+
});
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (normalized === "enteredreviewmode") {
|
|
1482
|
+
const target = firstString(item, ["review", "target", "label"]) ?? "changes";
|
|
1483
|
+
this.upsertItem(conversationId, {
|
|
1484
|
+
...base,
|
|
1485
|
+
kind: "review",
|
|
1486
|
+
text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
|
|
1487
|
+
});
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (normalized === "contextcompaction") {
|
|
1492
|
+
this.upsertItem(conversationId, {
|
|
1493
|
+
...base,
|
|
1494
|
+
kind: "context_compaction",
|
|
1495
|
+
text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
|
|
1496
|
+
});
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
private handleSubagentItem(
|
|
1504
|
+
item: Record<string, unknown>,
|
|
1505
|
+
status: AgentToolCall["status"],
|
|
1506
|
+
streaming: boolean,
|
|
1507
|
+
): void {
|
|
1508
|
+
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1509
|
+
if (!conversationId) return;
|
|
1510
|
+
const subagent = decodeSubagentAction(item, status);
|
|
1511
|
+
if (!subagent) return;
|
|
1512
|
+
const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
|
|
1513
|
+
const text = summarizeSubagentAction(subagent);
|
|
1514
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1515
|
+
this.upsertItem(conversationId, {
|
|
1516
|
+
id: itemId,
|
|
1517
|
+
conversationId,
|
|
1518
|
+
type: "status",
|
|
1519
|
+
kind: "subagent_action",
|
|
1520
|
+
role: "system",
|
|
1521
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnId,
|
|
1522
|
+
itemId,
|
|
1523
|
+
text,
|
|
1524
|
+
subagent,
|
|
1525
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1526
|
+
updatedAt: Date.now(),
|
|
1527
|
+
isStreaming: streaming,
|
|
1528
|
+
});
|
|
1529
|
+
this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
|
|
1533
|
+
const raw = asRecord(params) ?? {};
|
|
1534
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1535
|
+
if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1536
|
+
const structuredInput = decodeStructuredInput(raw);
|
|
1537
|
+
if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
|
|
1538
|
+
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1539
|
+
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1540
|
+
this.upsertItem(conversationId, {
|
|
1541
|
+
id: `input:${structuredInput.requestId}`,
|
|
1542
|
+
conversationId,
|
|
1543
|
+
type: "status",
|
|
1544
|
+
kind: "user_input_prompt",
|
|
1545
|
+
role: "system",
|
|
1546
|
+
text,
|
|
1547
|
+
structuredInput,
|
|
1548
|
+
metadata: { inputPending: true },
|
|
1549
|
+
createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
|
|
1550
|
+
updatedAt: Date.now(),
|
|
1551
|
+
});
|
|
1552
|
+
this.updateConversationPreview(conversationId, "需要用户输入", "running");
|
|
1553
|
+
if (!waitForResponse) return;
|
|
1554
|
+
return new Promise((resolve) => {
|
|
1555
|
+
const timer = setTimeout(() => {
|
|
1556
|
+
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1557
|
+
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1558
|
+
resolve(formatStructuredInputResponse({}));
|
|
1559
|
+
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1560
|
+
inputPending: false,
|
|
1561
|
+
inputError: "等待用户输入超时",
|
|
1562
|
+
});
|
|
1563
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1564
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1032
1568
|
private toolCallFromItem(
|
|
1033
1569
|
item: Record<string, unknown>,
|
|
1034
1570
|
fallbackStatus: AgentToolCall["status"],
|
|
@@ -1036,13 +1572,14 @@ export class AgentWorkspaceProxy {
|
|
|
1036
1572
|
const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
|
|
1037
1573
|
if (!itemId) return undefined;
|
|
1038
1574
|
const itemType = firstString(item, ["type"]);
|
|
1575
|
+
const normalizedItemType = normalizedIdentifier(itemType);
|
|
1039
1576
|
const name = toolNameFromItem(item);
|
|
1040
1577
|
if (!name && !isToolItemType(itemType)) return undefined;
|
|
1041
1578
|
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
1042
1579
|
const rawOutput =
|
|
1043
1580
|
firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
1044
1581
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
1045
|
-
const output =
|
|
1582
|
+
const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
|
|
1046
1583
|
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
1047
1584
|
: rawOutput ?? bufferedOutput;
|
|
1048
1585
|
return {
|
|
@@ -1124,8 +1661,10 @@ export class AgentWorkspaceProxy {
|
|
|
1124
1661
|
));
|
|
1125
1662
|
this.permissionSources.delete(payload.requestId);
|
|
1126
1663
|
} else {
|
|
1127
|
-
this.
|
|
1128
|
-
|
|
1664
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
1665
|
+
const respondClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
1666
|
+
respondClient?.respondPermission({
|
|
1667
|
+
sessionId: conversation?.agentSessionId,
|
|
1129
1668
|
requestId: payload.requestId,
|
|
1130
1669
|
outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
|
|
1131
1670
|
optionId: selectedOptionId,
|
|
@@ -1134,6 +1673,41 @@ export class AgentWorkspaceProxy {
|
|
|
1134
1673
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
1135
1674
|
}
|
|
1136
1675
|
|
|
1676
|
+
private respondStructuredInput(payload: {
|
|
1677
|
+
conversationId: string;
|
|
1678
|
+
requestId: string;
|
|
1679
|
+
answers: Record<string, string[]>;
|
|
1680
|
+
}): void {
|
|
1681
|
+
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
1682
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
1683
|
+
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
1684
|
+
if (waiter) {
|
|
1685
|
+
clearTimeout(waiter.timer);
|
|
1686
|
+
this.structuredInputWaiters.delete(payload.requestId);
|
|
1687
|
+
waiter.resolve(formatStructuredInputResponse(payload.answers));
|
|
1688
|
+
}
|
|
1689
|
+
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1690
|
+
inputPending: false,
|
|
1691
|
+
inputSubmitted: true,
|
|
1692
|
+
answers: payload.answers,
|
|
1693
|
+
});
|
|
1694
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private markStructuredInput(
|
|
1698
|
+
conversationId: string,
|
|
1699
|
+
requestId: string,
|
|
1700
|
+
metadata: Record<string, unknown>,
|
|
1701
|
+
): void {
|
|
1702
|
+
const item = this.findItem(conversationId, `input:${requestId}`);
|
|
1703
|
+
if (!item) return;
|
|
1704
|
+
this.upsertItem(conversationId, {
|
|
1705
|
+
...item,
|
|
1706
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
1707
|
+
updatedAt: Date.now(),
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1137
1711
|
private addItem(conversationId: string, item: AgentTimelineItem): void {
|
|
1138
1712
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
1139
1713
|
timeline.push(item);
|
|
@@ -1173,11 +1747,20 @@ export class AgentWorkspaceProxy {
|
|
|
1173
1747
|
};
|
|
1174
1748
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
1175
1749
|
this.toolConversationIds.set(nextToolCall.id, conversationId);
|
|
1750
|
+
const kind: AgentTimelineKind = nextToolCall.name.includes("文件")
|
|
1751
|
+
? "file_change"
|
|
1752
|
+
: nextToolCall.name.includes("命令")
|
|
1753
|
+
? "command_execution"
|
|
1754
|
+
: "tool_activity";
|
|
1176
1755
|
this.upsertItem(conversationId, {
|
|
1177
1756
|
id: `tool:${nextToolCall.id}`,
|
|
1178
1757
|
conversationId,
|
|
1179
1758
|
type: "tool_call",
|
|
1759
|
+
kind,
|
|
1760
|
+
itemId: nextToolCall.id,
|
|
1180
1761
|
toolCall: nextToolCall,
|
|
1762
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
|
|
1763
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
|
|
1181
1764
|
createdAt: nextToolCall.createdAt ?? Date.now(),
|
|
1182
1765
|
updatedAt: Date.now(),
|
|
1183
1766
|
});
|
|
@@ -1348,12 +1931,12 @@ export class AgentWorkspaceProxy {
|
|
|
1348
1931
|
return undefined;
|
|
1349
1932
|
}
|
|
1350
1933
|
|
|
1351
|
-
private
|
|
1934
|
+
private handleProviderExit(provider: AgentProvider, message: string): void {
|
|
1935
|
+
this.clients.delete(provider);
|
|
1936
|
+
this.agentProtocols.delete(provider);
|
|
1352
1937
|
this.cancelPendingPermissions();
|
|
1353
|
-
this.status = "error";
|
|
1354
|
-
this.error = message;
|
|
1355
|
-
this.client = undefined;
|
|
1356
1938
|
for (const conversation of this.conversations.values()) {
|
|
1939
|
+
if (conversation.provider !== provider) continue;
|
|
1357
1940
|
conversation.status = "error";
|
|
1358
1941
|
conversation.lastMessagePreview = message;
|
|
1359
1942
|
conversation.lastActivityAt = Date.now();
|
|
@@ -1366,6 +1949,7 @@ export class AgentWorkspaceProxy {
|
|
|
1366
1949
|
createdAt: Date.now(),
|
|
1367
1950
|
});
|
|
1368
1951
|
}
|
|
1952
|
+
this.sendCapabilities();
|
|
1369
1953
|
}
|
|
1370
1954
|
|
|
1371
1955
|
private cancelPendingPermissions(conversationId?: string): void {
|
|
@@ -1380,6 +1964,19 @@ export class AgentWorkspaceProxy {
|
|
|
1380
1964
|
this.permissionSources.delete(requestId);
|
|
1381
1965
|
}
|
|
1382
1966
|
this.permissionWaiters.clear();
|
|
1967
|
+
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
1968
|
+
clearTimeout(waiter.timer);
|
|
1969
|
+
waiter.resolve(formatStructuredInputResponse({}));
|
|
1970
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
1971
|
+
if (pending) {
|
|
1972
|
+
this.markStructuredInput(pending.conversationId, requestId, {
|
|
1973
|
+
inputPending: false,
|
|
1974
|
+
inputError: "已停止",
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
this.pendingStructuredInputs.delete(requestId);
|
|
1978
|
+
}
|
|
1979
|
+
this.structuredInputWaiters.clear();
|
|
1383
1980
|
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
1384
1981
|
}
|
|
1385
1982
|
|
|
@@ -1452,11 +2049,21 @@ function isPermissionRequestMethod(method: string): boolean {
|
|
|
1452
2049
|
return (
|
|
1453
2050
|
method === "session/request_permission" ||
|
|
1454
2051
|
method.endsWith("/requestApproval") ||
|
|
1455
|
-
method === "mcpServer/elicitation/request"
|
|
1456
|
-
method === "item/tool/requestUserInput"
|
|
2052
|
+
method === "mcpServer/elicitation/request"
|
|
1457
2053
|
);
|
|
1458
2054
|
}
|
|
1459
2055
|
|
|
2056
|
+
function formatStructuredInputResponse(answers: Record<string, string[]>): unknown {
|
|
2057
|
+
return {
|
|
2058
|
+
answers: Object.fromEntries(
|
|
2059
|
+
Object.entries(answers).map(([questionId, values]) => [
|
|
2060
|
+
questionId,
|
|
2061
|
+
{ answers: values.map((value) => value.trim()).filter(Boolean) },
|
|
2062
|
+
]),
|
|
2063
|
+
),
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1460
2067
|
function formatPermissionResponse(
|
|
1461
2068
|
source: string | undefined,
|
|
1462
2069
|
outcome: "allow" | "deny" | "cancelled",
|