linkshell-cli 0.5.1 → 0.5.4
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 +23 -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 +81 -21
- 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 +14 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +845 -88
- 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.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +87 -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 +37 -0
- package/dist/cli/src/runtime/bridge-session.js +318 -69
- 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 +5436 -3886
- package/dist/shared-protocol/src/index.js +127 -13
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +29 -1
- package/src/runtime/acp/acp-client.ts +96 -21
- package/src/runtime/acp/agent-workspace.ts +916 -83
- package/src/runtime/acp/claude-sdk-client.ts +352 -31
- package/src/runtime/acp/claude-stream-json-client.ts +89 -16
- package/src/runtime/acp/json-rpc.ts +5 -0
- package/src/runtime/bridge-session.ts +343 -75
- 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
|
}
|
|
@@ -1237,6 +1280,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1237
1280
|
const raw = asRecord(value);
|
|
1238
1281
|
const sessionsValue =
|
|
1239
1282
|
Array.isArray(value) ? value :
|
|
1283
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1240
1284
|
Array.isArray(raw?.threads) ? raw.threads :
|
|
1241
1285
|
Array.isArray(raw?.sessions) ? raw.sessions :
|
|
1242
1286
|
Array.isArray(raw?.items) ? raw.items :
|
|
@@ -1262,7 +1306,7 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1262
1306
|
result.push({
|
|
1263
1307
|
id,
|
|
1264
1308
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
1265
|
-
title: firstString(source, ["title", "name", "summary"]),
|
|
1309
|
+
title: firstString(source, ["title", "name", "summary", "preview"]),
|
|
1266
1310
|
model: firstString(source, ["model", "modelId"]),
|
|
1267
1311
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1268
1312
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
@@ -1271,10 +1315,279 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1271
1315
|
return result;
|
|
1272
1316
|
}
|
|
1273
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
|
+
|
|
1274
1586
|
export class AgentWorkspaceProxy {
|
|
1275
1587
|
private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
|
|
1276
1588
|
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
1277
1589
|
private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
|
|
1590
|
+
private providerErrors = new Map<AgentProvider, string>();
|
|
1278
1591
|
private initialized = false;
|
|
1279
1592
|
private status: AgentStatus = "unavailable";
|
|
1280
1593
|
private error: string | undefined;
|
|
@@ -1284,6 +1597,9 @@ export class AgentWorkspaceProxy {
|
|
|
1284
1597
|
private conversations = new Map<string, AgentConversation>();
|
|
1285
1598
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
1286
1599
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
1600
|
+
// Opaque codex turns cursor per conversation, pointing at OLDER history to
|
|
1601
|
+
// page through (captured at hydration, advanced on each history.request).
|
|
1602
|
+
private historyCursors = new Map<string, string | undefined>();
|
|
1287
1603
|
private toolOutputBuffers = new Map<string, string>();
|
|
1288
1604
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
1289
1605
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
@@ -1334,6 +1650,11 @@ export class AgentWorkspaceProxy {
|
|
|
1334
1650
|
this.sendSnapshot(payload.conversationId);
|
|
1335
1651
|
break;
|
|
1336
1652
|
}
|
|
1653
|
+
case "agent.v2.history.request": {
|
|
1654
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1655
|
+
await this.loadOlderHistory(payload);
|
|
1656
|
+
break;
|
|
1657
|
+
}
|
|
1337
1658
|
case "agent.v2.prompt": {
|
|
1338
1659
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1339
1660
|
await this.sendPrompt(payload);
|
|
@@ -1347,11 +1668,21 @@ export class AgentWorkspaceProxy {
|
|
|
1347
1668
|
case "agent.v2.cancel": {
|
|
1348
1669
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
1349
1670
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1671
|
+
if (!conversation) break;
|
|
1672
|
+
const turnId = this.currentTurnIds.get(payload.conversationId);
|
|
1673
|
+
if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
|
|
1674
|
+
this.rejectAgentAction(
|
|
1675
|
+
conversation,
|
|
1676
|
+
"无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。",
|
|
1677
|
+
conversation.status,
|
|
1678
|
+
);
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1350
1681
|
this.cancelPendingPermissions(payload.conversationId);
|
|
1351
|
-
const cancelClient =
|
|
1682
|
+
const cancelClient = this.clientForProvider(conversation.provider);
|
|
1352
1683
|
cancelClient?.cancel({
|
|
1353
|
-
sessionId: conversation
|
|
1354
|
-
turnId
|
|
1684
|
+
sessionId: conversation.agentSessionId,
|
|
1685
|
+
turnId,
|
|
1355
1686
|
});
|
|
1356
1687
|
this.forgetCurrentTurn(payload.conversationId);
|
|
1357
1688
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
@@ -1382,10 +1713,39 @@ export class AgentWorkspaceProxy {
|
|
|
1382
1713
|
return this.clients.get(provider);
|
|
1383
1714
|
}
|
|
1384
1715
|
|
|
1716
|
+
private shouldRecycleProviderClient(provider: AgentProvider, error: unknown): boolean {
|
|
1717
|
+
if (provider !== "codex") return false;
|
|
1718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1719
|
+
return /ACP agent is not running|ACP request timed out|Transport channel closed|TokenRefreshFailed|Failed to parse server response|channel closed|EPIPE|ECONNRESET/i.test(message);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private async recycleProviderClient(
|
|
1723
|
+
provider: AgentProvider,
|
|
1724
|
+
error: unknown,
|
|
1725
|
+
): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
|
|
1726
|
+
if (!this.shouldRecycleProviderClient(provider, error)) return undefined;
|
|
1727
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1728
|
+
const existing = this.clients.get(provider);
|
|
1729
|
+
if (existing) {
|
|
1730
|
+
try { existing.stop(); } catch {}
|
|
1731
|
+
}
|
|
1732
|
+
this.clients.delete(provider);
|
|
1733
|
+
this.agentProtocols.delete(provider);
|
|
1734
|
+
this.providerErrors.set(provider, message);
|
|
1735
|
+
return this.ensureProviderClient(provider);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1385
1738
|
private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
|
|
1386
1739
|
return this.agentProtocols.get(provider);
|
|
1387
1740
|
}
|
|
1388
1741
|
|
|
1742
|
+
private defaultModelForProvider(provider: AgentProvider): string | undefined {
|
|
1743
|
+
const capabilities = this.providerCapabilities.get(provider);
|
|
1744
|
+
const defaultModel = capabilities?.defaultModel?.trim();
|
|
1745
|
+
if (defaultModel) return defaultModel;
|
|
1746
|
+
return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1389
1749
|
private async initialize(): Promise<void> {
|
|
1390
1750
|
if (this.initialized) return;
|
|
1391
1751
|
this.initialized = true;
|
|
@@ -1452,6 +1812,7 @@ export class AgentWorkspaceProxy {
|
|
|
1452
1812
|
try {
|
|
1453
1813
|
const client = await tryCreateClient(resolved);
|
|
1454
1814
|
this.clients.set(provider, client);
|
|
1815
|
+
this.providerErrors.delete(provider);
|
|
1455
1816
|
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
1456
1817
|
this.status = "idle";
|
|
1457
1818
|
this.error = undefined;
|
|
@@ -1471,6 +1832,7 @@ export class AgentWorkspaceProxy {
|
|
|
1471
1832
|
};
|
|
1472
1833
|
const client = await tryCreateClient(fallback);
|
|
1473
1834
|
this.clients.set(provider, client);
|
|
1835
|
+
this.providerErrors.delete(provider);
|
|
1474
1836
|
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
1475
1837
|
this.status = "idle";
|
|
1476
1838
|
this.error = undefined;
|
|
@@ -1485,6 +1847,8 @@ export class AgentWorkspaceProxy {
|
|
|
1485
1847
|
if (this.input.verbose) {
|
|
1486
1848
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1487
1849
|
}
|
|
1850
|
+
this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1851
|
+
this.sendCapabilities();
|
|
1488
1852
|
return undefined;
|
|
1489
1853
|
}
|
|
1490
1854
|
}
|
|
@@ -1532,12 +1896,21 @@ export class AgentWorkspaceProxy {
|
|
|
1532
1896
|
await this.initialize();
|
|
1533
1897
|
for (const [provider, client] of this.clients) {
|
|
1534
1898
|
try {
|
|
1535
|
-
|
|
1899
|
+
let activeClient = client;
|
|
1900
|
+
let result: unknown;
|
|
1901
|
+
try {
|
|
1902
|
+
result = await activeClient.listSessions();
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
1905
|
+
if (!recovered) throw error;
|
|
1906
|
+
activeClient = recovered;
|
|
1907
|
+
result = await activeClient.listSessions();
|
|
1908
|
+
}
|
|
1536
1909
|
for (const remote of parseRemoteSessions(result)) {
|
|
1537
1910
|
const agentSessionId = remote.id;
|
|
1538
1911
|
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1539
1912
|
const now = Date.now();
|
|
1540
|
-
const conversationId = existingId ??
|
|
1913
|
+
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1541
1914
|
const existing = this.conversations.get(conversationId);
|
|
1542
1915
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1543
1916
|
const conversation: AgentConversation = {
|
|
@@ -1568,6 +1941,104 @@ export class AgentWorkspaceProxy {
|
|
|
1568
1941
|
}
|
|
1569
1942
|
}
|
|
1570
1943
|
|
|
1944
|
+
private async hydrateConversationFromProvider(
|
|
1945
|
+
conversation: AgentConversation,
|
|
1946
|
+
seedResult?: unknown,
|
|
1947
|
+
): Promise<void> {
|
|
1948
|
+
let client = this.clientForProvider(conversation.provider);
|
|
1949
|
+
if (!client || !conversation.agentSessionId) return;
|
|
1950
|
+
let source = seedResult;
|
|
1951
|
+
const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
|
|
1952
|
+
if (!hasSeedTurns) {
|
|
1953
|
+
const readSession = (client as { readSession?: (input: { sessionId: string; includeTurns?: boolean }) => Promise<unknown> }).readSession;
|
|
1954
|
+
if (typeof readSession === "function") {
|
|
1955
|
+
try {
|
|
1956
|
+
source = await readSession.call(client, {
|
|
1957
|
+
sessionId: conversation.agentSessionId,
|
|
1958
|
+
includeTurns: true,
|
|
1959
|
+
});
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
const recovered = await this.recycleProviderClient(conversation.provider, error);
|
|
1962
|
+
if (recovered) client = recovered;
|
|
1963
|
+
if (this.input.verbose) {
|
|
1964
|
+
process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
|
|
1969
|
+
const listTurns = (client as { listTurns?: (input: {
|
|
1970
|
+
sessionId: string;
|
|
1971
|
+
limit?: number;
|
|
1972
|
+
cursor?: string;
|
|
1973
|
+
sortDirection?: "asc" | "desc";
|
|
1974
|
+
itemsView?: "summary" | "full";
|
|
1975
|
+
}) => Promise<unknown> }).listTurns;
|
|
1976
|
+
if (typeof listTurns === "function") {
|
|
1977
|
+
try {
|
|
1978
|
+
let turnsResult: unknown;
|
|
1979
|
+
try {
|
|
1980
|
+
turnsResult = await listTurns.call(client, {
|
|
1981
|
+
sessionId: conversation.agentSessionId,
|
|
1982
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1983
|
+
sortDirection: "desc",
|
|
1984
|
+
itemsView: "full",
|
|
1985
|
+
});
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
const recovered = await this.recycleProviderClient(conversation.provider, error);
|
|
1988
|
+
if (!recovered) throw error;
|
|
1989
|
+
client = recovered;
|
|
1990
|
+
const recoveredListTurns = (client as { listTurns?: typeof listTurns }).listTurns;
|
|
1991
|
+
if (typeof recoveredListTurns !== "function") throw error;
|
|
1992
|
+
turnsResult = await recoveredListTurns.call(client, {
|
|
1993
|
+
sessionId: conversation.agentSessionId,
|
|
1994
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1995
|
+
sortDirection: "desc",
|
|
1996
|
+
itemsView: "full",
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
2000
|
+
if (turnsThread) source = { thread: turnsThread };
|
|
2001
|
+
// Capture the opaque cursor pointing at OLDER turns so the client
|
|
2002
|
+
// can page further back via agent.v2.history.request.
|
|
2003
|
+
const olderCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
|
|
2004
|
+
this.historyCursors.set(conversation.id, olderCursor);
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
if (this.input.verbose) {
|
|
2007
|
+
process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const thread = threadFromProviderResult(source);
|
|
2015
|
+
const activeTurnId = activeTurnIdFromThread(thread);
|
|
2016
|
+
if (activeTurnId) this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
2017
|
+
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
2018
|
+
if (model && !conversation.model) conversation.model = model;
|
|
2019
|
+
|
|
2020
|
+
const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
2021
|
+
if (hydratedItems.length === 0) return;
|
|
2022
|
+
const existing = this.timelines.get(conversation.id) ?? [];
|
|
2023
|
+
const merged = new Map<string, AgentTimelineItem>();
|
|
2024
|
+
for (const item of [...existing, ...hydratedItems]) {
|
|
2025
|
+
merged.set(item.id, item);
|
|
2026
|
+
}
|
|
2027
|
+
const nextItems = [...merged.values()]
|
|
2028
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2029
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
2030
|
+
this.timelines.set(conversation.id, nextItems);
|
|
2031
|
+
for (const item of nextItems) {
|
|
2032
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
2033
|
+
}
|
|
2034
|
+
const lastPreview = [...nextItems].reverse()
|
|
2035
|
+
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
2036
|
+
.find(Boolean);
|
|
2037
|
+
if (lastPreview && !conversation.lastMessagePreview) {
|
|
2038
|
+
conversation.lastMessagePreview = lastPreview;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1571
2042
|
private sendCapabilities(): void {
|
|
1572
2043
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1573
2044
|
const client = this.clients.get(provider);
|
|
@@ -1587,7 +2058,7 @@ export class AgentWorkspaceProxy {
|
|
|
1587
2058
|
id: provider,
|
|
1588
2059
|
label: providerLabel(provider),
|
|
1589
2060
|
enabled,
|
|
1590
|
-
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
2061
|
+
reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
|
|
1591
2062
|
supportsImages,
|
|
1592
2063
|
supportsPermission,
|
|
1593
2064
|
supportsPlan: enabled,
|
|
@@ -1659,24 +2130,71 @@ export class AgentWorkspaceProxy {
|
|
|
1659
2130
|
);
|
|
1660
2131
|
}
|
|
1661
2132
|
|
|
1662
|
-
|
|
1663
|
-
if (!client) {
|
|
1664
|
-
return this.openFailure(
|
|
1665
|
-
payload,
|
|
1666
|
-
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
1667
|
-
);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
const cwd = payload.cwd ?? this.input.cwd;
|
|
2133
|
+
let cwd = payload.cwd ?? this.input.cwd;
|
|
1671
2134
|
let agentSessionId = payload.agentSessionId;
|
|
1672
2135
|
let existingConversation =
|
|
1673
2136
|
(payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
1674
2137
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
2138
|
+
if (!payload.cwd && existingConversation?.cwd) cwd = existingConversation.cwd;
|
|
2139
|
+
|
|
2140
|
+
let client = await this.ensureProviderClient(provider);
|
|
2141
|
+
if (!client) {
|
|
2142
|
+
const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
|
|
2143
|
+
if (existingConversation) {
|
|
2144
|
+
this.activeConversationId = existingConversation.id;
|
|
2145
|
+
this.input.send(createEnvelope({
|
|
2146
|
+
type: "agent.v2.conversation.opened",
|
|
2147
|
+
sessionId: this.input.sessionId,
|
|
2148
|
+
payload: {
|
|
2149
|
+
conversation: existingConversation,
|
|
2150
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
2151
|
+
requestedConversationId: payload.conversationId,
|
|
2152
|
+
providerError: message,
|
|
2153
|
+
},
|
|
2154
|
+
}));
|
|
2155
|
+
this.sendCapabilities();
|
|
2156
|
+
return existingConversation;
|
|
2157
|
+
}
|
|
2158
|
+
if (payload.conversationId) {
|
|
2159
|
+
return this.openOfflineHistory(payload, message, cwd);
|
|
2160
|
+
}
|
|
2161
|
+
return this.openFailure(payload, message, cwd);
|
|
2162
|
+
}
|
|
1675
2163
|
|
|
1676
2164
|
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
1677
|
-
|
|
2165
|
+
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
2166
|
+
if (
|
|
2167
|
+
payload.conversationId &&
|
|
2168
|
+
payload.conversationId === requestedCanonicalId &&
|
|
2169
|
+
existingConversation.id !== payload.conversationId
|
|
2170
|
+
) {
|
|
1678
2171
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1679
2172
|
}
|
|
2173
|
+
if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
|
|
2174
|
+
try {
|
|
2175
|
+
const existingAgentSessionId = existingConversation.agentSessionId!;
|
|
2176
|
+
let result: unknown;
|
|
2177
|
+
try {
|
|
2178
|
+
result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
2181
|
+
if (!recovered) throw error;
|
|
2182
|
+
client = recovered;
|
|
2183
|
+
result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
2184
|
+
}
|
|
2185
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
2186
|
+
if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
|
|
2187
|
+
this.conversationByAgentSessionId.delete(existingAgentSessionId);
|
|
2188
|
+
existingConversation.agentSessionId = nextAgentSessionId;
|
|
2189
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
|
|
2190
|
+
}
|
|
2191
|
+
await this.hydrateConversationFromProvider(existingConversation, result);
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
if (this.input.verbose) {
|
|
2194
|
+
process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
1680
2198
|
this.activeConversationId = existingConversation.id;
|
|
1681
2199
|
this.input.send(createEnvelope({
|
|
1682
2200
|
type: "agent.v2.conversation.opened",
|
|
@@ -1684,18 +2202,29 @@ export class AgentWorkspaceProxy {
|
|
|
1684
2202
|
payload: {
|
|
1685
2203
|
conversation: existingConversation,
|
|
1686
2204
|
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
2205
|
+
requestedConversationId: payload.conversationId,
|
|
1687
2206
|
},
|
|
1688
2207
|
}));
|
|
1689
2208
|
return existingConversation;
|
|
1690
2209
|
}
|
|
1691
2210
|
|
|
1692
2211
|
try {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2212
|
+
let result: unknown;
|
|
2213
|
+
try {
|
|
2214
|
+
result = agentSessionId
|
|
2215
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
2216
|
+
: await client.newSession({ cwd });
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
2219
|
+
if (!recovered) throw error;
|
|
2220
|
+
client = recovered;
|
|
2221
|
+
result = agentSessionId
|
|
2222
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
2223
|
+
: await client.newSession({ cwd });
|
|
2224
|
+
}
|
|
1696
2225
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
1697
2226
|
const now = Date.now();
|
|
1698
|
-
const conversationId =
|
|
2227
|
+
const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1699
2228
|
const conversation: AgentConversation = {
|
|
1700
2229
|
...existingConversation,
|
|
1701
2230
|
id: conversationId,
|
|
@@ -1717,10 +2246,15 @@ export class AgentWorkspaceProxy {
|
|
|
1717
2246
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1718
2247
|
this.activeConversationId = conversation.id;
|
|
1719
2248
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
2249
|
+
await this.hydrateConversationFromProvider(conversation, result);
|
|
1720
2250
|
this.input.send(createEnvelope({
|
|
1721
2251
|
type: "agent.v2.conversation.opened",
|
|
1722
2252
|
sessionId: this.input.sessionId,
|
|
1723
|
-
payload: {
|
|
2253
|
+
payload: {
|
|
2254
|
+
conversation,
|
|
2255
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2256
|
+
requestedConversationId: payload.conversationId,
|
|
2257
|
+
},
|
|
1724
2258
|
}));
|
|
1725
2259
|
return conversation;
|
|
1726
2260
|
} catch (error) {
|
|
@@ -1772,15 +2306,71 @@ export class AgentWorkspaceProxy {
|
|
|
1772
2306
|
this.input.send(createEnvelope({
|
|
1773
2307
|
type: "agent.v2.conversation.opened",
|
|
1774
2308
|
sessionId: this.input.sessionId,
|
|
1775
|
-
payload: {
|
|
2309
|
+
payload: {
|
|
2310
|
+
conversation,
|
|
2311
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2312
|
+
requestedConversationId: payload.conversationId,
|
|
2313
|
+
},
|
|
1776
2314
|
}));
|
|
1777
2315
|
return conversation;
|
|
1778
2316
|
}
|
|
1779
2317
|
|
|
2318
|
+
private openOfflineHistory(
|
|
2319
|
+
payload: {
|
|
2320
|
+
conversationId?: string;
|
|
2321
|
+
agentSessionId?: string;
|
|
2322
|
+
cwd?: string;
|
|
2323
|
+
provider?: AgentProvider;
|
|
2324
|
+
model?: string;
|
|
2325
|
+
reasoningEffort?: string;
|
|
2326
|
+
permissionMode?: AgentPermissionMode;
|
|
2327
|
+
collaborationMode?: AgentCollaborationMode;
|
|
2328
|
+
title?: string;
|
|
2329
|
+
},
|
|
2330
|
+
message: string,
|
|
2331
|
+
cwd = payload.cwd ?? this.input.cwd,
|
|
2332
|
+
): AgentConversation {
|
|
2333
|
+
const now = Date.now();
|
|
2334
|
+
const conversation: AgentConversation = {
|
|
2335
|
+
id: payload.conversationId!,
|
|
2336
|
+
agentSessionId: payload.agentSessionId,
|
|
2337
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
2338
|
+
cwd,
|
|
2339
|
+
title: payload.title ?? titleFromCwd(cwd),
|
|
2340
|
+
model: payload.model,
|
|
2341
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2342
|
+
permissionMode: payload.permissionMode,
|
|
2343
|
+
collaborationMode: payload.collaborationMode,
|
|
2344
|
+
status: "idle",
|
|
2345
|
+
archived: false,
|
|
2346
|
+
lastActivityAt: now,
|
|
2347
|
+
createdAt: now,
|
|
2348
|
+
};
|
|
2349
|
+
this.conversations.set(conversation.id, conversation);
|
|
2350
|
+
if (conversation.agentSessionId) {
|
|
2351
|
+
this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
|
|
2352
|
+
}
|
|
2353
|
+
this.activeConversationId = conversation.id;
|
|
2354
|
+
this.input.send(createEnvelope({
|
|
2355
|
+
type: "agent.v2.conversation.opened",
|
|
2356
|
+
sessionId: this.input.sessionId,
|
|
2357
|
+
payload: {
|
|
2358
|
+
conversation,
|
|
2359
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2360
|
+
requestedConversationId: payload.conversationId,
|
|
2361
|
+
providerError: message,
|
|
2362
|
+
},
|
|
2363
|
+
}));
|
|
2364
|
+
this.sendCapabilities();
|
|
2365
|
+
return conversation;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
1780
2368
|
private async sendPrompt(payload: {
|
|
1781
2369
|
conversationId: string;
|
|
1782
2370
|
clientMessageId: string;
|
|
1783
2371
|
contentBlocks: AgentContentBlock[];
|
|
2372
|
+
delivery?: "auto" | "new_turn" | "steer";
|
|
2373
|
+
targetTurnId?: string;
|
|
1784
2374
|
model?: string;
|
|
1785
2375
|
reasoningEffort?: string;
|
|
1786
2376
|
permissionMode?: AgentPermissionMode;
|
|
@@ -1791,39 +2381,28 @@ export class AgentWorkspaceProxy {
|
|
|
1791
2381
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
1792
2382
|
if (!conversation) return;
|
|
1793
2383
|
if (!conversation.agentSessionId) {
|
|
1794
|
-
this.
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
1799
|
-
createdAt: Date.now(),
|
|
1800
|
-
});
|
|
2384
|
+
this.rejectAgentAction(
|
|
2385
|
+
conversation,
|
|
2386
|
+
"Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
2387
|
+
);
|
|
1801
2388
|
return;
|
|
1802
2389
|
}
|
|
1803
|
-
const client = this.clientForProvider(conversation.provider);
|
|
2390
|
+
const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
|
|
1804
2391
|
if (!client) {
|
|
1805
|
-
this.
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
createdAt: Date.now(),
|
|
1811
|
-
});
|
|
2392
|
+
this.rejectAgentAction(
|
|
2393
|
+
conversation,
|
|
2394
|
+
this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
|
|
2395
|
+
"error",
|
|
2396
|
+
);
|
|
1812
2397
|
return;
|
|
1813
2398
|
}
|
|
1814
2399
|
|
|
1815
2400
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
1816
2401
|
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
id: id("error"),
|
|
1822
|
-
conversationId: conversation.id,
|
|
1823
|
-
type: "error",
|
|
1824
|
-
error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
|
|
1825
|
-
createdAt: Date.now(),
|
|
1826
|
-
});
|
|
2402
|
+
this.rejectAgentAction(
|
|
2403
|
+
conversation,
|
|
2404
|
+
"当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
|
|
2405
|
+
);
|
|
1827
2406
|
return;
|
|
1828
2407
|
}
|
|
1829
2408
|
|
|
@@ -1832,7 +2411,35 @@ export class AgentWorkspaceProxy {
|
|
|
1832
2411
|
reasoningEffort: conversation.reasoningEffort,
|
|
1833
2412
|
permissionMode: conversation.permissionMode,
|
|
1834
2413
|
};
|
|
1835
|
-
|
|
2414
|
+
const effectiveModel = payload.model ??
|
|
2415
|
+
conversation.model ??
|
|
2416
|
+
(protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
|
|
2417
|
+
const wasRunning = conversation.status === "running";
|
|
2418
|
+
const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
|
|
2419
|
+
const shouldSteer = Boolean(
|
|
2420
|
+
payload.delivery !== "new_turn" &&
|
|
2421
|
+
wasRunning &&
|
|
2422
|
+
protocol === "codex-app-server" &&
|
|
2423
|
+
activeTurnId,
|
|
2424
|
+
);
|
|
2425
|
+
if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
|
|
2426
|
+
this.addItem(conversation.id, {
|
|
2427
|
+
id: id("error"),
|
|
2428
|
+
conversationId: conversation.id,
|
|
2429
|
+
type: "error",
|
|
2430
|
+
error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
|
|
2431
|
+
createdAt: Date.now(),
|
|
2432
|
+
});
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
|
|
2436
|
+
this.rejectAgentAction(
|
|
2437
|
+
conversation,
|
|
2438
|
+
"当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。",
|
|
2439
|
+
);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
conversation.model = effectiveModel ?? conversation.model;
|
|
1836
2443
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1837
2444
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1838
2445
|
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
@@ -1840,14 +2447,14 @@ export class AgentWorkspaceProxy {
|
|
|
1840
2447
|
conversation.lastActivityAt = Date.now();
|
|
1841
2448
|
this.activeConversationId = conversation.id;
|
|
1842
2449
|
|
|
1843
|
-
if (
|
|
2450
|
+
if (effectiveModel && effectiveModel !== priorSettings.model) {
|
|
1844
2451
|
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1845
|
-
const label = runtimeCapabilities?.models?.find((m) => m.id ===
|
|
2452
|
+
const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
|
|
1846
2453
|
this.emitNotice({
|
|
1847
2454
|
conversationId: conversation.id,
|
|
1848
2455
|
kind: "model_changed",
|
|
1849
2456
|
title: `已切换模型 · ${label}`,
|
|
1850
|
-
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${
|
|
2457
|
+
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
|
|
1851
2458
|
});
|
|
1852
2459
|
}
|
|
1853
2460
|
if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
|
|
@@ -1873,21 +2480,62 @@ export class AgentWorkspaceProxy {
|
|
|
1873
2480
|
role: "user",
|
|
1874
2481
|
content: payload.contentBlocks,
|
|
1875
2482
|
text: userText,
|
|
2483
|
+
metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
|
|
1876
2484
|
createdAt: Date.now(),
|
|
1877
2485
|
});
|
|
1878
2486
|
this.emitConversation(conversation);
|
|
1879
2487
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
sessionId: conversation.agentSessionId
|
|
2488
|
+
const promptAsNewTurn = () =>
|
|
2489
|
+
client.prompt({
|
|
2490
|
+
sessionId: conversation.agentSessionId!,
|
|
1883
2491
|
content: payload.contentBlocks,
|
|
1884
2492
|
clientMessageId: payload.clientMessageId,
|
|
1885
|
-
model:
|
|
2493
|
+
model: effectiveModel,
|
|
1886
2494
|
reasoningEffort: payload.reasoningEffort,
|
|
1887
2495
|
permissionMode: payload.permissionMode,
|
|
1888
2496
|
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1889
2497
|
cwd: conversation.cwd,
|
|
1890
2498
|
});
|
|
2499
|
+
|
|
2500
|
+
try {
|
|
2501
|
+
const steer = (client as { steer?: (input: { sessionId: string; turnId: string; content: AgentContentBlock[] }) => Promise<unknown> }).steer;
|
|
2502
|
+
let result: unknown;
|
|
2503
|
+
if (shouldSteer && typeof steer === "function") {
|
|
2504
|
+
try {
|
|
2505
|
+
result = await steer.call(client, {
|
|
2506
|
+
sessionId: conversation.agentSessionId,
|
|
2507
|
+
turnId: activeTurnId!,
|
|
2508
|
+
content: payload.contentBlocks,
|
|
2509
|
+
});
|
|
2510
|
+
} catch (steerError) {
|
|
2511
|
+
this.forgetCurrentTurn(conversation.id, activeTurnId);
|
|
2512
|
+
const userItem = this.findItem(conversation.id, payload.clientMessageId);
|
|
2513
|
+
if (userItem) {
|
|
2514
|
+
this.upsertItem(conversation.id, {
|
|
2515
|
+
...userItem,
|
|
2516
|
+
metadata: {
|
|
2517
|
+
...(userItem.metadata ?? {}),
|
|
2518
|
+
delivery: "new_turn",
|
|
2519
|
+
fallbackFrom: "steer",
|
|
2520
|
+
failedTargetTurnId: activeTurnId,
|
|
2521
|
+
},
|
|
2522
|
+
updatedAt: Date.now(),
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
this.addItem(conversation.id, {
|
|
2526
|
+
id: id("status"),
|
|
2527
|
+
conversationId: conversation.id,
|
|
2528
|
+
type: "status",
|
|
2529
|
+
role: "system",
|
|
2530
|
+
text: "Codex 未接收运行中追加输入,已改为发送新消息。",
|
|
2531
|
+
status: "running",
|
|
2532
|
+
createdAt: Date.now(),
|
|
2533
|
+
});
|
|
2534
|
+
result = await promptAsNewTurn();
|
|
2535
|
+
}
|
|
2536
|
+
} else {
|
|
2537
|
+
result = await promptAsNewTurn();
|
|
2538
|
+
}
|
|
1891
2539
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
1892
2540
|
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1893
2541
|
if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
@@ -2120,6 +2768,9 @@ export class AgentWorkspaceProxy {
|
|
|
2120
2768
|
}
|
|
2121
2769
|
|
|
2122
2770
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
2771
|
+
if (method === "claude/askUserQuestion") {
|
|
2772
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
2773
|
+
}
|
|
2123
2774
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2124
2775
|
return this.handleStructuredInput(params, true);
|
|
2125
2776
|
}
|
|
@@ -2167,7 +2818,6 @@ export class AgentWorkspaceProxy {
|
|
|
2167
2818
|
if (
|
|
2168
2819
|
method.startsWith("account/") ||
|
|
2169
2820
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2170
|
-
method === "thread/status/changed" ||
|
|
2171
2821
|
method === "thread/tokenUsage/updated" ||
|
|
2172
2822
|
method === "serverRequest/resolved" ||
|
|
2173
2823
|
method === "mcpServer/oauthLogin/completed"
|
|
@@ -2176,6 +2826,25 @@ export class AgentWorkspaceProxy {
|
|
|
2176
2826
|
}
|
|
2177
2827
|
|
|
2178
2828
|
const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
|
|
2829
|
+
if (method === "thread/status/changed") {
|
|
2830
|
+
const status = agentStatusFromThreadStatus(params);
|
|
2831
|
+
if (conversationId && status) {
|
|
2832
|
+
const raw = asRecord(params);
|
|
2833
|
+
const turnId = this.extractTurnId(raw);
|
|
2834
|
+
if (turnId && (status === "running" || status === "waiting_permission")) {
|
|
2835
|
+
this.rememberTurnConversationId(conversationId, turnId);
|
|
2836
|
+
}
|
|
2837
|
+
if (status === "idle" || status === "error") {
|
|
2838
|
+
this.forgetCurrentTurn(conversationId, turnId);
|
|
2839
|
+
}
|
|
2840
|
+
const message =
|
|
2841
|
+
status === "error"
|
|
2842
|
+
? firstString(raw, ["message", "error", "reason"])
|
|
2843
|
+
: undefined;
|
|
2844
|
+
this.updateConversationStatus(conversationId, status, message);
|
|
2845
|
+
}
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2179
2848
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2180
2849
|
this.handleStructuredInput(params);
|
|
2181
2850
|
return;
|
|
@@ -2685,9 +3354,8 @@ export class AgentWorkspaceProxy {
|
|
|
2685
3354
|
const raw = asRecord(params) ?? {};
|
|
2686
3355
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
2687
3356
|
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
2688
|
-
const formatResponse =
|
|
2689
|
-
|
|
2690
|
-
: formatStructuredInputResponse;
|
|
3357
|
+
const formatResponse = (answers: Record<string, string[]>, input?: AgentStructuredInput) =>
|
|
3358
|
+
formatStructuredInputResponseForSource(source, answers, input);
|
|
2691
3359
|
if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
2692
3360
|
const structuredInput = decodeStructuredInput(raw);
|
|
2693
3361
|
if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
@@ -2711,13 +3379,13 @@ export class AgentWorkspaceProxy {
|
|
|
2711
3379
|
const timer = setTimeout(() => {
|
|
2712
3380
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
2713
3381
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
2714
|
-
resolve(formatResponse({}));
|
|
3382
|
+
resolve(formatResponse({}, structuredInput));
|
|
2715
3383
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
2716
3384
|
inputPending: false,
|
|
2717
3385
|
inputError: "等待用户输入超时",
|
|
2718
3386
|
});
|
|
2719
3387
|
}, PERMISSION_TIMEOUT_MS);
|
|
2720
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
3388
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
|
|
2721
3389
|
});
|
|
2722
3390
|
}
|
|
2723
3391
|
|
|
@@ -2758,11 +3426,20 @@ export class AgentWorkspaceProxy {
|
|
|
2758
3426
|
if (!conversationId) return waitForResponse ? Promise.resolve({ outcome: { outcome: "cancelled" } }) : undefined;
|
|
2759
3427
|
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
2760
3428
|
const rawToolCall = asRecord(raw.toolCall) ?? raw;
|
|
3429
|
+
// Codex app-server command/file approvals carry `command`/`reason`/`cwd`
|
|
3430
|
+
// (not toolCall/context). Surface them so the card actually shows what's
|
|
3431
|
+
// being approved; fall back to the Claude/MCP shape otherwise.
|
|
3432
|
+
const codexCommand = firstString(raw, ["command"]);
|
|
3433
|
+
const codexCwd = firstString(raw, ["cwd"]);
|
|
2761
3434
|
const permission: AgentPermission = {
|
|
2762
3435
|
requestId,
|
|
2763
|
-
toolName:
|
|
2764
|
-
|
|
2765
|
-
|
|
3436
|
+
toolName:
|
|
3437
|
+
firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]) ??
|
|
3438
|
+
(codexCommand ? "shell" : undefined),
|
|
3439
|
+
toolInput: codexCommand
|
|
3440
|
+
? (codexCwd ? `$ ${codexCommand}\n# cwd: ${codexCwd}` : `$ ${codexCommand}`)
|
|
3441
|
+
: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
3442
|
+
context: firstString(raw, ["context", "description", "message", "title", "reason"]),
|
|
2766
3443
|
options: parsePermissionOptions(raw.options),
|
|
2767
3444
|
};
|
|
2768
3445
|
this.pendingPermissions.set(requestId, permission);
|
|
@@ -2773,6 +3450,12 @@ export class AgentWorkspaceProxy {
|
|
|
2773
3450
|
conversationId,
|
|
2774
3451
|
type: "permission",
|
|
2775
3452
|
permission,
|
|
3453
|
+
metadata: {
|
|
3454
|
+
protocol: "v2",
|
|
3455
|
+
permissionLive: true,
|
|
3456
|
+
permissionExpired: false,
|
|
3457
|
+
permissionPending: false,
|
|
3458
|
+
},
|
|
2776
3459
|
createdAt: Date.now(),
|
|
2777
3460
|
updatedAt: Date.now(),
|
|
2778
3461
|
};
|
|
@@ -2803,10 +3486,13 @@ export class AgentWorkspaceProxy {
|
|
|
2803
3486
|
optionId?: string;
|
|
2804
3487
|
}): void {
|
|
2805
3488
|
const permission = this.pendingPermissions.get(payload.requestId);
|
|
3489
|
+
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
3490
|
+
const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
|
|
3491
|
+
const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
|
|
3492
|
+
if (!permission && !waiter && alreadyResolved) return;
|
|
2806
3493
|
this.pendingPermissions.delete(payload.requestId);
|
|
2807
3494
|
const selectedOptionId =
|
|
2808
3495
|
payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
2809
|
-
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
2810
3496
|
if (waiter) {
|
|
2811
3497
|
clearTimeout(waiter.timer);
|
|
2812
3498
|
this.permissionWaiters.delete(payload.requestId);
|
|
@@ -2832,7 +3518,9 @@ export class AgentWorkspaceProxy {
|
|
|
2832
3518
|
permissionError: undefined,
|
|
2833
3519
|
permissionPending: false,
|
|
2834
3520
|
});
|
|
2835
|
-
|
|
3521
|
+
if (permission || waiter || existingItem) {
|
|
3522
|
+
this.updateConversationStatus(payload.conversationId, "running");
|
|
3523
|
+
}
|
|
2836
3524
|
}
|
|
2837
3525
|
|
|
2838
3526
|
private respondStructuredInput(payload: {
|
|
@@ -2841,15 +3529,15 @@ export class AgentWorkspaceProxy {
|
|
|
2841
3529
|
answers: Record<string, string[]>;
|
|
2842
3530
|
}): void {
|
|
2843
3531
|
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
2844
|
-
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2845
3532
|
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
3533
|
+
const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
|
|
3534
|
+
const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
|
|
3535
|
+
if (!pending && !waiter && alreadySubmitted) return;
|
|
3536
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2846
3537
|
if (waiter) {
|
|
2847
3538
|
clearTimeout(waiter.timer);
|
|
2848
3539
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
2849
|
-
|
|
2850
|
-
? formatMcpElicitationResponse
|
|
2851
|
-
: formatStructuredInputResponse;
|
|
2852
|
-
waiter.resolve(formatResponse(payload.answers));
|
|
3540
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
|
|
2853
3541
|
}
|
|
2854
3542
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
2855
3543
|
inputPending: false,
|
|
@@ -2858,7 +3546,9 @@ export class AgentWorkspaceProxy {
|
|
|
2858
3546
|
inputError: undefined,
|
|
2859
3547
|
answers: payload.answers,
|
|
2860
3548
|
});
|
|
2861
|
-
|
|
3549
|
+
if (pending || waiter || existingItem) {
|
|
3550
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
3551
|
+
}
|
|
2862
3552
|
}
|
|
2863
3553
|
|
|
2864
3554
|
private markPermission(
|
|
@@ -3058,7 +3748,7 @@ export class AgentWorkspaceProxy {
|
|
|
3058
3748
|
this.input.send(createEnvelope({
|
|
3059
3749
|
type: "agent.v2.event",
|
|
3060
3750
|
sessionId: this.input.sessionId,
|
|
3061
|
-
payload: { conversationId, conversation, item },
|
|
3751
|
+
payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
|
|
3062
3752
|
}));
|
|
3063
3753
|
}
|
|
3064
3754
|
|
|
@@ -3066,7 +3756,7 @@ export class AgentWorkspaceProxy {
|
|
|
3066
3756
|
this.input.send(createEnvelope({
|
|
3067
3757
|
type: "agent.v2.event",
|
|
3068
3758
|
sessionId: this.input.sessionId,
|
|
3069
|
-
payload: { conversationId: conversation.id, conversation },
|
|
3759
|
+
payload: { conversationId: conversation.id, conversation: { ...conversation } },
|
|
3070
3760
|
}));
|
|
3071
3761
|
}
|
|
3072
3762
|
|
|
@@ -3101,6 +3791,21 @@ export class AgentWorkspaceProxy {
|
|
|
3101
3791
|
}));
|
|
3102
3792
|
}
|
|
3103
3793
|
|
|
3794
|
+
private rejectAgentAction(
|
|
3795
|
+
conversation: AgentConversation,
|
|
3796
|
+
message: string,
|
|
3797
|
+
status: AgentStatus = "idle",
|
|
3798
|
+
): void {
|
|
3799
|
+
this.updateConversationStatus(conversation.id, status, message);
|
|
3800
|
+
this.addItem(conversation.id, {
|
|
3801
|
+
id: id("error"),
|
|
3802
|
+
conversationId: conversation.id,
|
|
3803
|
+
type: "error",
|
|
3804
|
+
error: message,
|
|
3805
|
+
createdAt: Date.now(),
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3104
3809
|
private updateConversationPreview(
|
|
3105
3810
|
conversationId: string,
|
|
3106
3811
|
text: string,
|
|
@@ -3143,6 +3848,76 @@ export class AgentWorkspaceProxy {
|
|
|
3143
3848
|
}));
|
|
3144
3849
|
}
|
|
3145
3850
|
|
|
3851
|
+
/** Page OLDER transcript history for a conversation (client scrolling up).
|
|
3852
|
+
* Only Codex's app-server exposes paginated reads; other providers report
|
|
3853
|
+
* no more history. Pure pass-through — does not touch the bounded live
|
|
3854
|
+
* timeline. The client prepends the returned (ascending) items. */
|
|
3855
|
+
private async loadOlderHistory(payload: {
|
|
3856
|
+
conversationId: string;
|
|
3857
|
+
cursor?: string;
|
|
3858
|
+
limit: number;
|
|
3859
|
+
}): Promise<void> {
|
|
3860
|
+
const sendResult = (
|
|
3861
|
+
items: AgentTimelineItem[],
|
|
3862
|
+
nextCursor?: string,
|
|
3863
|
+
): void => {
|
|
3864
|
+
this.input.send(createEnvelope({
|
|
3865
|
+
type: "agent.v2.history.result",
|
|
3866
|
+
sessionId: this.input.sessionId,
|
|
3867
|
+
payload: {
|
|
3868
|
+
conversationId: payload.conversationId,
|
|
3869
|
+
items,
|
|
3870
|
+
nextCursor,
|
|
3871
|
+
hasMore: Boolean(nextCursor),
|
|
3872
|
+
},
|
|
3873
|
+
}));
|
|
3874
|
+
};
|
|
3875
|
+
|
|
3876
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3877
|
+
if (!conversation?.agentSessionId) return sendResult([], undefined);
|
|
3878
|
+
|
|
3879
|
+
const client = this.clientForProvider(conversation.provider);
|
|
3880
|
+
const listTurns = (client as {
|
|
3881
|
+
listTurns?: (input: {
|
|
3882
|
+
sessionId: string;
|
|
3883
|
+
limit?: number;
|
|
3884
|
+
cursor?: string;
|
|
3885
|
+
sortDirection?: "asc" | "desc";
|
|
3886
|
+
itemsView?: "summary" | "full";
|
|
3887
|
+
}) => Promise<unknown>;
|
|
3888
|
+
} | undefined)?.listTurns;
|
|
3889
|
+
if (typeof listTurns !== "function") return sendResult([], undefined);
|
|
3890
|
+
|
|
3891
|
+
// Client-supplied cursor wins; else the one captured at hydration. Absent
|
|
3892
|
+
// cursor means we're already at the start of history.
|
|
3893
|
+
const cursor = payload.cursor ?? this.historyCursors.get(payload.conversationId);
|
|
3894
|
+
if (!cursor) return sendResult([], undefined);
|
|
3895
|
+
|
|
3896
|
+
try {
|
|
3897
|
+
const turnsResult = await listTurns.call(client, {
|
|
3898
|
+
sessionId: conversation.agentSessionId,
|
|
3899
|
+
limit: payload.limit,
|
|
3900
|
+
cursor,
|
|
3901
|
+
sortDirection: "desc",
|
|
3902
|
+
itemsView: "full",
|
|
3903
|
+
});
|
|
3904
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
3905
|
+
const items = turnsThread
|
|
3906
|
+
? timelineItemsFromProviderThread({ thread: turnsThread }, payload.conversationId)
|
|
3907
|
+
: [];
|
|
3908
|
+
const nextCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
|
|
3909
|
+
this.historyCursors.set(payload.conversationId, nextCursor);
|
|
3910
|
+
sendResult(items, nextCursor);
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
if (this.input.verbose) {
|
|
3913
|
+
process.stderr.write(
|
|
3914
|
+
`[agent:v2] history page failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
sendResult([], undefined);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3146
3921
|
private conversationIdFromParams(params: unknown): string | undefined {
|
|
3147
3922
|
const raw = asRecord(params);
|
|
3148
3923
|
if (!raw) return undefined;
|
|
@@ -3228,12 +4003,22 @@ export class AgentWorkspaceProxy {
|
|
|
3228
4003
|
private handleProviderExit(provider: AgentProvider, message: string): void {
|
|
3229
4004
|
this.clients.delete(provider);
|
|
3230
4005
|
this.agentProtocols.delete(provider);
|
|
3231
|
-
this.
|
|
4006
|
+
this.providerErrors.set(provider, message);
|
|
4007
|
+
for (const conversation of this.conversations.values()) {
|
|
4008
|
+
if (conversation.provider === provider && conversation.status === "waiting_permission") {
|
|
4009
|
+
this.cancelPendingPermissions(conversation.id, false);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
3232
4012
|
for (const conversation of this.conversations.values()) {
|
|
3233
4013
|
if (conversation.provider !== provider) continue;
|
|
4014
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
|
|
4015
|
+
this.emitConversation(conversation);
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
3234
4018
|
conversation.status = "error";
|
|
3235
4019
|
conversation.lastMessagePreview = message;
|
|
3236
4020
|
conversation.lastActivityAt = Date.now();
|
|
4021
|
+
this.forgetCurrentTurn(conversation.id);
|
|
3237
4022
|
this.emitConversation(conversation);
|
|
3238
4023
|
this.addItem(conversation.id, {
|
|
3239
4024
|
id: id("error"),
|
|
@@ -3246,8 +4031,9 @@ export class AgentWorkspaceProxy {
|
|
|
3246
4031
|
this.sendCapabilities();
|
|
3247
4032
|
}
|
|
3248
4033
|
|
|
3249
|
-
private cancelPendingPermissions(conversationId?: string): void {
|
|
4034
|
+
private cancelPendingPermissions(conversationId?: string, updateStatus = true): void {
|
|
3250
4035
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
4036
|
+
if (conversationId && this.itemConversationIds.get(requestId) !== conversationId) continue;
|
|
3251
4037
|
clearTimeout(waiter.timer);
|
|
3252
4038
|
waiter.resolve(formatPermissionResponse(
|
|
3253
4039
|
this.permissionSources.get(requestId),
|
|
@@ -3257,11 +4043,12 @@ export class AgentWorkspaceProxy {
|
|
|
3257
4043
|
this.pendingPermissions.delete(requestId);
|
|
3258
4044
|
this.permissionSources.delete(requestId);
|
|
3259
4045
|
}
|
|
3260
|
-
this.permissionWaiters.clear();
|
|
4046
|
+
if (!conversationId) this.permissionWaiters.clear();
|
|
3261
4047
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3262
|
-
clearTimeout(waiter.timer);
|
|
3263
|
-
waiter.resolve(formatStructuredInputResponse({}));
|
|
3264
4048
|
const pending = this.pendingStructuredInputs.get(requestId);
|
|
4049
|
+
if (conversationId && pending?.conversationId !== conversationId) continue;
|
|
4050
|
+
clearTimeout(waiter.timer);
|
|
4051
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
|
|
3265
4052
|
if (pending) {
|
|
3266
4053
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3267
4054
|
inputPending: false,
|
|
@@ -3269,9 +4056,10 @@ export class AgentWorkspaceProxy {
|
|
|
3269
4056
|
});
|
|
3270
4057
|
}
|
|
3271
4058
|
this.pendingStructuredInputs.delete(requestId);
|
|
4059
|
+
this.structuredInputWaiters.delete(requestId);
|
|
3272
4060
|
}
|
|
3273
|
-
this.structuredInputWaiters.clear();
|
|
3274
|
-
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
4061
|
+
if (!conversationId) this.structuredInputWaiters.clear();
|
|
4062
|
+
if (conversationId && updateStatus) this.updateConversationStatus(conversationId, "idle");
|
|
3275
4063
|
}
|
|
3276
4064
|
|
|
3277
4065
|
private extractSessionId(value: unknown): string | undefined {
|
|
@@ -3359,6 +4147,37 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
|
|
|
3359
4147
|
};
|
|
3360
4148
|
}
|
|
3361
4149
|
|
|
4150
|
+
function formatClaudeAskUserQuestionResponse(
|
|
4151
|
+
answers: Record<string, string[]>,
|
|
4152
|
+
input?: AgentStructuredInput,
|
|
4153
|
+
): unknown {
|
|
4154
|
+
const questions = input?.questions ?? [];
|
|
4155
|
+
const byQuestionText: Record<string, string | string[]> = {};
|
|
4156
|
+
for (const question of questions) {
|
|
4157
|
+
const values = answers[question.id] ?? answers[question.question] ?? [];
|
|
4158
|
+
const cleaned = values.map((value) => value.trim()).filter(Boolean);
|
|
4159
|
+
if (cleaned.length === 0) continue;
|
|
4160
|
+
byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
|
|
4161
|
+
? cleaned
|
|
4162
|
+
: cleaned[0] ?? "";
|
|
4163
|
+
}
|
|
4164
|
+
return {
|
|
4165
|
+
behavior: "allow",
|
|
4166
|
+
updatedInput: {
|
|
4167
|
+
questions: questions.map((question) => ({
|
|
4168
|
+
question: question.question,
|
|
4169
|
+
header: question.header,
|
|
4170
|
+
options: question.options?.map((option) => ({
|
|
4171
|
+
label: option.label,
|
|
4172
|
+
description: option.description,
|
|
4173
|
+
})) ?? [],
|
|
4174
|
+
multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
|
|
4175
|
+
})),
|
|
4176
|
+
answers: byQuestionText,
|
|
4177
|
+
},
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3362
4181
|
function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
|
|
3363
4182
|
const content = Object.fromEntries(
|
|
3364
4183
|
Object.entries(answers).map(([questionId, values]) => [
|
|
@@ -3373,6 +4192,20 @@ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknow
|
|
|
3373
4192
|
};
|
|
3374
4193
|
}
|
|
3375
4194
|
|
|
4195
|
+
function formatStructuredInputResponseForSource(
|
|
4196
|
+
source: string | undefined,
|
|
4197
|
+
answers: Record<string, string[]>,
|
|
4198
|
+
input?: AgentStructuredInput,
|
|
4199
|
+
): unknown {
|
|
4200
|
+
if (source === "mcpServer/elicitation/request") {
|
|
4201
|
+
return formatMcpElicitationResponse(answers);
|
|
4202
|
+
}
|
|
4203
|
+
if (source === "claude/askUserQuestion") {
|
|
4204
|
+
return formatClaudeAskUserQuestionResponse(answers, input);
|
|
4205
|
+
}
|
|
4206
|
+
return formatStructuredInputResponse(answers);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
3376
4209
|
function formatPermissionResponse(
|
|
3377
4210
|
source: string | undefined,
|
|
3378
4211
|
outcome: "allow" | "deny" | "cancelled",
|