linkshell-cli 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
- package/dist/cli/src/runtime/acp/acp-client.js +40 -2
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +690 -81
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +69 -14
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
- package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
- package/dist/cli/src/runtime/acp-relay.js +73 -0
- package/dist/cli/src/runtime/acp-relay.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +5 -0
- package/dist/cli/src/runtime/bridge-session.js +104 -57
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +2505 -2479
- package/dist/shared-protocol/src/index.js +46 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/runtime/acp/acp-client.ts +53 -2
- package/src/runtime/acp/agent-workspace.ts +752 -76
- package/src/runtime/acp/claude-sdk-client.ts +352 -31
- package/src/runtime/acp/claude-stream-json-client.ts +66 -16
- package/src/runtime/acp/json-rpc.ts +5 -0
- package/src/runtime/bridge-session.ts +114 -61
- package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
- package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
- package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +0 -1
|
@@ -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;
|
|
@@ -1098,11 +1390,18 @@ export class AgentWorkspaceProxy {
|
|
|
1098
1390
|
case "agent.v2.cancel": {
|
|
1099
1391
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
1100
1392
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1393
|
+
if (!conversation)
|
|
1394
|
+
break;
|
|
1395
|
+
const turnId = this.currentTurnIds.get(payload.conversationId);
|
|
1396
|
+
if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
|
|
1397
|
+
this.rejectAgentAction(conversation, "无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。", conversation.status);
|
|
1398
|
+
break;
|
|
1399
|
+
}
|
|
1101
1400
|
this.cancelPendingPermissions(payload.conversationId);
|
|
1102
|
-
const cancelClient =
|
|
1401
|
+
const cancelClient = this.clientForProvider(conversation.provider);
|
|
1103
1402
|
cancelClient?.cancel({
|
|
1104
|
-
sessionId: conversation
|
|
1105
|
-
turnId
|
|
1403
|
+
sessionId: conversation.agentSessionId,
|
|
1404
|
+
turnId,
|
|
1106
1405
|
});
|
|
1107
1406
|
this.forgetCurrentTurn(payload.conversationId);
|
|
1108
1407
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
@@ -1133,6 +1432,13 @@ export class AgentWorkspaceProxy {
|
|
|
1133
1432
|
protocolForProvider(provider) {
|
|
1134
1433
|
return this.agentProtocols.get(provider);
|
|
1135
1434
|
}
|
|
1435
|
+
defaultModelForProvider(provider) {
|
|
1436
|
+
const capabilities = this.providerCapabilities.get(provider);
|
|
1437
|
+
const defaultModel = capabilities?.defaultModel?.trim();
|
|
1438
|
+
if (defaultModel)
|
|
1439
|
+
return defaultModel;
|
|
1440
|
+
return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
|
|
1441
|
+
}
|
|
1136
1442
|
async initialize() {
|
|
1137
1443
|
if (this.initialized)
|
|
1138
1444
|
return;
|
|
@@ -1197,6 +1503,7 @@ export class AgentWorkspaceProxy {
|
|
|
1197
1503
|
try {
|
|
1198
1504
|
const client = await tryCreateClient(resolved);
|
|
1199
1505
|
this.clients.set(provider, client);
|
|
1506
|
+
this.providerErrors.delete(provider);
|
|
1200
1507
|
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
1201
1508
|
this.status = "idle";
|
|
1202
1509
|
this.error = undefined;
|
|
@@ -1217,6 +1524,7 @@ export class AgentWorkspaceProxy {
|
|
|
1217
1524
|
};
|
|
1218
1525
|
const client = await tryCreateClient(fallback);
|
|
1219
1526
|
this.clients.set(provider, client);
|
|
1527
|
+
this.providerErrors.delete(provider);
|
|
1220
1528
|
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
1221
1529
|
this.status = "idle";
|
|
1222
1530
|
this.error = undefined;
|
|
@@ -1232,6 +1540,8 @@ export class AgentWorkspaceProxy {
|
|
|
1232
1540
|
if (this.input.verbose) {
|
|
1233
1541
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1234
1542
|
}
|
|
1543
|
+
this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1544
|
+
this.sendCapabilities();
|
|
1235
1545
|
return undefined;
|
|
1236
1546
|
}
|
|
1237
1547
|
}
|
|
@@ -1281,7 +1591,7 @@ export class AgentWorkspaceProxy {
|
|
|
1281
1591
|
const agentSessionId = remote.id;
|
|
1282
1592
|
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1283
1593
|
const now = Date.now();
|
|
1284
|
-
const conversationId = existingId ??
|
|
1594
|
+
const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1285
1595
|
const existing = this.conversations.get(conversationId);
|
|
1286
1596
|
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1287
1597
|
const conversation = {
|
|
@@ -1312,6 +1622,78 @@ export class AgentWorkspaceProxy {
|
|
|
1312
1622
|
}
|
|
1313
1623
|
}
|
|
1314
1624
|
}
|
|
1625
|
+
async hydrateConversationFromProvider(conversation, seedResult) {
|
|
1626
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1627
|
+
if (!client || !conversation.agentSessionId)
|
|
1628
|
+
return;
|
|
1629
|
+
let source = seedResult;
|
|
1630
|
+
const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
|
|
1631
|
+
if (!hasSeedTurns) {
|
|
1632
|
+
const readSession = client.readSession;
|
|
1633
|
+
if (typeof readSession === "function") {
|
|
1634
|
+
try {
|
|
1635
|
+
source = await readSession.call(client, {
|
|
1636
|
+
sessionId: conversation.agentSessionId,
|
|
1637
|
+
includeTurns: true,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
catch (error) {
|
|
1641
|
+
if (this.input.verbose) {
|
|
1642
|
+
process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
|
|
1647
|
+
const listTurns = client.listTurns;
|
|
1648
|
+
if (typeof listTurns === "function") {
|
|
1649
|
+
try {
|
|
1650
|
+
const turnsResult = await listTurns.call(client, {
|
|
1651
|
+
sessionId: conversation.agentSessionId,
|
|
1652
|
+
limit: MAX_TIMELINE_ITEMS,
|
|
1653
|
+
sortDirection: "desc",
|
|
1654
|
+
itemsView: "full",
|
|
1655
|
+
});
|
|
1656
|
+
const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
|
|
1657
|
+
if (turnsThread)
|
|
1658
|
+
source = { thread: turnsThread };
|
|
1659
|
+
}
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
if (this.input.verbose) {
|
|
1662
|
+
process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const thread = threadFromProviderResult(source);
|
|
1669
|
+
const activeTurnId = activeTurnIdFromThread(thread);
|
|
1670
|
+
if (activeTurnId)
|
|
1671
|
+
this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
1672
|
+
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
1673
|
+
if (model && !conversation.model)
|
|
1674
|
+
conversation.model = model;
|
|
1675
|
+
const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
1676
|
+
if (hydratedItems.length === 0)
|
|
1677
|
+
return;
|
|
1678
|
+
const existing = this.timelines.get(conversation.id) ?? [];
|
|
1679
|
+
const merged = new Map();
|
|
1680
|
+
for (const item of [...existing, ...hydratedItems]) {
|
|
1681
|
+
merged.set(item.id, item);
|
|
1682
|
+
}
|
|
1683
|
+
const nextItems = [...merged.values()]
|
|
1684
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
1685
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
1686
|
+
this.timelines.set(conversation.id, nextItems);
|
|
1687
|
+
for (const item of nextItems) {
|
|
1688
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
1689
|
+
}
|
|
1690
|
+
const lastPreview = [...nextItems].reverse()
|
|
1691
|
+
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
1692
|
+
.find(Boolean);
|
|
1693
|
+
if (lastPreview && !conversation.lastMessagePreview) {
|
|
1694
|
+
conversation.lastMessagePreview = lastPreview;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1315
1697
|
sendCapabilities() {
|
|
1316
1698
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1317
1699
|
const client = this.clients.get(provider);
|
|
@@ -1328,7 +1710,7 @@ export class AgentWorkspaceProxy {
|
|
|
1328
1710
|
id: provider,
|
|
1329
1711
|
label: providerLabel(provider),
|
|
1330
1712
|
enabled,
|
|
1331
|
-
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1713
|
+
reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
|
|
1332
1714
|
supportsImages,
|
|
1333
1715
|
supportsPermission,
|
|
1334
1716
|
supportsPlan: enabled,
|
|
@@ -1385,18 +1767,60 @@ export class AgentWorkspaceProxy {
|
|
|
1385
1767
|
if (!this.input.availableProviders.includes(provider)) {
|
|
1386
1768
|
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
1387
1769
|
}
|
|
1388
|
-
|
|
1389
|
-
if (!client) {
|
|
1390
|
-
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
1391
|
-
}
|
|
1392
|
-
const cwd = payload.cwd ?? this.input.cwd;
|
|
1770
|
+
let cwd = payload.cwd ?? this.input.cwd;
|
|
1393
1771
|
let agentSessionId = payload.agentSessionId;
|
|
1394
1772
|
let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
1395
1773
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
1774
|
+
if (!payload.cwd && existingConversation?.cwd)
|
|
1775
|
+
cwd = existingConversation.cwd;
|
|
1776
|
+
const client = await this.ensureProviderClient(provider);
|
|
1777
|
+
if (!client) {
|
|
1778
|
+
const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
|
|
1779
|
+
if (existingConversation) {
|
|
1780
|
+
this.activeConversationId = existingConversation.id;
|
|
1781
|
+
this.input.send(createEnvelope({
|
|
1782
|
+
type: "agent.v2.conversation.opened",
|
|
1783
|
+
sessionId: this.input.sessionId,
|
|
1784
|
+
payload: {
|
|
1785
|
+
conversation: existingConversation,
|
|
1786
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1787
|
+
requestedConversationId: payload.conversationId,
|
|
1788
|
+
providerError: message,
|
|
1789
|
+
},
|
|
1790
|
+
}));
|
|
1791
|
+
this.sendCapabilities();
|
|
1792
|
+
return existingConversation;
|
|
1793
|
+
}
|
|
1794
|
+
if (payload.conversationId) {
|
|
1795
|
+
return this.openOfflineHistory(payload, message, cwd);
|
|
1796
|
+
}
|
|
1797
|
+
return this.openFailure(payload, message, cwd);
|
|
1798
|
+
}
|
|
1396
1799
|
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
1397
|
-
|
|
1800
|
+
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
1801
|
+
if (payload.conversationId &&
|
|
1802
|
+
payload.conversationId === requestedCanonicalId &&
|
|
1803
|
+
existingConversation.id !== payload.conversationId) {
|
|
1398
1804
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1399
1805
|
}
|
|
1806
|
+
if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
|
|
1807
|
+
try {
|
|
1808
|
+
const existingAgentSessionId = existingConversation.agentSessionId;
|
|
1809
|
+
const result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
|
|
1810
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
1811
|
+
if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
|
|
1812
|
+
this.conversationByAgentSessionId.delete(existingAgentSessionId);
|
|
1813
|
+
existingConversation.agentSessionId = nextAgentSessionId;
|
|
1814
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
|
|
1815
|
+
}
|
|
1816
|
+
await this.hydrateConversationFromProvider(existingConversation, result);
|
|
1817
|
+
}
|
|
1818
|
+
catch (error) {
|
|
1819
|
+
if (this.input.verbose) {
|
|
1820
|
+
process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1400
1824
|
this.activeConversationId = existingConversation.id;
|
|
1401
1825
|
this.input.send(createEnvelope({
|
|
1402
1826
|
type: "agent.v2.conversation.opened",
|
|
@@ -1404,6 +1828,7 @@ export class AgentWorkspaceProxy {
|
|
|
1404
1828
|
payload: {
|
|
1405
1829
|
conversation: existingConversation,
|
|
1406
1830
|
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1831
|
+
requestedConversationId: payload.conversationId,
|
|
1407
1832
|
},
|
|
1408
1833
|
}));
|
|
1409
1834
|
return existingConversation;
|
|
@@ -1414,7 +1839,7 @@ export class AgentWorkspaceProxy {
|
|
|
1414
1839
|
: await client.newSession({ cwd });
|
|
1415
1840
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
1416
1841
|
const now = Date.now();
|
|
1417
|
-
const conversationId =
|
|
1842
|
+
const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
1418
1843
|
const conversation = {
|
|
1419
1844
|
...existingConversation,
|
|
1420
1845
|
id: conversationId,
|
|
@@ -1436,10 +1861,15 @@ export class AgentWorkspaceProxy {
|
|
|
1436
1861
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1437
1862
|
this.activeConversationId = conversation.id;
|
|
1438
1863
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1864
|
+
await this.hydrateConversationFromProvider(conversation, result);
|
|
1439
1865
|
this.input.send(createEnvelope({
|
|
1440
1866
|
type: "agent.v2.conversation.opened",
|
|
1441
1867
|
sessionId: this.input.sessionId,
|
|
1442
|
-
payload: {
|
|
1868
|
+
payload: {
|
|
1869
|
+
conversation,
|
|
1870
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
1871
|
+
requestedConversationId: payload.conversationId,
|
|
1872
|
+
},
|
|
1443
1873
|
}));
|
|
1444
1874
|
return conversation;
|
|
1445
1875
|
}
|
|
@@ -1478,70 +1908,111 @@ export class AgentWorkspaceProxy {
|
|
|
1478
1908
|
this.input.send(createEnvelope({
|
|
1479
1909
|
type: "agent.v2.conversation.opened",
|
|
1480
1910
|
sessionId: this.input.sessionId,
|
|
1481
|
-
payload: {
|
|
1911
|
+
payload: {
|
|
1912
|
+
conversation,
|
|
1913
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
1914
|
+
requestedConversationId: payload.conversationId,
|
|
1915
|
+
},
|
|
1482
1916
|
}));
|
|
1483
1917
|
return conversation;
|
|
1484
1918
|
}
|
|
1919
|
+
openOfflineHistory(payload, message, cwd = payload.cwd ?? this.input.cwd) {
|
|
1920
|
+
const now = Date.now();
|
|
1921
|
+
const conversation = {
|
|
1922
|
+
id: payload.conversationId,
|
|
1923
|
+
agentSessionId: payload.agentSessionId,
|
|
1924
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
1925
|
+
cwd,
|
|
1926
|
+
title: payload.title ?? titleFromCwd(cwd),
|
|
1927
|
+
model: payload.model,
|
|
1928
|
+
reasoningEffort: payload.reasoningEffort,
|
|
1929
|
+
permissionMode: payload.permissionMode,
|
|
1930
|
+
collaborationMode: payload.collaborationMode,
|
|
1931
|
+
status: "idle",
|
|
1932
|
+
archived: false,
|
|
1933
|
+
lastActivityAt: now,
|
|
1934
|
+
createdAt: now,
|
|
1935
|
+
};
|
|
1936
|
+
this.conversations.set(conversation.id, conversation);
|
|
1937
|
+
if (conversation.agentSessionId) {
|
|
1938
|
+
this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
|
|
1939
|
+
}
|
|
1940
|
+
this.activeConversationId = conversation.id;
|
|
1941
|
+
this.input.send(createEnvelope({
|
|
1942
|
+
type: "agent.v2.conversation.opened",
|
|
1943
|
+
sessionId: this.input.sessionId,
|
|
1944
|
+
payload: {
|
|
1945
|
+
conversation,
|
|
1946
|
+
snapshot: this.timelines.get(conversation.id) ?? [],
|
|
1947
|
+
requestedConversationId: payload.conversationId,
|
|
1948
|
+
providerError: message,
|
|
1949
|
+
},
|
|
1950
|
+
}));
|
|
1951
|
+
this.sendCapabilities();
|
|
1952
|
+
return conversation;
|
|
1953
|
+
}
|
|
1485
1954
|
async sendPrompt(payload) {
|
|
1486
1955
|
const conversation = this.conversations.get(payload.conversationId) ??
|
|
1487
1956
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
1488
1957
|
if (!conversation)
|
|
1489
1958
|
return;
|
|
1490
1959
|
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
|
-
});
|
|
1960
|
+
this.rejectAgentAction(conversation, "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
|
|
1498
1961
|
return;
|
|
1499
1962
|
}
|
|
1500
|
-
const client = this.clientForProvider(conversation.provider);
|
|
1963
|
+
const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
|
|
1501
1964
|
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
|
-
});
|
|
1965
|
+
this.rejectAgentAction(conversation, this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
|
|
1509
1966
|
return;
|
|
1510
1967
|
}
|
|
1511
1968
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
1512
1969
|
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
|
|
1513
|
-
conversation
|
|
1514
|
-
|
|
1515
|
-
|
|
1970
|
+
this.rejectAgentAction(conversation, "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。");
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const priorSettings = {
|
|
1974
|
+
model: conversation.model,
|
|
1975
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1976
|
+
permissionMode: conversation.permissionMode,
|
|
1977
|
+
};
|
|
1978
|
+
const effectiveModel = payload.model ??
|
|
1979
|
+
conversation.model ??
|
|
1980
|
+
(protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
|
|
1981
|
+
const wasRunning = conversation.status === "running";
|
|
1982
|
+
const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
|
|
1983
|
+
const shouldSteer = Boolean(payload.delivery !== "new_turn" &&
|
|
1984
|
+
wasRunning &&
|
|
1985
|
+
protocol === "codex-app-server" &&
|
|
1986
|
+
activeTurnId);
|
|
1987
|
+
if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
|
|
1516
1988
|
this.addItem(conversation.id, {
|
|
1517
1989
|
id: id("error"),
|
|
1518
1990
|
conversationId: conversation.id,
|
|
1519
1991
|
type: "error",
|
|
1520
|
-
error:
|
|
1992
|
+
error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
|
|
1521
1993
|
createdAt: Date.now(),
|
|
1522
1994
|
});
|
|
1523
1995
|
return;
|
|
1524
1996
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
conversation.model = payload.model ?? conversation.model;
|
|
1997
|
+
if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
|
|
1998
|
+
this.rejectAgentAction(conversation, "当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。");
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
conversation.model = effectiveModel ?? conversation.model;
|
|
1531
2002
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1532
2003
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1533
2004
|
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1534
2005
|
conversation.status = "running";
|
|
1535
2006
|
conversation.lastActivityAt = Date.now();
|
|
1536
2007
|
this.activeConversationId = conversation.id;
|
|
1537
|
-
if (
|
|
2008
|
+
if (effectiveModel && effectiveModel !== priorSettings.model) {
|
|
1538
2009
|
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1539
|
-
const label = runtimeCapabilities?.models?.find((m) => m.id ===
|
|
2010
|
+
const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
|
|
1540
2011
|
this.emitNotice({
|
|
1541
2012
|
conversationId: conversation.id,
|
|
1542
2013
|
kind: "model_changed",
|
|
1543
2014
|
title: `已切换模型 · ${label}`,
|
|
1544
|
-
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${
|
|
2015
|
+
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
|
|
1545
2016
|
});
|
|
1546
2017
|
}
|
|
1547
2018
|
if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
|
|
@@ -1566,20 +2037,61 @@ export class AgentWorkspaceProxy {
|
|
|
1566
2037
|
role: "user",
|
|
1567
2038
|
content: payload.contentBlocks,
|
|
1568
2039
|
text: userText,
|
|
2040
|
+
metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
|
|
1569
2041
|
createdAt: Date.now(),
|
|
1570
2042
|
});
|
|
1571
2043
|
this.emitConversation(conversation);
|
|
2044
|
+
const promptAsNewTurn = () => client.prompt({
|
|
2045
|
+
sessionId: conversation.agentSessionId,
|
|
2046
|
+
content: payload.contentBlocks,
|
|
2047
|
+
clientMessageId: payload.clientMessageId,
|
|
2048
|
+
model: effectiveModel,
|
|
2049
|
+
reasoningEffort: payload.reasoningEffort,
|
|
2050
|
+
permissionMode: payload.permissionMode,
|
|
2051
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
2052
|
+
cwd: conversation.cwd,
|
|
2053
|
+
});
|
|
1572
2054
|
try {
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2055
|
+
const steer = client.steer;
|
|
2056
|
+
let result;
|
|
2057
|
+
if (shouldSteer && typeof steer === "function") {
|
|
2058
|
+
try {
|
|
2059
|
+
result = await steer.call(client, {
|
|
2060
|
+
sessionId: conversation.agentSessionId,
|
|
2061
|
+
turnId: activeTurnId,
|
|
2062
|
+
content: payload.contentBlocks,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
catch (steerError) {
|
|
2066
|
+
this.forgetCurrentTurn(conversation.id, activeTurnId);
|
|
2067
|
+
const userItem = this.findItem(conversation.id, payload.clientMessageId);
|
|
2068
|
+
if (userItem) {
|
|
2069
|
+
this.upsertItem(conversation.id, {
|
|
2070
|
+
...userItem,
|
|
2071
|
+
metadata: {
|
|
2072
|
+
...(userItem.metadata ?? {}),
|
|
2073
|
+
delivery: "new_turn",
|
|
2074
|
+
fallbackFrom: "steer",
|
|
2075
|
+
failedTargetTurnId: activeTurnId,
|
|
2076
|
+
},
|
|
2077
|
+
updatedAt: Date.now(),
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
this.addItem(conversation.id, {
|
|
2081
|
+
id: id("status"),
|
|
2082
|
+
conversationId: conversation.id,
|
|
2083
|
+
type: "status",
|
|
2084
|
+
role: "system",
|
|
2085
|
+
text: "Codex 未接收运行中追加输入,已改为发送新消息。",
|
|
2086
|
+
status: "running",
|
|
2087
|
+
createdAt: Date.now(),
|
|
2088
|
+
});
|
|
2089
|
+
result = await promptAsNewTurn();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
else {
|
|
2093
|
+
result = await promptAsNewTurn();
|
|
2094
|
+
}
|
|
1583
2095
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
1584
2096
|
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1585
2097
|
if (conversation.agentSessionId)
|
|
@@ -1784,6 +2296,9 @@ export class AgentWorkspaceProxy {
|
|
|
1784
2296
|
}
|
|
1785
2297
|
}
|
|
1786
2298
|
handleRequest(method, params) {
|
|
2299
|
+
if (method === "claude/askUserQuestion") {
|
|
2300
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
2301
|
+
}
|
|
1787
2302
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1788
2303
|
return this.handleStructuredInput(params, true);
|
|
1789
2304
|
}
|
|
@@ -1829,13 +2344,30 @@ export class AgentWorkspaceProxy {
|
|
|
1829
2344
|
}
|
|
1830
2345
|
if (method.startsWith("account/") ||
|
|
1831
2346
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
1832
|
-
method === "thread/status/changed" ||
|
|
1833
2347
|
method === "thread/tokenUsage/updated" ||
|
|
1834
2348
|
method === "serverRequest/resolved" ||
|
|
1835
2349
|
method === "mcpServer/oauthLogin/completed") {
|
|
1836
2350
|
return;
|
|
1837
2351
|
}
|
|
1838
2352
|
const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
|
|
2353
|
+
if (method === "thread/status/changed") {
|
|
2354
|
+
const status = agentStatusFromThreadStatus(params);
|
|
2355
|
+
if (conversationId && status) {
|
|
2356
|
+
const raw = asRecord(params);
|
|
2357
|
+
const turnId = this.extractTurnId(raw);
|
|
2358
|
+
if (turnId && (status === "running" || status === "waiting_permission")) {
|
|
2359
|
+
this.rememberTurnConversationId(conversationId, turnId);
|
|
2360
|
+
}
|
|
2361
|
+
if (status === "idle" || status === "error") {
|
|
2362
|
+
this.forgetCurrentTurn(conversationId, turnId);
|
|
2363
|
+
}
|
|
2364
|
+
const message = status === "error"
|
|
2365
|
+
? firstString(raw, ["message", "error", "reason"])
|
|
2366
|
+
: undefined;
|
|
2367
|
+
this.updateConversationStatus(conversationId, status, message);
|
|
2368
|
+
}
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
1839
2371
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1840
2372
|
this.handleStructuredInput(params);
|
|
1841
2373
|
return;
|
|
@@ -2345,9 +2877,7 @@ export class AgentWorkspaceProxy {
|
|
|
2345
2877
|
const raw = asRecord(params) ?? {};
|
|
2346
2878
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
2347
2879
|
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
2348
|
-
const formatResponse = source
|
|
2349
|
-
? formatMcpElicitationResponse
|
|
2350
|
-
: formatStructuredInputResponse;
|
|
2880
|
+
const formatResponse = (answers, input) => formatStructuredInputResponseForSource(source, answers, input);
|
|
2351
2881
|
if (!conversationId)
|
|
2352
2882
|
return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
2353
2883
|
const structuredInput = decodeStructuredInput(raw);
|
|
@@ -2374,13 +2904,13 @@ export class AgentWorkspaceProxy {
|
|
|
2374
2904
|
const timer = setTimeout(() => {
|
|
2375
2905
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
2376
2906
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
2377
|
-
resolve(formatResponse({}));
|
|
2907
|
+
resolve(formatResponse({}, structuredInput));
|
|
2378
2908
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
2379
2909
|
inputPending: false,
|
|
2380
2910
|
inputError: "等待用户输入超时",
|
|
2381
2911
|
});
|
|
2382
2912
|
}, PERMISSION_TIMEOUT_MS);
|
|
2383
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
2913
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
|
|
2384
2914
|
});
|
|
2385
2915
|
}
|
|
2386
2916
|
toolCallFromItem(item, fallbackStatus) {
|
|
@@ -2430,6 +2960,12 @@ export class AgentWorkspaceProxy {
|
|
|
2430
2960
|
conversationId,
|
|
2431
2961
|
type: "permission",
|
|
2432
2962
|
permission,
|
|
2963
|
+
metadata: {
|
|
2964
|
+
protocol: "v2",
|
|
2965
|
+
permissionLive: true,
|
|
2966
|
+
permissionExpired: false,
|
|
2967
|
+
permissionPending: false,
|
|
2968
|
+
},
|
|
2433
2969
|
createdAt: Date.now(),
|
|
2434
2970
|
updatedAt: Date.now(),
|
|
2435
2971
|
};
|
|
@@ -2454,9 +2990,13 @@ export class AgentWorkspaceProxy {
|
|
|
2454
2990
|
}
|
|
2455
2991
|
respondPermission(payload) {
|
|
2456
2992
|
const permission = this.pendingPermissions.get(payload.requestId);
|
|
2993
|
+
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
2994
|
+
const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
|
|
2995
|
+
const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
|
|
2996
|
+
if (!permission && !waiter && alreadyResolved)
|
|
2997
|
+
return;
|
|
2457
2998
|
this.pendingPermissions.delete(payload.requestId);
|
|
2458
2999
|
const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
|
|
2459
|
-
const waiter = this.permissionWaiters.get(payload.requestId);
|
|
2460
3000
|
if (waiter) {
|
|
2461
3001
|
clearTimeout(waiter.timer);
|
|
2462
3002
|
this.permissionWaiters.delete(payload.requestId);
|
|
@@ -2479,19 +3019,22 @@ export class AgentWorkspaceProxy {
|
|
|
2479
3019
|
permissionError: undefined,
|
|
2480
3020
|
permissionPending: false,
|
|
2481
3021
|
});
|
|
2482
|
-
|
|
3022
|
+
if (permission || waiter || existingItem) {
|
|
3023
|
+
this.updateConversationStatus(payload.conversationId, "running");
|
|
3024
|
+
}
|
|
2483
3025
|
}
|
|
2484
3026
|
respondStructuredInput(payload) {
|
|
2485
3027
|
const pending = this.pendingStructuredInputs.get(payload.requestId);
|
|
2486
|
-
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2487
3028
|
const waiter = this.structuredInputWaiters.get(payload.requestId);
|
|
3029
|
+
const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
|
|
3030
|
+
const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
|
|
3031
|
+
if (!pending && !waiter && alreadySubmitted)
|
|
3032
|
+
return;
|
|
3033
|
+
this.pendingStructuredInputs.delete(payload.requestId);
|
|
2488
3034
|
if (waiter) {
|
|
2489
3035
|
clearTimeout(waiter.timer);
|
|
2490
3036
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
2491
|
-
|
|
2492
|
-
? formatMcpElicitationResponse
|
|
2493
|
-
: formatStructuredInputResponse;
|
|
2494
|
-
waiter.resolve(formatResponse(payload.answers));
|
|
3037
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
|
|
2495
3038
|
}
|
|
2496
3039
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
2497
3040
|
inputPending: false,
|
|
@@ -2500,7 +3043,9 @@ export class AgentWorkspaceProxy {
|
|
|
2500
3043
|
inputError: undefined,
|
|
2501
3044
|
answers: payload.answers,
|
|
2502
3045
|
});
|
|
2503
|
-
|
|
3046
|
+
if (pending || waiter || existingItem) {
|
|
3047
|
+
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
3048
|
+
}
|
|
2504
3049
|
}
|
|
2505
3050
|
markPermission(conversationId, requestId, metadata) {
|
|
2506
3051
|
const item = this.findItem(conversationId, `permission:${requestId}`);
|
|
@@ -2675,14 +3220,14 @@ export class AgentWorkspaceProxy {
|
|
|
2675
3220
|
this.input.send(createEnvelope({
|
|
2676
3221
|
type: "agent.v2.event",
|
|
2677
3222
|
sessionId: this.input.sessionId,
|
|
2678
|
-
payload: { conversationId, conversation, item },
|
|
3223
|
+
payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
|
|
2679
3224
|
}));
|
|
2680
3225
|
}
|
|
2681
3226
|
emitConversation(conversation) {
|
|
2682
3227
|
this.input.send(createEnvelope({
|
|
2683
3228
|
type: "agent.v2.event",
|
|
2684
3229
|
sessionId: this.input.sessionId,
|
|
2685
|
-
payload: { conversationId: conversation.id, conversation },
|
|
3230
|
+
payload: { conversationId: conversation.id, conversation: { ...conversation } },
|
|
2686
3231
|
}));
|
|
2687
3232
|
}
|
|
2688
3233
|
emitStatus(conversationId, status, text) {
|
|
@@ -2702,6 +3247,16 @@ export class AgentWorkspaceProxy {
|
|
|
2702
3247
|
payload: input,
|
|
2703
3248
|
}));
|
|
2704
3249
|
}
|
|
3250
|
+
rejectAgentAction(conversation, message, status = "idle") {
|
|
3251
|
+
this.updateConversationStatus(conversation.id, status, message);
|
|
3252
|
+
this.addItem(conversation.id, {
|
|
3253
|
+
id: id("error"),
|
|
3254
|
+
conversationId: conversation.id,
|
|
3255
|
+
type: "error",
|
|
3256
|
+
error: message,
|
|
3257
|
+
createdAt: Date.now(),
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
2705
3260
|
updateConversationPreview(conversationId, text, status) {
|
|
2706
3261
|
const conversation = this.conversations.get(conversationId);
|
|
2707
3262
|
if (!conversation)
|
|
@@ -2823,13 +3378,23 @@ export class AgentWorkspaceProxy {
|
|
|
2823
3378
|
handleProviderExit(provider, message) {
|
|
2824
3379
|
this.clients.delete(provider);
|
|
2825
3380
|
this.agentProtocols.delete(provider);
|
|
2826
|
-
this.
|
|
3381
|
+
this.providerErrors.set(provider, message);
|
|
3382
|
+
for (const conversation of this.conversations.values()) {
|
|
3383
|
+
if (conversation.provider === provider && conversation.status === "waiting_permission") {
|
|
3384
|
+
this.cancelPendingPermissions(conversation.id, false);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
2827
3387
|
for (const conversation of this.conversations.values()) {
|
|
2828
3388
|
if (conversation.provider !== provider)
|
|
2829
3389
|
continue;
|
|
3390
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
|
|
3391
|
+
this.emitConversation(conversation);
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
2830
3394
|
conversation.status = "error";
|
|
2831
3395
|
conversation.lastMessagePreview = message;
|
|
2832
3396
|
conversation.lastActivityAt = Date.now();
|
|
3397
|
+
this.forgetCurrentTurn(conversation.id);
|
|
2833
3398
|
this.emitConversation(conversation);
|
|
2834
3399
|
this.addItem(conversation.id, {
|
|
2835
3400
|
id: id("error"),
|
|
@@ -2841,18 +3406,23 @@ export class AgentWorkspaceProxy {
|
|
|
2841
3406
|
}
|
|
2842
3407
|
this.sendCapabilities();
|
|
2843
3408
|
}
|
|
2844
|
-
cancelPendingPermissions(conversationId) {
|
|
3409
|
+
cancelPendingPermissions(conversationId, updateStatus = true) {
|
|
2845
3410
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3411
|
+
if (conversationId && this.itemConversationIds.get(requestId) !== conversationId)
|
|
3412
|
+
continue;
|
|
2846
3413
|
clearTimeout(waiter.timer);
|
|
2847
3414
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
2848
3415
|
this.pendingPermissions.delete(requestId);
|
|
2849
3416
|
this.permissionSources.delete(requestId);
|
|
2850
3417
|
}
|
|
2851
|
-
|
|
3418
|
+
if (!conversationId)
|
|
3419
|
+
this.permissionWaiters.clear();
|
|
2852
3420
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
2853
|
-
clearTimeout(waiter.timer);
|
|
2854
|
-
waiter.resolve(formatStructuredInputResponse({}));
|
|
2855
3421
|
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3422
|
+
if (conversationId && pending?.conversationId !== conversationId)
|
|
3423
|
+
continue;
|
|
3424
|
+
clearTimeout(waiter.timer);
|
|
3425
|
+
waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
|
|
2856
3426
|
if (pending) {
|
|
2857
3427
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
2858
3428
|
inputPending: false,
|
|
@@ -2860,9 +3430,11 @@ export class AgentWorkspaceProxy {
|
|
|
2860
3430
|
});
|
|
2861
3431
|
}
|
|
2862
3432
|
this.pendingStructuredInputs.delete(requestId);
|
|
3433
|
+
this.structuredInputWaiters.delete(requestId);
|
|
2863
3434
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
3435
|
+
if (!conversationId)
|
|
3436
|
+
this.structuredInputWaiters.clear();
|
|
3437
|
+
if (conversationId && updateStatus)
|
|
2866
3438
|
this.updateConversationStatus(conversationId, "idle");
|
|
2867
3439
|
}
|
|
2868
3440
|
extractSessionId(value) {
|
|
@@ -2940,6 +3512,34 @@ function formatStructuredInputResponse(answers) {
|
|
|
2940
3512
|
])),
|
|
2941
3513
|
};
|
|
2942
3514
|
}
|
|
3515
|
+
function formatClaudeAskUserQuestionResponse(answers, input) {
|
|
3516
|
+
const questions = input?.questions ?? [];
|
|
3517
|
+
const byQuestionText = {};
|
|
3518
|
+
for (const question of questions) {
|
|
3519
|
+
const values = answers[question.id] ?? answers[question.question] ?? [];
|
|
3520
|
+
const cleaned = values.map((value) => value.trim()).filter(Boolean);
|
|
3521
|
+
if (cleaned.length === 0)
|
|
3522
|
+
continue;
|
|
3523
|
+
byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
|
|
3524
|
+
? cleaned
|
|
3525
|
+
: cleaned[0] ?? "";
|
|
3526
|
+
}
|
|
3527
|
+
return {
|
|
3528
|
+
behavior: "allow",
|
|
3529
|
+
updatedInput: {
|
|
3530
|
+
questions: questions.map((question) => ({
|
|
3531
|
+
question: question.question,
|
|
3532
|
+
header: question.header,
|
|
3533
|
+
options: question.options?.map((option) => ({
|
|
3534
|
+
label: option.label,
|
|
3535
|
+
description: option.description,
|
|
3536
|
+
})) ?? [],
|
|
3537
|
+
multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
|
|
3538
|
+
})),
|
|
3539
|
+
answers: byQuestionText,
|
|
3540
|
+
},
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
2943
3543
|
function formatMcpElicitationResponse(answers) {
|
|
2944
3544
|
const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
|
|
2945
3545
|
questionId,
|
|
@@ -2951,6 +3551,15 @@ function formatMcpElicitationResponse(answers) {
|
|
|
2951
3551
|
_meta: { source: "linkshell" },
|
|
2952
3552
|
};
|
|
2953
3553
|
}
|
|
3554
|
+
function formatStructuredInputResponseForSource(source, answers, input) {
|
|
3555
|
+
if (source === "mcpServer/elicitation/request") {
|
|
3556
|
+
return formatMcpElicitationResponse(answers);
|
|
3557
|
+
}
|
|
3558
|
+
if (source === "claude/askUserQuestion") {
|
|
3559
|
+
return formatClaudeAskUserQuestionResponse(answers, input);
|
|
3560
|
+
}
|
|
3561
|
+
return formatStructuredInputResponse(answers);
|
|
3562
|
+
}
|
|
2954
3563
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
2955
3564
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
2956
3565
|
return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
|