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
|
@@ -11,6 +11,12 @@ const MAX_TIMELINE_ITEMS = 200;
|
|
|
11
11
|
function id(prefix) {
|
|
12
12
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13
13
|
}
|
|
14
|
+
function normalizeAgentSessionIdSegment(value) {
|
|
15
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
16
|
+
}
|
|
17
|
+
export function makeAgentV2RemoteConversationId(provider, agentSessionId) {
|
|
18
|
+
return `agent-remote-${provider}-${normalizeAgentSessionIdSegment(agentSessionId)}`;
|
|
19
|
+
}
|
|
14
20
|
function stringify(value) {
|
|
15
21
|
if (typeof value === "string")
|
|
16
22
|
return value;
|
|
@@ -66,6 +72,12 @@ function extractItem(value) {
|
|
|
66
72
|
return undefined;
|
|
67
73
|
return asRecord(raw.item) ?? raw;
|
|
68
74
|
}
|
|
75
|
+
function extractTurn(value) {
|
|
76
|
+
const raw = asRecord(value);
|
|
77
|
+
if (!raw)
|
|
78
|
+
return undefined;
|
|
79
|
+
return asRecord(raw.turn) ?? raw;
|
|
80
|
+
}
|
|
69
81
|
function stringifyDefined(value) {
|
|
70
82
|
if (value === undefined || value === null || value === "")
|
|
71
83
|
return undefined;
|
|
@@ -107,6 +119,29 @@ function normalizePlanStatus(value) {
|
|
|
107
119
|
return "in_progress";
|
|
108
120
|
return "pending";
|
|
109
121
|
}
|
|
122
|
+
function agentStatusFromThreadStatus(value) {
|
|
123
|
+
const raw = asRecord(value);
|
|
124
|
+
const status = typeof value === "string"
|
|
125
|
+
? value
|
|
126
|
+
: firstString(raw, ["status", "state", "phase", "type"]) ??
|
|
127
|
+
firstString(asRecord(raw?.status), ["status", "state", "phase", "type"]);
|
|
128
|
+
const normalized = normalizedIdentifier(status);
|
|
129
|
+
if (!normalized)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (["active", "running", "inprogress", "busy", "working", "streaming"].includes(normalized)) {
|
|
132
|
+
return "running";
|
|
133
|
+
}
|
|
134
|
+
if (["idle", "ready", "completed", "complete", "done", "finished"].includes(normalized)) {
|
|
135
|
+
return "idle";
|
|
136
|
+
}
|
|
137
|
+
if (["waitingpermission", "waitingforpermission", "requirespermission", "needspermission"].includes(normalized)) {
|
|
138
|
+
return "waiting_permission";
|
|
139
|
+
}
|
|
140
|
+
if (["error", "failed", "systemerror", "fatal"].includes(normalized)) {
|
|
141
|
+
return "error";
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
110
145
|
function nameFromToolMethod(method) {
|
|
111
146
|
if (method.includes("commandExecution"))
|
|
112
147
|
return "命令";
|
|
@@ -473,7 +508,9 @@ function parseStructuredInputQuestion(value, index) {
|
|
|
473
508
|
question,
|
|
474
509
|
isOther: raw.isOther === true,
|
|
475
510
|
isSecret: raw.isSecret === true || raw.secret === true,
|
|
476
|
-
selectionLimit:
|
|
511
|
+
selectionLimit: raw.multiSelect === true
|
|
512
|
+
? options.length || undefined
|
|
513
|
+
: firstNumber(raw, ["selectionLimit", "maxSelections"]),
|
|
477
514
|
options: options.length > 0 ? options : undefined,
|
|
478
515
|
};
|
|
479
516
|
}
|
|
@@ -1004,10 +1041,11 @@ function parseTimestamp(value) {
|
|
|
1004
1041
|
function parseRemoteSessions(value) {
|
|
1005
1042
|
const raw = asRecord(value);
|
|
1006
1043
|
const sessionsValue = Array.isArray(value) ? value :
|
|
1007
|
-
Array.isArray(raw?.
|
|
1008
|
-
Array.isArray(raw?.
|
|
1009
|
-
Array.isArray(raw?.
|
|
1010
|
-
|
|
1044
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1045
|
+
Array.isArray(raw?.threads) ? raw.threads :
|
|
1046
|
+
Array.isArray(raw?.sessions) ? raw.sessions :
|
|
1047
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
1048
|
+
[];
|
|
1011
1049
|
const result = [];
|
|
1012
1050
|
for (const entry of sessionsValue) {
|
|
1013
1051
|
const session = asRecord(entry);
|
|
@@ -1024,7 +1062,7 @@ function parseRemoteSessions(value) {
|
|
|
1024
1062
|
result.push({
|
|
1025
1063
|
id,
|
|
1026
1064
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
1027
|
-
title: firstString(source, ["title", "name", "summary"]),
|
|
1065
|
+
title: firstString(source, ["title", "name", "summary", "preview"]),
|
|
1028
1066
|
model: firstString(source, ["model", "modelId"]),
|
|
1029
1067
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1030
1068
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
@@ -1032,11 +1070,265 @@ function parseRemoteSessions(value) {
|
|
|
1032
1070
|
}
|
|
1033
1071
|
return result;
|
|
1034
1072
|
}
|
|
1073
|
+
function threadFromProviderResult(value) {
|
|
1074
|
+
const raw = asRecord(value);
|
|
1075
|
+
return asRecord(raw?.thread) ?? raw;
|
|
1076
|
+
}
|
|
1077
|
+
function threadFromTurnsListResult(sessionId, value) {
|
|
1078
|
+
const raw = asRecord(value);
|
|
1079
|
+
const order = firstString(raw, ["sortDirection", "order", "sortOrder", "sort"]);
|
|
1080
|
+
const turns = Array.isArray(raw?.turns) ? raw.turns :
|
|
1081
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1082
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
1083
|
+
Array.isArray(value) ? value :
|
|
1084
|
+
undefined;
|
|
1085
|
+
if (!turns)
|
|
1086
|
+
return undefined;
|
|
1087
|
+
const extracted = turns
|
|
1088
|
+
.map(extractTurn)
|
|
1089
|
+
.filter((turn) => Boolean(turn));
|
|
1090
|
+
const chronological = order && /asc/i.test(order)
|
|
1091
|
+
? extracted
|
|
1092
|
+
: [...extracted].reverse();
|
|
1093
|
+
return { id: sessionId, turns: chronological };
|
|
1094
|
+
}
|
|
1095
|
+
function turnItemsFromThread(thread) {
|
|
1096
|
+
if (!thread)
|
|
1097
|
+
return [];
|
|
1098
|
+
const turnsValue = Array.isArray(thread.turns)
|
|
1099
|
+
? thread.turns
|
|
1100
|
+
: Array.isArray(thread.items)
|
|
1101
|
+
? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items }]
|
|
1102
|
+
: [];
|
|
1103
|
+
const result = [];
|
|
1104
|
+
for (const turnEntry of turnsValue) {
|
|
1105
|
+
const turn = extractTurn(turnEntry);
|
|
1106
|
+
if (!turn)
|
|
1107
|
+
continue;
|
|
1108
|
+
const turnId = firstString(turn, ["id", "turnId"]);
|
|
1109
|
+
const createdAt = parseTimestamp(turn.createdAt ??
|
|
1110
|
+
turn.created_at ??
|
|
1111
|
+
turn.startedAt ??
|
|
1112
|
+
turn.started_at ??
|
|
1113
|
+
turn.completedAt ??
|
|
1114
|
+
turn.completed_at ??
|
|
1115
|
+
turn.updatedAt ??
|
|
1116
|
+
turn.updated_at);
|
|
1117
|
+
const itemsRecord = asRecord(turn.items);
|
|
1118
|
+
const items = Array.isArray(turn.items)
|
|
1119
|
+
? turn.items
|
|
1120
|
+
: Array.isArray(itemsRecord?.data)
|
|
1121
|
+
? itemsRecord.data
|
|
1122
|
+
: Array.isArray(itemsRecord?.items)
|
|
1123
|
+
? itemsRecord.items
|
|
1124
|
+
: Array.isArray(turn.item)
|
|
1125
|
+
? turn.item
|
|
1126
|
+
: Array.isArray(turn.events)
|
|
1127
|
+
? turn.events
|
|
1128
|
+
: [];
|
|
1129
|
+
for (const itemEntry of items) {
|
|
1130
|
+
const item = extractItem(itemEntry);
|
|
1131
|
+
if (!item)
|
|
1132
|
+
continue;
|
|
1133
|
+
result.push({
|
|
1134
|
+
...item,
|
|
1135
|
+
...(turnId && !firstString(item, ["turnId"]) ? { turnId } : {}),
|
|
1136
|
+
__turnCreatedAt: createdAt,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
function activeTurnIdFromThread(thread) {
|
|
1143
|
+
if (!thread)
|
|
1144
|
+
return undefined;
|
|
1145
|
+
const turnsValue = Array.isArray(thread.turns)
|
|
1146
|
+
? thread.turns
|
|
1147
|
+
: Array.isArray(thread.items)
|
|
1148
|
+
? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items, status: thread.status }]
|
|
1149
|
+
: [];
|
|
1150
|
+
let activeTurnId;
|
|
1151
|
+
for (const turnEntry of turnsValue) {
|
|
1152
|
+
const turn = extractTurn(turnEntry);
|
|
1153
|
+
if (!turn)
|
|
1154
|
+
continue;
|
|
1155
|
+
const turnId = firstString(turn, ["id", "turnId"]);
|
|
1156
|
+
if (!turnId)
|
|
1157
|
+
continue;
|
|
1158
|
+
const status = firstString(turn, ["status", "state", "phase"]) ??
|
|
1159
|
+
firstString(asRecord(turn.status), ["status", "state", "phase", "type"]);
|
|
1160
|
+
const normalized = normalizedIdentifier(status);
|
|
1161
|
+
if ([
|
|
1162
|
+
"active",
|
|
1163
|
+
"running",
|
|
1164
|
+
"inprogress",
|
|
1165
|
+
"busy",
|
|
1166
|
+
"working",
|
|
1167
|
+
"streaming",
|
|
1168
|
+
"waitingpermission",
|
|
1169
|
+
"waitingforpermission",
|
|
1170
|
+
"requirespermission",
|
|
1171
|
+
"needspermission",
|
|
1172
|
+
].includes(normalized)) {
|
|
1173
|
+
activeTurnId = turnId;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return activeTurnId;
|
|
1177
|
+
}
|
|
1178
|
+
function timelineItemFromProviderItem(item, conversationId, index) {
|
|
1179
|
+
const itemType = firstString(item, ["type"]);
|
|
1180
|
+
const normalized = normalizedIdentifier(itemType);
|
|
1181
|
+
const itemId = firstString(item, ["id", "itemId", "messageId", "toolCallId"]) ?? `history-${index + 1}`;
|
|
1182
|
+
const createdAt = parseTimestamp(item.createdAt ?? item.created_at ?? item.timestamp ?? item.__turnCreatedAt) ??
|
|
1183
|
+
Date.now() + index;
|
|
1184
|
+
const base = {
|
|
1185
|
+
id: itemId,
|
|
1186
|
+
conversationId,
|
|
1187
|
+
turnId: firstString(item, ["turnId"]),
|
|
1188
|
+
itemId,
|
|
1189
|
+
createdAt,
|
|
1190
|
+
updatedAt: parseTimestamp(item.updatedAt ?? item.updated_at) ?? createdAt,
|
|
1191
|
+
isStreaming: false,
|
|
1192
|
+
};
|
|
1193
|
+
if (normalized === "usermessage" ||
|
|
1194
|
+
normalized === "userinput" ||
|
|
1195
|
+
(normalized === "message" && item.role === "user")) {
|
|
1196
|
+
const content = contentBlocksFromItem(item);
|
|
1197
|
+
const text = textFromBlocks(content);
|
|
1198
|
+
if (!content.length && !text)
|
|
1199
|
+
return undefined;
|
|
1200
|
+
return {
|
|
1201
|
+
...base,
|
|
1202
|
+
type: "message",
|
|
1203
|
+
kind: "chat",
|
|
1204
|
+
role: "user",
|
|
1205
|
+
content,
|
|
1206
|
+
text,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (normalized === "agentmessage" ||
|
|
1210
|
+
normalized === "assistantmessage" ||
|
|
1211
|
+
(normalized === "message" && item.role !== "user")) {
|
|
1212
|
+
const content = contentBlocksFromItem(item);
|
|
1213
|
+
const text = textFromBlocks(content);
|
|
1214
|
+
if (!content.length && !text)
|
|
1215
|
+
return undefined;
|
|
1216
|
+
return {
|
|
1217
|
+
...base,
|
|
1218
|
+
type: "message",
|
|
1219
|
+
kind: "chat",
|
|
1220
|
+
role: item.role === "system" ? "system" : "assistant",
|
|
1221
|
+
content,
|
|
1222
|
+
text,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1226
|
+
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1227
|
+
stringifyDefined(item.contentItems ?? item.summary);
|
|
1228
|
+
if (!text)
|
|
1229
|
+
return undefined;
|
|
1230
|
+
return {
|
|
1231
|
+
...base,
|
|
1232
|
+
type: "status",
|
|
1233
|
+
kind: "thinking",
|
|
1234
|
+
role: "system",
|
|
1235
|
+
text,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (normalized === "enteredreviewmode" || normalized === "exitedreviewmode") {
|
|
1239
|
+
const text = firstString(item, ["review", "text", "content", "summary", "message"]);
|
|
1240
|
+
return {
|
|
1241
|
+
...base,
|
|
1242
|
+
type: "status",
|
|
1243
|
+
kind: "review",
|
|
1244
|
+
role: "system",
|
|
1245
|
+
text: text ?? (normalized === "enteredreviewmode" ? "正在审查" : "审查已完成"),
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (normalized === "contextcompaction") {
|
|
1249
|
+
return {
|
|
1250
|
+
...base,
|
|
1251
|
+
type: "status",
|
|
1252
|
+
kind: "context_compaction",
|
|
1253
|
+
role: "system",
|
|
1254
|
+
text: firstString(item, ["text", "summary", "message"]) ?? "上下文已压缩",
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
if (isSubagentItemType(itemType)) {
|
|
1258
|
+
const subagent = decodeSubagentAction(item, normalizeToolStatus(item.status, true));
|
|
1259
|
+
if (!subagent)
|
|
1260
|
+
return undefined;
|
|
1261
|
+
return {
|
|
1262
|
+
...base,
|
|
1263
|
+
type: "status",
|
|
1264
|
+
kind: "subagent_action",
|
|
1265
|
+
role: "system",
|
|
1266
|
+
text: summarizeSubagentAction(subagent),
|
|
1267
|
+
subagent,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
if (isToolItemType(itemType) || toolNameFromItem(item)) {
|
|
1271
|
+
const toolId = firstString(item, ["id", "itemId", "toolCallId"]) ?? itemId;
|
|
1272
|
+
const toolCall = {
|
|
1273
|
+
id: toolId,
|
|
1274
|
+
name: toolNameFromItem(item) ?? "工具",
|
|
1275
|
+
input: toolInputFromItem(item),
|
|
1276
|
+
output: normalized === "filechange" || normalized === "diff"
|
|
1277
|
+
? extractDiffText(item) ?? firstString(item, ["output", "summary"])
|
|
1278
|
+
: firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
1279
|
+
stringifyDefined(item.result ?? item.error ?? item.contentItems),
|
|
1280
|
+
createdAt,
|
|
1281
|
+
status: normalizeToolStatus(item.status, true),
|
|
1282
|
+
};
|
|
1283
|
+
const kind = toolCall.name.includes("文件") || normalized === "filechange" || normalized === "diff"
|
|
1284
|
+
? "file_change"
|
|
1285
|
+
: toolCall.name.includes("命令") || normalized === "commandexecution"
|
|
1286
|
+
? "command_execution"
|
|
1287
|
+
: "tool_activity";
|
|
1288
|
+
return {
|
|
1289
|
+
...base,
|
|
1290
|
+
id: `tool:${toolId}`,
|
|
1291
|
+
type: "tool_call",
|
|
1292
|
+
kind,
|
|
1293
|
+
itemId: toolId,
|
|
1294
|
+
toolCall,
|
|
1295
|
+
commandExecution: kind === "command_execution" ? commandExecutionFromTool(toolCall) : undefined,
|
|
1296
|
+
fileChange: kind === "file_change" ? fileChangeFromTool(toolCall) : undefined,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
return undefined;
|
|
1300
|
+
}
|
|
1301
|
+
function timelineItemsFromProviderThread(value, conversationId) {
|
|
1302
|
+
const items = turnItemsFromThread(threadFromProviderResult(value))
|
|
1303
|
+
.map((item, index) => timelineItemFromProviderItem(item, conversationId, index))
|
|
1304
|
+
.filter((item) => Boolean(item));
|
|
1305
|
+
const byId = new Map();
|
|
1306
|
+
for (const item of items) {
|
|
1307
|
+
byId.set(item.id, item);
|
|
1308
|
+
}
|
|
1309
|
+
return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).slice(-MAX_TIMELINE_ITEMS);
|
|
1310
|
+
}
|
|
1311
|
+
function previewFromTimelineItem(item) {
|
|
1312
|
+
if (item.error)
|
|
1313
|
+
return item.error;
|
|
1314
|
+
if (item.text)
|
|
1315
|
+
return item.text;
|
|
1316
|
+
if (item.content?.length)
|
|
1317
|
+
return textFromBlocks(item.content);
|
|
1318
|
+
if (item.toolCall)
|
|
1319
|
+
return `${item.toolCall.name} · ${item.toolCall.status}`;
|
|
1320
|
+
if (item.permission)
|
|
1321
|
+
return `需要授权 ${item.permission.toolName ?? ""}`.trim();
|
|
1322
|
+
if (item.subagent)
|
|
1323
|
+
return summarizeSubagentAction(item.subagent);
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1035
1326
|
export class AgentWorkspaceProxy {
|
|
1036
1327
|
input;
|
|
1037
1328
|
clients = new Map();
|
|
1038
1329
|
agentProtocols = new Map();
|
|
1039
1330
|
providerCapabilities = new Map();
|
|
1331
|
+
providerErrors = new Map();
|
|
1040
1332
|
initialized = false;
|
|
1041
1333
|
status = "unavailable";
|
|
1042
1334
|
error;
|
|
@@ -1046,6 +1338,9 @@ export class AgentWorkspaceProxy {
|
|
|
1046
1338
|
conversations = new Map();
|
|
1047
1339
|
conversationByAgentSessionId = new Map();
|
|
1048
1340
|
timelines = new Map();
|
|
1341
|
+
// Opaque codex turns cursor per conversation, pointing at OLDER history to
|
|
1342
|
+
// page through (captured at hydration, advanced on each history.request).
|
|
1343
|
+
historyCursors = new Map();
|
|
1049
1344
|
toolOutputBuffers = new Map();
|
|
1050
1345
|
pendingPermissions = new Map();
|
|
1051
1346
|
permissionWaiters = new Map();
|
|
@@ -1085,6 +1380,11 @@ export class AgentWorkspaceProxy {
|
|
|
1085
1380
|
this.sendSnapshot(payload.conversationId);
|
|
1086
1381
|
break;
|
|
1087
1382
|
}
|
|
1383
|
+
case "agent.v2.history.request": {
|
|
1384
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1385
|
+
await this.loadOlderHistory(payload);
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1088
1388
|
case "agent.v2.prompt": {
|
|
1089
1389
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1090
1390
|
await this.sendPrompt(payload);
|
|
@@ -1098,11 +1398,18 @@ export class AgentWorkspaceProxy {
|
|
|
1098
1398
|
case "agent.v2.cancel": {
|
|
1099
1399
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
1100
1400
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1401
|
+
if (!conversation)
|
|
1402
|
+
break;
|
|
1403
|
+
const turnId = this.currentTurnIds.get(payload.conversationId);
|
|
1404
|
+
if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
|
|
1405
|
+
this.rejectAgentAction(conversation, "无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。", conversation.status);
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1101
1408
|
this.cancelPendingPermissions(payload.conversationId);
|
|
1102
|
-
const cancelClient =
|
|
1409
|
+
const cancelClient = this.clientForProvider(conversation.provider);
|
|
1103
1410
|
cancelClient?.cancel({
|
|
1104
|
-
sessionId: conversation
|
|
1105
|
-
turnId
|
|
1411
|
+
sessionId: conversation.agentSessionId,
|
|
1412
|
+
turnId,
|
|
1106
1413
|
});
|
|
1107
1414
|
this.forgetCurrentTurn(payload.conversationId);
|
|
1108
1415
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
@@ -1130,9 +1437,38 @@ export class AgentWorkspaceProxy {
|
|
|
1130
1437
|
clientForProvider(provider) {
|
|
1131
1438
|
return this.clients.get(provider);
|
|
1132
1439
|
}
|
|
1440
|
+
shouldRecycleProviderClient(provider, error) {
|
|
1441
|
+
if (provider !== "codex")
|
|
1442
|
+
return false;
|
|
1443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1444
|
+
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);
|
|
1445
|
+
}
|
|
1446
|
+
async recycleProviderClient(provider, error) {
|
|
1447
|
+
if (!this.shouldRecycleProviderClient(provider, error))
|
|
1448
|
+
return undefined;
|
|
1449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1450
|
+
const existing = this.clients.get(provider);
|
|
1451
|
+
if (existing) {
|
|
1452
|
+
try {
|
|
1453
|
+
existing.stop();
|
|
1454
|
+
}
|
|
1455
|
+
catch { }
|
|
1456
|
+
}
|
|
1457
|
+
this.clients.delete(provider);
|
|
1458
|
+
this.agentProtocols.delete(provider);
|
|
1459
|
+
this.providerErrors.set(provider, message);
|
|
1460
|
+
return this.ensureProviderClient(provider);
|
|
1461
|
+
}
|
|
1133
1462
|
protocolForProvider(provider) {
|
|
1134
1463
|
return this.agentProtocols.get(provider);
|
|
1135
1464
|
}
|
|
1465
|
+
defaultModelForProvider(provider) {
|
|
1466
|
+
const capabilities = this.providerCapabilities.get(provider);
|
|
1467
|
+
const defaultModel = capabilities?.defaultModel?.trim();
|
|
1468
|
+
if (defaultModel)
|
|
1469
|
+
return defaultModel;
|
|
1470
|
+
return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
|
|
1471
|
+
}
|
|
1136
1472
|
async initialize() {
|
|
1137
1473
|
if (this.initialized)
|
|
1138
1474
|
return;
|
|
@@ -1197,6 +1533,7 @@ export class AgentWorkspaceProxy {
|
|
|
1197
1533
|
try {
|
|
1198
1534
|
const client = await tryCreateClient(resolved);
|
|
1199
1535
|
this.clients.set(provider, client);
|
|
1536
|
+
this.providerErrors.delete(provider);
|
|
1200
1537
|
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
1201
1538
|
this.status = "idle";
|
|
1202
1539
|
this.error = undefined;
|
|
@@ -1217,6 +1554,7 @@ export class AgentWorkspaceProxy {
|
|
|
1217
1554
|
};
|
|
1218
1555
|
const client = await tryCreateClient(fallback);
|
|
1219
1556
|
this.clients.set(provider, client);
|
|
1557
|
+
this.providerErrors.delete(provider);
|
|
1220
1558
|
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
1221
1559
|
this.status = "idle";
|
|
1222
1560
|
this.error = undefined;
|
|
@@ -1232,6 +1570,8 @@ export class AgentWorkspaceProxy {
|
|
|
1232
1570
|
if (this.input.verbose) {
|
|
1233
1571
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1234
1572
|
}
|
|
1573
|
+
this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1574
|
+
this.sendCapabilities();
|
|
1235
1575
|
return undefined;
|
|
1236
1576
|
}
|
|
1237
1577
|
}
|
|
@@ -1276,12 +1616,23 @@ export class AgentWorkspaceProxy {
|
|
|
1276
1616
|
await this.initialize();
|
|
1277
1617
|
for (const [provider, client] of this.clients) {
|
|
1278
1618
|
try {
|
|
1279
|
-
|
|
1619
|
+
let activeClient = client;
|
|
1620
|
+
let result;
|
|
1621
|
+
try {
|
|
1622
|
+
result = await activeClient.listSessions();
|
|
1623
|
+
}
|
|
1624
|
+
catch (error) {
|
|
1625
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
1626
|
+
if (!recovered)
|
|
1627
|
+
throw error;
|
|
1628
|
+
activeClient = recovered;
|
|
1629
|
+
result = await activeClient.listSessions();
|
|
1630
|
+
}
|
|
1280
1631
|
for (const remote of parseRemoteSessions(result)) {
|
|
1281
1632
|
const agentSessionId = remote.id;
|
|
1282
1633
|
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1283
1634
|
const now = Date.now();
|
|
1284
|
-
const conversationId = existingId ??
|
|
1635
|
+
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1285
1636
|
const existing = this.conversations.get(conversationId);
|
|
1286
1637
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1287
1638
|
const conversation = {
|
|
@@ -1312,6 +1663,103 @@ export class AgentWorkspaceProxy {
|
|
|
1312
1663
|
}
|
|
1313
1664
|
}
|
|
1314
1665
|
}
|
|
1666
|
+
async hydrateConversationFromProvider(conversation, seedResult) {
|
|
1667
|
+
let client = this.clientForProvider(conversation.provider);
|
|
1668
|
+
if (!client || !conversation.agentSessionId)
|
|
1669
|
+
return;
|
|
1670
|
+
let source = seedResult;
|
|
1671
|
+
const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
|
|
1672
|
+
if (!hasSeedTurns) {
|
|
1673
|
+
const readSession = client.readSession;
|
|
1674
|
+
if (typeof readSession === "function") {
|
|
1675
|
+
try {
|
|
1676
|
+
source = await readSession.call(client, {
|
|
1677
|
+
sessionId: conversation.agentSessionId,
|
|
1678
|
+
includeTurns: true,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
const recovered = await this.recycleProviderClient(conversation.provider, error);
|
|
1683
|
+
if (recovered)
|
|
1684
|
+
client = recovered;
|
|
1685
|
+
if (this.input.verbose) {
|
|
1686
|
+
process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
|
|
1691
|
+
const listTurns = client.listTurns;
|
|
1692
|
+
if (typeof listTurns === "function") {
|
|
1693
|
+
try {
|
|
1694
|
+
let turnsResult;
|
|
1695
|
+
try {
|
|
1696
|
+
turnsResult = await listTurns.call(client, {
|
|
1697
|
+
sessionId: conversation.agentSessionId,
|
|
1698
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1699
|
+
sortDirection: "desc",
|
|
1700
|
+
itemsView: "full",
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
catch (error) {
|
|
1704
|
+
const recovered = await this.recycleProviderClient(conversation.provider, error);
|
|
1705
|
+
if (!recovered)
|
|
1706
|
+
throw error;
|
|
1707
|
+
client = recovered;
|
|
1708
|
+
const recoveredListTurns = client.listTurns;
|
|
1709
|
+
if (typeof recoveredListTurns !== "function")
|
|
1710
|
+
throw error;
|
|
1711
|
+
turnsResult = await recoveredListTurns.call(client, {
|
|
1712
|
+
sessionId: conversation.agentSessionId,
|
|
1713
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1714
|
+
sortDirection: "desc",
|
|
1715
|
+
itemsView: "full",
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
1719
|
+
if (turnsThread)
|
|
1720
|
+
source = { thread: turnsThread };
|
|
1721
|
+
// Capture the opaque cursor pointing at OLDER turns so the client
|
|
1722
|
+
// can page further back via agent.v2.history.request.
|
|
1723
|
+
const olderCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
|
|
1724
|
+
this.historyCursors.set(conversation.id, olderCursor);
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
if (this.input.verbose) {
|
|
1728
|
+
process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const thread = threadFromProviderResult(source);
|
|
1735
|
+
const activeTurnId = activeTurnIdFromThread(thread);
|
|
1736
|
+
if (activeTurnId)
|
|
1737
|
+
this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
1738
|
+
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
1739
|
+
if (model && !conversation.model)
|
|
1740
|
+
conversation.model = model;
|
|
1741
|
+
const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
1742
|
+
if (hydratedItems.length === 0)
|
|
1743
|
+
return;
|
|
1744
|
+
const existing = this.timelines.get(conversation.id) ?? [];
|
|
1745
|
+
const merged = new Map();
|
|
1746
|
+
for (const item of [...existing, ...hydratedItems]) {
|
|
1747
|
+
merged.set(item.id, item);
|
|
1748
|
+
}
|
|
1749
|
+
const nextItems = [...merged.values()]
|
|
1750
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
1751
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
1752
|
+
this.timelines.set(conversation.id, nextItems);
|
|
1753
|
+
for (const item of nextItems) {
|
|
1754
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
1755
|
+
}
|
|
1756
|
+
const lastPreview = [...nextItems].reverse()
|
|
1757
|
+
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
1758
|
+
.find(Boolean);
|
|
1759
|
+
if (lastPreview && !conversation.lastMessagePreview) {
|
|
1760
|
+
conversation.lastMessagePreview = lastPreview;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1315
1763
|
sendCapabilities() {
|
|
1316
1764
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1317
1765
|
const client = this.clients.get(provider);
|
|
@@ -1328,7 +1776,7 @@ export class AgentWorkspaceProxy {
|
|
|
1328
1776
|
id: provider,
|
|
1329
1777
|
label: providerLabel(provider),
|
|
1330
1778
|
enabled,
|
|
1331
|
-
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1779
|
+
reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
|
|
1332
1780
|
supportsImages,
|
|
1333
1781
|
supportsPermission,
|
|
1334
1782
|
supportsPlan: enabled,
|
|
@@ -1385,18 +1833,70 @@ export class AgentWorkspaceProxy {
|
|
|
1385
1833
|
if (!this.input.availableProviders.includes(provider)) {
|
|
1386
1834
|
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
1387
1835
|
}
|
|
1388
|
-
|
|
1389
|
-
if (!client) {
|
|
1390
|
-
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
1391
|
-
}
|
|
1392
|
-
const cwd = payload.cwd ?? this.input.cwd;
|
|
1836
|
+
let cwd = payload.cwd ?? this.input.cwd;
|
|
1393
1837
|
let agentSessionId = payload.agentSessionId;
|
|
1394
1838
|
let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
1395
1839
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
1840
|
+
if (!payload.cwd && existingConversation?.cwd)
|
|
1841
|
+
cwd = existingConversation.cwd;
|
|
1842
|
+
let client = await this.ensureProviderClient(provider);
|
|
1843
|
+
if (!client) {
|
|
1844
|
+
const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
|
|
1845
|
+
if (existingConversation) {
|
|
1846
|
+
this.activeConversationId = existingConversation.id;
|
|
1847
|
+
this.input.send(createEnvelope({
|
|
1848
|
+
type: "agent.v2.conversation.opened",
|
|
1849
|
+
sessionId: this.input.sessionId,
|
|
1850
|
+
payload: {
|
|
1851
|
+
conversation: existingConversation,
|
|
1852
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1853
|
+
requestedConversationId: payload.conversationId,
|
|
1854
|
+
providerError: message,
|
|
1855
|
+
},
|
|
1856
|
+
}));
|
|
1857
|
+
this.sendCapabilities();
|
|
1858
|
+
return existingConversation;
|
|
1859
|
+
}
|
|
1860
|
+
if (payload.conversationId) {
|
|
1861
|
+
return this.openOfflineHistory(payload, message, cwd);
|
|
1862
|
+
}
|
|
1863
|
+
return this.openFailure(payload, message, cwd);
|
|
1864
|
+
}
|
|
1396
1865
|
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
1397
|
-
|
|
1866
|
+
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
1867
|
+
if (payload.conversationId &&
|
|
1868
|
+
payload.conversationId === requestedCanonicalId &&
|
|
1869
|
+
existingConversation.id !== payload.conversationId) {
|
|
1398
1870
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1399
1871
|
}
|
|
1872
|
+
if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
|
|
1873
|
+
try {
|
|
1874
|
+
const existingAgentSessionId = existingConversation.agentSessionId;
|
|
1875
|
+
let result;
|
|
1876
|
+
try {
|
|
1877
|
+
result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
1878
|
+
}
|
|
1879
|
+
catch (error) {
|
|
1880
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
1881
|
+
if (!recovered)
|
|
1882
|
+
throw error;
|
|
1883
|
+
client = recovered;
|
|
1884
|
+
result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
1885
|
+
}
|
|
1886
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
1887
|
+
if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
|
|
1888
|
+
this.conversationByAgentSessionId.delete(existingAgentSessionId);
|
|
1889
|
+
existingConversation.agentSessionId = nextAgentSessionId;
|
|
1890
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
|
|
1891
|
+
}
|
|
1892
|
+
await this.hydrateConversationFromProvider(existingConversation, result);
|
|
1893
|
+
}
|
|
1894
|
+
catch (error) {
|
|
1895
|
+
if (this.input.verbose) {
|
|
1896
|
+
process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1400
1900
|
this.activeConversationId = existingConversation.id;
|
|
1401
1901
|
this.input.send(createEnvelope({
|
|
1402
1902
|
type: "agent.v2.conversation.opened",
|
|
@@ -1404,17 +1904,30 @@ export class AgentWorkspaceProxy {
|
|
|
1404
1904
|
payload: {
|
|
1405
1905
|
conversation: existingConversation,
|
|
1406
1906
|
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1907
|
+
requestedConversationId: payload.conversationId,
|
|
1407
1908
|
},
|
|
1408
1909
|
}));
|
|
1409
1910
|
return existingConversation;
|
|
1410
1911
|
}
|
|
1411
1912
|
try {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1913
|
+
let result;
|
|
1914
|
+
try {
|
|
1915
|
+
result = agentSessionId
|
|
1916
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
1917
|
+
: await client.newSession({ cwd });
|
|
1918
|
+
}
|
|
1919
|
+
catch (error) {
|
|
1920
|
+
const recovered = await this.recycleProviderClient(provider, error);
|
|
1921
|
+
if (!recovered)
|
|
1922
|
+
throw error;
|
|
1923
|
+
client = recovered;
|
|
1924
|
+
result = agentSessionId
|
|
1925
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
1926
|
+
: await client.newSession({ cwd });
|
|
1927
|
+
}
|
|
1415
1928
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
1416
1929
|
const now = Date.now();
|
|
1417
|
-
const conversationId =
|
|
1930
|
+
const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1418
1931
|
const conversation = {
|
|
1419
1932
|
...existingConversation,
|
|
1420
1933
|
id: conversationId,
|
|
@@ -1436,10 +1949,15 @@ export class AgentWorkspaceProxy {
|
|
|
1436
1949
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1437
1950
|
this.activeConversationId = conversation.id;
|
|
1438
1951
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1952
|
+
await this.hydrateConversationFromProvider(conversation, result);
|
|
1439
1953
|
this.input.send(createEnvelope({
|
|
1440
1954
|
type: "agent.v2.conversation.opened",
|
|
1441
1955
|
sessionId: this.input.sessionId,
|
|
1442
|
-
payload: {
|
|
1956
|
+
payload: {
|
|
1957
|
+
conversation,
|
|
1958
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
1959
|
+
requestedConversationId: payload.conversationId,
|
|
1960
|
+
},
|
|
1443
1961
|
}));
|
|
1444
1962
|
return conversation;
|
|
1445
1963
|
}
|
|
@@ -1478,8 +1996,47 @@ export class AgentWorkspaceProxy {
|
|
|
1478
1996
|
this.input.send(createEnvelope({
|
|
1479
1997
|
type: "agent.v2.conversation.opened",
|
|
1480
1998
|
sessionId: this.input.sessionId,
|
|
1481
|
-
payload: {
|
|
1999
|
+
payload: {
|
|
2000
|
+
conversation,
|
|
2001
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2002
|
+
requestedConversationId: payload.conversationId,
|
|
2003
|
+
},
|
|
2004
|
+
}));
|
|
2005
|
+
return conversation;
|
|
2006
|
+
}
|
|
2007
|
+
openOfflineHistory(payload, message, cwd = payload.cwd ?? this.input.cwd) {
|
|
2008
|
+
const now = Date.now();
|
|
2009
|
+
const conversation = {
|
|
2010
|
+
id: payload.conversationId,
|
|
2011
|
+
agentSessionId: payload.agentSessionId,
|
|
2012
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
2013
|
+
cwd,
|
|
2014
|
+
title: payload.title ?? titleFromCwd(cwd),
|
|
2015
|
+
model: payload.model,
|
|
2016
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2017
|
+
permissionMode: payload.permissionMode,
|
|
2018
|
+
collaborationMode: payload.collaborationMode,
|
|
2019
|
+
status: "idle",
|
|
2020
|
+
archived: false,
|
|
2021
|
+
lastActivityAt: now,
|
|
2022
|
+
createdAt: now,
|
|
2023
|
+
};
|
|
2024
|
+
this.conversations.set(conversation.id, conversation);
|
|
2025
|
+
if (conversation.agentSessionId) {
|
|
2026
|
+
this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
|
|
2027
|
+
}
|
|
2028
|
+
this.activeConversationId = conversation.id;
|
|
2029
|
+
this.input.send(createEnvelope({
|
|
2030
|
+
type: "agent.v2.conversation.opened",
|
|
2031
|
+
sessionId: this.input.sessionId,
|
|
2032
|
+
payload: {
|
|
2033
|
+
conversation,
|
|
2034
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
2035
|
+
requestedConversationId: payload.conversationId,
|
|
2036
|
+
providerError: message,
|
|
2037
|
+
},
|
|
1482
2038
|
}));
|
|
2039
|
+
this.sendCapabilities();
|
|
1483
2040
|
return conversation;
|
|
1484
2041
|
}
|
|
1485
2042
|
async sendPrompt(payload) {
|
|
@@ -1488,60 +2045,62 @@ export class AgentWorkspaceProxy {
|
|
|
1488
2045
|
if (!conversation)
|
|
1489
2046
|
return;
|
|
1490
2047
|
if (!conversation.agentSessionId) {
|
|
1491
|
-
this.
|
|
1492
|
-
id: id("error"),
|
|
1493
|
-
conversationId: payload.conversationId,
|
|
1494
|
-
type: "error",
|
|
1495
|
-
error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
1496
|
-
createdAt: Date.now(),
|
|
1497
|
-
});
|
|
2048
|
+
this.rejectAgentAction(conversation, "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
|
|
1498
2049
|
return;
|
|
1499
2050
|
}
|
|
1500
|
-
const client = this.clientForProvider(conversation.provider);
|
|
2051
|
+
const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
|
|
1501
2052
|
if (!client) {
|
|
1502
|
-
this.
|
|
1503
|
-
id: id("error"),
|
|
1504
|
-
conversationId: conversation.id,
|
|
1505
|
-
type: "error",
|
|
1506
|
-
error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
|
|
1507
|
-
createdAt: Date.now(),
|
|
1508
|
-
});
|
|
2053
|
+
this.rejectAgentAction(conversation, this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
|
|
1509
2054
|
return;
|
|
1510
2055
|
}
|
|
1511
2056
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
1512
2057
|
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
|
|
1513
|
-
conversation
|
|
1514
|
-
|
|
1515
|
-
|
|
2058
|
+
this.rejectAgentAction(conversation, "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。");
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const priorSettings = {
|
|
2062
|
+
model: conversation.model,
|
|
2063
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
2064
|
+
permissionMode: conversation.permissionMode,
|
|
2065
|
+
};
|
|
2066
|
+
const effectiveModel = payload.model ??
|
|
2067
|
+
conversation.model ??
|
|
2068
|
+
(protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
|
|
2069
|
+
const wasRunning = conversation.status === "running";
|
|
2070
|
+
const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
|
|
2071
|
+
const shouldSteer = Boolean(payload.delivery !== "new_turn" &&
|
|
2072
|
+
wasRunning &&
|
|
2073
|
+
protocol === "codex-app-server" &&
|
|
2074
|
+
activeTurnId);
|
|
2075
|
+
if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
|
|
1516
2076
|
this.addItem(conversation.id, {
|
|
1517
2077
|
id: id("error"),
|
|
1518
2078
|
conversationId: conversation.id,
|
|
1519
2079
|
type: "error",
|
|
1520
|
-
error:
|
|
2080
|
+
error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
|
|
1521
2081
|
createdAt: Date.now(),
|
|
1522
2082
|
});
|
|
1523
2083
|
return;
|
|
1524
2084
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
conversation.model = payload.model ?? conversation.model;
|
|
2085
|
+
if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
|
|
2086
|
+
this.rejectAgentAction(conversation, "当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。");
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
conversation.model = effectiveModel ?? conversation.model;
|
|
1531
2090
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1532
2091
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1533
2092
|
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1534
2093
|
conversation.status = "running";
|
|
1535
2094
|
conversation.lastActivityAt = Date.now();
|
|
1536
2095
|
this.activeConversationId = conversation.id;
|
|
1537
|
-
if (
|
|
2096
|
+
if (effectiveModel && effectiveModel !== priorSettings.model) {
|
|
1538
2097
|
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1539
|
-
const label = runtimeCapabilities?.models?.find((m) => m.id ===
|
|
2098
|
+
const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
|
|
1540
2099
|
this.emitNotice({
|
|
1541
2100
|
conversationId: conversation.id,
|
|
1542
2101
|
kind: "model_changed",
|
|
1543
2102
|
title: `已切换模型 · ${label}`,
|
|
1544
|
-
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${
|
|
2103
|
+
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
|
|
1545
2104
|
});
|
|
1546
2105
|
}
|
|
1547
2106
|
if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
|
|
@@ -1566,20 +2125,61 @@ export class AgentWorkspaceProxy {
|
|
|
1566
2125
|
role: "user",
|
|
1567
2126
|
content: payload.contentBlocks,
|
|
1568
2127
|
text: userText,
|
|
2128
|
+
metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
|
|
1569
2129
|
createdAt: Date.now(),
|
|
1570
2130
|
});
|
|
1571
2131
|
this.emitConversation(conversation);
|
|
2132
|
+
const promptAsNewTurn = () => client.prompt({
|
|
2133
|
+
sessionId: conversation.agentSessionId,
|
|
2134
|
+
content: payload.contentBlocks,
|
|
2135
|
+
clientMessageId: payload.clientMessageId,
|
|
2136
|
+
model: effectiveModel,
|
|
2137
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2138
|
+
permissionMode: payload.permissionMode,
|
|
2139
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
2140
|
+
cwd: conversation.cwd,
|
|
2141
|
+
});
|
|
1572
2142
|
try {
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2143
|
+
const steer = client.steer;
|
|
2144
|
+
let result;
|
|
2145
|
+
if (shouldSteer && typeof steer === "function") {
|
|
2146
|
+
try {
|
|
2147
|
+
result = await steer.call(client, {
|
|
2148
|
+
sessionId: conversation.agentSessionId,
|
|
2149
|
+
turnId: activeTurnId,
|
|
2150
|
+
content: payload.contentBlocks,
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
catch (steerError) {
|
|
2154
|
+
this.forgetCurrentTurn(conversation.id, activeTurnId);
|
|
2155
|
+
const userItem = this.findItem(conversation.id, payload.clientMessageId);
|
|
2156
|
+
if (userItem) {
|
|
2157
|
+
this.upsertItem(conversation.id, {
|
|
2158
|
+
...userItem,
|
|
2159
|
+
metadata: {
|
|
2160
|
+
...(userItem.metadata ?? {}),
|
|
2161
|
+
delivery: "new_turn",
|
|
2162
|
+
fallbackFrom: "steer",
|
|
2163
|
+
failedTargetTurnId: activeTurnId,
|
|
2164
|
+
},
|
|
2165
|
+
updatedAt: Date.now(),
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
this.addItem(conversation.id, {
|
|
2169
|
+
id: id("status"),
|
|
2170
|
+
conversationId: conversation.id,
|
|
2171
|
+
type: "status",
|
|
2172
|
+
role: "system",
|
|
2173
|
+
text: "Codex 未接收运行中追加输入,已改为发送新消息。",
|
|
2174
|
+
status: "running",
|
|
2175
|
+
createdAt: Date.now(),
|
|
2176
|
+
});
|
|
2177
|
+
result = await promptAsNewTurn();
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
result = await promptAsNewTurn();
|
|
2182
|
+
}
|
|
1583
2183
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
1584
2184
|
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1585
2185
|
if (conversation.agentSessionId)
|
|
@@ -1784,6 +2384,9 @@ export class AgentWorkspaceProxy {
|
|
|
1784
2384
|
}
|
|
1785
2385
|
}
|
|
1786
2386
|
handleRequest(method, params) {
|
|
2387
|
+
if (method === "claude/askUserQuestion") {
|
|
2388
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
2389
|
+
}
|
|
1787
2390
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1788
2391
|
return this.handleStructuredInput(params, true);
|
|
1789
2392
|
}
|
|
@@ -1829,13 +2432,30 @@ export class AgentWorkspaceProxy {
|
|
|
1829
2432
|
}
|
|
1830
2433
|
if (method.startsWith("account/") ||
|
|
1831
2434
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
1832
|
-
method === "thread/status/changed" ||
|
|
1833
2435
|
method === "thread/tokenUsage/updated" ||
|
|
1834
2436
|
method === "serverRequest/resolved" ||
|
|
1835
2437
|
method === "mcpServer/oauthLogin/completed") {
|
|
1836
2438
|
return;
|
|
1837
2439
|
}
|
|
1838
2440
|
const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
|
|
2441
|
+
if (method === "thread/status/changed") {
|
|
2442
|
+
const status = agentStatusFromThreadStatus(params);
|
|
2443
|
+
if (conversationId && status) {
|
|
2444
|
+
const raw = asRecord(params);
|
|
2445
|
+
const turnId = this.extractTurnId(raw);
|
|
2446
|
+
if (turnId && (status === "running" || status === "waiting_permission")) {
|
|
2447
|
+
this.rememberTurnConversationId(conversationId, turnId);
|
|
2448
|
+
}
|
|
2449
|
+
if (status === "idle" || status === "error") {
|
|
2450
|
+
this.forgetCurrentTurn(conversationId, turnId);
|
|
2451
|
+
}
|
|
2452
|
+
const message = status === "error"
|
|
2453
|
+
? firstString(raw, ["message", "error", "reason"])
|
|
2454
|
+
: undefined;
|
|
2455
|
+
this.updateConversationStatus(conversationId, status, message);
|
|
2456
|
+
}
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
1839
2459
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1840
2460
|
this.handleStructuredInput(params);
|
|
1841
2461
|
return;
|
|
@@ -2345,9 +2965,7 @@ export class AgentWorkspaceProxy {
|
|
|
2345
2965
|
const raw = asRecord(params) ?? {};
|
|
2346
2966
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
2347
2967
|
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
2348
|
-
const formatResponse = source
|
|
2349
|
-
? formatMcpElicitationResponse
|
|
2350
|
-
: formatStructuredInputResponse;
|
|
2968
|
+
const formatResponse = (answers, input) => formatStructuredInputResponseForSource(source, answers, input);
|
|
2351
2969
|
if (!conversationId)
|
|
2352
2970
|
return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
2353
2971
|
const structuredInput = decodeStructuredInput(raw);
|
|
@@ -2374,13 +2992,13 @@ export class AgentWorkspaceProxy {
|
|
|
2374
2992
|
const timer = setTimeout(() => {
|
|
2375
2993
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
2376
2994
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
2377
|
-
resolve(formatResponse({}));
|
|
2995
|
+
resolve(formatResponse({}, structuredInput));
|
|
2378
2996
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
2379
2997
|
inputPending: false,
|
|
2380
2998
|
inputError: "等待用户输入超时",
|
|
2381
2999
|
});
|
|
2382
3000
|
}, PERMISSION_TIMEOUT_MS);
|
|
2383
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
3001
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
|
|
2384
3002
|
});
|
|
2385
3003
|
}
|
|
2386
3004
|
toolCallFromItem(item, fallbackStatus) {
|
|
@@ -2414,11 +3032,19 @@ export class AgentWorkspaceProxy {
|
|
|
2414
3032
|
return waitForResponse ? Promise.resolve({ outcome: { outcome: "cancelled" } }) : undefined;
|
|
2415
3033
|
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
2416
3034
|
const rawToolCall = asRecord(raw.toolCall) ?? raw;
|
|
3035
|
+
// Codex app-server command/file approvals carry `command`/`reason`/`cwd`
|
|
3036
|
+
// (not toolCall/context). Surface them so the card actually shows what's
|
|
3037
|
+
// being approved; fall back to the Claude/MCP shape otherwise.
|
|
3038
|
+
const codexCommand = firstString(raw, ["command"]);
|
|
3039
|
+
const codexCwd = firstString(raw, ["cwd"]);
|
|
2417
3040
|
const permission = {
|
|
2418
3041
|
requestId,
|
|
2419
|
-
toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"])
|
|
2420
|
-
|
|
2421
|
-
|
|
3042
|
+
toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]) ??
|
|
3043
|
+
(codexCommand ? "shell" : undefined),
|
|
3044
|
+
toolInput: codexCommand
|
|
3045
|
+
? (codexCwd ? `$ ${codexCommand}\n# cwd: ${codexCwd}` : `$ ${codexCommand}`)
|
|
3046
|
+
: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
3047
|
+
context: firstString(raw, ["context", "description", "message", "title", "reason"]),
|
|
2422
3048
|
options: parsePermissionOptions(raw.options),
|
|
2423
3049
|
};
|
|
2424
3050
|
this.pendingPermissions.set(requestId, permission);
|
|
@@ -2430,6 +3056,12 @@ export class AgentWorkspaceProxy {
|
|
|
2430
3056
|
conversationId,
|
|
2431
3057
|
type: "permission",
|
|
2432
3058
|
permission,
|
|
3059
|
+
metadata: {
|
|
3060
|
+
protocol: "v2",
|
|
3061
|
+
permissionLive: true,
|
|
3062
|
+
permissionExpired: false,
|
|
3063
|
+
permissionPending: false,
|
|
3064
|
+
},
|
|
2433
3065
|
createdAt: Date.now(),
|
|
2434
3066
|
updatedAt: Date.now(),
|
|
2435
3067
|
};
|
|
@@ -2454,9 +3086,13 @@ export class AgentWorkspaceProxy {
|
|
|
2454
3086
|
}
|
|
2455
3087
|
respondPermission(payload) {
|
|
2456
3088
|
const permission = this.pendingPermissions.get(payload.requestId);
|
|
3089
|
+
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
3090
|
+
const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
|
|
3091
|
+
const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
|
|
3092
|
+
if (!permission && !waiter && alreadyResolved)
|
|
3093
|
+
return;
|
|
2457
3094
|
this.pendingPermissions.delete(payload.requestId);
|
|
2458
3095
|
const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
2459
|
-
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
2460
3096
|
if (waiter) {
|
|
2461
3097
|
clearTimeout(waiter.timer);
|
|
2462
3098
|
this.permissionWaiters.delete(payload.requestId);
|
|
@@ -2479,19 +3115,22 @@ export class AgentWorkspaceProxy {
|
|
|
2479
3115
|
permissionError: undefined,
|
|
2480
3116
|
permissionPending: false,
|
|
2481
3117
|
});
|
|
2482
|
-
|
|
3118
|
+
if (permission || waiter || existingItem) {
|
|
3119
|
+
this.updateConversationStatus(payload.conversationId, "running");
|
|
3120
|
+
}
|
|
2483
3121
|
}
|
|
2484
3122
|
respondStructuredInput(payload) {
|
|
2485
3123
|
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
2486
|
-
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2487
3124
|
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
3125
|
+
const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
|
|
3126
|
+
const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
|
|
3127
|
+
if (!pending && !waiter && alreadySubmitted)
|
|
3128
|
+
return;
|
|
3129
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2488
3130
|
if (waiter) {
|
|
2489
3131
|
clearTimeout(waiter.timer);
|
|
2490
3132
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
2491
|
-
|
|
2492
|
-
? formatMcpElicitationResponse
|
|
2493
|
-
: formatStructuredInputResponse;
|
|
2494
|
-
waiter.resolve(formatResponse(payload.answers));
|
|
3133
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
|
|
2495
3134
|
}
|
|
2496
3135
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
2497
3136
|
inputPending: false,
|
|
@@ -2500,7 +3139,9 @@ export class AgentWorkspaceProxy {
|
|
|
2500
3139
|
inputError: undefined,
|
|
2501
3140
|
answers: payload.answers,
|
|
2502
3141
|
});
|
|
2503
|
-
|
|
3142
|
+
if (pending || waiter || existingItem) {
|
|
3143
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
3144
|
+
}
|
|
2504
3145
|
}
|
|
2505
3146
|
markPermission(conversationId, requestId, metadata) {
|
|
2506
3147
|
const item = this.findItem(conversationId, `permission:${requestId}`);
|
|
@@ -2675,14 +3316,14 @@ export class AgentWorkspaceProxy {
|
|
|
2675
3316
|
this.input.send(createEnvelope({
|
|
2676
3317
|
type: "agent.v2.event",
|
|
2677
3318
|
sessionId: this.input.sessionId,
|
|
2678
|
-
payload: { conversationId, conversation, item },
|
|
3319
|
+
payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
|
|
2679
3320
|
}));
|
|
2680
3321
|
}
|
|
2681
3322
|
emitConversation(conversation) {
|
|
2682
3323
|
this.input.send(createEnvelope({
|
|
2683
3324
|
type: "agent.v2.event",
|
|
2684
3325
|
sessionId: this.input.sessionId,
|
|
2685
|
-
payload: { conversationId: conversation.id, conversation },
|
|
3326
|
+
payload: { conversationId: conversation.id, conversation: { ...conversation } },
|
|
2686
3327
|
}));
|
|
2687
3328
|
}
|
|
2688
3329
|
emitStatus(conversationId, status, text) {
|
|
@@ -2702,6 +3343,16 @@ export class AgentWorkspaceProxy {
|
|
|
2702
3343
|
payload: input,
|
|
2703
3344
|
}));
|
|
2704
3345
|
}
|
|
3346
|
+
rejectAgentAction(conversation, message, status = "idle") {
|
|
3347
|
+
this.updateConversationStatus(conversation.id, status, message);
|
|
3348
|
+
this.addItem(conversation.id, {
|
|
3349
|
+
id: id("error"),
|
|
3350
|
+
conversationId: conversation.id,
|
|
3351
|
+
type: "error",
|
|
3352
|
+
error: message,
|
|
3353
|
+
createdAt: Date.now(),
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
2705
3356
|
updateConversationPreview(conversationId, text, status) {
|
|
2706
3357
|
const conversation = this.conversations.get(conversationId);
|
|
2707
3358
|
if (!conversation)
|
|
@@ -2737,6 +3388,58 @@ export class AgentWorkspaceProxy {
|
|
|
2737
3388
|
},
|
|
2738
3389
|
}));
|
|
2739
3390
|
}
|
|
3391
|
+
/** Page OLDER transcript history for a conversation (client scrolling up).
|
|
3392
|
+
* Only Codex's app-server exposes paginated reads; other providers report
|
|
3393
|
+
* no more history. Pure pass-through — does not touch the bounded live
|
|
3394
|
+
* timeline. The client prepends the returned (ascending) items. */
|
|
3395
|
+
async loadOlderHistory(payload) {
|
|
3396
|
+
const sendResult = (items, nextCursor) => {
|
|
3397
|
+
this.input.send(createEnvelope({
|
|
3398
|
+
type: "agent.v2.history.result",
|
|
3399
|
+
sessionId: this.input.sessionId,
|
|
3400
|
+
payload: {
|
|
3401
|
+
conversationId: payload.conversationId,
|
|
3402
|
+
items,
|
|
3403
|
+
nextCursor,
|
|
3404
|
+
hasMore: Boolean(nextCursor),
|
|
3405
|
+
},
|
|
3406
|
+
}));
|
|
3407
|
+
};
|
|
3408
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3409
|
+
if (!conversation?.agentSessionId)
|
|
3410
|
+
return sendResult([], undefined);
|
|
3411
|
+
const client = this.clientForProvider(conversation.provider);
|
|
3412
|
+
const listTurns = client?.listTurns;
|
|
3413
|
+
if (typeof listTurns !== "function")
|
|
3414
|
+
return sendResult([], undefined);
|
|
3415
|
+
// Client-supplied cursor wins; else the one captured at hydration. Absent
|
|
3416
|
+
// cursor means we're already at the start of history.
|
|
3417
|
+
const cursor = payload.cursor ?? this.historyCursors.get(payload.conversationId);
|
|
3418
|
+
if (!cursor)
|
|
3419
|
+
return sendResult([], undefined);
|
|
3420
|
+
try {
|
|
3421
|
+
const turnsResult = await listTurns.call(client, {
|
|
3422
|
+
sessionId: conversation.agentSessionId,
|
|
3423
|
+
limit: payload.limit,
|
|
3424
|
+
cursor,
|
|
3425
|
+
sortDirection: "desc",
|
|
3426
|
+
itemsView: "full",
|
|
3427
|
+
});
|
|
3428
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
3429
|
+
const items = turnsThread
|
|
3430
|
+
? timelineItemsFromProviderThread({ thread: turnsThread }, payload.conversationId)
|
|
3431
|
+
: [];
|
|
3432
|
+
const nextCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
|
|
3433
|
+
this.historyCursors.set(payload.conversationId, nextCursor);
|
|
3434
|
+
sendResult(items, nextCursor);
|
|
3435
|
+
}
|
|
3436
|
+
catch (error) {
|
|
3437
|
+
if (this.input.verbose) {
|
|
3438
|
+
process.stderr.write(`[agent:v2] history page failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3439
|
+
}
|
|
3440
|
+
sendResult([], undefined);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
2740
3443
|
conversationIdFromParams(params) {
|
|
2741
3444
|
const raw = asRecord(params);
|
|
2742
3445
|
if (!raw)
|
|
@@ -2823,13 +3526,23 @@ export class AgentWorkspaceProxy {
|
|
|
2823
3526
|
handleProviderExit(provider, message) {
|
|
2824
3527
|
this.clients.delete(provider);
|
|
2825
3528
|
this.agentProtocols.delete(provider);
|
|
2826
|
-
this.
|
|
3529
|
+
this.providerErrors.set(provider, message);
|
|
3530
|
+
for (const conversation of this.conversations.values()) {
|
|
3531
|
+
if (conversation.provider === provider && conversation.status === "waiting_permission") {
|
|
3532
|
+
this.cancelPendingPermissions(conversation.id, false);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
2827
3535
|
for (const conversation of this.conversations.values()) {
|
|
2828
3536
|
if (conversation.provider !== provider)
|
|
2829
3537
|
continue;
|
|
3538
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
|
|
3539
|
+
this.emitConversation(conversation);
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
2830
3542
|
conversation.status = "error";
|
|
2831
3543
|
conversation.lastMessagePreview = message;
|
|
2832
3544
|
conversation.lastActivityAt = Date.now();
|
|
3545
|
+
this.forgetCurrentTurn(conversation.id);
|
|
2833
3546
|
this.emitConversation(conversation);
|
|
2834
3547
|
this.addItem(conversation.id, {
|
|
2835
3548
|
id: id("error"),
|
|
@@ -2841,18 +3554,23 @@ export class AgentWorkspaceProxy {
|
|
|
2841
3554
|
}
|
|
2842
3555
|
this.sendCapabilities();
|
|
2843
3556
|
}
|
|
2844
|
-
cancelPendingPermissions(conversationId) {
|
|
3557
|
+
cancelPendingPermissions(conversationId, updateStatus = true) {
|
|
2845
3558
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3559
|
+
if (conversationId && this.itemConversationIds.get(requestId) !== conversationId)
|
|
3560
|
+
continue;
|
|
2846
3561
|
clearTimeout(waiter.timer);
|
|
2847
3562
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
2848
3563
|
this.pendingPermissions.delete(requestId);
|
|
2849
3564
|
this.permissionSources.delete(requestId);
|
|
2850
3565
|
}
|
|
2851
|
-
|
|
3566
|
+
if (!conversationId)
|
|
3567
|
+
this.permissionWaiters.clear();
|
|
2852
3568
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
2853
|
-
clearTimeout(waiter.timer);
|
|
2854
|
-
waiter.resolve(formatStructuredInputResponse({}));
|
|
2855
3569
|
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3570
|
+
if (conversationId && pending?.conversationId !== conversationId)
|
|
3571
|
+
continue;
|
|
3572
|
+
clearTimeout(waiter.timer);
|
|
3573
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
|
|
2856
3574
|
if (pending) {
|
|
2857
3575
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
2858
3576
|
inputPending: false,
|
|
@@ -2860,9 +3578,11 @@ export class AgentWorkspaceProxy {
|
|
|
2860
3578
|
});
|
|
2861
3579
|
}
|
|
2862
3580
|
this.pendingStructuredInputs.delete(requestId);
|
|
3581
|
+
this.structuredInputWaiters.delete(requestId);
|
|
2863
3582
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
3583
|
+
if (!conversationId)
|
|
3584
|
+
this.structuredInputWaiters.clear();
|
|
3585
|
+
if (conversationId && updateStatus)
|
|
2866
3586
|
this.updateConversationStatus(conversationId, "idle");
|
|
2867
3587
|
}
|
|
2868
3588
|
extractSessionId(value) {
|
|
@@ -2940,6 +3660,34 @@ function formatStructuredInputResponse(answers) {
|
|
|
2940
3660
|
])),
|
|
2941
3661
|
};
|
|
2942
3662
|
}
|
|
3663
|
+
function formatClaudeAskUserQuestionResponse(answers, input) {
|
|
3664
|
+
const questions = input?.questions ?? [];
|
|
3665
|
+
const byQuestionText = {};
|
|
3666
|
+
for (const question of questions) {
|
|
3667
|
+
const values = answers[question.id] ?? answers[question.question] ?? [];
|
|
3668
|
+
const cleaned = values.map((value) => value.trim()).filter(Boolean);
|
|
3669
|
+
if (cleaned.length === 0)
|
|
3670
|
+
continue;
|
|
3671
|
+
byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
|
|
3672
|
+
? cleaned
|
|
3673
|
+
: cleaned[0] ?? "";
|
|
3674
|
+
}
|
|
3675
|
+
return {
|
|
3676
|
+
behavior: "allow",
|
|
3677
|
+
updatedInput: {
|
|
3678
|
+
questions: questions.map((question) => ({
|
|
3679
|
+
question: question.question,
|
|
3680
|
+
header: question.header,
|
|
3681
|
+
options: question.options?.map((option) => ({
|
|
3682
|
+
label: option.label,
|
|
3683
|
+
description: option.description,
|
|
3684
|
+
})) ?? [],
|
|
3685
|
+
multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
|
|
3686
|
+
})),
|
|
3687
|
+
answers: byQuestionText,
|
|
3688
|
+
},
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
2943
3691
|
function formatMcpElicitationResponse(answers) {
|
|
2944
3692
|
const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
|
|
2945
3693
|
questionId,
|
|
@@ -2951,6 +3699,15 @@ function formatMcpElicitationResponse(answers) {
|
|
|
2951
3699
|
_meta: { source: "linkshell" },
|
|
2952
3700
|
};
|
|
2953
3701
|
}
|
|
3702
|
+
function formatStructuredInputResponseForSource(source, answers, input) {
|
|
3703
|
+
if (source === "mcpServer/elicitation/request") {
|
|
3704
|
+
return formatMcpElicitationResponse(answers);
|
|
3705
|
+
}
|
|
3706
|
+
if (source === "claude/askUserQuestion") {
|
|
3707
|
+
return formatClaudeAskUserQuestionResponse(answers, input);
|
|
3708
|
+
}
|
|
3709
|
+
return formatStructuredInputResponse(answers);
|
|
3710
|
+
}
|
|
2954
3711
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
2955
3712
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
2956
3713
|
return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
|