linkshell-cli 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
- package/dist/cli/src/runtime/acp/acp-client.js +40 -2
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +728 -98
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +69 -14
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
- package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
- package/dist/cli/src/runtime/acp-relay.js +73 -0
- package/dist/cli/src/runtime/acp-relay.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +5 -0
- package/dist/cli/src/runtime/bridge-session.js +104 -57
- 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 +2505 -2479
- package/dist/shared-protocol/src/index.js +46 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/runtime/acp/acp-client.ts +53 -2
- package/src/runtime/acp/agent-workspace.ts +784 -89
- package/src/runtime/acp/claude-sdk-client.ts +352 -31
- package/src/runtime/acp/claude-stream-json-client.ts +66 -16
- package/src/runtime/acp/json-rpc.ts +5 -0
- package/src/runtime/bridge-session.ts +114 -61
- package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
- package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
- package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +0 -1
|
@@ -201,6 +201,7 @@ interface PendingStructuredInputWaiter {
|
|
|
201
201
|
resolve: (value: unknown) => void;
|
|
202
202
|
timer: ReturnType<typeof setTimeout>;
|
|
203
203
|
source?: string;
|
|
204
|
+
input: AgentStructuredInput;
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -210,6 +211,17 @@ function id(prefix: string): string {
|
|
|
210
211
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
function normalizeAgentSessionIdSegment(value: string): string {
|
|
215
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function makeAgentV2RemoteConversationId(
|
|
219
|
+
provider: AgentProvider,
|
|
220
|
+
agentSessionId: string,
|
|
221
|
+
): string {
|
|
222
|
+
return `agent-remote-${provider}-${normalizeAgentSessionIdSegment(agentSessionId)}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
213
225
|
function stringify(value: unknown): string {
|
|
214
226
|
if (typeof value === "string") return value;
|
|
215
227
|
try {
|
|
@@ -264,6 +276,12 @@ function extractItem(value: unknown): Record<string, unknown> | undefined {
|
|
|
264
276
|
return asRecord(raw.item) ?? raw;
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
function extractTurn(value: unknown): Record<string, unknown> | undefined {
|
|
280
|
+
const raw = asRecord(value);
|
|
281
|
+
if (!raw) return undefined;
|
|
282
|
+
return asRecord(raw.turn) ?? raw;
|
|
283
|
+
}
|
|
284
|
+
|
|
267
285
|
function stringifyDefined(value: unknown): string | undefined {
|
|
268
286
|
if (value === undefined || value === null || value === "") return undefined;
|
|
269
287
|
return stringify(value);
|
|
@@ -302,6 +320,29 @@ function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
|
|
|
302
320
|
return "pending";
|
|
303
321
|
}
|
|
304
322
|
|
|
323
|
+
function agentStatusFromThreadStatus(value: unknown): AgentStatus | undefined {
|
|
324
|
+
const raw = asRecord(value);
|
|
325
|
+
const status = typeof value === "string"
|
|
326
|
+
? value
|
|
327
|
+
: firstString(raw, ["status", "state", "phase", "type"]) ??
|
|
328
|
+
firstString(asRecord(raw?.status), ["status", "state", "phase", "type"]);
|
|
329
|
+
const normalized = normalizedIdentifier(status);
|
|
330
|
+
if (!normalized) return undefined;
|
|
331
|
+
if (["active", "running", "inprogress", "busy", "working", "streaming"].includes(normalized)) {
|
|
332
|
+
return "running";
|
|
333
|
+
}
|
|
334
|
+
if (["idle", "ready", "completed", "complete", "done", "finished"].includes(normalized)) {
|
|
335
|
+
return "idle";
|
|
336
|
+
}
|
|
337
|
+
if (["waitingpermission", "waitingforpermission", "requirespermission", "needspermission"].includes(normalized)) {
|
|
338
|
+
return "waiting_permission";
|
|
339
|
+
}
|
|
340
|
+
if (["error", "failed", "systemerror", "fatal"].includes(normalized)) {
|
|
341
|
+
return "error";
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
305
346
|
function nameFromToolMethod(method: string): string {
|
|
306
347
|
if (method.includes("commandExecution")) return "命令";
|
|
307
348
|
if (method.includes("fileChange")) return "文件修改";
|
|
@@ -670,7 +711,9 @@ function parseStructuredInputQuestion(value: unknown, index: number): AgentStruc
|
|
|
670
711
|
question,
|
|
671
712
|
isOther: raw.isOther === true,
|
|
672
713
|
isSecret: raw.isSecret === true || raw.secret === true,
|
|
673
|
-
selectionLimit:
|
|
714
|
+
selectionLimit: raw.multiSelect === true
|
|
715
|
+
? options.length || undefined
|
|
716
|
+
: firstNumber(raw, ["selectionLimit", "maxSelections"]),
|
|
674
717
|
options: options.length > 0 ? options : undefined,
|
|
675
718
|
};
|
|
676
719
|
}
|
|
@@ -766,16 +809,12 @@ interface ProviderRuntimeCapabilities {
|
|
|
766
809
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
767
810
|
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
|
|
768
811
|
|
|
812
|
+
// Codex `model/list` is the source of truth — these are only used when the RPC
|
|
813
|
+
// returns nothing so mobile still sees a usable picker.
|
|
769
814
|
const CODEX_FALLBACK_MODELS: AgentModelOption[] = [
|
|
770
815
|
{ id: "default", label: "默认模型" },
|
|
771
|
-
{ id: "gpt-5", label: "GPT-5" },
|
|
772
|
-
{ id: "gpt-5-codex", label: "GPT-5 Codex" },
|
|
773
|
-
{ id: "gpt-5-pro", label: "GPT-5 Pro" },
|
|
774
|
-
{ id: "gpt-5-high", label: "GPT-5 High" },
|
|
775
|
-
{ id: "o3", label: "o3" },
|
|
776
|
-
{ id: "o3-mini", label: "o3-mini" },
|
|
777
816
|
];
|
|
778
|
-
const CODEX_FALLBACK_DEFAULT_MODEL = "
|
|
817
|
+
const CODEX_FALLBACK_DEFAULT_MODEL = "default";
|
|
779
818
|
const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
|
|
780
819
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
781
820
|
"add-dir",
|
|
@@ -1161,10 +1200,13 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1161
1200
|
const raw = asRecord(value);
|
|
1162
1201
|
const modelsValue =
|
|
1163
1202
|
Array.isArray(value) ? value :
|
|
1203
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1164
1204
|
Array.isArray(raw?.models) ? raw.models :
|
|
1165
1205
|
Array.isArray(raw?.items) ? raw.items :
|
|
1166
1206
|
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
1167
1207
|
[];
|
|
1208
|
+
let inferredDefault: string | undefined;
|
|
1209
|
+
const efforts = new Set<string>();
|
|
1168
1210
|
const models = modelsValue
|
|
1169
1211
|
.map((entry, index) => {
|
|
1170
1212
|
const model = asRecord(entry);
|
|
@@ -1173,24 +1215,44 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1173
1215
|
? { id: entry, label: entry }
|
|
1174
1216
|
: undefined;
|
|
1175
1217
|
}
|
|
1218
|
+
if (model.hidden === true) return undefined;
|
|
1176
1219
|
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
1177
|
-
const label = firstString(model, ["
|
|
1220
|
+
const label = firstString(model, ["displayName", "label", "title", "name"]) ?? modelId;
|
|
1221
|
+
if (model.isDefault === true && !inferredDefault) inferredDefault = modelId;
|
|
1222
|
+
const supported = Array.isArray(model.supportedReasoningEfforts) ? model.supportedReasoningEfforts : [];
|
|
1223
|
+
for (const item of supported) {
|
|
1224
|
+
const effort = typeof item === "string"
|
|
1225
|
+
? item
|
|
1226
|
+
: firstString(asRecord(item), ["reasoningEffort", "id", "value"]);
|
|
1227
|
+
if (effort && ALL_REASONING_EFFORTS.includes(effort as typeof ALL_REASONING_EFFORTS[number])) {
|
|
1228
|
+
efforts.add(effort);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1178
1231
|
return { id: modelId, label };
|
|
1179
1232
|
})
|
|
1180
1233
|
.filter((entry): entry is AgentModelOption => Boolean(entry));
|
|
1181
1234
|
const defaultModel =
|
|
1182
1235
|
firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
1183
|
-
firstString(asRecord(raw?.defaults), ["model"])
|
|
1184
|
-
|
|
1236
|
+
firstString(asRecord(raw?.defaults), ["model"]) ??
|
|
1237
|
+
inferredDefault;
|
|
1238
|
+
const explicitEffortsValue =
|
|
1185
1239
|
Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
1186
1240
|
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
1187
1241
|
Array.isArray(raw?.efforts) ? raw.efforts :
|
|
1188
1242
|
undefined;
|
|
1189
|
-
|
|
1190
|
-
|
|
1243
|
+
if (explicitEffortsValue) {
|
|
1244
|
+
for (const item of explicitEffortsValue) {
|
|
1245
|
+
if (typeof item === "string" && ALL_REASONING_EFFORTS.includes(item as typeof ALL_REASONING_EFFORTS[number])) {
|
|
1246
|
+
efforts.add(item);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const reasoningEfforts = efforts.size
|
|
1251
|
+
? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
|
|
1252
|
+
: undefined;
|
|
1191
1253
|
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
|
|
1192
1254
|
return {
|
|
1193
|
-
...(models.length > 0 ? { models
|
|
1255
|
+
...(models.length > 0 ? { models } : {}),
|
|
1194
1256
|
...(defaultModel ? { defaultModel } : {}),
|
|
1195
1257
|
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1196
1258
|
};
|
|
@@ -1218,6 +1280,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1218
1280
|
const raw = asRecord(value);
|
|
1219
1281
|
const sessionsValue =
|
|
1220
1282
|
Array.isArray(value) ? value :
|
|
1283
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1221
1284
|
Array.isArray(raw?.threads) ? raw.threads :
|
|
1222
1285
|
Array.isArray(raw?.sessions) ? raw.sessions :
|
|
1223
1286
|
Array.isArray(raw?.items) ? raw.items :
|
|
@@ -1243,7 +1306,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1243
1306
|
result.push({
|
|
1244
1307
|
id,
|
|
1245
1308
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
1246
|
-
title: firstString(source, ["title", "name", "summary"]),
|
|
1309
|
+
title: firstString(source, ["title", "name", "summary", "preview"]),
|
|
1247
1310
|
model: firstString(source, ["model", "modelId"]),
|
|
1248
1311
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1249
1312
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
@@ -1252,10 +1315,279 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1252
1315
|
return result;
|
|
1253
1316
|
}
|
|
1254
1317
|
|
|
1318
|
+
function threadFromProviderResult(value: unknown): Record<string, unknown> | undefined {
|
|
1319
|
+
const raw = asRecord(value);
|
|
1320
|
+
return asRecord(raw?.thread) ?? raw;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function threadFromTurnsListResult(sessionId: string, value: unknown): Record<string, unknown> | undefined {
|
|
1324
|
+
const raw = asRecord(value);
|
|
1325
|
+
const order = firstString(raw, ["sortDirection", "order", "sortOrder", "sort"]);
|
|
1326
|
+
const turns =
|
|
1327
|
+
Array.isArray(raw?.turns) ? raw.turns :
|
|
1328
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1329
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
1330
|
+
Array.isArray(value) ? value :
|
|
1331
|
+
undefined;
|
|
1332
|
+
if (!turns) return undefined;
|
|
1333
|
+
const extracted = turns
|
|
1334
|
+
.map(extractTurn)
|
|
1335
|
+
.filter((turn): turn is Record<string, unknown> => Boolean(turn));
|
|
1336
|
+
const chronological = order && /asc/i.test(order)
|
|
1337
|
+
? extracted
|
|
1338
|
+
: [...extracted].reverse();
|
|
1339
|
+
return { id: sessionId, turns: chronological };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function turnItemsFromThread(thread: Record<string, unknown> | undefined): Record<string, unknown>[] {
|
|
1343
|
+
if (!thread) return [];
|
|
1344
|
+
const turnsValue = Array.isArray(thread.turns)
|
|
1345
|
+
? thread.turns
|
|
1346
|
+
: Array.isArray(thread.items)
|
|
1347
|
+
? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items }]
|
|
1348
|
+
: [];
|
|
1349
|
+
const result: Record<string, unknown>[] = [];
|
|
1350
|
+
for (const turnEntry of turnsValue) {
|
|
1351
|
+
const turn = extractTurn(turnEntry);
|
|
1352
|
+
if (!turn) continue;
|
|
1353
|
+
const turnId = firstString(turn, ["id", "turnId"]);
|
|
1354
|
+
const createdAt = parseTimestamp(
|
|
1355
|
+
turn.createdAt ??
|
|
1356
|
+
turn.created_at ??
|
|
1357
|
+
turn.startedAt ??
|
|
1358
|
+
turn.started_at ??
|
|
1359
|
+
turn.completedAt ??
|
|
1360
|
+
turn.completed_at ??
|
|
1361
|
+
turn.updatedAt ??
|
|
1362
|
+
turn.updated_at,
|
|
1363
|
+
);
|
|
1364
|
+
const itemsRecord = asRecord(turn.items);
|
|
1365
|
+
const items = Array.isArray(turn.items)
|
|
1366
|
+
? turn.items
|
|
1367
|
+
: Array.isArray(itemsRecord?.data)
|
|
1368
|
+
? itemsRecord.data
|
|
1369
|
+
: Array.isArray(itemsRecord?.items)
|
|
1370
|
+
? itemsRecord.items
|
|
1371
|
+
: Array.isArray(turn.item)
|
|
1372
|
+
? turn.item
|
|
1373
|
+
: Array.isArray(turn.events)
|
|
1374
|
+
? turn.events
|
|
1375
|
+
: [];
|
|
1376
|
+
for (const itemEntry of items) {
|
|
1377
|
+
const item = extractItem(itemEntry);
|
|
1378
|
+
if (!item) continue;
|
|
1379
|
+
result.push({
|
|
1380
|
+
...item,
|
|
1381
|
+
...(turnId && !firstString(item, ["turnId"]) ? { turnId } : {}),
|
|
1382
|
+
__turnCreatedAt: createdAt,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function activeTurnIdFromThread(thread: Record<string, unknown> | undefined): string | undefined {
|
|
1390
|
+
if (!thread) return undefined;
|
|
1391
|
+
const turnsValue = Array.isArray(thread.turns)
|
|
1392
|
+
? thread.turns
|
|
1393
|
+
: Array.isArray(thread.items)
|
|
1394
|
+
? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items, status: thread.status }]
|
|
1395
|
+
: [];
|
|
1396
|
+
let activeTurnId: string | undefined;
|
|
1397
|
+
for (const turnEntry of turnsValue) {
|
|
1398
|
+
const turn = extractTurn(turnEntry);
|
|
1399
|
+
if (!turn) continue;
|
|
1400
|
+
const turnId = firstString(turn, ["id", "turnId"]);
|
|
1401
|
+
if (!turnId) continue;
|
|
1402
|
+
const status =
|
|
1403
|
+
firstString(turn, ["status", "state", "phase"]) ??
|
|
1404
|
+
firstString(asRecord(turn.status), ["status", "state", "phase", "type"]);
|
|
1405
|
+
const normalized = normalizedIdentifier(status);
|
|
1406
|
+
if ([
|
|
1407
|
+
"active",
|
|
1408
|
+
"running",
|
|
1409
|
+
"inprogress",
|
|
1410
|
+
"busy",
|
|
1411
|
+
"working",
|
|
1412
|
+
"streaming",
|
|
1413
|
+
"waitingpermission",
|
|
1414
|
+
"waitingforpermission",
|
|
1415
|
+
"requirespermission",
|
|
1416
|
+
"needspermission",
|
|
1417
|
+
].includes(normalized)) {
|
|
1418
|
+
activeTurnId = turnId;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return activeTurnId;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function timelineItemFromProviderItem(
|
|
1425
|
+
item: Record<string, unknown>,
|
|
1426
|
+
conversationId: string,
|
|
1427
|
+
index: number,
|
|
1428
|
+
): AgentTimelineItem | undefined {
|
|
1429
|
+
const itemType = firstString(item, ["type"]);
|
|
1430
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1431
|
+
const itemId = firstString(item, ["id", "itemId", "messageId", "toolCallId"]) ?? `history-${index + 1}`;
|
|
1432
|
+
const createdAt =
|
|
1433
|
+
parseTimestamp(item.createdAt ?? item.created_at ?? item.timestamp ?? item.__turnCreatedAt) ??
|
|
1434
|
+
Date.now() + index;
|
|
1435
|
+
const base = {
|
|
1436
|
+
id: itemId,
|
|
1437
|
+
conversationId,
|
|
1438
|
+
turnId: firstString(item, ["turnId"]),
|
|
1439
|
+
itemId,
|
|
1440
|
+
createdAt,
|
|
1441
|
+
updatedAt: parseTimestamp(item.updatedAt ?? item.updated_at) ?? createdAt,
|
|
1442
|
+
isStreaming: false,
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
if (
|
|
1446
|
+
normalized === "usermessage" ||
|
|
1447
|
+
normalized === "userinput" ||
|
|
1448
|
+
(normalized === "message" && item.role === "user")
|
|
1449
|
+
) {
|
|
1450
|
+
const content = contentBlocksFromItem(item);
|
|
1451
|
+
const text = textFromBlocks(content);
|
|
1452
|
+
if (!content.length && !text) return undefined;
|
|
1453
|
+
return {
|
|
1454
|
+
...base,
|
|
1455
|
+
type: "message",
|
|
1456
|
+
kind: "chat",
|
|
1457
|
+
role: "user",
|
|
1458
|
+
content,
|
|
1459
|
+
text,
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (
|
|
1464
|
+
normalized === "agentmessage" ||
|
|
1465
|
+
normalized === "assistantmessage" ||
|
|
1466
|
+
(normalized === "message" && item.role !== "user")
|
|
1467
|
+
) {
|
|
1468
|
+
const content = contentBlocksFromItem(item);
|
|
1469
|
+
const text = textFromBlocks(content);
|
|
1470
|
+
if (!content.length && !text) return undefined;
|
|
1471
|
+
return {
|
|
1472
|
+
...base,
|
|
1473
|
+
type: "message",
|
|
1474
|
+
kind: "chat",
|
|
1475
|
+
role: item.role === "system" ? "system" : "assistant",
|
|
1476
|
+
content,
|
|
1477
|
+
text,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1482
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1483
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1484
|
+
if (!text) return undefined;
|
|
1485
|
+
return {
|
|
1486
|
+
...base,
|
|
1487
|
+
type: "status",
|
|
1488
|
+
kind: "thinking",
|
|
1489
|
+
role: "system",
|
|
1490
|
+
text,
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
if (normalized === "enteredreviewmode" || normalized === "exitedreviewmode") {
|
|
1495
|
+
const text = firstString(item, ["review", "text", "content", "summary", "message"]);
|
|
1496
|
+
return {
|
|
1497
|
+
...base,
|
|
1498
|
+
type: "status",
|
|
1499
|
+
kind: "review",
|
|
1500
|
+
role: "system",
|
|
1501
|
+
text: text ?? (normalized === "enteredreviewmode" ? "正在审查" : "审查已完成"),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if (normalized === "contextcompaction") {
|
|
1506
|
+
return {
|
|
1507
|
+
...base,
|
|
1508
|
+
type: "status",
|
|
1509
|
+
kind: "context_compaction",
|
|
1510
|
+
role: "system",
|
|
1511
|
+
text: firstString(item, ["text", "summary", "message"]) ?? "上下文已压缩",
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (isSubagentItemType(itemType)) {
|
|
1516
|
+
const subagent = decodeSubagentAction(item, normalizeToolStatus(item.status, true));
|
|
1517
|
+
if (!subagent) return undefined;
|
|
1518
|
+
return {
|
|
1519
|
+
...base,
|
|
1520
|
+
type: "status",
|
|
1521
|
+
kind: "subagent_action",
|
|
1522
|
+
role: "system",
|
|
1523
|
+
text: summarizeSubagentAction(subagent),
|
|
1524
|
+
subagent,
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (isToolItemType(itemType) || toolNameFromItem(item)) {
|
|
1529
|
+
const toolId = firstString(item, ["id", "itemId", "toolCallId"]) ?? itemId;
|
|
1530
|
+
const toolCall: AgentToolCall = {
|
|
1531
|
+
id: toolId,
|
|
1532
|
+
name: toolNameFromItem(item) ?? "工具",
|
|
1533
|
+
input: toolInputFromItem(item),
|
|
1534
|
+
output:
|
|
1535
|
+
normalized === "filechange" || normalized === "diff"
|
|
1536
|
+
? extractDiffText(item) ?? firstString(item, ["output", "summary"])
|
|
1537
|
+
: firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
1538
|
+
stringifyDefined(item.result ?? item.error ?? item.contentItems),
|
|
1539
|
+
createdAt,
|
|
1540
|
+
status: normalizeToolStatus(item.status, true),
|
|
1541
|
+
};
|
|
1542
|
+
const kind: AgentTimelineKind = toolCall.name.includes("文件") || normalized === "filechange" || normalized === "diff"
|
|
1543
|
+
? "file_change"
|
|
1544
|
+
: toolCall.name.includes("命令") || normalized === "commandexecution"
|
|
1545
|
+
? "command_execution"
|
|
1546
|
+
: "tool_activity";
|
|
1547
|
+
return {
|
|
1548
|
+
...base,
|
|
1549
|
+
id: `tool:${toolId}`,
|
|
1550
|
+
type: "tool_call",
|
|
1551
|
+
kind,
|
|
1552
|
+
itemId: toolId,
|
|
1553
|
+
toolCall,
|
|
1554
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(toolCall) : undefined,
|
|
1555
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(toolCall) : undefined,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return undefined;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function timelineItemsFromProviderThread(
|
|
1563
|
+
value: unknown,
|
|
1564
|
+
conversationId: string,
|
|
1565
|
+
): AgentTimelineItem[] {
|
|
1566
|
+
const items = turnItemsFromThread(threadFromProviderResult(value))
|
|
1567
|
+
.map((item, index) => timelineItemFromProviderItem(item, conversationId, index))
|
|
1568
|
+
.filter((item): item is AgentTimelineItem => Boolean(item));
|
|
1569
|
+
const byId = new Map<string, AgentTimelineItem>();
|
|
1570
|
+
for (const item of items) {
|
|
1571
|
+
byId.set(item.id, item);
|
|
1572
|
+
}
|
|
1573
|
+
return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).slice(-MAX_TIMELINE_ITEMS);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function previewFromTimelineItem(item: AgentTimelineItem): string | undefined {
|
|
1577
|
+
if (item.error) return item.error;
|
|
1578
|
+
if (item.text) return item.text;
|
|
1579
|
+
if (item.content?.length) return textFromBlocks(item.content);
|
|
1580
|
+
if (item.toolCall) return `${item.toolCall.name} · ${item.toolCall.status}`;
|
|
1581
|
+
if (item.permission) return `需要授权 ${item.permission.toolName ?? ""}`.trim();
|
|
1582
|
+
if (item.subagent) return summarizeSubagentAction(item.subagent);
|
|
1583
|
+
return undefined;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1255
1586
|
export class AgentWorkspaceProxy {
|
|
1256
1587
|
private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
|
|
1257
1588
|
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
1258
1589
|
private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
|
|
1590
|
+
private providerErrors = new Map<AgentProvider, string>();
|
|
1259
1591
|
private initialized = false;
|
|
1260
1592
|
private status: AgentStatus = "unavailable";
|
|
1261
1593
|
private error: string | undefined;
|
|
@@ -1328,11 +1660,21 @@ export class AgentWorkspaceProxy {
|
|
|
1328
1660
|
case "agent.v2.cancel": {
|
|
1329
1661
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
1330
1662
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1663
|
+
if (!conversation) break;
|
|
1664
|
+
const turnId = this.currentTurnIds.get(payload.conversationId);
|
|
1665
|
+
if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
|
|
1666
|
+
this.rejectAgentAction(
|
|
1667
|
+
conversation,
|
|
1668
|
+
"无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。",
|
|
1669
|
+
conversation.status,
|
|
1670
|
+
);
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1331
1673
|
this.cancelPendingPermissions(payload.conversationId);
|
|
1332
|
-
const cancelClient =
|
|
1674
|
+
const cancelClient = this.clientForProvider(conversation.provider);
|
|
1333
1675
|
cancelClient?.cancel({
|
|
1334
|
-
sessionId: conversation
|
|
1335
|
-
turnId
|
|
1676
|
+
sessionId: conversation.agentSessionId,
|
|
1677
|
+
turnId,
|
|
1336
1678
|
});
|
|
1337
1679
|
this.forgetCurrentTurn(payload.conversationId);
|
|
1338
1680
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
@@ -1367,6 +1709,13 @@ export class AgentWorkspaceProxy {
|
|
|
1367
1709
|
return this.agentProtocols.get(provider);
|
|
1368
1710
|
}
|
|
1369
1711
|
|
|
1712
|
+
private defaultModelForProvider(provider: AgentProvider): string | undefined {
|
|
1713
|
+
const capabilities = this.providerCapabilities.get(provider);
|
|
1714
|
+
const defaultModel = capabilities?.defaultModel?.trim();
|
|
1715
|
+
if (defaultModel) return defaultModel;
|
|
1716
|
+
return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1370
1719
|
private async initialize(): Promise<void> {
|
|
1371
1720
|
if (this.initialized) return;
|
|
1372
1721
|
this.initialized = true;
|
|
@@ -1433,6 +1782,7 @@ export class AgentWorkspaceProxy {
|
|
|
1433
1782
|
try {
|
|
1434
1783
|
const client = await tryCreateClient(resolved);
|
|
1435
1784
|
this.clients.set(provider, client);
|
|
1785
|
+
this.providerErrors.delete(provider);
|
|
1436
1786
|
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
1437
1787
|
this.status = "idle";
|
|
1438
1788
|
this.error = undefined;
|
|
@@ -1452,6 +1802,7 @@ export class AgentWorkspaceProxy {
|
|
|
1452
1802
|
};
|
|
1453
1803
|
const client = await tryCreateClient(fallback);
|
|
1454
1804
|
this.clients.set(provider, client);
|
|
1805
|
+
this.providerErrors.delete(provider);
|
|
1455
1806
|
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
1456
1807
|
this.status = "idle";
|
|
1457
1808
|
this.error = undefined;
|
|
@@ -1466,6 +1817,8 @@ export class AgentWorkspaceProxy {
|
|
|
1466
1817
|
if (this.input.verbose) {
|
|
1467
1818
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1468
1819
|
}
|
|
1820
|
+
this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1821
|
+
this.sendCapabilities();
|
|
1469
1822
|
return undefined;
|
|
1470
1823
|
}
|
|
1471
1824
|
}
|
|
@@ -1518,7 +1871,7 @@ export class AgentWorkspaceProxy {
|
|
|
1518
1871
|
const agentSessionId = remote.id;
|
|
1519
1872
|
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1520
1873
|
const now = Date.now();
|
|
1521
|
-
const conversationId = existingId ??
|
|
1874
|
+
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1522
1875
|
const existing = this.conversations.get(conversationId);
|
|
1523
1876
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1524
1877
|
const conversation: AgentConversation = {
|
|
@@ -1549,6 +1902,83 @@ export class AgentWorkspaceProxy {
|
|
|
1549
1902
|
}
|
|
1550
1903
|
}
|
|
1551
1904
|
|
|
1905
|
+
private async hydrateConversationFromProvider(
|
|
1906
|
+
conversation: AgentConversation,
|
|
1907
|
+
seedResult?: unknown,
|
|
1908
|
+
): Promise<void> {
|
|
1909
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1910
|
+
if (!client || !conversation.agentSessionId) return;
|
|
1911
|
+
let source = seedResult;
|
|
1912
|
+
const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
|
|
1913
|
+
if (!hasSeedTurns) {
|
|
1914
|
+
const readSession = (client as { readSession?: (input: { sessionId: string; includeTurns?: boolean }) => Promise<unknown> }).readSession;
|
|
1915
|
+
if (typeof readSession === "function") {
|
|
1916
|
+
try {
|
|
1917
|
+
source = await readSession.call(client, {
|
|
1918
|
+
sessionId: conversation.agentSessionId,
|
|
1919
|
+
includeTurns: true,
|
|
1920
|
+
});
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (this.input.verbose) {
|
|
1923
|
+
process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
|
|
1928
|
+
const listTurns = (client as { listTurns?: (input: {
|
|
1929
|
+
sessionId: string;
|
|
1930
|
+
limit?: number;
|
|
1931
|
+
cursor?: string;
|
|
1932
|
+
sortDirection?: "asc" | "desc";
|
|
1933
|
+
itemsView?: "summary" | "full";
|
|
1934
|
+
}) => Promise<unknown> }).listTurns;
|
|
1935
|
+
if (typeof listTurns === "function") {
|
|
1936
|
+
try {
|
|
1937
|
+
const turnsResult = await listTurns.call(client, {
|
|
1938
|
+
sessionId: conversation.agentSessionId,
|
|
1939
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1940
|
+
sortDirection: "desc",
|
|
1941
|
+
itemsView: "full",
|
|
1942
|
+
});
|
|
1943
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
1944
|
+
if (turnsThread) source = { thread: turnsThread };
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
if (this.input.verbose) {
|
|
1947
|
+
process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const thread = threadFromProviderResult(source);
|
|
1955
|
+
const activeTurnId = activeTurnIdFromThread(thread);
|
|
1956
|
+
if (activeTurnId) this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
1957
|
+
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
1958
|
+
if (model && !conversation.model) conversation.model = model;
|
|
1959
|
+
|
|
1960
|
+
const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
1961
|
+
if (hydratedItems.length === 0) return;
|
|
1962
|
+
const existing = this.timelines.get(conversation.id) ?? [];
|
|
1963
|
+
const merged = new Map<string, AgentTimelineItem>();
|
|
1964
|
+
for (const item of [...existing, ...hydratedItems]) {
|
|
1965
|
+
merged.set(item.id, item);
|
|
1966
|
+
}
|
|
1967
|
+
const nextItems = [...merged.values()]
|
|
1968
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
1969
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
1970
|
+
this.timelines.set(conversation.id, nextItems);
|
|
1971
|
+
for (const item of nextItems) {
|
|
1972
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
1973
|
+
}
|
|
1974
|
+
const lastPreview = [...nextItems].reverse()
|
|
1975
|
+
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
1976
|
+
.find(Boolean);
|
|
1977
|
+
if (lastPreview && !conversation.lastMessagePreview) {
|
|
1978
|
+
conversation.lastMessagePreview = lastPreview;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1552
1982
|
private sendCapabilities(): void {
|
|
1553
1983
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1554
1984
|
const client = this.clients.get(provider);
|
|
@@ -1568,7 +1998,7 @@ export class AgentWorkspaceProxy {
|
|
|
1568
1998
|
id: provider,
|
|
1569
1999
|
label: providerLabel(provider),
|
|
1570
2000
|
enabled,
|
|
1571
|
-
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
2001
|
+
reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
|
|
1572
2002
|
supportsImages,
|
|
1573
2003
|
supportsPermission,
|
|
1574
2004
|
supportsPlan: enabled,
|
|
@@ -1640,24 +2070,63 @@ export class AgentWorkspaceProxy {
|
|
|
1640
2070
|
);
|
|
1641
2071
|
}
|
|
1642
2072
|
|
|
1643
|
-
|
|
1644
|
-
if (!client) {
|
|
1645
|
-
return this.openFailure(
|
|
1646
|
-
payload,
|
|
1647
|
-
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
1648
|
-
);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
const cwd = payload.cwd ?? this.input.cwd;
|
|
2073
|
+
let cwd = payload.cwd ?? this.input.cwd;
|
|
1652
2074
|
let agentSessionId = payload.agentSessionId;
|
|
1653
2075
|
let existingConversation =
|
|
1654
2076
|
(payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
1655
2077
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
2078
|
+
if (!payload.cwd && existingConversation?.cwd) cwd = existingConversation.cwd;
|
|
2079
|
+
|
|
2080
|
+
const client = await this.ensureProviderClient(provider);
|
|
2081
|
+
if (!client) {
|
|
2082
|
+
const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
|
|
2083
|
+
if (existingConversation) {
|
|
2084
|
+
this.activeConversationId = existingConversation.id;
|
|
2085
|
+
this.input.send(createEnvelope({
|
|
2086
|
+
type: "agent.v2.conversation.opened",
|
|
2087
|
+
sessionId: this.input.sessionId,
|
|
2088
|
+
payload: {
|
|
2089
|
+
conversation: existingConversation,
|
|
2090
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
2091
|
+
requestedConversationId: payload.conversationId,
|
|
2092
|
+
providerError: message,
|
|
2093
|
+
},
|
|
2094
|
+
}));
|
|
2095
|
+
this.sendCapabilities();
|
|
2096
|
+
return existingConversation;
|
|
2097
|
+
}
|
|
2098
|
+
if (payload.conversationId) {
|
|
2099
|
+
return this.openOfflineHistory(payload, message, cwd);
|
|
2100
|
+
}
|
|
2101
|
+
return this.openFailure(payload, message, cwd);
|
|
2102
|
+
}
|
|
1656
2103
|
|
|
1657
2104
|
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
1658
|
-
|
|
2105
|
+
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
2106
|
+
if (
|
|
2107
|
+
payload.conversationId &&
|
|
2108
|
+
payload.conversationId === requestedCanonicalId &&
|
|
2109
|
+
existingConversation.id !== payload.conversationId
|
|
2110
|
+
) {
|
|
1659
2111
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1660
2112
|
}
|
|
2113
|
+
if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
|
|
2114
|
+
try {
|
|
2115
|
+
const existingAgentSessionId = existingConversation.agentSessionId!;
|
|
2116
|
+
const result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
2117
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
2118
|
+
if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
|
|
2119
|
+
this.conversationByAgentSessionId.delete(existingAgentSessionId);
|
|
2120
|
+
existingConversation.agentSessionId = nextAgentSessionId;
|
|
2121
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
|
|
2122
|
+
}
|
|
2123
|
+
await this.hydrateConversationFromProvider(existingConversation, result);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
if (this.input.verbose) {
|
|
2126
|
+
process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
1661
2130
|
this.activeConversationId = existingConversation.id;
|
|
1662
2131
|
this.input.send(createEnvelope({
|
|
1663
2132
|
type: "agent.v2.conversation.opened",
|
|
@@ -1665,6 +2134,7 @@ export class AgentWorkspaceProxy {
|
|
|
1665
2134
|
payload: {
|
|
1666
2135
|
conversation: existingConversation,
|
|
1667
2136
|
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
2137
|
+
requestedConversationId: payload.conversationId,
|
|
1668
2138
|
},
|
|
1669
2139
|
}));
|
|
1670
2140
|
return existingConversation;
|
|
@@ -1676,7 +2146,7 @@ export class AgentWorkspaceProxy {
|
|
|
1676
2146
|
: await client.newSession({ cwd });
|
|
1677
2147
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
1678
2148
|
const now = Date.now();
|
|
1679
|
-
const conversationId =
|
|
2149
|
+
const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1680
2150
|
const conversation: AgentConversation = {
|
|
1681
2151
|
...existingConversation,
|
|
1682
2152
|
id: conversationId,
|
|
@@ -1698,10 +2168,15 @@ export class AgentWorkspaceProxy {
|
|
|
1698
2168
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1699
2169
|
this.activeConversationId = conversation.id;
|
|
1700
2170
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
2171
|
+
await this.hydrateConversationFromProvider(conversation, result);
|
|
1701
2172
|
this.input.send(createEnvelope({
|
|
1702
2173
|
type: "agent.v2.conversation.opened",
|
|
1703
2174
|
sessionId: this.input.sessionId,
|
|
1704
|
-
payload: {
|
|
2175
|
+
payload: {
|
|
2176
|
+
conversation,
|
|
2177
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2178
|
+
requestedConversationId: payload.conversationId,
|
|
2179
|
+
},
|
|
1705
2180
|
}));
|
|
1706
2181
|
return conversation;
|
|
1707
2182
|
} catch (error) {
|
|
@@ -1753,15 +2228,71 @@ export class AgentWorkspaceProxy {
|
|
|
1753
2228
|
this.input.send(createEnvelope({
|
|
1754
2229
|
type: "agent.v2.conversation.opened",
|
|
1755
2230
|
sessionId: this.input.sessionId,
|
|
1756
|
-
payload: {
|
|
2231
|
+
payload: {
|
|
2232
|
+
conversation,
|
|
2233
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2234
|
+
requestedConversationId: payload.conversationId,
|
|
2235
|
+
},
|
|
1757
2236
|
}));
|
|
1758
2237
|
return conversation;
|
|
1759
2238
|
}
|
|
1760
2239
|
|
|
2240
|
+
private openOfflineHistory(
|
|
2241
|
+
payload: {
|
|
2242
|
+
conversationId?: string;
|
|
2243
|
+
agentSessionId?: string;
|
|
2244
|
+
cwd?: string;
|
|
2245
|
+
provider?: AgentProvider;
|
|
2246
|
+
model?: string;
|
|
2247
|
+
reasoningEffort?: string;
|
|
2248
|
+
permissionMode?: AgentPermissionMode;
|
|
2249
|
+
collaborationMode?: AgentCollaborationMode;
|
|
2250
|
+
title?: string;
|
|
2251
|
+
},
|
|
2252
|
+
message: string,
|
|
2253
|
+
cwd = payload.cwd ?? this.input.cwd,
|
|
2254
|
+
): AgentConversation {
|
|
2255
|
+
const now = Date.now();
|
|
2256
|
+
const conversation: AgentConversation = {
|
|
2257
|
+
id: payload.conversationId!,
|
|
2258
|
+
agentSessionId: payload.agentSessionId,
|
|
2259
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
2260
|
+
cwd,
|
|
2261
|
+
title: payload.title ?? titleFromCwd(cwd),
|
|
2262
|
+
model: payload.model,
|
|
2263
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2264
|
+
permissionMode: payload.permissionMode,
|
|
2265
|
+
collaborationMode: payload.collaborationMode,
|
|
2266
|
+
status: "idle",
|
|
2267
|
+
archived: false,
|
|
2268
|
+
lastActivityAt: now,
|
|
2269
|
+
createdAt: now,
|
|
2270
|
+
};
|
|
2271
|
+
this.conversations.set(conversation.id, conversation);
|
|
2272
|
+
if (conversation.agentSessionId) {
|
|
2273
|
+
this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
|
|
2274
|
+
}
|
|
2275
|
+
this.activeConversationId = conversation.id;
|
|
2276
|
+
this.input.send(createEnvelope({
|
|
2277
|
+
type: "agent.v2.conversation.opened",
|
|
2278
|
+
sessionId: this.input.sessionId,
|
|
2279
|
+
payload: {
|
|
2280
|
+
conversation,
|
|
2281
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2282
|
+
requestedConversationId: payload.conversationId,
|
|
2283
|
+
providerError: message,
|
|
2284
|
+
},
|
|
2285
|
+
}));
|
|
2286
|
+
this.sendCapabilities();
|
|
2287
|
+
return conversation;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1761
2290
|
private async sendPrompt(payload: {
|
|
1762
2291
|
conversationId: string;
|
|
1763
2292
|
clientMessageId: string;
|
|
1764
2293
|
contentBlocks: AgentContentBlock[];
|
|
2294
|
+
delivery?: "auto" | "new_turn" | "steer";
|
|
2295
|
+
targetTurnId?: string;
|
|
1765
2296
|
model?: string;
|
|
1766
2297
|
reasoningEffort?: string;
|
|
1767
2298
|
permissionMode?: AgentPermissionMode;
|
|
@@ -1772,39 +2303,28 @@ export class AgentWorkspaceProxy {
|
|
|
1772
2303
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
1773
2304
|
if (!conversation) return;
|
|
1774
2305
|
if (!conversation.agentSessionId) {
|
|
1775
|
-
this.
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
1780
|
-
createdAt: Date.now(),
|
|
1781
|
-
});
|
|
2306
|
+
this.rejectAgentAction(
|
|
2307
|
+
conversation,
|
|
2308
|
+
"Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
2309
|
+
);
|
|
1782
2310
|
return;
|
|
1783
2311
|
}
|
|
1784
|
-
const client = this.clientForProvider(conversation.provider);
|
|
2312
|
+
const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
|
|
1785
2313
|
if (!client) {
|
|
1786
|
-
this.
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
createdAt: Date.now(),
|
|
1792
|
-
});
|
|
2314
|
+
this.rejectAgentAction(
|
|
2315
|
+
conversation,
|
|
2316
|
+
this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
|
|
2317
|
+
"error",
|
|
2318
|
+
);
|
|
1793
2319
|
return;
|
|
1794
2320
|
}
|
|
1795
2321
|
|
|
1796
2322
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
1797
2323
|
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
id: id("error"),
|
|
1803
|
-
conversationId: conversation.id,
|
|
1804
|
-
type: "error",
|
|
1805
|
-
error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
|
|
1806
|
-
createdAt: Date.now(),
|
|
1807
|
-
});
|
|
2324
|
+
this.rejectAgentAction(
|
|
2325
|
+
conversation,
|
|
2326
|
+
"当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
|
|
2327
|
+
);
|
|
1808
2328
|
return;
|
|
1809
2329
|
}
|
|
1810
2330
|
|
|
@@ -1813,7 +2333,35 @@ export class AgentWorkspaceProxy {
|
|
|
1813
2333
|
reasoningEffort: conversation.reasoningEffort,
|
|
1814
2334
|
permissionMode: conversation.permissionMode,
|
|
1815
2335
|
};
|
|
1816
|
-
|
|
2336
|
+
const effectiveModel = payload.model ??
|
|
2337
|
+
conversation.model ??
|
|
2338
|
+
(protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
|
|
2339
|
+
const wasRunning = conversation.status === "running";
|
|
2340
|
+
const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
|
|
2341
|
+
const shouldSteer = Boolean(
|
|
2342
|
+
payload.delivery !== "new_turn" &&
|
|
2343
|
+
wasRunning &&
|
|
2344
|
+
protocol === "codex-app-server" &&
|
|
2345
|
+
activeTurnId,
|
|
2346
|
+
);
|
|
2347
|
+
if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
|
|
2348
|
+
this.addItem(conversation.id, {
|
|
2349
|
+
id: id("error"),
|
|
2350
|
+
conversationId: conversation.id,
|
|
2351
|
+
type: "error",
|
|
2352
|
+
error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
|
|
2353
|
+
createdAt: Date.now(),
|
|
2354
|
+
});
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
|
|
2358
|
+
this.rejectAgentAction(
|
|
2359
|
+
conversation,
|
|
2360
|
+
"当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。",
|
|
2361
|
+
);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
conversation.model = effectiveModel ?? conversation.model;
|
|
1817
2365
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1818
2366
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1819
2367
|
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
@@ -1821,14 +2369,14 @@ export class AgentWorkspaceProxy {
|
|
|
1821
2369
|
conversation.lastActivityAt = Date.now();
|
|
1822
2370
|
this.activeConversationId = conversation.id;
|
|
1823
2371
|
|
|
1824
|
-
if (
|
|
2372
|
+
if (effectiveModel && effectiveModel !== priorSettings.model) {
|
|
1825
2373
|
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1826
|
-
const label = runtimeCapabilities?.models?.find((m) => m.id ===
|
|
2374
|
+
const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
|
|
1827
2375
|
this.emitNotice({
|
|
1828
2376
|
conversationId: conversation.id,
|
|
1829
2377
|
kind: "model_changed",
|
|
1830
2378
|
title: `已切换模型 · ${label}`,
|
|
1831
|
-
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${
|
|
2379
|
+
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
|
|
1832
2380
|
});
|
|
1833
2381
|
}
|
|
1834
2382
|
if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
|
|
@@ -1854,21 +2402,62 @@ export class AgentWorkspaceProxy {
|
|
|
1854
2402
|
role: "user",
|
|
1855
2403
|
content: payload.contentBlocks,
|
|
1856
2404
|
text: userText,
|
|
2405
|
+
metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
|
|
1857
2406
|
createdAt: Date.now(),
|
|
1858
2407
|
});
|
|
1859
2408
|
this.emitConversation(conversation);
|
|
1860
2409
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
sessionId: conversation.agentSessionId
|
|
2410
|
+
const promptAsNewTurn = () =>
|
|
2411
|
+
client.prompt({
|
|
2412
|
+
sessionId: conversation.agentSessionId!,
|
|
1864
2413
|
content: payload.contentBlocks,
|
|
1865
2414
|
clientMessageId: payload.clientMessageId,
|
|
1866
|
-
model:
|
|
2415
|
+
model: effectiveModel,
|
|
1867
2416
|
reasoningEffort: payload.reasoningEffort,
|
|
1868
2417
|
permissionMode: payload.permissionMode,
|
|
1869
2418
|
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1870
2419
|
cwd: conversation.cwd,
|
|
1871
2420
|
});
|
|
2421
|
+
|
|
2422
|
+
try {
|
|
2423
|
+
const steer = (client as { steer?: (input: { sessionId: string; turnId: string; content: AgentContentBlock[] }) => Promise<unknown> }).steer;
|
|
2424
|
+
let result: unknown;
|
|
2425
|
+
if (shouldSteer && typeof steer === "function") {
|
|
2426
|
+
try {
|
|
2427
|
+
result = await steer.call(client, {
|
|
2428
|
+
sessionId: conversation.agentSessionId,
|
|
2429
|
+
turnId: activeTurnId!,
|
|
2430
|
+
content: payload.contentBlocks,
|
|
2431
|
+
});
|
|
2432
|
+
} catch (steerError) {
|
|
2433
|
+
this.forgetCurrentTurn(conversation.id, activeTurnId);
|
|
2434
|
+
const userItem = this.findItem(conversation.id, payload.clientMessageId);
|
|
2435
|
+
if (userItem) {
|
|
2436
|
+
this.upsertItem(conversation.id, {
|
|
2437
|
+
...userItem,
|
|
2438
|
+
metadata: {
|
|
2439
|
+
...(userItem.metadata ?? {}),
|
|
2440
|
+
delivery: "new_turn",
|
|
2441
|
+
fallbackFrom: "steer",
|
|
2442
|
+
failedTargetTurnId: activeTurnId,
|
|
2443
|
+
},
|
|
2444
|
+
updatedAt: Date.now(),
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
this.addItem(conversation.id, {
|
|
2448
|
+
id: id("status"),
|
|
2449
|
+
conversationId: conversation.id,
|
|
2450
|
+
type: "status",
|
|
2451
|
+
role: "system",
|
|
2452
|
+
text: "Codex 未接收运行中追加输入,已改为发送新消息。",
|
|
2453
|
+
status: "running",
|
|
2454
|
+
createdAt: Date.now(),
|
|
2455
|
+
});
|
|
2456
|
+
result = await promptAsNewTurn();
|
|
2457
|
+
}
|
|
2458
|
+
} else {
|
|
2459
|
+
result = await promptAsNewTurn();
|
|
2460
|
+
}
|
|
1872
2461
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
1873
2462
|
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1874
2463
|
if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
@@ -2101,6 +2690,9 @@ export class AgentWorkspaceProxy {
|
|
|
2101
2690
|
}
|
|
2102
2691
|
|
|
2103
2692
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
2693
|
+
if (method === "claude/askUserQuestion") {
|
|
2694
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
2695
|
+
}
|
|
2104
2696
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2105
2697
|
return this.handleStructuredInput(params, true);
|
|
2106
2698
|
}
|
|
@@ -2148,7 +2740,6 @@ export class AgentWorkspaceProxy {
|
|
|
2148
2740
|
if (
|
|
2149
2741
|
method.startsWith("account/") ||
|
|
2150
2742
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2151
|
-
method === "thread/status/changed" ||
|
|
2152
2743
|
method === "thread/tokenUsage/updated" ||
|
|
2153
2744
|
method === "serverRequest/resolved" ||
|
|
2154
2745
|
method === "mcpServer/oauthLogin/completed"
|
|
@@ -2157,6 +2748,25 @@ export class AgentWorkspaceProxy {
|
|
|
2157
2748
|
}
|
|
2158
2749
|
|
|
2159
2750
|
const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
|
|
2751
|
+
if (method === "thread/status/changed") {
|
|
2752
|
+
const status = agentStatusFromThreadStatus(params);
|
|
2753
|
+
if (conversationId && status) {
|
|
2754
|
+
const raw = asRecord(params);
|
|
2755
|
+
const turnId = this.extractTurnId(raw);
|
|
2756
|
+
if (turnId && (status === "running" || status === "waiting_permission")) {
|
|
2757
|
+
this.rememberTurnConversationId(conversationId, turnId);
|
|
2758
|
+
}
|
|
2759
|
+
if (status === "idle" || status === "error") {
|
|
2760
|
+
this.forgetCurrentTurn(conversationId, turnId);
|
|
2761
|
+
}
|
|
2762
|
+
const message =
|
|
2763
|
+
status === "error"
|
|
2764
|
+
? firstString(raw, ["message", "error", "reason"])
|
|
2765
|
+
: undefined;
|
|
2766
|
+
this.updateConversationStatus(conversationId, status, message);
|
|
2767
|
+
}
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2160
2770
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2161
2771
|
this.handleStructuredInput(params);
|
|
2162
2772
|
return;
|
|
@@ -2666,9 +3276,8 @@ export class AgentWorkspaceProxy {
|
|
|
2666
3276
|
const raw = asRecord(params) ?? {};
|
|
2667
3277
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
2668
3278
|
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
2669
|
-
const formatResponse =
|
|
2670
|
-
|
|
2671
|
-
: formatStructuredInputResponse;
|
|
3279
|
+
const formatResponse = (answers: Record<string, string[]>, input?: AgentStructuredInput) =>
|
|
3280
|
+
formatStructuredInputResponseForSource(source, answers, input);
|
|
2672
3281
|
if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
2673
3282
|
const structuredInput = decodeStructuredInput(raw);
|
|
2674
3283
|
if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
@@ -2692,13 +3301,13 @@ export class AgentWorkspaceProxy {
|
|
|
2692
3301
|
const timer = setTimeout(() => {
|
|
2693
3302
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
2694
3303
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
2695
|
-
resolve(formatResponse({}));
|
|
3304
|
+
resolve(formatResponse({}, structuredInput));
|
|
2696
3305
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
2697
3306
|
inputPending: false,
|
|
2698
3307
|
inputError: "等待用户输入超时",
|
|
2699
3308
|
});
|
|
2700
3309
|
}, PERMISSION_TIMEOUT_MS);
|
|
2701
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
3310
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
|
|
2702
3311
|
});
|
|
2703
3312
|
}
|
|
2704
3313
|
|
|
@@ -2754,6 +3363,12 @@ export class AgentWorkspaceProxy {
|
|
|
2754
3363
|
conversationId,
|
|
2755
3364
|
type: "permission",
|
|
2756
3365
|
permission,
|
|
3366
|
+
metadata: {
|
|
3367
|
+
protocol: "v2",
|
|
3368
|
+
permissionLive: true,
|
|
3369
|
+
permissionExpired: false,
|
|
3370
|
+
permissionPending: false,
|
|
3371
|
+
},
|
|
2757
3372
|
createdAt: Date.now(),
|
|
2758
3373
|
updatedAt: Date.now(),
|
|
2759
3374
|
};
|
|
@@ -2784,10 +3399,13 @@ export class AgentWorkspaceProxy {
|
|
|
2784
3399
|
optionId?: string;
|
|
2785
3400
|
}): void {
|
|
2786
3401
|
const permission = this.pendingPermissions.get(payload.requestId);
|
|
3402
|
+
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
3403
|
+
const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
|
|
3404
|
+
const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
|
|
3405
|
+
if (!permission && !waiter && alreadyResolved) return;
|
|
2787
3406
|
this.pendingPermissions.delete(payload.requestId);
|
|
2788
3407
|
const selectedOptionId =
|
|
2789
3408
|
payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
2790
|
-
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
2791
3409
|
if (waiter) {
|
|
2792
3410
|
clearTimeout(waiter.timer);
|
|
2793
3411
|
this.permissionWaiters.delete(payload.requestId);
|
|
@@ -2813,7 +3431,9 @@ export class AgentWorkspaceProxy {
|
|
|
2813
3431
|
permissionError: undefined,
|
|
2814
3432
|
permissionPending: false,
|
|
2815
3433
|
});
|
|
2816
|
-
|
|
3434
|
+
if (permission || waiter || existingItem) {
|
|
3435
|
+
this.updateConversationStatus(payload.conversationId, "running");
|
|
3436
|
+
}
|
|
2817
3437
|
}
|
|
2818
3438
|
|
|
2819
3439
|
private respondStructuredInput(payload: {
|
|
@@ -2822,15 +3442,15 @@ export class AgentWorkspaceProxy {
|
|
|
2822
3442
|
answers: Record<string, string[]>;
|
|
2823
3443
|
}): void {
|
|
2824
3444
|
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
2825
|
-
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2826
3445
|
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
3446
|
+
const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
|
|
3447
|
+
const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
|
|
3448
|
+
if (!pending && !waiter && alreadySubmitted) return;
|
|
3449
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2827
3450
|
if (waiter) {
|
|
2828
3451
|
clearTimeout(waiter.timer);
|
|
2829
3452
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
2830
|
-
|
|
2831
|
-
? formatMcpElicitationResponse
|
|
2832
|
-
: formatStructuredInputResponse;
|
|
2833
|
-
waiter.resolve(formatResponse(payload.answers));
|
|
3453
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
|
|
2834
3454
|
}
|
|
2835
3455
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
2836
3456
|
inputPending: false,
|
|
@@ -2839,7 +3459,9 @@ export class AgentWorkspaceProxy {
|
|
|
2839
3459
|
inputError: undefined,
|
|
2840
3460
|
answers: payload.answers,
|
|
2841
3461
|
});
|
|
2842
|
-
|
|
3462
|
+
if (pending || waiter || existingItem) {
|
|
3463
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
3464
|
+
}
|
|
2843
3465
|
}
|
|
2844
3466
|
|
|
2845
3467
|
private markPermission(
|
|
@@ -3039,7 +3661,7 @@ export class AgentWorkspaceProxy {
|
|
|
3039
3661
|
this.input.send(createEnvelope({
|
|
3040
3662
|
type: "agent.v2.event",
|
|
3041
3663
|
sessionId: this.input.sessionId,
|
|
3042
|
-
payload: { conversationId, conversation, item },
|
|
3664
|
+
payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
|
|
3043
3665
|
}));
|
|
3044
3666
|
}
|
|
3045
3667
|
|
|
@@ -3047,7 +3669,7 @@ export class AgentWorkspaceProxy {
|
|
|
3047
3669
|
this.input.send(createEnvelope({
|
|
3048
3670
|
type: "agent.v2.event",
|
|
3049
3671
|
sessionId: this.input.sessionId,
|
|
3050
|
-
payload: { conversationId: conversation.id, conversation },
|
|
3672
|
+
payload: { conversationId: conversation.id, conversation: { ...conversation } },
|
|
3051
3673
|
}));
|
|
3052
3674
|
}
|
|
3053
3675
|
|
|
@@ -3082,6 +3704,21 @@ export class AgentWorkspaceProxy {
|
|
|
3082
3704
|
}));
|
|
3083
3705
|
}
|
|
3084
3706
|
|
|
3707
|
+
private rejectAgentAction(
|
|
3708
|
+
conversation: AgentConversation,
|
|
3709
|
+
message: string,
|
|
3710
|
+
status: AgentStatus = "idle",
|
|
3711
|
+
): void {
|
|
3712
|
+
this.updateConversationStatus(conversation.id, status, message);
|
|
3713
|
+
this.addItem(conversation.id, {
|
|
3714
|
+
id: id("error"),
|
|
3715
|
+
conversationId: conversation.id,
|
|
3716
|
+
type: "error",
|
|
3717
|
+
error: message,
|
|
3718
|
+
createdAt: Date.now(),
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3085
3722
|
private updateConversationPreview(
|
|
3086
3723
|
conversationId: string,
|
|
3087
3724
|
text: string,
|
|
@@ -3209,12 +3846,22 @@ export class AgentWorkspaceProxy {
|
|
|
3209
3846
|
private handleProviderExit(provider: AgentProvider, message: string): void {
|
|
3210
3847
|
this.clients.delete(provider);
|
|
3211
3848
|
this.agentProtocols.delete(provider);
|
|
3212
|
-
this.
|
|
3849
|
+
this.providerErrors.set(provider, message);
|
|
3850
|
+
for (const conversation of this.conversations.values()) {
|
|
3851
|
+
if (conversation.provider === provider && conversation.status === "waiting_permission") {
|
|
3852
|
+
this.cancelPendingPermissions(conversation.id, false);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3213
3855
|
for (const conversation of this.conversations.values()) {
|
|
3214
3856
|
if (conversation.provider !== provider) continue;
|
|
3857
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
|
|
3858
|
+
this.emitConversation(conversation);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3215
3861
|
conversation.status = "error";
|
|
3216
3862
|
conversation.lastMessagePreview = message;
|
|
3217
3863
|
conversation.lastActivityAt = Date.now();
|
|
3864
|
+
this.forgetCurrentTurn(conversation.id);
|
|
3218
3865
|
this.emitConversation(conversation);
|
|
3219
3866
|
this.addItem(conversation.id, {
|
|
3220
3867
|
id: id("error"),
|
|
@@ -3227,8 +3874,9 @@ export class AgentWorkspaceProxy {
|
|
|
3227
3874
|
this.sendCapabilities();
|
|
3228
3875
|
}
|
|
3229
3876
|
|
|
3230
|
-
private cancelPendingPermissions(conversationId?: string): void {
|
|
3877
|
+
private cancelPendingPermissions(conversationId?: string, updateStatus = true): void {
|
|
3231
3878
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3879
|
+
if (conversationId && this.itemConversationIds.get(requestId) !== conversationId) continue;
|
|
3232
3880
|
clearTimeout(waiter.timer);
|
|
3233
3881
|
waiter.resolve(formatPermissionResponse(
|
|
3234
3882
|
this.permissionSources.get(requestId),
|
|
@@ -3238,11 +3886,12 @@ export class AgentWorkspaceProxy {
|
|
|
3238
3886
|
this.pendingPermissions.delete(requestId);
|
|
3239
3887
|
this.permissionSources.delete(requestId);
|
|
3240
3888
|
}
|
|
3241
|
-
this.permissionWaiters.clear();
|
|
3889
|
+
if (!conversationId) this.permissionWaiters.clear();
|
|
3242
3890
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3243
|
-
clearTimeout(waiter.timer);
|
|
3244
|
-
waiter.resolve(formatStructuredInputResponse({}));
|
|
3245
3891
|
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3892
|
+
if (conversationId && pending?.conversationId !== conversationId) continue;
|
|
3893
|
+
clearTimeout(waiter.timer);
|
|
3894
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
|
|
3246
3895
|
if (pending) {
|
|
3247
3896
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3248
3897
|
inputPending: false,
|
|
@@ -3250,9 +3899,10 @@ export class AgentWorkspaceProxy {
|
|
|
3250
3899
|
});
|
|
3251
3900
|
}
|
|
3252
3901
|
this.pendingStructuredInputs.delete(requestId);
|
|
3902
|
+
this.structuredInputWaiters.delete(requestId);
|
|
3253
3903
|
}
|
|
3254
|
-
this.structuredInputWaiters.clear();
|
|
3255
|
-
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
3904
|
+
if (!conversationId) this.structuredInputWaiters.clear();
|
|
3905
|
+
if (conversationId && updateStatus) this.updateConversationStatus(conversationId, "idle");
|
|
3256
3906
|
}
|
|
3257
3907
|
|
|
3258
3908
|
private extractSessionId(value: unknown): string | undefined {
|
|
@@ -3340,6 +3990,37 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
|
|
|
3340
3990
|
};
|
|
3341
3991
|
}
|
|
3342
3992
|
|
|
3993
|
+
function formatClaudeAskUserQuestionResponse(
|
|
3994
|
+
answers: Record<string, string[]>,
|
|
3995
|
+
input?: AgentStructuredInput,
|
|
3996
|
+
): unknown {
|
|
3997
|
+
const questions = input?.questions ?? [];
|
|
3998
|
+
const byQuestionText: Record<string, string | string[]> = {};
|
|
3999
|
+
for (const question of questions) {
|
|
4000
|
+
const values = answers[question.id] ?? answers[question.question] ?? [];
|
|
4001
|
+
const cleaned = values.map((value) => value.trim()).filter(Boolean);
|
|
4002
|
+
if (cleaned.length === 0) continue;
|
|
4003
|
+
byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
|
|
4004
|
+
? cleaned
|
|
4005
|
+
: cleaned[0] ?? "";
|
|
4006
|
+
}
|
|
4007
|
+
return {
|
|
4008
|
+
behavior: "allow",
|
|
4009
|
+
updatedInput: {
|
|
4010
|
+
questions: questions.map((question) => ({
|
|
4011
|
+
question: question.question,
|
|
4012
|
+
header: question.header,
|
|
4013
|
+
options: question.options?.map((option) => ({
|
|
4014
|
+
label: option.label,
|
|
4015
|
+
description: option.description,
|
|
4016
|
+
})) ?? [],
|
|
4017
|
+
multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
|
|
4018
|
+
})),
|
|
4019
|
+
answers: byQuestionText,
|
|
4020
|
+
},
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
|
|
3343
4024
|
function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
|
|
3344
4025
|
const content = Object.fromEntries(
|
|
3345
4026
|
Object.entries(answers).map(([questionId, values]) => [
|
|
@@ -3354,6 +4035,20 @@ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknow
|
|
|
3354
4035
|
};
|
|
3355
4036
|
}
|
|
3356
4037
|
|
|
4038
|
+
function formatStructuredInputResponseForSource(
|
|
4039
|
+
source: string | undefined,
|
|
4040
|
+
answers: Record<string, string[]>,
|
|
4041
|
+
input?: AgentStructuredInput,
|
|
4042
|
+
): unknown {
|
|
4043
|
+
if (source === "mcpServer/elicitation/request") {
|
|
4044
|
+
return formatMcpElicitationResponse(answers);
|
|
4045
|
+
}
|
|
4046
|
+
if (source === "claude/askUserQuestion") {
|
|
4047
|
+
return formatClaudeAskUserQuestionResponse(answers, input);
|
|
4048
|
+
}
|
|
4049
|
+
return formatStructuredInputResponse(answers);
|
|
4050
|
+
}
|
|
4051
|
+
|
|
3357
4052
|
function formatPermissionResponse(
|
|
3358
4053
|
source: string | undefined,
|
|
3359
4054
|
outcome: "allow" | "deny" | "cancelled",
|