@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.1
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/package.json +3 -3
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1539 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/{db → adapter/cloudflare}/schema.ts +16 -83
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +182 -0
- package/src/config/index.ts +14 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
- package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +98 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +76 -10
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +175 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +481 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/mcp.ts +1 -1
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/submission/admission.ts +275 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +61 -118
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
- package/src/tools/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
- package/src/{pi/tool → tools}/base.ts +14 -30
- package/src/tools/compiler.ts +327 -0
- package/src/{pi/tool → tools}/core-host.ts +3 -3
- package/src/{pi/tool → tools}/core.ts +3 -3
- package/src/tools/decision-gate.ts +227 -0
- package/src/{pi/tool → tools}/gateway.ts +1 -1
- package/src/tools/human-decisions.ts +127 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/interaction/index.ts +1 -0
- package/src/{pi/tool → tools}/mcp.ts +1 -1
- package/src/{pi/tool → tools}/schedule.ts +3 -3
- package/src/{pi/tool → tools}/skill.ts +4 -4
- package/src/{kernel → tools}/subagent-runtime.ts +1 -1
- package/src/{pi/tool → tools}/subagent.ts +4 -4
- package/src/tools/temporary-agent/approvals.ts +126 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
- package/src/tools/temporary-agent/core.ts +53 -0
- package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
- package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
- package/src/{pi/tool → tools}/web-search/api.ts +1 -1
- package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
- package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
- package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- /package/src/{kernel → config}/degradation.ts +0 -0
- /package/src/{kernel → config}/runtime-config.ts +0 -0
- /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- /package/src/{pi/tool → tools}/declared.ts +0 -0
- /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
- /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
- /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
- /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
- /package/src/{pi/tool → tools}/time.ts +0 -0
- /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
- /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
|
@@ -2,7 +2,7 @@ import { TurnQueue } from "agents/chat";
|
|
|
2
2
|
import {
|
|
3
3
|
isTerminalSubmissionStatus,
|
|
4
4
|
type SubmissionStatus,
|
|
5
|
-
} from "
|
|
5
|
+
} from "./repository";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 本文件统一管理一次提交从接收到结束的过程。
|
|
@@ -130,10 +130,9 @@ export type SubmissionOutcome = "succeeded" | "failed" | "aborted";
|
|
|
130
130
|
* @remarks
|
|
131
131
|
* `SubmissionLifecycle` 把它交给 `execute`,Runtime 据此选择执行前置动作。
|
|
132
132
|
*
|
|
133
|
-
* `recovery` 和 `continuation`
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* 而且重放成本会随让步次数增长。
|
|
133
|
+
* `recovery` 和 `continuation` 必须分开:前者的进程内配置已经丢失,需重新装配原 runtime key 再打开 Session;
|
|
134
|
+
* 后者是本轮主动让出执行片,持久 operation 和已固定配置保持完好。
|
|
135
|
+
* 区分两者可避免主动让出时重复加载原 runtime key。
|
|
137
136
|
*/
|
|
138
137
|
export type SubmissionExecutionMode =
|
|
139
138
|
/** 首次执行一条新接收的提交。 */
|
|
@@ -156,6 +155,8 @@ export interface SubmissionInput<
|
|
|
156
155
|
> {
|
|
157
156
|
requestId: string;
|
|
158
157
|
idempotencyKey?: string;
|
|
158
|
+
/** Only a repository-validated accepted Steer may transfer despite a full new-work queue. */
|
|
159
|
+
isSteerHandoff?: true;
|
|
159
160
|
/**
|
|
160
161
|
* 准备接收所需数据,并返回同步创建记录的函数。
|
|
161
162
|
*
|
|
@@ -202,10 +203,6 @@ interface SubmissionLifecycleOptions<
|
|
|
202
203
|
submissionId: string,
|
|
203
204
|
mode: SubmissionExecutionMode,
|
|
204
205
|
): Promise<TSubmission>;
|
|
205
|
-
// 作用:记录可恢复的取消意图。
|
|
206
|
-
// 调用:`cancel` 在同一存储事务内与取消原因一起写入。
|
|
207
|
-
// 原因:持久化里程碑可以让恢复路径看到已经发生的取消。
|
|
208
|
-
appendAbortIntent(submission: TSubmission, reason: string): void;
|
|
209
206
|
// 作用:把一条非终态提交写成最终结果。
|
|
210
207
|
// 调用:`finish` 在重读并确认仍非终态后调用。
|
|
211
208
|
// 原因:状态映射和其他终态副作用必须由 Runtime 在一个边界完成。
|
|
@@ -217,7 +214,9 @@ interface SubmissionLifecycleOptions<
|
|
|
217
214
|
// 作用:通知当前执行器中断运行。
|
|
218
215
|
// 调用:`cancel` 命中活动执行器,或 `activate` 发现早到取消时调用。
|
|
219
216
|
// 原因:生命周期不依赖具体执行器类型,只通过这个回调中断。
|
|
220
|
-
abortActive(active: TActive): void
|
|
217
|
+
abortActive(active: TActive): void | Promise<void>;
|
|
218
|
+
/** Close a persisted operation after its process-local executor has parked. */
|
|
219
|
+
abortDormant(submission: TSubmission): void | Promise<void>;
|
|
221
220
|
/**
|
|
222
221
|
* 中断活动执行器,但让这条提交留给持久续跑接手。
|
|
223
222
|
*
|
|
@@ -244,6 +243,7 @@ export class SubmissionLifecycle<
|
|
|
244
243
|
private readonly queue = new TurnQueue();
|
|
245
244
|
private readonly executions = new Map<string, Promise<TSubmission>>();
|
|
246
245
|
private readonly activeBySubmission = new Map<string, TActive>();
|
|
246
|
+
private readonly terminalCommits = new Map<string, Promise<TSubmission>>();
|
|
247
247
|
|
|
248
248
|
// #region 提交与恢复
|
|
249
249
|
|
|
@@ -322,10 +322,10 @@ export class SubmissionLifecycle<
|
|
|
322
322
|
* 等当前执行片退出后,接着跑同一条非终态 Submission。
|
|
323
323
|
*
|
|
324
324
|
* @remarks
|
|
325
|
-
* `
|
|
325
|
+
* `_piHarnessContinuation` 在计划让步或 Provider 等待的唤醒里调用。先 await 当前执行片,
|
|
326
326
|
* 是为了避开 `executions` 的实例内去重 —— 否则这次唤醒会被合流到正在退场的那一片上。
|
|
327
327
|
*
|
|
328
|
-
* 走 `continuation` 而不是 `recovery
|
|
328
|
+
* 走 `continuation` 而不是 `recovery`:这一轮什么都没丢,只需重新 drive 同一个持久 operation。
|
|
329
329
|
*/
|
|
330
330
|
async continueAfterCurrent(submissionId: string): Promise<TSubmission> {
|
|
331
331
|
const current = this.executions.get(submissionId);
|
|
@@ -382,16 +382,15 @@ export class SubmissionLifecycle<
|
|
|
382
382
|
rejected: false,
|
|
383
383
|
};
|
|
384
384
|
}
|
|
385
|
-
if (
|
|
386
|
-
this.options.store.countPending() >=
|
|
387
|
-
MAX_PENDING_SUBMISSIONS
|
|
385
|
+
if (!input.isSteerHandoff &&
|
|
386
|
+
this.options.store.countPending() >= MAX_PENDING_SUBMISSIONS
|
|
388
387
|
) {
|
|
389
388
|
throw new SubmissionQueueFullError();
|
|
390
389
|
}
|
|
391
390
|
const submission = create();
|
|
392
391
|
return {
|
|
393
392
|
submission,
|
|
394
|
-
admitted: submission.
|
|
393
|
+
admitted: !isTerminalSubmissionStatus(submission.status),
|
|
395
394
|
rejected: submission.admissionRejected === true,
|
|
396
395
|
};
|
|
397
396
|
});
|
|
@@ -520,17 +519,27 @@ export class SubmissionLifecycle<
|
|
|
520
519
|
*
|
|
521
520
|
* 写入前重读最新记录并保留已有终态,避免较晚到达的结果覆盖先完成的结果。
|
|
522
521
|
*/
|
|
523
|
-
|
|
522
|
+
finish(
|
|
524
523
|
submission: TSubmission,
|
|
525
524
|
outcome: SubmissionOutcome,
|
|
526
525
|
message?: string,
|
|
527
526
|
): Promise<TSubmission> {
|
|
527
|
+
const existing = this.terminalCommits.get(submission.submissionId);
|
|
528
|
+
if (existing) return existing;
|
|
528
529
|
const latest = this.options.store.find(submission.submissionId);
|
|
529
530
|
if (!latest) {
|
|
530
|
-
|
|
531
|
+
return Promise.reject(
|
|
532
|
+
new Error(`Unknown SpringBrand submission: ${submission.submissionId}`),
|
|
533
|
+
);
|
|
531
534
|
}
|
|
532
|
-
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
533
|
-
|
|
535
|
+
if (isTerminalSubmissionStatus(latest.status)) return Promise.resolve(latest);
|
|
536
|
+
const commit = this.options.commitTerminal(latest, outcome, message);
|
|
537
|
+
this.terminalCommits.set(submission.submissionId, commit);
|
|
538
|
+
void commit.then(
|
|
539
|
+
() => this.terminalCommits.delete(submission.submissionId),
|
|
540
|
+
() => this.terminalCommits.delete(submission.submissionId),
|
|
541
|
+
);
|
|
542
|
+
return commit;
|
|
534
543
|
}
|
|
535
544
|
|
|
536
545
|
/**
|
|
@@ -648,14 +657,16 @@ export class SubmissionLifecycle<
|
|
|
648
657
|
if (!submission || isTerminalSubmissionStatus(submission.status)) {
|
|
649
658
|
return { ok: false };
|
|
650
659
|
}
|
|
651
|
-
this.options.store.transaction(() =>
|
|
652
|
-
this.options.store.updateAbortReason(submissionId, reason)
|
|
653
|
-
|
|
654
|
-
});
|
|
660
|
+
this.options.store.transaction(() =>
|
|
661
|
+
this.options.store.updateAbortReason(submissionId, reason)
|
|
662
|
+
);
|
|
655
663
|
const active = this.activeBySubmission.get(submissionId);
|
|
656
|
-
if (active) this.options.abortActive(active);
|
|
664
|
+
if (active) await this.options.abortActive(active);
|
|
665
|
+
else if (submission.status === "running") {
|
|
666
|
+
await this.options.abortDormant(submission);
|
|
667
|
+
}
|
|
657
668
|
await this.finish(submission, "aborted", reason);
|
|
658
|
-
|
|
669
|
+
this.pump();
|
|
659
670
|
return { ok: true };
|
|
660
671
|
}
|
|
661
672
|
|
|
@@ -699,12 +710,14 @@ export class SubmissionLifecycle<
|
|
|
699
710
|
this.options.store.transaction(() => {
|
|
700
711
|
for (const submission of submissions) {
|
|
701
712
|
this.options.store.updateAbortReason(submission.submissionId, reason);
|
|
702
|
-
this.options.appendAbortIntent(submission, reason);
|
|
703
713
|
}
|
|
704
714
|
});
|
|
705
715
|
for (const submission of submissions) {
|
|
706
716
|
const active = this.activeBySubmission.get(submission.submissionId);
|
|
707
|
-
if (active) this.options.abortActive(active);
|
|
717
|
+
if (active) await this.options.abortActive(active);
|
|
718
|
+
else if (submission.status === "running") {
|
|
719
|
+
await this.options.abortDormant(submission);
|
|
720
|
+
}
|
|
708
721
|
await this.finish(submission, "aborted", reason);
|
|
709
722
|
}
|
|
710
723
|
this.pump();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
+
import type { LaneSnapshot } from "@earendil-works/pi-agent-core";
|
|
2
3
|
|
|
3
4
|
// Submission 的统一定义见 ./index.ts。
|
|
4
5
|
// #region 状态与行类型
|
|
@@ -9,11 +10,6 @@ export type SubmissionStatus =
|
|
|
9
10
|
| "aborted"
|
|
10
11
|
| "skipped"
|
|
11
12
|
| "error";
|
|
12
|
-
export type SubmissionRecoveryReason =
|
|
13
|
-
| "no_meaningful_model_progress"
|
|
14
|
-
| "server_tool_timeout"
|
|
15
|
-
| "transient_model_error"
|
|
16
|
-
| "context_overflow";
|
|
17
13
|
|
|
18
14
|
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
19
15
|
"completed",
|
|
@@ -23,7 +19,7 @@ const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
|
23
19
|
]);
|
|
24
20
|
|
|
25
21
|
// 判断 Submission 是否已经结束。
|
|
26
|
-
// Runtime、SubmissionLifecycle 和
|
|
22
|
+
// Runtime、SubmissionLifecycle 和 HarnessDecisionGate 在决定是否继续执行、等待或接受审批时调用。
|
|
27
23
|
// 终态集合收口在一处防止各生命周期的判断漂移;增加状态时必须先确认它是否真的不可再继续。
|
|
28
24
|
export function isTerminalSubmissionStatus(
|
|
29
25
|
status: SubmissionStatus,
|
|
@@ -42,20 +38,21 @@ export interface StoredSubmission {
|
|
|
42
38
|
completedAt: number | null;
|
|
43
39
|
assemblyRevision: string;
|
|
44
40
|
assemblyDescriptor: string;
|
|
41
|
+
runtimeKey: string;
|
|
45
42
|
assistantMessageId: string;
|
|
46
43
|
abortReason: string | null;
|
|
47
44
|
queuedInputJson: string | null;
|
|
48
45
|
queuedUiMessageJson: string | null;
|
|
49
46
|
userMessageId: string | null;
|
|
50
47
|
regenerateMessageId: string | null;
|
|
51
|
-
recoveryErrorCount: number;
|
|
52
|
-
recoveryReason: SubmissionRecoveryReason | null;
|
|
53
48
|
runId: string | null;
|
|
54
49
|
retryable?: boolean;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reuse durable stop intent when transferring work out of the source Submission. */
|
|
53
|
+
export function submissionAbortReason(submission: StoredSubmission | null): string | undefined {
|
|
54
|
+
return submission?.abortReason ||
|
|
55
|
+
(submission?.status === "aborted" ? submission.error || "Cancelled" : undefined);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
export interface NewSubmission {
|
|
@@ -65,6 +62,7 @@ export interface NewSubmission {
|
|
|
65
62
|
createdAt: number;
|
|
66
63
|
assemblyRevision: string;
|
|
67
64
|
assemblyDescriptor: string;
|
|
65
|
+
runtimeKey?: string;
|
|
68
66
|
assistantMessageId: string;
|
|
69
67
|
queuedInputJson?: string | null;
|
|
70
68
|
queuedUiMessageJson?: string | null;
|
|
@@ -84,18 +82,15 @@ type SubmissionRow = {
|
|
|
84
82
|
completed_at: number | null;
|
|
85
83
|
assembly_revision: string;
|
|
86
84
|
assembly_descriptor: string;
|
|
85
|
+
runtime_key: string;
|
|
87
86
|
assistant_message_id: string;
|
|
88
87
|
abort_reason: string | null;
|
|
89
88
|
queued_input_json: string | null;
|
|
90
89
|
queued_ui_message_json: string | null;
|
|
91
90
|
user_message_id: string | null;
|
|
92
91
|
regenerate_message_id: string | null;
|
|
93
|
-
recovery_error_count: number;
|
|
94
|
-
recovery_reason: string | null;
|
|
95
92
|
run_id: string | null;
|
|
96
93
|
admission_retryable: number | null;
|
|
97
|
-
model_turns: number;
|
|
98
|
-
continuation_id: string | null;
|
|
99
94
|
};
|
|
100
95
|
|
|
101
96
|
// #endregion
|
|
@@ -115,20 +110,17 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
115
110
|
completedAt: row.completed_at,
|
|
116
111
|
assemblyRevision: row.assembly_revision,
|
|
117
112
|
assemblyDescriptor: row.assembly_descriptor,
|
|
113
|
+
runtimeKey: row.runtime_key,
|
|
118
114
|
assistantMessageId: row.assistant_message_id,
|
|
119
115
|
abortReason: row.abort_reason,
|
|
120
116
|
queuedInputJson: row.queued_input_json,
|
|
121
117
|
queuedUiMessageJson: row.queued_ui_message_json,
|
|
122
118
|
userMessageId: row.user_message_id,
|
|
123
119
|
regenerateMessageId: row.regenerate_message_id,
|
|
124
|
-
recoveryErrorCount: row.recovery_error_count,
|
|
125
|
-
recoveryReason: row.recovery_reason as SubmissionRecoveryReason | null,
|
|
126
120
|
runId: row.run_id,
|
|
127
121
|
retryable: row.admission_retryable === null
|
|
128
122
|
? undefined
|
|
129
123
|
: row.admission_retryable === 1,
|
|
130
|
-
modelTurns: row.model_turns ?? 0,
|
|
131
|
-
continuationId: row.continuation_id,
|
|
132
124
|
};
|
|
133
125
|
}
|
|
134
126
|
|
|
@@ -138,7 +130,14 @@ export class SubmissionRepository {
|
|
|
138
130
|
// 保存当前 Agent SQLite 的查询入口。
|
|
139
131
|
// RuntimeDatabase 构造时调用,SubmissionLifecycle 和 Runtime 随后共享这个 Repository。
|
|
140
132
|
// Submission 的准入、执行和恢复必须看同一 Agent 存储,不能混入其他 Durable Object 的 sql。
|
|
141
|
-
constructor(
|
|
133
|
+
constructor(
|
|
134
|
+
private readonly sql: SqlTaggedTemplate,
|
|
135
|
+
private readonly transact: <T>(run: () => T) => T,
|
|
136
|
+
) {}
|
|
137
|
+
|
|
138
|
+
transaction<T>(run: () => T): T {
|
|
139
|
+
return this.transact(run);
|
|
140
|
+
}
|
|
142
141
|
|
|
143
142
|
// 按 submissionId 读取一条完整 Submission,不存在时返回 null。
|
|
144
143
|
// Runtime 和 SubmissionLifecycle 在准入、执行、取消、结束与恢复时调用,调用方必须处理 null。
|
|
@@ -147,12 +146,11 @@ export class SubmissionRepository {
|
|
|
147
146
|
const row = this.sql<SubmissionRow>`
|
|
148
147
|
SELECT submission_id, request_id, idempotency_key, status, accepted,
|
|
149
148
|
error, created_at, completed_at, assembly_revision,
|
|
150
|
-
assembly_descriptor, assistant_message_id, abort_reason,
|
|
149
|
+
assembly_descriptor, runtime_key, assistant_message_id, abort_reason,
|
|
151
150
|
queued_input_json, queued_ui_message_json, user_message_id,
|
|
152
|
-
regenerate_message_id,
|
|
151
|
+
regenerate_message_id,
|
|
153
152
|
run_id,
|
|
154
|
-
admission_retryable
|
|
155
|
-
model_turns, continuation_id
|
|
153
|
+
admission_retryable
|
|
156
154
|
FROM pi_submissions
|
|
157
155
|
WHERE submission_id = ${id}
|
|
158
156
|
`[0];
|
|
@@ -250,7 +248,7 @@ export class SubmissionRepository {
|
|
|
250
248
|
this.sql`
|
|
251
249
|
INSERT INTO pi_submissions (
|
|
252
250
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
253
|
-
created_at, assembly_revision, assembly_descriptor,
|
|
251
|
+
created_at, assembly_revision, assembly_descriptor, runtime_key,
|
|
254
252
|
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
255
253
|
user_message_id, regenerate_message_id,
|
|
256
254
|
run_id
|
|
@@ -263,6 +261,7 @@ export class SubmissionRepository {
|
|
|
263
261
|
${s.createdAt},
|
|
264
262
|
${s.assemblyRevision},
|
|
265
263
|
${s.assemblyDescriptor},
|
|
264
|
+
${s.runtimeKey ?? ""},
|
|
266
265
|
${s.assistantMessageId},
|
|
267
266
|
${s.queuedInputJson ?? null},
|
|
268
267
|
${s.queuedUiMessageJson ?? null},
|
|
@@ -280,17 +279,19 @@ export class SubmissionRepository {
|
|
|
280
279
|
createdAt: number;
|
|
281
280
|
code: string;
|
|
282
281
|
retryable: boolean;
|
|
282
|
+
userMessageId?: string;
|
|
283
|
+
assistantMessageId?: string;
|
|
283
284
|
}): void {
|
|
284
285
|
this.sql`
|
|
285
286
|
INSERT INTO pi_submissions (
|
|
286
287
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
287
288
|
error, created_at, completed_at, assembly_revision,
|
|
288
|
-
assembly_descriptor, assistant_message_id, abort_reason,
|
|
289
|
-
|
|
289
|
+
assembly_descriptor, runtime_key, assistant_message_id, abort_reason,
|
|
290
|
+
run_id, admission_retryable, user_message_id
|
|
290
291
|
) VALUES (
|
|
291
292
|
${s.submissionId}, ${s.requestId}, ${s.idempotencyKey}, 'skipped', 0,
|
|
292
|
-
${s.code}, ${s.createdAt}, ${s.createdAt}, '', '', '', NULL,
|
|
293
|
-
|
|
293
|
+
${s.code}, ${s.createdAt}, ${s.createdAt}, '', '', '', ${s.assistantMessageId ?? s.submissionId}, NULL,
|
|
294
|
+
${s.submissionId}, ${s.retryable ? 1 : 0}, ${s.userMessageId ?? null}
|
|
294
295
|
)
|
|
295
296
|
`;
|
|
296
297
|
}
|
|
@@ -309,24 +310,6 @@ export class SubmissionRepository {
|
|
|
309
310
|
return updated.length === 1;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
// 把一条已 pin 的 Submission 换到另一对 revision / descriptor 上。
|
|
313
|
-
// Runtime 在「停在人机等待期间换了装配」之后调用,必须与那次装配迁移里程碑同事务。
|
|
314
|
-
// 与 pinAssembly 的区别就是它要求已经 pin 过:首次 pin 走 pinAssembly,
|
|
315
|
-
// 这里不接受空值,避免把一次漏写的准入 pin 伪装成一次迁移。
|
|
316
|
-
repinAssembly(id: string, revision: string, descriptor: string): boolean {
|
|
317
|
-
const updated = this.sql<{ submission_id: string }>`
|
|
318
|
-
UPDATE pi_submissions
|
|
319
|
-
SET assembly_revision = ${revision},
|
|
320
|
-
assembly_descriptor = ${descriptor}
|
|
321
|
-
WHERE submission_id = ${id}
|
|
322
|
-
AND assembly_revision <> ''
|
|
323
|
-
AND assembly_descriptor <> ''
|
|
324
|
-
AND status IN ('pending', 'running')
|
|
325
|
-
RETURNING submission_id
|
|
326
|
-
`;
|
|
327
|
-
return updated.length === 1;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
313
|
// 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
|
|
331
314
|
// SubmissionLifecycle 开始 Turn 时调用,调用方用返回值识别过期或重复的状态切换。
|
|
332
315
|
// Cloudflare SQLite 查询是同步的,两条语句之间不会让其他事件交错;UPDATE 再次带旧状态条件,不能去掉这层幂等保护。
|
|
@@ -393,84 +376,44 @@ export class SubmissionRepository {
|
|
|
393
376
|
`;
|
|
394
377
|
}
|
|
395
378
|
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
reason: SubmissionRecoveryReason,
|
|
400
|
-
): number {
|
|
401
|
-
return this.sql<{ recovery_error_count: number }>`
|
|
402
|
-
UPDATE pi_submissions
|
|
403
|
-
SET recovery_error_count = recovery_error_count + 1,
|
|
404
|
-
recovery_reason = ${reason}
|
|
405
|
-
WHERE submission_id = ${id}
|
|
406
|
-
AND status IN ('pending', 'running')
|
|
407
|
-
RETURNING recovery_error_count
|
|
408
|
-
`[0]?.recovery_error_count ?? 0;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
claimContextOverflowRecovery(id: string): boolean {
|
|
412
|
-
return Boolean(this.sql<{ claimed: number }>`
|
|
413
|
-
UPDATE pi_submissions
|
|
414
|
-
SET context_overflow_retried = 1
|
|
415
|
-
WHERE submission_id = ${id}
|
|
416
|
-
AND status IN ('pending', 'running')
|
|
417
|
-
AND context_overflow_retried = 0
|
|
418
|
-
RETURNING 1 AS claimed
|
|
419
|
-
`[0]?.claimed);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* 记录一个让步执行片的累计模型回合数和续跑标识。
|
|
424
|
-
*
|
|
425
|
-
* @remarks
|
|
426
|
-
* Runtime 在一片主动让出后、排出续跑调度之前调用。
|
|
427
|
-
*
|
|
428
|
-
* 两个值必须一起写:全局回合预算靠 `model_turns` 拦住不收敛的工具循环,
|
|
429
|
-
* 续跑去重和看门狗靠 `continuation_id` 判断这一轮到底有没有推进。
|
|
430
|
-
* 只写非终态记录,避免一次迟到的写入复活已经结束的 Submission。
|
|
431
|
-
*/
|
|
432
|
-
recordYieldedSlice(
|
|
433
|
-
id: string,
|
|
434
|
-
modelTurns: number,
|
|
435
|
-
continuationId: string,
|
|
436
|
-
): void {
|
|
437
|
-
this.sql`
|
|
438
|
-
UPDATE pi_submissions
|
|
439
|
-
SET model_turns = ${modelTurns},
|
|
440
|
-
continuation_id = ${continuationId}
|
|
441
|
-
WHERE submission_id = ${id}
|
|
442
|
-
AND status IN ('pending', 'running')
|
|
443
|
-
`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/** 记录累计模型回合数,不改动续跑标识。终态提交前的最后一次结算用它。 */
|
|
447
|
-
recordModelTurns(id: string, modelTurns: number): void {
|
|
379
|
+
// 持久化 Session entry 到 Submission 的关联,使 Host 可以从 Pi 分支
|
|
380
|
+
// 回填 message、ToolResult 和 compaction 的用量与结算 Outbox。
|
|
381
|
+
linkSessionEntry(entryId: string, submissionId: string): void {
|
|
448
382
|
this.sql`
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
AND status IN ('pending', 'running')
|
|
383
|
+
INSERT OR IGNORE INTO pi_session_entry_submission
|
|
384
|
+
(entry_id, submission_id)
|
|
385
|
+
VALUES (${entryId}, ${submissionId})
|
|
453
386
|
`;
|
|
454
387
|
}
|
|
455
388
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
389
|
+
/** Recover ownership from the matching Pi operation, using only its active branch. */
|
|
390
|
+
associateSessionEntries(
|
|
391
|
+
submissionId: string,
|
|
392
|
+
snapshot: Pick<LaneSnapshot, "operation" | "lastResult" | "tipId" | "transcript">,
|
|
393
|
+
): void {
|
|
394
|
+
const boundary = snapshot.operation?.id === submissionId
|
|
395
|
+
? { fromTipId: snapshot.operation.fromTipId, tipId: snapshot.tipId }
|
|
396
|
+
: snapshot.lastResult?.operationId === submissionId
|
|
397
|
+
? snapshot.lastResult
|
|
398
|
+
: undefined;
|
|
399
|
+
if (!boundary) return;
|
|
400
|
+
const start = boundary.fromTipId === null
|
|
401
|
+
? 0
|
|
402
|
+
: snapshot.transcript.findIndex(({ id }) => id === boundary.fromTipId) + 1;
|
|
403
|
+
const end = boundary.tipId === null
|
|
404
|
+
? snapshot.transcript.length
|
|
405
|
+
: snapshot.transcript.findIndex(({ id }) => id === boundary.tipId) + 1;
|
|
406
|
+
for (const entry of snapshot.transcript.slice(start, end || undefined)) {
|
|
407
|
+
this.linkSessionEntry(entry.id, submissionId);
|
|
408
|
+
}
|
|
463
409
|
}
|
|
464
410
|
|
|
465
|
-
|
|
466
|
-
// Transcript.storedMessages 读取 Pi Session 分支时调用,它用 Map 为每条消息补回 submissionId。
|
|
467
|
-
// 关联表由 Pi Session 存储拥有,本方法只读不写;不能在 RuntimeDatabase.clearAll 中单独删它而破坏 Transcript 的所有权。
|
|
468
|
-
listMessageSubmissionLinks(): Map<string, string> {
|
|
411
|
+
listSessionEntrySubmissionLinks(): Map<string, string> {
|
|
469
412
|
const rows = this.sql<{
|
|
470
|
-
|
|
413
|
+
entry_id: string;
|
|
471
414
|
submission_id: string;
|
|
472
|
-
}>`SELECT
|
|
473
|
-
return new Map(rows.map((r) => [r.
|
|
415
|
+
}>`SELECT entry_id, submission_id FROM pi_session_entry_submission`;
|
|
416
|
+
return new Map(rows.map((r) => [r.entry_id, r.submission_id] as [string, string]));
|
|
474
417
|
}
|
|
475
418
|
}
|
|
476
419
|
// #endregion
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { CancelQueuedResult, LaneSnapshot } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { UIMessage } from "ai";
|
|
3
|
+
import type { ConversationHistory } from "../conversation/history";
|
|
4
|
+
import type { SubmitMessageOptions } from "./admission";
|
|
5
|
+
import type { SubmissionHandle } from "./lifecycle";
|
|
6
|
+
import { submissionAbortReason, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
7
|
+
import type { PiCanonicalUserInput } from "../conversation/message/contract";
|
|
8
|
+
import type { MessageUiRepository } from "../conversation/repository";
|
|
9
|
+
import type { PendingSteer, SteerRepository } from "./steer-repository";
|
|
10
|
+
|
|
11
|
+
interface PendingSteerDeliveryOptions {
|
|
12
|
+
steers: SteerRepository;
|
|
13
|
+
messageUi: MessageUiRepository;
|
|
14
|
+
transaction<T>(run: () => T): T;
|
|
15
|
+
enqueue(message: PiCanonicalUserInput): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reconciles Host steer receipts with Pi queue identities, without owning execution. */
|
|
19
|
+
export class PendingSteerDelivery {
|
|
20
|
+
constructor(private readonly options: PendingSteerDeliveryOptions) {}
|
|
21
|
+
|
|
22
|
+
async queue(steer: PendingSteer): Promise<void> {
|
|
23
|
+
const entryId = await this.options.enqueue(
|
|
24
|
+
JSON.parse(steer.canonicalJson) as PiCanonicalUserInput,
|
|
25
|
+
);
|
|
26
|
+
this.link(steer, entryId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Called after Pi commits the original queued user entry. */
|
|
30
|
+
settle(entryId: string): boolean {
|
|
31
|
+
const steer = this.options.steers.findByHarnessEntryId(entryId);
|
|
32
|
+
if (!steer) return false;
|
|
33
|
+
this.options.transaction(() => {
|
|
34
|
+
this.persistSidecar(steer, entryId);
|
|
35
|
+
this.options.steers.deleteByMessageId(steer.messageId);
|
|
36
|
+
});
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async reconcile(
|
|
41
|
+
submissionId: string,
|
|
42
|
+
snapshot: Pick<LaneSnapshot, "queues" | "transcript">,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const pending = this.options.steers.listForSubmission(submissionId);
|
|
45
|
+
const queuedIds = new Set(snapshot.queues.map(({ entryId }) => entryId));
|
|
46
|
+
const transcriptIds = new Set(snapshot.transcript.map(({ id }) => id));
|
|
47
|
+
const linkedIds = new Set(pending.flatMap(({ harnessEntryId }) =>
|
|
48
|
+
harnessEntryId ? [harnessEntryId] : []
|
|
49
|
+
));
|
|
50
|
+
const unlinked = snapshot.queues.filter((entry) =>
|
|
51
|
+
entry.kind === "steer" && entry.type === "message" &&
|
|
52
|
+
!linkedIds.has(entry.entryId)
|
|
53
|
+
);
|
|
54
|
+
// Both durable queues preserve acceptance order; identical text is not identity.
|
|
55
|
+
for (const steer of pending) {
|
|
56
|
+
if (steer.harnessEntryId && transcriptIds.has(steer.harnessEntryId)) {
|
|
57
|
+
this.settle(steer.harnessEntryId);
|
|
58
|
+
} else if (steer.harnessEntryId && queuedIds.has(steer.harnessEntryId)) {
|
|
59
|
+
this.options.transaction(() =>
|
|
60
|
+
this.persistSidecar(steer, steer.harnessEntryId!)
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
const queued = !steer.harnessEntryId ? unlinked.shift() : undefined;
|
|
64
|
+
if (queued) this.link(steer, queued.entryId);
|
|
65
|
+
else await this.queue(steer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private link(steer: PendingSteer, entryId: string): void {
|
|
71
|
+
this.options.transaction(() => {
|
|
72
|
+
if (steer.harnessEntryId && steer.harnessEntryId !== entryId) {
|
|
73
|
+
this.options.messageUi.delete(steer.harnessEntryId);
|
|
74
|
+
}
|
|
75
|
+
this.options.steers.linkHarnessEntry(steer.messageId, entryId);
|
|
76
|
+
this.persistSidecar(steer, entryId);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private persistSidecar(steer: PendingSteer, entryId: string): void {
|
|
81
|
+
if (steer.uiMessageJson) {
|
|
82
|
+
this.options.messageUi.upsert(entryId, steer.uiMessageJson);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/** Keep accepted steers visible on stop, or transfer unconsumed messages to new Submissions. */
|
|
89
|
+
export async function finishPendingSteers(options: {
|
|
90
|
+
submissionId: string;
|
|
91
|
+
aborted: boolean;
|
|
92
|
+
submissions: SubmissionRepository;
|
|
93
|
+
cancelHandoff(submissionId: string, reason: string): StoredSubmission;
|
|
94
|
+
steers: SteerRepository;
|
|
95
|
+
messageUi: MessageUiRepository;
|
|
96
|
+
transaction<T>(run: () => T): T;
|
|
97
|
+
conversation: Pick<ConversationHistory, "append">;
|
|
98
|
+
cancelQueued(entryId: string): Promise<CancelQueuedResult | undefined>;
|
|
99
|
+
submit(
|
|
100
|
+
message: PiCanonicalUserInput,
|
|
101
|
+
options: SubmitMessageOptions,
|
|
102
|
+
): Promise<SubmissionHandle<StoredSubmission>>;
|
|
103
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
104
|
+
}): Promise<void> {
|
|
105
|
+
for (const steer of options.steers.listForSubmission(options.submissionId)) {
|
|
106
|
+
const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
|
|
107
|
+
const userMessage = steer.uiMessageJson
|
|
108
|
+
? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
|
|
109
|
+
: undefined;
|
|
110
|
+
const requestId = `${options.submissionId}:steer:${steer.steerId}`;
|
|
111
|
+
if (options.aborted) {
|
|
112
|
+
options.transaction(() => {
|
|
113
|
+
let handoff = options.submissions.findByRequestId(requestId);
|
|
114
|
+
if (handoff?.status === "pending") {
|
|
115
|
+
const reason = submissionAbortReason(options.submissions.find(options.submissionId)) ?? "Cancelled";
|
|
116
|
+
options.submissions.updateAbortReason(handoff.submissionId, reason);
|
|
117
|
+
handoff = options.cancelHandoff(handoff.submissionId, reason);
|
|
118
|
+
}
|
|
119
|
+
if (steer.harnessEntryId) options.messageUi.delete(steer.harnessEntryId);
|
|
120
|
+
options.conversation.append(steer.messageId, message, {
|
|
121
|
+
submissionId: handoff?.status === "aborted" || handoff?.status === "skipped"
|
|
122
|
+
? handoff.submissionId
|
|
123
|
+
: options.submissionId,
|
|
124
|
+
...(userMessage ? { userMessage } : {}),
|
|
125
|
+
});
|
|
126
|
+
options.steers.deleteByMessageId(steer.messageId);
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (steer.harnessEntryId) {
|
|
131
|
+
const cancelled = await options.cancelQueued(steer.harnessEntryId);
|
|
132
|
+
if (cancelled) {
|
|
133
|
+
if (!cancelled.ok) throw cancelled.error;
|
|
134
|
+
if (cancelled.value.kind === "already_consumed") {
|
|
135
|
+
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
options.transaction(() => options.messageUi.delete(steer.harnessEntryId!));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// A queued Submission moved to this Turn already owns messageId and is aborted.
|
|
142
|
+
// Handoff needs its own stable identity while preserving the user's message identity.
|
|
143
|
+
const submitted = await options.submit(message, {
|
|
144
|
+
requestId,
|
|
145
|
+
idempotencyKey: requestId,
|
|
146
|
+
userMessageId: steer.messageId,
|
|
147
|
+
steerHandoff: { submissionId: options.submissionId, steerId: steer.steerId },
|
|
148
|
+
...(userMessage ? { userMessage } : {}),
|
|
149
|
+
});
|
|
150
|
+
options.waitUntil(submitted.completion);
|
|
151
|
+
options.transaction(() => {
|
|
152
|
+
let handoff = options.submissions.find(submitted.receipt.submissionId) ?? submitted.receipt;
|
|
153
|
+
const abortReason = submissionAbortReason(options.submissions.find(options.submissionId));
|
|
154
|
+
if (abortReason && handoff.status === "pending" && handoff.requestId === requestId) {
|
|
155
|
+
options.submissions.updateAbortReason(handoff.submissionId, abortReason);
|
|
156
|
+
handoff = options.cancelHandoff(handoff.submissionId, abortReason);
|
|
157
|
+
}
|
|
158
|
+
if (handoff.status === "skipped" || handoff.status === "aborted") {
|
|
159
|
+
options.conversation.append(steer.messageId, message, {
|
|
160
|
+
submissionId: handoff.submissionId,
|
|
161
|
+
...(userMessage ? { userMessage } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
options.steers.deleteByMessageId(steer.messageId);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|