@keyou007/agent-bot 0.1.24-alpha.7 → 0.1.24
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 +28 -0
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/dist/codex/CodexProcessManager.js +3 -1
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +165 -23
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/CodexVersion.js +2 -2
- package/dist/codex/CodexVersion.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +2 -2
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/feishu/CardRenderer.js +10 -6
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +7 -3
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +26 -10
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/visibleTurnActivities.js +13 -0
- package/dist/presentation/visibleTurnActivities.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +84 -17
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/responseErrors.js +60 -0
- package/dist/runtime/responseErrors.js.map +1 -0
- package/docs/technical-reference.md +11 -1
- package/docs/technical-reference.zh.md +11 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +5 -3
- package/src/codex/CodexProcessManager.ts +3 -1
- package/src/codex/CodexRuntime.ts +158 -22
- package/src/codex/CodexVersion.ts +2 -2
- package/src/codex/ProviderSwitch.ts +2 -2
- package/src/feishu/CardRenderer.ts +10 -5
- package/src/feishu/FeishuConnector.ts +7 -3
- package/src/local-files/TurnPreviewPage.ts +25 -10
- package/src/presentation/visibleTurnActivities.ts +12 -0
- package/src/proxy/ProxySessionController.ts +90 -20
- package/src/runtime/responseErrors.ts +62 -0
- package/src/runtime/types.ts +7 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { Logger } from "pino";
|
|
2
3
|
import type { WSClient as LarkWsClient, WSConnectionStatus } from "@larksuiteoapi/node-sdk";
|
|
3
4
|
import type { AppConfig } from "../config/schema.js";
|
|
@@ -395,7 +396,7 @@ function toCardAction(data: unknown) {
|
|
|
395
396
|
const openId = event.operator?.open_id ?? event.operator?.user_id;
|
|
396
397
|
const chatId = event.context?.open_chat_id ?? event.chat_id;
|
|
397
398
|
return {
|
|
398
|
-
actionId: getEventId(data) ?? event.action.action_id ??
|
|
399
|
+
actionId: getEventId(data) ?? event.action.action_id ?? randomUUID(),
|
|
399
400
|
contextKey: chatId ? `chat_id:${chatId}` : `open_id:${openId}`,
|
|
400
401
|
userId: openId,
|
|
401
402
|
...(typeof event.context?.open_message_id === "string" ? { messageId: event.context.open_message_id } : {}),
|
|
@@ -415,8 +416,11 @@ function getCardActionValue(action: Record<string, unknown>): Record<string, unk
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
function getEventId(data: unknown): string | undefined {
|
|
418
|
-
|
|
419
|
-
|
|
419
|
+
// The SDK flattens header fields into the event passed to handlers.
|
|
420
|
+
const value = getNested<unknown>(data, ["header", "event_id"])
|
|
421
|
+
?? getNested(data, ["data", "header", "event_id"])
|
|
422
|
+
?? getNested(data, ["event_id"]);
|
|
423
|
+
return typeof value === "string" && value ? value : undefined;
|
|
420
424
|
}
|
|
421
425
|
|
|
422
426
|
function getFeishuEvent(data: unknown): Record<string, any> | undefined {
|
|
@@ -8,6 +8,8 @@ import type { ApprovalRequest, ToolState } from "../runtime/types.js";
|
|
|
8
8
|
import type { FileSummary, TurnActivity, TurnReasoningItem, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
9
9
|
import { displayFilePath, displayToolCommand, formatShellCommandForDisplay, toolStatusIcon } from "../feishu/CardRenderer.js";
|
|
10
10
|
import { turnReasoningItems } from "../presentation/turnReasoning.js";
|
|
11
|
+
import { visibleTurnActivities } from "../presentation/visibleTurnActivities.js";
|
|
12
|
+
import { displayRuntimeError } from "../runtime/responseErrors.js";
|
|
11
13
|
import { isFileUrl, rewriteMarkdownFileLinks } from "./MarkdownFileLinks.js";
|
|
12
14
|
|
|
13
15
|
const MARKDOWN = new MarkdownIt({
|
|
@@ -285,6 +287,10 @@ export const TURN_PREVIEW_CLIENT_SCRIPT = DIAGRAM_PREVIEW_CLIENT_SCRIPT + `(() =
|
|
|
285
287
|
};
|
|
286
288
|
const compatible = (current, next) => current.nodeType === next.nodeType && current.nodeName === next.nodeName;
|
|
287
289
|
const patchNode = (current, next) => {
|
|
290
|
+
if (!compatible(current, next)) {
|
|
291
|
+
current.replaceWith(next.cloneNode(true));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
288
294
|
// Summary snapshots never own a detail body that has already been fetched.
|
|
289
295
|
if (current.nodeType === 1 && current.hasAttribute("data-detail-body") && next.hasAttribute("data-detail-body")) return;
|
|
290
296
|
if (current.isEqualNode(next)) return;
|
|
@@ -620,16 +626,19 @@ export function renderTurnPreviewPatch(
|
|
|
620
626
|
state: TurnViewState, changed: Set<string>, removed: Set<string>,
|
|
621
627
|
localFileUrl: (filePath: string) => string | undefined, language: TurnPreviewLanguage,
|
|
622
628
|
): TurnPreviewSnapshot & { removed: string[]; before: Record<string, string> } {
|
|
629
|
+
const visible = visibleTurnActivities(state);
|
|
630
|
+
const visibleIds = new Set(visible.map((activity) => activity.id));
|
|
631
|
+
const suppressed = state.activities.filter((activity) => !visibleIds.has(activity.id)).map((activity) => activity.id);
|
|
623
632
|
const partial = { ...state,
|
|
624
|
-
activities:
|
|
633
|
+
activities: visible.filter((a) => changed.has(a.id)),
|
|
625
634
|
reasoningItems: state.reasoningItems?.filter((r) => changed.has(`reasoning:${r.itemId}`)),
|
|
626
635
|
};
|
|
627
636
|
const order = [
|
|
628
637
|
...(state.reasoningItems ?? []).filter((r) => !r.afterActivityId).map((r) => `reasoning:${r.itemId}`),
|
|
629
|
-
...
|
|
638
|
+
...visible.flatMap((a) => [a.id, ...(state.reasoningItems ?? []).filter((r) => r.afterActivityId === a.id).map((r) => `reasoning:${r.itemId}`)]),
|
|
630
639
|
];
|
|
631
640
|
const before = Object.fromEntries(order.flatMap((id, i) => changed.has(id) && order[i + 1] ? [[id, order[i + 1]]] : []));
|
|
632
|
-
return { ...renderTurnPreviewSnapshot(partial, localFileUrl, language, { deferDetails: true }), removed: [...removed], before };
|
|
641
|
+
return { ...renderTurnPreviewSnapshot(partial, localFileUrl, language, { deferDetails: true }), removed: [...new Set([...removed, ...suppressed])], before };
|
|
633
642
|
}
|
|
634
643
|
|
|
635
644
|
export function renderTurnPreviewSnapshot(
|
|
@@ -645,7 +654,7 @@ export function renderTurnPreviewSnapshot(
|
|
|
645
654
|
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
646
655
|
} : undefined;
|
|
647
656
|
const renderText = (value: string): string => renderMarkdown(value, state.projectCwd, localFileUrl, language);
|
|
648
|
-
const timeline = renderTimeline(state, renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd, options.deferDetails);
|
|
657
|
+
const timeline = renderTimeline({ ...state, activities: visibleTurnActivities(state) }, renderText, resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language, state.projectCwd, options.deferDetails);
|
|
649
658
|
const terminal = isTerminal(state.status);
|
|
650
659
|
const waitingForProgress = !terminal && !state.finalResponse && !state.assistantText
|
|
651
660
|
&& !state.error && !state.approval && state.status !== "waiting_for_approval"
|
|
@@ -666,7 +675,7 @@ export function renderTurnPreviewSnapshot(
|
|
|
666
675
|
`<section class="timeline" data-preview-key="timeline"${timelineContent ? "" : " hidden"}>${timelineContent}</section>`,
|
|
667
676
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl, options.deferDetails) : "",
|
|
668
677
|
state.approval ? renderApprovalNotice(state.approval, renderText, language) : "",
|
|
669
|
-
state.error ? `<section class="result error-result" data-preview-key="error"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
678
|
+
state.error ? `<section class="result error-result" data-preview-key="error"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(displayRuntimeError(state.error))}</pre></section>` : "",
|
|
670
679
|
state.finalResponse
|
|
671
680
|
? renderFinalAnswer(state.finalResponse, state.turnId, renderText, language)
|
|
672
681
|
: state.assistantText
|
|
@@ -792,8 +801,12 @@ function renderReasoningItem(
|
|
|
792
801
|
deferDetails: boolean,
|
|
793
802
|
): string {
|
|
794
803
|
const presentation = reasoningPresentation(item);
|
|
795
|
-
|
|
804
|
+
const hasBody = presentation.sections.some((section) => section.parts.length > 0);
|
|
805
|
+
if (!presentation.title && !hasBody) return "";
|
|
796
806
|
const title = presentation.title ?? previewLabels(language).reasoning;
|
|
807
|
+
if (!hasBody) {
|
|
808
|
+
return `<article class="reasoning-step" data-activity-id="reasoning:${escapeAttribute(item.itemId)}"><div class="reasoning-header" title="${escapeAttribute(title)}"><span class="reasoning-title">💭 ${escapeHtml(title)}</span></div></article>`;
|
|
809
|
+
}
|
|
797
810
|
const lazy = deferDetails ? lazyDetailAttributes(`reasoning:${item.itemId}`, reasoningRevision(item, cwd)) : "";
|
|
798
811
|
const body = deferDetails ? lazyDetailBody() : renderReasoningBody(item, cwd, localFileUrl, language, presentation);
|
|
799
812
|
return `<details class="reasoning-step" data-activity-id="reasoning:${escapeAttribute(item.itemId)}"${lazy}><summary class="reasoning-header" title="${escapeAttribute(title)}"><span class="reasoning-title">💭 ${escapeHtml(title)}</span></summary>${body}</details>`;
|
|
@@ -831,7 +844,7 @@ function renderActivity(
|
|
|
831
844
|
switch (activity.kind) {
|
|
832
845
|
case "assistant":
|
|
833
846
|
if (activity.id.startsWith("commentary:runtime-error:")) {
|
|
834
|
-
return `<article class="activity message runtime-error" data-activity="${escapeAttribute(activity.id)}"><pre>${escapeHtml(activity.text)}</pre></article>`;
|
|
847
|
+
return `<article class="activity message runtime-error" data-activity="${escapeAttribute(activity.id)}"><pre>${escapeHtml(displayRuntimeError(activity.text))}</pre></article>`;
|
|
835
848
|
}
|
|
836
849
|
return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
|
|
837
850
|
case "user":
|
|
@@ -1163,9 +1176,11 @@ export function renderTurnPreviewDetail(
|
|
|
1163
1176
|
}
|
|
1164
1177
|
if (key.startsWith("reasoning:")) {
|
|
1165
1178
|
const item = turnReasoningItems(state).find((item) => `reasoning:${item.itemId}` === key);
|
|
1166
|
-
if (!item
|
|
1179
|
+
if (!item) return undefined;
|
|
1180
|
+
const presentation = reasoningPresentation(item);
|
|
1181
|
+
if (!presentation.sections.some((section) => section.parts.length > 0)) return undefined;
|
|
1167
1182
|
return { key, revision: reasoningRevision(item, state.projectCwd),
|
|
1168
|
-
content: renderReasoningBody(item, state.projectCwd, localFileUrl, language) };
|
|
1183
|
+
content: renderReasoningBody(item, state.projectCwd, localFileUrl, language, presentation) };
|
|
1169
1184
|
}
|
|
1170
1185
|
const activity = state.activities?.find((item) => item.kind === "tool" && `tool:${item.id}` === key);
|
|
1171
1186
|
if (activity?.kind !== "tool") return undefined;
|
|
@@ -1334,7 +1349,7 @@ main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
|
|
|
1334
1349
|
.markdown > :first-child { margin-top: 0; }
|
|
1335
1350
|
.markdown > :last-child { margin-bottom: 0; }
|
|
1336
1351
|
.reasoning-step { margin:8px 0; border:1px solid var(--line); border-radius:7px; min-width:0; }
|
|
1337
|
-
.reasoning-header {
|
|
1352
|
+
.reasoning-header { display:flex; align-items:center; gap:8px; min-height:32px; padding:8px 10px; color:var(--muted); }
|
|
1338
1353
|
.reasoning-title { min-width:0; flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
1339
1354
|
.reasoning-section { margin:0 10px 10px; max-height:30em; overflow:auto; overflow-wrap:anywhere; }
|
|
1340
1355
|
.reasoning-part + .reasoning-part { border-top:1px solid var(--line); padding-top:8px; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RUNTIME_WARNING_PREFIXES, sameRuntimeError } from "../runtime/responseErrors.js";
|
|
2
|
+
import type { TurnActivity, TurnViewState } from "./turnViewTypes.js";
|
|
3
|
+
|
|
4
|
+
export function visibleTurnActivities(state: TurnViewState): TurnActivity[] {
|
|
5
|
+
if (state.status !== "failed" || !state.error) return state.activities;
|
|
6
|
+
return state.activities.filter((activity) => {
|
|
7
|
+
if (activity.id !== `commentary:runtime-error:${state.turnId}`
|
|
8
|
+
|| (activity.kind !== "assistant" && activity.kind !== "reasoning")) return true;
|
|
9
|
+
const prefix = RUNTIME_WARNING_PREFIXES.find((value) => activity.text.startsWith(value));
|
|
10
|
+
return !sameRuntimeError(prefix ? activity.text.slice(prefix.length) : activity.text, state.error!);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -59,6 +59,7 @@ import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.
|
|
|
59
59
|
import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
|
|
60
60
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
61
61
|
import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
62
|
+
import { displayRuntimeError, isResponseContinuationError } from "../runtime/responseErrors.js";
|
|
62
63
|
import type {
|
|
63
64
|
AgentRuntime,
|
|
64
65
|
ApprovalDecision,
|
|
@@ -76,6 +77,7 @@ import type {
|
|
|
76
77
|
RuntimeExecutionSettings,
|
|
77
78
|
RuntimePrompt,
|
|
78
79
|
RuntimeSession,
|
|
80
|
+
RuntimeSettingsReplacement,
|
|
79
81
|
} from "../runtime/types.js";
|
|
80
82
|
import {
|
|
81
83
|
isActiveShellCommandJob,
|
|
@@ -120,6 +122,7 @@ interface StartTurnOptions {
|
|
|
120
122
|
displayPrompt?: string;
|
|
121
123
|
preserveAttemptOnFailure?: boolean;
|
|
122
124
|
approvedPlanTurnId?: string;
|
|
125
|
+
responseContinuationTurnId?: string;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
interface PendingForwardAttachment {
|
|
@@ -566,6 +569,7 @@ export class ProxySessionController {
|
|
|
566
569
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
567
570
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
568
571
|
private readonly llmRetryingSessions = new Set<string>();
|
|
572
|
+
private readonly responseContinuations = new Map<string, { turnId: string; abort: AbortController }>();
|
|
569
573
|
private readonly planHandoffs = new Map<string, string>();
|
|
570
574
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
571
575
|
private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
@@ -3976,6 +3980,16 @@ export class ProxySessionController {
|
|
|
3976
3980
|
localImagePaths?: string[], options: StartTurnOptions = {},
|
|
3977
3981
|
): Promise<string> {
|
|
3978
3982
|
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3983
|
+
if (options.responseContinuationTurnId) {
|
|
3984
|
+
const current = this.store.getSession(loaded.record.localSessionId);
|
|
3985
|
+
if (!current || current.status === "closed" || current.lastTurnId !== options.responseContinuationTurnId
|
|
3986
|
+
|| this.sessionResets.has(current.localSessionId)
|
|
3987
|
+
|| this.responseContinuations.get(current.localSessionId)?.turnId !== options.responseContinuationTurnId
|
|
3988
|
+
|| this.store.getPendingProviderSettings(current.localSessionId)) {
|
|
3989
|
+
throw new Error("任务或运行设置已变化,未自动恢复模型续接。请发送消息继续。");
|
|
3990
|
+
}
|
|
3991
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
3992
|
+
}
|
|
3979
3993
|
try {
|
|
3980
3994
|
await this.applyPendingProviderSettings(loaded);
|
|
3981
3995
|
} catch (error) {
|
|
@@ -4050,17 +4064,27 @@ export class ProxySessionController {
|
|
|
4050
4064
|
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
4051
4065
|
throw new Error("方案执行已取消。");
|
|
4052
4066
|
}
|
|
4053
|
-
|
|
4054
|
-
loaded.record.localSessionId
|
|
4055
|
-
options.
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4067
|
+
if (options.responseContinuationTurnId) {
|
|
4068
|
+
const recovery = this.responseContinuations.get(loaded.record.localSessionId);
|
|
4069
|
+
if (recovery?.turnId !== options.responseContinuationTurnId
|
|
4070
|
+
|| this.sessionResets.has(loaded.record.localSessionId) || !loaded.runtime.recoverResponseContinuation) {
|
|
4071
|
+
throw new Error("模型续接恢复已取消。");
|
|
4072
|
+
}
|
|
4073
|
+
turnId = await loaded.runtime.recoverResponseContinuation(loaded.record.localSessionId, options.responseContinuationTurnId, recovery.abort.signal);
|
|
4074
|
+
} else {
|
|
4075
|
+
turnId = await loaded.runtime.startTurn(
|
|
4076
|
+
loaded.record.localSessionId,
|
|
4077
|
+
options.approvedPlanTurnId
|
|
4078
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
4079
|
+
: runtimePrompt(runtimeText, localImagePaths),
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4059
4082
|
break;
|
|
4060
4083
|
} catch (error) {
|
|
4061
4084
|
const message = error instanceof Error ? error.message : String(error);
|
|
4062
4085
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
4063
4086
|
const retryAttempt = attempt
|
|
4087
|
+
&& !options.responseContinuationTurnId
|
|
4064
4088
|
&& isRetryableLlmTurnFailure(message)
|
|
4065
4089
|
&& attempt.retryCount < MAX_LLM_TURN_RETRIES
|
|
4066
4090
|
? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
|
|
@@ -4794,7 +4818,23 @@ export class ProxySessionController {
|
|
|
4794
4818
|
: undefined;
|
|
4795
4819
|
if (event.type === "turn_failed") {
|
|
4796
4820
|
if (this.retriedFailureTurnIds.has(event.turnId)) return;
|
|
4797
|
-
|
|
4821
|
+
const source = this.store.getSession(event.sessionId);
|
|
4822
|
+
const responseContinuation = failedAttempt?.retryCount === 0 && failedAttempt.status !== "cancelling"
|
|
4823
|
+
&& source && source.status !== "closed" && source.lastTurnId === event.turnId
|
|
4824
|
+
&& !this.sessionResets.has(event.sessionId) && !this.store.getPendingProviderSettings(event.sessionId)
|
|
4825
|
+
&& Boolean(this.runtimes.forAgent(source.agentName).recoverResponseContinuation)
|
|
4826
|
+
&& isResponseContinuationError(event.message);
|
|
4827
|
+
if (failedAttempt && responseContinuation) {
|
|
4828
|
+
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
4829
|
+
if (retryAttempt) {
|
|
4830
|
+
this.retriedFailureTurnIds.add(event.turnId);
|
|
4831
|
+
this.responseContinuations.set(event.sessionId, { turnId: event.turnId, abort: new AbortController() });
|
|
4832
|
+
await this.retryFailedLlmTurn(event, retryAttempt, true);
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (failedAttempt && !this.responseContinuations.has(event.sessionId)
|
|
4837
|
+
&& isRetryableLlmTurnFailure(event.message) && failedAttempt.retryCount < MAX_LLM_TURN_RETRIES) {
|
|
4798
4838
|
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
4799
4839
|
if (retryAttempt) {
|
|
4800
4840
|
this.retriedFailureTurnIds.add(event.turnId);
|
|
@@ -4957,6 +4997,7 @@ export class ProxySessionController {
|
|
|
4957
4997
|
private async retryFailedLlmTurn(
|
|
4958
4998
|
event: Extract<RuntimeEvent, { type: "turn_failed" }>,
|
|
4959
4999
|
attempt: TurnAttemptRecord,
|
|
5000
|
+
responseContinuation = false,
|
|
4960
5001
|
): Promise<void> {
|
|
4961
5002
|
const retryNumber = attempt.retryCount;
|
|
4962
5003
|
const session = this.store.getSession(event.sessionId);
|
|
@@ -4974,14 +5015,17 @@ export class ProxySessionController {
|
|
|
4974
5015
|
attemptId: attempt.attemptId,
|
|
4975
5016
|
failedTurnId: event.turnId,
|
|
4976
5017
|
retryNumber,
|
|
4977
|
-
maxRetries: MAX_LLM_TURN_RETRIES,
|
|
5018
|
+
maxRetries: responseContinuation ? 1 : MAX_LLM_TURN_RETRIES,
|
|
5019
|
+
responseContinuation,
|
|
4978
5020
|
error: event.message,
|
|
4979
5021
|
});
|
|
4980
5022
|
|
|
4981
5023
|
try {
|
|
4982
5024
|
await this.outbound.onEvent({
|
|
4983
5025
|
...event,
|
|
4984
|
-
message:
|
|
5026
|
+
message: responseContinuation
|
|
5027
|
+
? `${event.message}\n\n模型续接状态失效,正在恢复连接并继续(1/1)。`
|
|
5028
|
+
: `${event.message}\n\n检测到临时模型服务错误,正在自动重试(${retryNumber}/${MAX_LLM_TURN_RETRIES})。`,
|
|
4985
5029
|
});
|
|
4986
5030
|
} catch (error) {
|
|
4987
5031
|
this.logger.warn(
|
|
@@ -4998,21 +5042,26 @@ export class ProxySessionController {
|
|
|
4998
5042
|
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
4999
5043
|
const retryTurnId = await this.startTurn(
|
|
5000
5044
|
loaded,
|
|
5001
|
-
llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
5045
|
+
responseContinuation ? "" : llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
5002
5046
|
replyTarget,
|
|
5003
|
-
attempt.localImagePaths,
|
|
5047
|
+
responseContinuation ? undefined : attempt.localImagePaths,
|
|
5004
5048
|
{
|
|
5005
5049
|
attemptId: attempt.attemptId,
|
|
5006
5050
|
messageId: attempt.messageId,
|
|
5007
|
-
displayPrompt: llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
5051
|
+
displayPrompt: responseContinuation ? `恢复模型续接:${attempt.promptText}` : llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
5008
5052
|
preserveAttemptOnFailure: true,
|
|
5053
|
+
...(responseContinuation ? { responseContinuationTurnId: event.turnId } : {}),
|
|
5009
5054
|
},
|
|
5010
5055
|
);
|
|
5011
5056
|
this.store.rebindPendingTurnMessages(session.localSessionId, event.turnId, retryTurnId);
|
|
5012
5057
|
} catch (error) {
|
|
5013
|
-
this.
|
|
5014
|
-
this.store.
|
|
5015
|
-
|
|
5058
|
+
const aborted = responseContinuation && !this.responseContinuations.has(session.localSessionId);
|
|
5059
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: aborted ? "cancelled" : "failed" });
|
|
5060
|
+
const latest = this.store.getSession(session.localSessionId);
|
|
5061
|
+
if (!responseContinuation || (latest?.lastTurnId === event.turnId && latest.status !== "closed")) {
|
|
5062
|
+
this.store.updateSession(session.localSessionId, { status: aborted ? "ready" : "failed" });
|
|
5063
|
+
}
|
|
5064
|
+
await this.finalizeTurnMessageReactions(event.turnId, aborted ? "cancelled" : "failed").catch((reactionError: unknown) => {
|
|
5016
5065
|
this.logger.warn(
|
|
5017
5066
|
{ error: reactionError, sessionId: session.localSessionId, turnId: event.turnId },
|
|
5018
5067
|
"Failed to finalize reactions after an LLM retry could not start.",
|
|
@@ -5022,9 +5071,16 @@ export class ProxySessionController {
|
|
|
5022
5071
|
{ error, sessionId: session.localSessionId, retryNumber },
|
|
5023
5072
|
"Failed to start an automatic LLM turn retry.",
|
|
5024
5073
|
);
|
|
5074
|
+
if (responseContinuation && !aborted) {
|
|
5075
|
+
await this.outbound.sendText(attempt.contextKey,
|
|
5076
|
+
`模型续接恢复未能启动:${displayRuntimeError(runtimeErrorMessage(error))}。请发送消息继续。`);
|
|
5077
|
+
}
|
|
5025
5078
|
await this.scheduleNextQueuedPrompt(session.localSessionId);
|
|
5026
5079
|
} finally {
|
|
5027
5080
|
this.llmRetryingSessions.delete(session.localSessionId);
|
|
5081
|
+
if (this.responseContinuations.get(session.localSessionId)?.turnId === event.turnId) {
|
|
5082
|
+
this.responseContinuations.delete(session.localSessionId);
|
|
5083
|
+
}
|
|
5028
5084
|
}
|
|
5029
5085
|
}
|
|
5030
5086
|
|
|
@@ -5291,6 +5347,8 @@ export class ProxySessionController {
|
|
|
5291
5347
|
}
|
|
5292
5348
|
|
|
5293
5349
|
private async cancelSession(record: SessionRecord): Promise<void> {
|
|
5350
|
+
this.responseContinuations.get(record.localSessionId)?.abort.abort();
|
|
5351
|
+
this.responseContinuations.delete(record.localSessionId);
|
|
5294
5352
|
this.planHandoffs.delete(record.localSessionId);
|
|
5295
5353
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5296
5354
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
@@ -5370,6 +5428,8 @@ export class ProxySessionController {
|
|
|
5370
5428
|
}
|
|
5371
5429
|
|
|
5372
5430
|
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
5431
|
+
this.responseContinuations.get(sessionId)?.abort.abort();
|
|
5432
|
+
this.responseContinuations.delete(sessionId);
|
|
5373
5433
|
this.planHandoffs.delete(sessionId);
|
|
5374
5434
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
5375
5435
|
const reset = previous.catch(() => undefined).then(operation);
|
|
@@ -5844,9 +5904,16 @@ export class ProxySessionController {
|
|
|
5844
5904
|
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5845
5905
|
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
5846
5906
|
const remoteSessionId = loaded.session.remoteSessionId;
|
|
5847
|
-
const persist = (candidate: RuntimeSession): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5907
|
+
const persist = (candidate: RuntimeSession, replacement?: RuntimeSettingsReplacement): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5848
5908
|
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
5849
|
-
|
|
5909
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
5910
|
+
if (replacement?.emptyRemoteSessionId !== remoteSessionId || !saved || saved.remoteSessionId !== remoteSessionId
|
|
5911
|
+
|| saved.status === "closed" || saved.status === "running" || saved.lastTurnId || loaded.session.activeTurnId || candidate.activeTurnId
|
|
5912
|
+
|| candidate.localSessionId !== saved.localSessionId || !candidate.remoteSessionId
|
|
5913
|
+
|| candidate.agentName !== saved.agentName || candidate.cwd !== saved.cwd || candidate.runtimeKind !== loaded.session.runtimeKind
|
|
5914
|
+
|| this.store.hasTurnAttemptsForSession(saved.localSessionId) || this.store.getForkHistorySource(saved.localSessionId)) {
|
|
5915
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;仅允许已验证的空会话更换底层任务。");
|
|
5916
|
+
}
|
|
5850
5917
|
}
|
|
5851
5918
|
assertProviderSettingsApplied(candidate, settings);
|
|
5852
5919
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
@@ -5854,6 +5921,7 @@ export class ProxySessionController {
|
|
|
5854
5921
|
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
5855
5922
|
try {
|
|
5856
5923
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5924
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
5857
5925
|
modelProvider: candidate.modelProvider,
|
|
5858
5926
|
model: candidate.model,
|
|
5859
5927
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -5866,6 +5934,7 @@ export class ProxySessionController {
|
|
|
5866
5934
|
}
|
|
5867
5935
|
});
|
|
5868
5936
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5937
|
+
loaded.record = this.store.getSession(loaded.record.localSessionId) ?? loaded.record;
|
|
5869
5938
|
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
5870
5939
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5871
5940
|
return session;
|
|
@@ -7107,6 +7176,7 @@ export class ProxySessionController {
|
|
|
7107
7176
|
}
|
|
7108
7177
|
|
|
7109
7178
|
private async createDirectoryFromCard(contextKey: string, action: CardAction): Promise<void> {
|
|
7179
|
+
const updateMessageId = requiredCardMessageId(action.messageId);
|
|
7110
7180
|
const directory = directoryActionPath(action.value.directory);
|
|
7111
7181
|
await this.assertBrowsableDirectory(directory);
|
|
7112
7182
|
const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
|
|
@@ -7120,9 +7190,9 @@ export class ProxySessionController {
|
|
|
7120
7190
|
if (code === "EEXIST") throw new Error(`目录已存在:${newDirectory}`);
|
|
7121
7191
|
throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
|
|
7122
7192
|
}
|
|
7123
|
-
await this.openDirectoryBrowser(contextKey,
|
|
7124
|
-
updateMessageId
|
|
7125
|
-
page:
|
|
7193
|
+
await this.openDirectoryBrowser(contextKey, newDirectory, {
|
|
7194
|
+
updateMessageId,
|
|
7195
|
+
page: 0,
|
|
7126
7196
|
});
|
|
7127
7197
|
await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
|
|
7128
7198
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const RUNTIME_WARNING_PREFIXES = ["运行请求失败,等待 Agent 返回最终状态:", "运行请求出错,Agent 正在重试:"];
|
|
4
|
+
|
|
5
|
+
const responseErrorSchema = z.object({
|
|
6
|
+
type: z.literal("error"),
|
|
7
|
+
status: z.number().optional(),
|
|
8
|
+
error: z.object({
|
|
9
|
+
message: z.string().min(1),
|
|
10
|
+
code: z.string().nullish(),
|
|
11
|
+
param: z.string().nullish(),
|
|
12
|
+
type: z.string().nullish(),
|
|
13
|
+
}).passthrough(),
|
|
14
|
+
}).passthrough();
|
|
15
|
+
|
|
16
|
+
function parseResponseError(message: string): { value: z.infer<typeof responseErrorSchema>; notice: string } | undefined {
|
|
17
|
+
// Agent Bot may append a recovery notice after the upstream JSON error.
|
|
18
|
+
const boundary = message.indexOf("\n\n");
|
|
19
|
+
for (const end of [message.length, ...(boundary >= 0 ? [boundary] : [])]) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = responseErrorSchema.safeParse(JSON.parse(message.slice(0, end)));
|
|
22
|
+
if (parsed.success) return { value: parsed.data, notice: message.slice(end) };
|
|
23
|
+
} catch {
|
|
24
|
+
// Non-JSON runtime errors remain unchanged.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isResponseContinuationError(message: string): boolean {
|
|
31
|
+
const parsed = parseResponseError(message);
|
|
32
|
+
return parsed?.value.error.code === "unsupported_persisted_item_context"
|
|
33
|
+
&& parsed.value.error.param === "previous_response_id"
|
|
34
|
+
&& parsed.value.error.type === "invalid_request_error"
|
|
35
|
+
&& parsed.value.status === 400
|
|
36
|
+
&& !parsed.notice;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function displayRuntimeError(message: string): string {
|
|
40
|
+
const prefix = RUNTIME_WARNING_PREFIXES.find((value) => message.startsWith(value));
|
|
41
|
+
const parsed = parseResponseError(prefix ? message.slice(prefix.length) : message);
|
|
42
|
+
if (!parsed) return message;
|
|
43
|
+
const { error, status } = parsed.value;
|
|
44
|
+
const details = [status === undefined ? undefined : `HTTP ${status}`, error.code, error.param];
|
|
45
|
+
return (prefix ?? "") + `${error.message}\n${details.filter(Boolean).join(" · ")}${parsed.notice}`.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function sameRuntimeError(warning: string, terminal: string): boolean {
|
|
49
|
+
if (warning.trim() === terminal.trim()) return true;
|
|
50
|
+
const parsed = parseResponseError(terminal);
|
|
51
|
+
if (parsed) {
|
|
52
|
+
const other = parseResponseError(warning);
|
|
53
|
+
return Boolean(other && !other.notice && (!parsed.notice || isRecoveryNotice(parsed.notice))
|
|
54
|
+
&& JSON.stringify(other.value) === JSON.stringify(parsed.value));
|
|
55
|
+
}
|
|
56
|
+
const suffix = terminal.slice(warning.trim().length);
|
|
57
|
+
return terminal.startsWith(warning.trim()) && isRecoveryNotice(suffix);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isRecoveryNotice(text: string): boolean {
|
|
61
|
+
return /^\n\n(?:检测到临时模型服务错误,正在自动重试|模型续接状态失效,正在恢复连接并继续|已自动重试)/u.test(text);
|
|
62
|
+
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -270,6 +270,10 @@ export interface RuntimeExecutionSettings {
|
|
|
270
270
|
permissionMode: PermissionMode;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
export interface RuntimeSettingsReplacement {
|
|
274
|
+
emptyRemoteSessionId: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
273
277
|
export interface RuntimeReleaseResult {
|
|
274
278
|
status: "released" | "busy";
|
|
275
279
|
activeSessionIds?: string[];
|
|
@@ -304,6 +308,8 @@ export interface AgentRuntime {
|
|
|
304
308
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
305
309
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
306
310
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|
|
311
|
+
// Continue a verified Responses replay failure without resubmitting user input.
|
|
312
|
+
recoverResponseContinuation?(sessionId: string, failedTurnId: string, signal?: AbortSignal): Promise<string>;
|
|
307
313
|
steerTurn(sessionId: string, turnId: string, prompt: RuntimePrompt): Promise<void>;
|
|
308
314
|
cancelTurn(sessionId: string, turnId: string): Promise<void>;
|
|
309
315
|
interruptRemoteTurn?(remoteSessionId: string, turnId: string): Promise<void>;
|
|
@@ -320,7 +326,7 @@ export interface AgentRuntime {
|
|
|
320
326
|
sessionId: string,
|
|
321
327
|
settings: RuntimeExecutionSettings,
|
|
322
328
|
// Called after remote verification, before releasing the task's operation lock.
|
|
323
|
-
persist?: (session: RuntimeSession) => Promise<void>,
|
|
329
|
+
persist?: (session: RuntimeSession, replacement?: RuntimeSettingsReplacement) => Promise<void>,
|
|
324
330
|
): Promise<RuntimeSession>;
|
|
325
331
|
respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
|
|
326
332
|
listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
|