@keyou007/agent-bot 0.1.24-alpha.0 → 0.1.24-alpha.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/CHANGELOG.md +17 -0
- package/README.md +24 -4
- package/README.zh.md +24 -4
- package/dist/cli/SelfUpdateRunner.js +2 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +90 -2
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +160 -12
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +14 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +42 -11
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +70 -4
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +2 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +16 -1
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +37 -7
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +58 -38
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +2 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +7 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +12 -0
- package/src/cli/SelfUpdateRunner.ts +2 -0
- package/src/codex/CodexEventMapper.ts +97 -3
- package/src/codex/CodexRuntime.ts +163 -13
- package/src/commands/CommandRouter.ts +13 -2
- package/src/commands/commandTypes.ts +2 -2
- package/src/feishu/CardRenderer.ts +48 -12
- package/src/feishu/FeishuMarkdown.ts +69 -3
- package/src/feishu/FeishuTurnPresenter.ts +1 -0
- package/src/index.ts +4 -0
- package/src/local-files/TurnPreviewPage.ts +16 -1
- package/src/presentation/TurnStateReducer.ts +49 -9
- package/src/presentation/turnViewTypes.ts +4 -0
- package/src/proxy/ProxySessionController.ts +61 -40
- package/src/runtime/types.ts +13 -1
- package/src/supervision/SafeRestartNotifier.ts +2 -0
- package/src/supervision/SafeRestartScheduler.ts +7 -0
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ConversationTurn,
|
|
9
9
|
ForkRuntimeSessionInput,
|
|
10
10
|
ModelOption,
|
|
11
|
+
ModelListResult,
|
|
11
12
|
ModelProviderOption,
|
|
12
13
|
PermissionMode,
|
|
13
14
|
RemoteSessionActivity,
|
|
@@ -28,7 +29,7 @@ import type {
|
|
|
28
29
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
29
30
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
30
31
|
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
31
|
-
import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
32
|
+
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
32
33
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
33
34
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
34
35
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
@@ -51,6 +52,7 @@ const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
|
51
52
|
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
52
53
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
53
54
|
const BUILT_IN_CODEX_PROVIDER_ID = "openai";
|
|
55
|
+
const BUILT_IN_TRAEX_PROVIDER_ID = "trae";
|
|
54
56
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
55
57
|
// Wait for its response; connection closure still rejects the request.
|
|
56
58
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
@@ -79,9 +81,12 @@ export interface AppServerClientProvider {
|
|
|
79
81
|
interface CodexSession extends RuntimeSession {
|
|
80
82
|
activeTurnStartedAt?: number;
|
|
81
83
|
terminalTurnIds: Set<string>;
|
|
84
|
+
modeChangeRequestIds: Set<string>;
|
|
82
85
|
finalText: string;
|
|
83
86
|
generatedImagePaths: string[];
|
|
84
87
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
88
|
+
unphasedMessages: Map<string, string>;
|
|
89
|
+
unphasedFinalMessageId?: string;
|
|
85
90
|
needsResume: boolean;
|
|
86
91
|
canReplaceEmptyThread: boolean;
|
|
87
92
|
settingsRecoveryError?: string;
|
|
@@ -96,11 +101,20 @@ interface PendingApproval {
|
|
|
96
101
|
resolve: (value: { decision: ApprovalDecision }) => void;
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
interface PendingModeChange {
|
|
105
|
+
sessionId: string;
|
|
106
|
+
turnId: string;
|
|
107
|
+
requestId: string;
|
|
108
|
+
targetMode: "plan" | "default";
|
|
109
|
+
responding: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
export class CodexRuntime implements AgentRuntime {
|
|
100
113
|
readonly kind = "codex" as const;
|
|
101
114
|
private readonly sessions = new Map<string, CodexSession>();
|
|
102
115
|
private readonly listeners = new Set<(event: RuntimeEvent) => void>();
|
|
103
116
|
private readonly approvals = new Map<string, PendingApproval>();
|
|
117
|
+
private readonly modeChanges = new Map<string, PendingModeChange>();
|
|
104
118
|
private attachedClient?: AppServerClient;
|
|
105
119
|
private unsubscribe?: () => void;
|
|
106
120
|
private readonly unsubscribeDisconnect?: () => void;
|
|
@@ -832,6 +846,34 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
832
846
|
requestId: string,
|
|
833
847
|
decision: ApprovalDecision,
|
|
834
848
|
): Promise<void> {
|
|
849
|
+
const modeChange = this.modeChanges.get(requestId);
|
|
850
|
+
if (modeChange) {
|
|
851
|
+
if (modeChange.sessionId !== sessionId
|
|
852
|
+
|| this.sessions.get(sessionId)?.activeTurnId !== modeChange.turnId) {
|
|
853
|
+
throw new Error("Approval request is no longer pending.");
|
|
854
|
+
}
|
|
855
|
+
if (decision !== "accept" && decision !== "decline" && decision !== "cancel") {
|
|
856
|
+
throw new Error("Invalid mode change decision.");
|
|
857
|
+
}
|
|
858
|
+
if (modeChange.responding) throw new Error("Approval response is already in progress.");
|
|
859
|
+
modeChange.responding = true;
|
|
860
|
+
try {
|
|
861
|
+
const client = await this.client();
|
|
862
|
+
if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
|
|
863
|
+
await client.request("mode/change/respond", {
|
|
864
|
+
threadId: this.requireSession(sessionId).remoteSessionId,
|
|
865
|
+
requestId: modeChange.requestId,
|
|
866
|
+
decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
|
|
867
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
868
|
+
if (this.modeChanges.get(requestId) === modeChange) {
|
|
869
|
+
this.modeChanges.delete(requestId);
|
|
870
|
+
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision });
|
|
871
|
+
}
|
|
872
|
+
} finally {
|
|
873
|
+
modeChange.responding = false;
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
835
877
|
const pending = this.approvals.get(requestId);
|
|
836
878
|
if (!pending || pending.sessionId !== sessionId) throw new Error("Approval request is no longer pending.");
|
|
837
879
|
this.approvals.delete(requestId);
|
|
@@ -840,10 +882,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
840
882
|
}
|
|
841
883
|
|
|
842
884
|
async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
|
|
885
|
+
return (await this.listModelsWithStatus(modelProvider, fallbackModel)).models;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async listModelsWithStatus(modelProvider?: string, fallbackModel?: string): Promise<ModelListResult> {
|
|
843
889
|
const requestedProvider = modelProvider?.trim();
|
|
890
|
+
const agentFamily = this.provider.getAgentFamily?.();
|
|
844
891
|
if (!requestedProvider
|
|
845
|
-
|| (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID &&
|
|
846
|
-
|
|
892
|
+
|| (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && agentFamily !== "traex")
|
|
893
|
+
|| (requestedProvider === BUILT_IN_TRAEX_PROVIDER_ID && agentFamily === "traex")) {
|
|
894
|
+
return { models: await this.listCatalogModels() };
|
|
847
895
|
}
|
|
848
896
|
const config = await this.readConfig();
|
|
849
897
|
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
@@ -859,19 +907,23 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
859
907
|
? stringValue(config.model)?.trim()
|
|
860
908
|
: undefined;
|
|
861
909
|
try {
|
|
862
|
-
|
|
910
|
+
const models = await fetchProviderModels({
|
|
863
911
|
providerId: requestedProvider,
|
|
864
912
|
config: (isRecord(providerConfig) ? providerConfig : {}) as ProviderModelConfig,
|
|
865
913
|
catalog,
|
|
866
914
|
configuredDefaultModel,
|
|
867
915
|
environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
|
|
868
916
|
});
|
|
917
|
+
return { models };
|
|
869
918
|
} catch (error) {
|
|
870
919
|
const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
|
|
871
920
|
if (!fallback) throw error;
|
|
872
921
|
this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id },
|
|
873
922
|
"Provider model discovery failed; using the current or configured model as a fallback.");
|
|
874
|
-
return
|
|
923
|
+
return {
|
|
924
|
+
models: [fallback],
|
|
925
|
+
warning: `${error instanceof Error ? error.message : "Provider 模型列表读取失败。"} 暂时仅显示当前或已配置模型,服务可用性未确认。`,
|
|
926
|
+
};
|
|
875
927
|
}
|
|
876
928
|
}
|
|
877
929
|
|
|
@@ -971,6 +1023,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
971
1023
|
this.unsubscribeDisconnect?.();
|
|
972
1024
|
for (const pending of this.approvals.values()) pending.resolve({ decision: "cancel" });
|
|
973
1025
|
this.approvals.clear();
|
|
1026
|
+
this.modeChanges.clear();
|
|
974
1027
|
this.sessionSyncs.clear();
|
|
975
1028
|
this.sessions.clear();
|
|
976
1029
|
this.provider.close();
|
|
@@ -1064,13 +1117,50 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1064
1117
|
return;
|
|
1065
1118
|
}
|
|
1066
1119
|
const sessionId = session.localSessionId;
|
|
1067
|
-
if (mapped.kind === "
|
|
1120
|
+
if (mapped.kind === "mode_change_requested") {
|
|
1121
|
+
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1122
|
+
if (session.modeChangeRequestIds.has(requestId)) return;
|
|
1123
|
+
session.modeChangeRequestIds.add(requestId);
|
|
1124
|
+
this.modeChanges.set(requestId, {
|
|
1125
|
+
sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
|
|
1126
|
+
targetMode: mapped.targetMode, responding: false,
|
|
1127
|
+
});
|
|
1128
|
+
this.emit({
|
|
1129
|
+
type: "approval_requested", sessionId, turnId: mapped.turnId,
|
|
1130
|
+
request: {
|
|
1131
|
+
id: requestId,
|
|
1132
|
+
kind: "mode_change",
|
|
1133
|
+
title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
|
|
1134
|
+
reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
|
|
1135
|
+
.filter(Boolean).join("\n"),
|
|
1136
|
+
options: [
|
|
1137
|
+
{ id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
|
|
1138
|
+
{ id: "decline", label: "Reject" },
|
|
1139
|
+
{ id: "cancel", label: "Cancel Request" },
|
|
1140
|
+
],
|
|
1141
|
+
},
|
|
1142
|
+
});
|
|
1143
|
+
} else if (mapped.kind === "mode_change_resolved") {
|
|
1144
|
+
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1145
|
+
const pending = this.modeChanges.get(requestId);
|
|
1146
|
+
if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
|
|
1147
|
+
|| pending.targetMode !== mapped.targetMode) return;
|
|
1148
|
+
this.modeChanges.delete(requestId);
|
|
1149
|
+
this.emit({
|
|
1150
|
+
type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
|
|
1151
|
+
decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
|
|
1152
|
+
});
|
|
1153
|
+
} else if (mapped.kind === "token_usage") {
|
|
1068
1154
|
this.emit({
|
|
1069
1155
|
type: "token_usage_updated",
|
|
1070
1156
|
sessionId,
|
|
1071
1157
|
turnId: mapped.turnId,
|
|
1072
1158
|
lastTokens: mapped.lastTokens,
|
|
1073
1159
|
cumulativeTokens: mapped.cumulativeTokens,
|
|
1160
|
+
lastTotalTokens: mapped.lastTotalTokens,
|
|
1161
|
+
cumulativeTotalTokens: mapped.cumulativeTotalTokens,
|
|
1162
|
+
lastCachedTokens: mapped.lastCachedTokens,
|
|
1163
|
+
cumulativeCachedTokens: mapped.cumulativeCachedTokens,
|
|
1074
1164
|
contextTokens: mapped.contextTokens,
|
|
1075
1165
|
});
|
|
1076
1166
|
} else if (mapped.kind === "context_compaction") {
|
|
@@ -1086,8 +1176,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1086
1176
|
});
|
|
1087
1177
|
} else if (mapped.kind === "agent_message_phase") {
|
|
1088
1178
|
session.messagePhases.set(mapped.itemId, mapped.phase);
|
|
1179
|
+
if (mapped.phase === "final_answer") this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
|
|
1180
|
+
else if (session.unphasedFinalMessageId === mapped.itemId) session.unphasedFinalMessageId = undefined;
|
|
1089
1181
|
} else if (mapped.kind === "agent_delta") {
|
|
1090
|
-
|
|
1182
|
+
const phase = session.messagePhases.get(mapped.itemId);
|
|
1183
|
+
if (phase !== "final_answer") {
|
|
1184
|
+
// Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
|
|
1185
|
+
if (phase === undefined) {
|
|
1186
|
+
session.unphasedMessages.set(mapped.itemId, (session.unphasedMessages.get(mapped.itemId) ?? "") + mapped.text);
|
|
1187
|
+
session.unphasedFinalMessageId = mapped.itemId;
|
|
1188
|
+
}
|
|
1091
1189
|
this.emit({
|
|
1092
1190
|
type: "progress",
|
|
1093
1191
|
sessionId,
|
|
@@ -1100,6 +1198,19 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1100
1198
|
session.finalText += mapped.text;
|
|
1101
1199
|
this.emit({ type: "agent_text_delta", sessionId, turnId: mapped.turnId, text: mapped.text });
|
|
1102
1200
|
}
|
|
1201
|
+
} else if (mapped.kind === "runtime_error") {
|
|
1202
|
+
this.logger.warn({ sessionId, turnId: mapped.turnId, willRetry: mapped.willRetry, error: mapped.message },
|
|
1203
|
+
"App Server reported a turn error.");
|
|
1204
|
+
// Only turn/completed terminates the turn; native retries keep the same progress card.
|
|
1205
|
+
this.emit({
|
|
1206
|
+
type: "progress",
|
|
1207
|
+
sessionId,
|
|
1208
|
+
turnId: mapped.turnId,
|
|
1209
|
+
activityId: `commentary:runtime-error:${mapped.turnId}`,
|
|
1210
|
+
text: `${mapped.willRetry ? "运行请求出错,Agent 正在重试" : "运行请求失败,等待 Agent 返回最终状态"}:${mapped.message}`,
|
|
1211
|
+
append: false,
|
|
1212
|
+
severity: "warning",
|
|
1213
|
+
});
|
|
1103
1214
|
} else if (mapped.kind === "progress") {
|
|
1104
1215
|
this.emit({
|
|
1105
1216
|
type: "progress",
|
|
@@ -1112,6 +1223,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1112
1223
|
} else if (mapped.kind === "plan") {
|
|
1113
1224
|
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
1114
1225
|
} else if (mapped.kind === "tool") {
|
|
1226
|
+
if (mapped.phase === "started") session.unphasedFinalMessageId = undefined;
|
|
1115
1227
|
if (
|
|
1116
1228
|
mapped.phase === "updated"
|
|
1117
1229
|
&& mapped.tool.kind === "image_generation"
|
|
@@ -1135,6 +1247,11 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1135
1247
|
delta: mapped.delta,
|
|
1136
1248
|
});
|
|
1137
1249
|
} else if (mapped.kind === "terminal") {
|
|
1250
|
+
if (mapped.status === "completed" && !session.finalText && session.unphasedFinalMessageId) {
|
|
1251
|
+
this.promoteUnphasedMessage(session, mapped.turnId, session.unphasedFinalMessageId);
|
|
1252
|
+
}
|
|
1253
|
+
session.unphasedMessages.clear();
|
|
1254
|
+
session.unphasedFinalMessageId = undefined;
|
|
1138
1255
|
session.activeTurnId = undefined;
|
|
1139
1256
|
session.activeTurnStartedAt = undefined;
|
|
1140
1257
|
session.terminalTurnIds.add(mapped.turnId);
|
|
@@ -1191,6 +1308,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1191
1308
|
return response;
|
|
1192
1309
|
}
|
|
1193
1310
|
|
|
1311
|
+
private promoteUnphasedMessage(session: CodexSession, turnId: string, itemId: string): void {
|
|
1312
|
+
const text = session.unphasedMessages.get(itemId);
|
|
1313
|
+
if (text === undefined) return;
|
|
1314
|
+
session.unphasedMessages.delete(itemId);
|
|
1315
|
+
if (session.unphasedFinalMessageId === itemId) session.unphasedFinalMessageId = undefined;
|
|
1316
|
+
session.finalText += text;
|
|
1317
|
+
this.emit({
|
|
1318
|
+
type: "agent_text_delta", sessionId: session.localSessionId, turnId, text,
|
|
1319
|
+
replacesActivityId: `commentary:${itemId}`,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1194
1323
|
private async resolveReasoningEffort(
|
|
1195
1324
|
input: CreateRuntimeSessionInput,
|
|
1196
1325
|
response: ThreadResponse,
|
|
@@ -1290,6 +1419,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1290
1419
|
}
|
|
1291
1420
|
session.activeTurnId = turnId;
|
|
1292
1421
|
session.activeTurnStartedAt = startedAt;
|
|
1422
|
+
session.modeChangeRequestIds.clear();
|
|
1423
|
+
session.unphasedMessages.clear();
|
|
1424
|
+
session.unphasedFinalMessageId = undefined;
|
|
1293
1425
|
session.finalText = "";
|
|
1294
1426
|
session.generatedImagePaths = [];
|
|
1295
1427
|
session.messagePhases.clear();
|
|
@@ -1308,6 +1440,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1308
1440
|
|
|
1309
1441
|
private finishSnapshotTurn(session: CodexSession, turn: CodexTurnSnapshot): void {
|
|
1310
1442
|
if (session.activeTurnId !== turn.id) return;
|
|
1443
|
+
if (turn.status === "completed") {
|
|
1444
|
+
for (const item of finalResponseMessages(turn)) {
|
|
1445
|
+
if (item.id) this.promoteUnphasedMessage(session, turn.id, item.id);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
session.unphasedMessages.clear();
|
|
1449
|
+
session.unphasedFinalMessageId = undefined;
|
|
1311
1450
|
session.activeTurnId = undefined;
|
|
1312
1451
|
session.activeTurnStartedAt = undefined;
|
|
1313
1452
|
session.terminalTurnIds.add(turn.id);
|
|
@@ -1321,7 +1460,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1321
1460
|
type: "turn_failed",
|
|
1322
1461
|
sessionId: session.localSessionId,
|
|
1323
1462
|
turnId: turn.id,
|
|
1324
|
-
message: turn.error
|
|
1463
|
+
message: formatCodexError(turn.error) ?? "App Server turn failed.",
|
|
1325
1464
|
});
|
|
1326
1465
|
return;
|
|
1327
1466
|
}
|
|
@@ -1375,9 +1514,11 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1375
1514
|
? [input.lastTurnId]
|
|
1376
1515
|
: [],
|
|
1377
1516
|
),
|
|
1517
|
+
modeChangeRequestIds: new Set(),
|
|
1378
1518
|
finalText: "",
|
|
1379
1519
|
generatedImagePaths: [],
|
|
1380
1520
|
messagePhases: new Map(),
|
|
1521
|
+
unphasedMessages: new Map(),
|
|
1381
1522
|
needsResume: false,
|
|
1382
1523
|
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1383
1524
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
@@ -1410,6 +1551,11 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1410
1551
|
}
|
|
1411
1552
|
|
|
1412
1553
|
private emit(event: RuntimeEvent): void {
|
|
1554
|
+
if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
1555
|
+
for (const [requestId, pending] of this.modeChanges) {
|
|
1556
|
+
if (pending.sessionId === event.sessionId && pending.turnId === event.turnId) this.modeChanges.delete(requestId);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1413
1559
|
for (const listener of this.listeners) listener(event);
|
|
1414
1560
|
}
|
|
1415
1561
|
|
|
@@ -1417,6 +1563,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1417
1563
|
this.unsubscribe?.();
|
|
1418
1564
|
this.unsubscribe = undefined;
|
|
1419
1565
|
this.attachedClient = undefined;
|
|
1566
|
+
this.modeChanges.clear();
|
|
1420
1567
|
for (const session of this.sessions.values()) {
|
|
1421
1568
|
session.needsResume = true;
|
|
1422
1569
|
const turnId = session.activeTurnId;
|
|
@@ -1481,6 +1628,7 @@ interface CodexTurnSnapshot {
|
|
|
1481
1628
|
id: string;
|
|
1482
1629
|
status: "completed" | "interrupted" | "failed" | "inProgress";
|
|
1483
1630
|
items?: Array<{
|
|
1631
|
+
id?: string;
|
|
1484
1632
|
type?: string;
|
|
1485
1633
|
text?: string;
|
|
1486
1634
|
content?: Array<{
|
|
@@ -1491,7 +1639,7 @@ interface CodexTurnSnapshot {
|
|
|
1491
1639
|
status?: string;
|
|
1492
1640
|
savedPath?: string;
|
|
1493
1641
|
}>;
|
|
1494
|
-
error?: { message?: string } | null;
|
|
1642
|
+
error?: { message?: string; additionalDetails?: string | null } | null;
|
|
1495
1643
|
startedAt?: number | null;
|
|
1496
1644
|
durationMs?: number | null;
|
|
1497
1645
|
}
|
|
@@ -1575,13 +1723,15 @@ function turnStartedAt(turn: CodexTurnSnapshot): number {
|
|
|
1575
1723
|
}
|
|
1576
1724
|
|
|
1577
1725
|
function extractFinalResponse(turn: CodexTurnSnapshot): string {
|
|
1726
|
+
return finalResponseMessages(turn).map((item) => item.text!.trim()).join("\n\n");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function finalResponseMessages(turn: CodexTurnSnapshot) {
|
|
1578
1730
|
const messages = (turn.items ?? []).filter(
|
|
1579
1731
|
(item) => item.type === "agentMessage" && typeof item.text === "string" && item.text.trim(),
|
|
1580
1732
|
);
|
|
1581
1733
|
const finalMessages = messages.filter((item) => item.phase === "final_answer");
|
|
1582
|
-
return
|
|
1583
|
-
.map((item) => item.text!.trim())
|
|
1584
|
-
.join("\n\n");
|
|
1734
|
+
return finalMessages.length ? finalMessages : messages.filter((item) => item.phase !== "commentary").slice(-1);
|
|
1585
1735
|
}
|
|
1586
1736
|
|
|
1587
1737
|
function extractGeneratedImagePaths(turn: CodexTurnSnapshot): string[] {
|
|
@@ -1648,7 +1798,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
1648
1798
|
finalResponse: lastTurn && lastTurn.status !== "inProgress"
|
|
1649
1799
|
? appendGeneratedImageMarkdown(extractFinalResponse(lastTurn), extractGeneratedImagePaths(lastTurn)) || undefined
|
|
1650
1800
|
: undefined,
|
|
1651
|
-
lastError: lastTurn?.error
|
|
1801
|
+
lastError: formatCodexError(lastTurn?.error),
|
|
1652
1802
|
lastTurnToolCount: toolCounts?.total,
|
|
1653
1803
|
lastTurnCompletedToolCount: toolCounts?.completed,
|
|
1654
1804
|
lastTurnFailedToolCount: toolCounts?.failed,
|
|
@@ -196,6 +196,7 @@ function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
|
|
|
196
196
|
title: options.title,
|
|
197
197
|
cwd: options.cwd,
|
|
198
198
|
...(options.projectless ? { projectless: true } : {}),
|
|
199
|
+
...(options.agentName ? { agentName: options.agentName } : {}),
|
|
199
200
|
};
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -206,6 +207,7 @@ function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgrou
|
|
|
206
207
|
title: options.title,
|
|
207
208
|
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
208
209
|
...(options.projectless ? { projectless: true } : {}),
|
|
210
|
+
...(options.agentName ? { agentName: options.agentName } : {}),
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -214,9 +216,10 @@ function parseNewTaskOptions(
|
|
|
214
216
|
args: string[],
|
|
215
217
|
directoryLabel: string,
|
|
216
218
|
exampleDirectory: string,
|
|
217
|
-
): { title?: string; cwd?: string; projectless: boolean } {
|
|
219
|
+
): { title?: string; cwd?: string; projectless: boolean; agentName?: string } {
|
|
218
220
|
const titleParts: string[] = [];
|
|
219
221
|
let cwd: string | undefined;
|
|
222
|
+
let agentName: string | undefined;
|
|
220
223
|
let projectless = false;
|
|
221
224
|
for (let index = 0; index < args.length; index += 1) {
|
|
222
225
|
const argument = args[index]!;
|
|
@@ -226,6 +229,13 @@ function parseNewTaskOptions(
|
|
|
226
229
|
projectless = true;
|
|
227
230
|
continue;
|
|
228
231
|
}
|
|
232
|
+
if (argument === "--agent") {
|
|
233
|
+
if (agentName !== undefined) throw new Error(`${commandName} 只能指定一次 --agent。`);
|
|
234
|
+
const name = args[++index];
|
|
235
|
+
if (!name?.trim() || name.startsWith("--")) throw new Error("请在 --agent 后指定 Agent 名称。");
|
|
236
|
+
agentName = name.trim();
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
229
239
|
if (argument !== "--dir") {
|
|
230
240
|
titleParts.push(argument);
|
|
231
241
|
continue;
|
|
@@ -233,7 +243,7 @@ function parseNewTaskOptions(
|
|
|
233
243
|
if (projectless) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
|
|
234
244
|
if (cwd !== undefined) throw new Error(`${commandName} 只能指定一次 --dir。`);
|
|
235
245
|
const directory = args[index + 1];
|
|
236
|
-
if (!directory || directory
|
|
246
|
+
if (!directory || directory.startsWith("--")) {
|
|
237
247
|
throw new Error(
|
|
238
248
|
`请在 --dir 后指定${directoryLabel},例如:${commandName} 修复会话列表 --dir ${exampleDirectory}。`,
|
|
239
249
|
);
|
|
@@ -245,6 +255,7 @@ function parseNewTaskOptions(
|
|
|
245
255
|
title: titleParts.join(" ").trim() || undefined,
|
|
246
256
|
cwd,
|
|
247
257
|
projectless,
|
|
258
|
+
agentName,
|
|
248
259
|
};
|
|
249
260
|
}
|
|
250
261
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type Command =
|
|
2
2
|
| { type: "shell"; command: string }
|
|
3
|
-
| { type: "new"; title?: string; cwd?: string; projectless?: boolean }
|
|
4
|
-
| { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean }
|
|
3
|
+
| { type: "new"; title?: string; cwd?: string; projectless?: boolean; agentName?: string }
|
|
4
|
+
| { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean; agentName?: string }
|
|
5
5
|
| { type: "dir"; directory?: string }
|
|
6
6
|
| { type: "file"; filePath: string }
|
|
7
7
|
| { type: "clone" | "clonegroup"; title?: string; agentName?: string }
|
|
@@ -64,6 +64,8 @@ export interface SafeRestartStatusView {
|
|
|
64
64
|
pendingFinalDeliveries: number;
|
|
65
65
|
waitingTasks: Array<{
|
|
66
66
|
id: string;
|
|
67
|
+
localSessionId: string;
|
|
68
|
+
isRunning: boolean;
|
|
67
69
|
title?: string;
|
|
68
70
|
}>;
|
|
69
71
|
}
|
|
@@ -88,6 +90,7 @@ export interface ModelSelectorCardView {
|
|
|
88
90
|
permissionMode?: PermissionMode;
|
|
89
91
|
unifiedSettings?: boolean;
|
|
90
92
|
notice?: string;
|
|
93
|
+
warning?: string;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
export interface ReasoningSelectorCardView {
|
|
@@ -100,6 +103,7 @@ export interface ReasoningSelectorCardView {
|
|
|
100
103
|
permissionMode?: PermissionMode;
|
|
101
104
|
unifiedSettings?: boolean;
|
|
102
105
|
notice?: string;
|
|
106
|
+
warning?: string;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
export interface ProviderSelectorCardView {
|
|
@@ -154,6 +158,7 @@ export interface ExecutionSettingsCardView {
|
|
|
154
158
|
models: ModelOption[];
|
|
155
159
|
reasoningOptions: ReasoningEffortOption[];
|
|
156
160
|
notice?: string;
|
|
161
|
+
warning?: string;
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
export interface CardSection {
|
|
@@ -545,6 +550,7 @@ export class CardRenderer {
|
|
|
545
550
|
]
|
|
546
551
|
: []),
|
|
547
552
|
...(view.notice ? [view.notice] : []),
|
|
553
|
+
...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
|
|
548
554
|
].join("\n")),
|
|
549
555
|
...(tabRow ? [tabRow] : []),
|
|
550
556
|
{ tag: "hr" },
|
|
@@ -639,7 +645,7 @@ export class CardRenderer {
|
|
|
639
645
|
}
|
|
640
646
|
|
|
641
647
|
elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
|
|
642
|
-
return sectionCard("运行设置", elements, view.notice ? "green" : "blue");
|
|
648
|
+
return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
|
|
643
649
|
}
|
|
644
650
|
|
|
645
651
|
renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
|
|
@@ -700,6 +706,7 @@ export class CardRenderer {
|
|
|
700
706
|
`**模型**:${inlineCode(view.model)}`,
|
|
701
707
|
`**当前思考模式**:${inlineCode(view.currentEffort ?? "默认")}`,
|
|
702
708
|
...(view.notice ? [view.notice] : []),
|
|
709
|
+
...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
|
|
703
710
|
].join("\n")),
|
|
704
711
|
{ tag: "hr" },
|
|
705
712
|
];
|
|
@@ -757,7 +764,7 @@ export class CardRenderer {
|
|
|
757
764
|
},
|
|
758
765
|
}]),
|
|
759
766
|
);
|
|
760
|
-
return sectionCard("思考模式", elements, view.notice ? "green" : "blue");
|
|
767
|
+
return sectionCard("思考模式", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
|
|
761
768
|
}
|
|
762
769
|
|
|
763
770
|
renderModelSelector(view: ModelSelectorCardView): Record<string, unknown> {
|
|
@@ -767,6 +774,7 @@ export class CardRenderer {
|
|
|
767
774
|
`**当前模型**:${inlineCode(view.currentModel ?? "默认")}`,
|
|
768
775
|
`**思考强度**:${inlineCode(view.reasoningEffort ?? "默认")}`,
|
|
769
776
|
...(view.notice ? [view.notice] : []),
|
|
777
|
+
...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
|
|
770
778
|
].join("\n")),
|
|
771
779
|
{ tag: "hr" },
|
|
772
780
|
];
|
|
@@ -825,7 +833,8 @@ export class CardRenderer {
|
|
|
825
833
|
}]),
|
|
826
834
|
);
|
|
827
835
|
}
|
|
828
|
-
return sectionCard(view.unifiedSettings ? "Provider 设置 · 模型" : "模型", elements,
|
|
836
|
+
return sectionCard(view.unifiedSettings ? "Provider 设置 · 模型" : "模型", elements,
|
|
837
|
+
view.warning ? "orange" : view.notice ? "green" : "blue");
|
|
829
838
|
}
|
|
830
839
|
|
|
831
840
|
renderPermissionSelector(view: PermissionSelectorCardView): Record<string, unknown> {
|
|
@@ -982,6 +991,7 @@ export class CardRenderer {
|
|
|
982
991
|
}
|
|
983
992
|
|
|
984
993
|
renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
|
|
994
|
+
const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
|
|
985
995
|
const countdown = view.phase === "countdown"
|
|
986
996
|
? `${Math.max(0, Math.ceil((view.remainingMs ?? 0) / 1_000))}s`
|
|
987
997
|
: view.phase === "restarting"
|
|
@@ -1011,16 +1021,34 @@ export class CardRenderer {
|
|
|
1011
1021
|
const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
|
|
1012
1022
|
if (view.waitingTasks.length > 0) {
|
|
1013
1023
|
const visible = view.waitingTasks.slice(0, 10);
|
|
1014
|
-
|
|
1015
|
-
|
|
1024
|
+
elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**`));
|
|
1025
|
+
for (const [index, task] of visible.entries()) {
|
|
1026
|
+
const label = `${index + 1}. ${task.title ? `${inlineCode(truncateText(task.title, 80))} · ` : ""}${inlineCode(task.id)}`;
|
|
1027
|
+
elements.push({
|
|
1028
|
+
tag: "column_set",
|
|
1029
|
+
flex_mode: "none",
|
|
1030
|
+
horizontal_spacing: "8px",
|
|
1031
|
+
columns: [
|
|
1032
|
+
{ tag: "column", width: "weighted", weight: 1, elements: [markdown(label)] },
|
|
1033
|
+
...(actionable && task.isRunning ? [{
|
|
1034
|
+
tag: "column",
|
|
1035
|
+
width: "auto",
|
|
1036
|
+
elements: [taskActionElement({
|
|
1037
|
+
text: "Stop",
|
|
1038
|
+
type: "danger",
|
|
1039
|
+
value: { action: "session_stop", sessionId: task.localSessionId, cardView: "safe_restart" },
|
|
1040
|
+
})],
|
|
1041
|
+
}] : []),
|
|
1042
|
+
],
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1016
1045
|
if (view.waitingTasks.length > visible.length) {
|
|
1017
|
-
|
|
1046
|
+
elements.push(markdown(`… 还有 ${view.waitingTasks.length - visible.length} 个任务`));
|
|
1018
1047
|
}
|
|
1019
|
-
elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**\n${taskLines.join("\n")}`));
|
|
1020
1048
|
} else {
|
|
1021
1049
|
elements.push({ tag: "hr" }, markdown("**当前等待的任务**:无"));
|
|
1022
1050
|
}
|
|
1023
|
-
if (
|
|
1051
|
+
if (actionable) {
|
|
1024
1052
|
elements.push(
|
|
1025
1053
|
{ tag: "hr" },
|
|
1026
1054
|
taskActionRow([{
|
|
@@ -1029,6 +1057,12 @@ export class CardRenderer {
|
|
|
1029
1057
|
action: "safe_restart_cancel",
|
|
1030
1058
|
scheduleId: String(view.scheduleId),
|
|
1031
1059
|
},
|
|
1060
|
+
}, {
|
|
1061
|
+
text: "ForceRestart",
|
|
1062
|
+
value: {
|
|
1063
|
+
action: "safe_restart_force",
|
|
1064
|
+
scheduleId: String(view.scheduleId),
|
|
1065
|
+
},
|
|
1032
1066
|
}]),
|
|
1033
1067
|
);
|
|
1034
1068
|
}
|
|
@@ -1665,13 +1699,12 @@ function renderTurnElements(
|
|
|
1665
1699
|
tag: "column_set",
|
|
1666
1700
|
flex_mode: "flow",
|
|
1667
1701
|
horizontal_spacing: "8px",
|
|
1668
|
-
vertical_spacing: "8px",
|
|
1669
1702
|
columns: request.options.map((option) => ({
|
|
1670
1703
|
tag: "column",
|
|
1671
1704
|
width: "auto",
|
|
1672
1705
|
elements: [{
|
|
1673
1706
|
tag: "button",
|
|
1674
|
-
text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
|
|
1707
|
+
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1675
1708
|
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1676
1709
|
behaviors: [{
|
|
1677
1710
|
type: "callback",
|
|
@@ -1735,13 +1768,12 @@ function renderGroupedTurnElements(
|
|
|
1735
1768
|
tag: "column_set",
|
|
1736
1769
|
flex_mode: "flow",
|
|
1737
1770
|
horizontal_spacing: "8px",
|
|
1738
|
-
vertical_spacing: "8px",
|
|
1739
1771
|
columns: request.options.map((option) => ({
|
|
1740
1772
|
tag: "column",
|
|
1741
1773
|
width: "auto",
|
|
1742
1774
|
elements: [{
|
|
1743
1775
|
tag: "button",
|
|
1744
|
-
text: { tag: "plain_text", content: approvalDecisionLabel(option.id) },
|
|
1776
|
+
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1745
1777
|
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1746
1778
|
behaviors: [{
|
|
1747
1779
|
type: "callback",
|
|
@@ -1815,6 +1847,10 @@ function renderActivity(
|
|
|
1815
1847
|
if (activity.kind === "assistant" || (activity.kind === "reasoning" && activity.id.startsWith("commentary:"))) {
|
|
1816
1848
|
const text = activity.text.trim();
|
|
1817
1849
|
if (!text) return [];
|
|
1850
|
+
if (activity.id.startsWith("commentary:runtime-error:")) {
|
|
1851
|
+
const chunks = fullAssistantText ? splitText(text, ACTIVITY_TEXT_CHUNK) : [text];
|
|
1852
|
+
return chunks.map((chunk) => markdown(codeBlock(chunk, fullAssistantText ? ACTIVITY_TEXT_CHUNK : MAX_LIVE_ASSISTANT_TEXT)));
|
|
1853
|
+
}
|
|
1818
1854
|
return fullAssistantText
|
|
1819
1855
|
? splitText(text, ACTIVITY_TEXT_CHUNK).map(markdown)
|
|
1820
1856
|
: [markdown(truncateText(text, MAX_LIVE_ASSISTANT_TEXT))];
|