@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.10

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.
Files changed (76) hide show
  1. package/package.json +12 -3
  2. package/src/adapter/cloudflare/index.ts +56 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
  5. package/src/adapter/cloudflare/sandbox/id.ts +23 -0
  6. package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
  7. package/src/adapter/cloudflare/subagent/definition.ts +574 -0
  8. package/src/adapter/cloudflare/subagent/runner.ts +175 -0
  9. package/src/adapter/cloudflare/subagent/tools.ts +254 -0
  10. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
  13. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  14. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  15. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  16. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  17. package/src/agent-tool-runtime.ts +152 -0
  18. package/src/db/agent-tool.repo.ts +27 -0
  19. package/src/db/index.ts +33 -0
  20. package/src/db/interaction.repo.ts +185 -0
  21. package/src/db/schema.ts +25 -1
  22. package/src/db/submission.repo.ts +63 -1
  23. package/src/index.ts +57 -21
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +73 -9
  26. package/src/kernel/interaction-lifecycle.ts +395 -0
  27. package/src/kernel/public-contracts.ts +2 -0
  28. package/src/kernel/recoverable-chat-agent.ts +104 -6
  29. package/src/kernel/runtime-assembly-view.ts +37 -0
  30. package/src/kernel/runtime-assembly.ts +41 -0
  31. package/src/kernel/runtime-config.ts +4 -0
  32. package/src/kernel/runtime-load.ts +191 -0
  33. package/src/kernel/state.ts +12 -1
  34. package/src/kernel/submission-lifecycle.ts +33 -2
  35. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  36. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  37. package/src/lib/mcp.ts +7 -3
  38. package/src/lib/prompt.ts +1 -1
  39. package/src/lib/telemetry-dev.ts +7 -4
  40. package/src/pi/assembly/context.ts +3 -3
  41. package/src/pi/assembly/extensions.ts +11 -22
  42. package/src/pi/assembly/snapshot.ts +6 -3
  43. package/src/pi/message/contract.ts +7 -0
  44. package/src/pi/message/conversion.ts +9 -1
  45. package/src/pi/runtime-adapter/assembly.ts +26 -31
  46. package/src/pi/runtime-adapter/execution.ts +198 -15
  47. package/src/pi/runtime-adapter/index.ts +24 -8
  48. package/src/pi/runtime-adapter/models.ts +382 -35
  49. package/src/pi/runtime-adapter/recovery.ts +188 -1
  50. package/src/pi/runtime-adapter/transcript.ts +61 -3
  51. package/src/pi/tool/ai-adapter.ts +58 -1
  52. package/src/pi/tool/base.ts +190 -12
  53. package/src/pi/tool/compiler.ts +34 -1
  54. package/src/pi/tool/core-host.ts +19 -24
  55. package/src/pi/tool/core.ts +30 -120
  56. package/src/pi/tool/gateway.ts +54 -0
  57. package/src/pi/tool/index.ts +2 -0
  58. package/src/pi/tool/mcp.ts +96 -68
  59. package/src/pi/tool/schedule.ts +41 -19
  60. package/src/pi/tool/skill.ts +126 -420
  61. package/src/pi/tool/subagent.ts +14 -2
  62. package/src/pi/tool/web-fetch.ts +281 -0
  63. package/src/pi/tool/web-search/api.ts +34 -18
  64. package/src/pi/tool/web-search/web-search.ts +0 -1
  65. package/src/pi/tool/workspace-revision.ts +64 -0
  66. package/src/pi/tool/workspace-sandbox.ts +105 -263
  67. package/src/pi/turn/index.ts +20 -0
  68. package/src/pi/turn/interaction.ts +181 -0
  69. package/src/pi/turn/tool-recovery.ts +244 -1
  70. package/src/runtime-agent-context.ts +112 -0
  71. package/src/runtime-agent.ts +568 -321
  72. package/src/{plugins.ts → runtime-assembler.ts} +372 -398
  73. package/src/runtime-definition.ts +175 -0
  74. package/src/runtime.ts +835 -204
  75. package/src/tool-registry.ts +143 -0
  76. package/src/workspace-versioning.ts +46 -0
@@ -294,9 +294,7 @@ export interface RuntimeMemoryPort {
294
294
  * 按标签覆盖一块热记忆。
295
295
  *
296
296
  * @remarks
297
- * Host 实现提供该能力,但当前 `packages/agent-runtime/src` 没有调用点。
298
- *
299
- * TODO(待确认): 确认后续写入方是否仍需通过这个公开 Port 提交热记忆。
297
+ * `set_context` 在校验标签和 token 预算后调用,Host 负责按当前 Agent 与会话持久化。
300
298
  */
301
299
  set(label: string, content: string): Promise<void>;
302
300
  }
@@ -524,6 +522,41 @@ export interface RuntimeSandboxPort {
524
522
 
525
523
  // #region Runtime 服务端口
526
524
 
525
+ /** Gateway MCP 发现后可供 Pi 装配的安全 Tool 描述。 */
526
+ export interface RuntimeGatewayMcpTool {
527
+ readonly name: string;
528
+ readonly title?: string;
529
+ readonly description?: string;
530
+ readonly inputSchema?: Record<string, unknown>;
531
+ }
532
+
533
+ /** Gateway safe Catalog 中经过认证的 Action Risk。 */
534
+ export type RuntimeGatewayActionRisk = "none" | "high";
535
+
536
+ /**
537
+ * 一次 Agent Runtime 独占的 Gateway MCP 逻辑 Session。
538
+ *
539
+ * Runtime Grant 由实现闭包持有;本接口只暴露发现、调用和可信风险解析,
540
+ * 因此 Grant 不会进入 Profile、Snapshot 描述或 transcript。
541
+ */
542
+ export interface RuntimeGatewaySession {
543
+ readonly tools: readonly RuntimeGatewayMcpTool[];
544
+ callTool(
545
+ name: string,
546
+ input: Readonly<Record<string, unknown>>,
547
+ signal?: AbortSignal,
548
+ ): Promise<unknown>;
549
+ resolveCapabilityRisk(
550
+ reference: string,
551
+ ): Promise<RuntimeGatewayActionRisk>;
552
+ close(): Promise<void>;
553
+ }
554
+
555
+ /** Host 在 Runtime 启动时用它 mint Grant 并打开唯一 Gateway MCP Session。 */
556
+ export interface RuntimeGatewayPort {
557
+ open(runtimeId: string): Promise<RuntimeGatewaySession>;
558
+ }
559
+
527
560
  /**
528
561
  * 描述一条模型可见的定时任务。
529
562
  *
@@ -697,6 +730,22 @@ export interface RuntimeModelUsageEvent {
697
730
  };
698
731
  }
699
732
 
733
+ /**
734
+ * 一次 Tool 调用刚开始,还不知道它会怎么结束。
735
+ *
736
+ * @remarks
737
+ * 结算事件刻意不带参数,因为它只用来记账;开始事件必须带,因为它唯一的用处
738
+ * 就是让 Host 立刻知道「正在写的是什么」——只有工具名的话,「write 开始了」
739
+ * 和「一个网页正在生成」之间的差别就丢了,而后者才是盯着屏幕的人要看的。
740
+ * 参数本来就随 Tool 输入里程碑落库,这里没有多暴露任何东西。
741
+ */
742
+ export interface RuntimeToolStartEvent {
743
+ readonly submissionId: string;
744
+ readonly toolCallId: string;
745
+ readonly toolName: string;
746
+ readonly args: unknown;
747
+ }
748
+
700
749
  export interface RuntimeToolSettlementEvent {
701
750
  readonly eventId: string;
702
751
  readonly submissionId: string;
@@ -728,6 +777,17 @@ export interface RuntimeTurnEventsPort {
728
777
  ): Promise<void>;
729
778
  /** 投递 Assistant 或 Compaction 的原始 provider usage。 */
730
779
  onModelUsage?(event: RuntimeModelUsageEvent): Promise<void>;
780
+ /**
781
+ * 投递一次 Tool 开始执行的事件,带上它的参数。
782
+ *
783
+ * @remarks
784
+ * 只在这次调用第一次落下 durable intent 时投递:恢复重放不会再报一次开始,
785
+ * 否则一个早就结束的 Tool 会在几分钟后再宣布自己「正在跑」。
786
+ *
787
+ * 这条是活性信号而不是账,所以不进 durable outbox:迟到的开始事件本身就是
788
+ * 错的,投递失败就让它丢掉,最终状态由 `onToolSettled` 兜底。
789
+ */
790
+ onToolStart?(event: RuntimeToolStartEvent): Promise<void>;
731
791
  /** 投递不含参数与结果的 Tool 最终结算事件。 */
732
792
  onToolSettled?(event: RuntimeToolSettlementEvent): Promise<void>;
733
793
  onApproval?(input: {
@@ -803,6 +863,8 @@ export interface RuntimeModelEndpoint {
803
863
  headers?: Readonly<Record<string, string>>;
804
864
  baseURL: string;
805
865
  models: readonly string[];
866
+ /** OpenRouter upstream provider pins keyed by model ID; pinned requests never fall back. */
867
+ openRouterProviderPins?: Readonly<Record<string, string>>;
806
868
  }
807
869
 
808
870
  /**
@@ -860,15 +922,15 @@ export interface RuntimePlatformPort {
860
922
  }
861
923
 
862
924
  /**
863
- * 汇总本次装配已授权的 Skill 来源。
925
+ * 汇总本次装配已配置的 Skill 来源。
864
926
  *
865
927
  * @remarks
866
928
  * Builder 在生成候选 Snapshot 时创建,Pi 组装在生成 Skill 工具时读取。
867
929
  *
868
- * catalog、来源与脚本策略一起保留,避免装配期访问来源或让授权在 Skill 之间漂移。
930
+ * catalog、来源与脚本策略一起保留,避免装配期重新访问来源。
869
931
  */
870
932
  export interface RuntimeSkillBindings {
871
- /** Already-authorized external sources and their isolated script policy. */
933
+ /** Configured external sources and their isolated script policy. */
872
934
  sources: readonly RuntimeSkillSourceBinding[];
873
935
  }
874
936
 
@@ -878,7 +940,7 @@ export interface RuntimeSkillBindings {
878
940
  * @remarks
879
941
  * Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
880
942
  *
881
- * 这是已授权结果,执行期不应为某个 Skill 自动放宽。
943
+ * 这是 Config 已解析的执行策略,执行期不应自动放宽。
882
944
  */
883
945
  export interface RuntimeSkillScriptPolicy {
884
946
  network: "none" | "full";
@@ -887,12 +949,12 @@ export interface RuntimeSkillScriptPolicy {
887
949
  }
888
950
 
889
951
  /**
890
- * 把一个已授权 Skill 来源与其脚本策略绑定。
952
+ * 把一个已配置 Skill 来源与其脚本策略放在同一快照中。
891
953
  *
892
954
  * @remarks
893
955
  * Builder 为每个 Skill 贡献创建,Pi 组装和 Skill 工具按名称查找并读取。
894
956
  *
895
- * 名称、内容来源与权限不分开存放,防止脚本改用其他 Skill 的授权。
957
+ * 名称、内容来源与脚本策略不分开存放,脚本 runner 才能按 Skill 名找到对应策略。
896
958
  */
897
959
  export interface RuntimeSkillSourceBinding {
898
960
  name: string;
@@ -913,6 +975,8 @@ export interface RuntimeSkillSourceBinding {
913
975
  export interface RuntimeBindings {
914
976
  provider: RuntimeProviderPort;
915
977
  platform: RuntimePlatformPort;
978
+ /** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
979
+ gateway?: RuntimeGatewayPort;
916
980
  workspace?: WorkspacePort;
917
981
  memory?: RuntimeMemoryPort;
918
982
  skills: RuntimeSkillBindings;
@@ -0,0 +1,395 @@
1
+ import {
2
+ isTerminalSubmissionStatus,
3
+ type RuntimeDatabase,
4
+ type StoredToolInteraction,
5
+ type SubmissionStatus,
6
+ } from "../db";
7
+ import type {
8
+ PiDurableMutation,
9
+ PiRecoveryCommand,
10
+ PiRecoveryDecision,
11
+ PiRuntimeAdapter,
12
+ PiToolInteraction,
13
+ PiToolInteractionCancelReason,
14
+ } from "../pi/runtime-adapter";
15
+ import type { ToolResultMessage } from "@earendil-works/pi-ai";
16
+
17
+ // #region 类型约定
18
+
19
+ /**
20
+ * 提供 interaction 流程定位一次持久化提交所需的最小信息。
21
+ *
22
+ * @remarks
23
+ * 与 `ApprovalSubmission` 同形。这里只要求 park/续跑真正需要的字段,
24
+ * 避免 interaction 流程依赖完整提交模型。
25
+ */
26
+ export interface InteractionSubmission {
27
+ readonly submissionId: string;
28
+ readonly requestId: string;
29
+ readonly assemblyRevision: string;
30
+ readonly status: SubmissionStatus;
31
+ }
32
+
33
+ /**
34
+ * 表示已经绑定到持久化提交的完整 interaction 记录。
35
+ */
36
+ export interface InteractionRecord extends PiToolInteraction {
37
+ readonly submissionId: string;
38
+ }
39
+
40
+ /**
41
+ * 描述一次响应或取消要写入的 ToolResult 载荷。
42
+ *
43
+ * `content` / `details` 由 Tool 自己的 `settle` 映射产出 —— 本流程不解释响应体语义。
44
+ * 取消不带载荷,由纯函数层用固定文案兜底。
45
+ */
46
+ export interface InteractionSettlementPayload {
47
+ readonly content: ToolResultMessage["content"];
48
+ readonly details: unknown;
49
+ }
50
+
51
+ // 宿主用这些依赖把 interaction 状态机接入数据库、Pi 恢复计算和 Turn 续跑。
52
+ interface InteractionLifecycleOptions<
53
+ TSubmission extends InteractionSubmission,
54
+ > {
55
+ readonly db: RuntimeDatabase;
56
+ readonly pi: PiRuntimeAdapter;
57
+ readonly findSubmission: (submissionId: string) => TSubmission | null;
58
+ readonly applyRecoveryMutations: (
59
+ submission: TSubmission,
60
+ mutations: readonly PiDurableMutation[],
61
+ ) => boolean;
62
+ readonly materializeRecoveredToolResults: (
63
+ submission: TSubmission,
64
+ ) => Promise<PiRecoveryDecision>;
65
+ /** 没有内存等待器时(DO 睡过一觉)把原 Turn 重新拉起来。 */
66
+ readonly resumeSubmission: (submissionId: string) => Promise<void>;
67
+ /**
68
+ * pending 集合发生变化后通知宿主重算活动状态投影。
69
+ *
70
+ * park 和结算都会触发。宿主的 needs-input 是 pending 行的纯函数,
71
+ * 没有这个回调,park 的那一刻不会有人去重算 —— 侧栏要等下一次广播才翻牌。
72
+ */
73
+ readonly onInteractionsChanged?: () => Promise<void>;
74
+ }
75
+
76
+ // #endregion
77
+
78
+ // #region interaction 流程 API
79
+
80
+ /**
81
+ * 管理「结果由客户端提供」的 Tool 从 park、等待、响应到续跑的完整过程。
82
+ *
83
+ * @remarks
84
+ * 与 `ApprovalLifecycle` 结构同源但显著更小 —— 不需要 `allow_level` 的 Host 协调、
85
+ * 不需要批准后 `retryTool`、不需要执行档位矩阵。两者的重复是**当前有意接受的**:
86
+ * 审批那条路已经跑在线上,等 interaction 跑稳后再考虑抽公共核,届时改的是这两个类,
87
+ * 不是恢复状态机。
88
+ *
89
+ * 续跑不走 interaction 专属的调度键:响应会同时写下一条权威 `tool-result` 里程碑,
90
+ * 因此复用 `planPiToolRecovery` 中既有的 `tool:<id>:settled` 分支。
91
+ */
92
+ export class InteractionLifecycle<
93
+ TSubmission extends InteractionSubmission,
94
+ > {
95
+ private readonly waiters = new Map<string, () => void>();
96
+
97
+ constructor(
98
+ private readonly options: InteractionLifecycleOptions<TSubmission>,
99
+ ) {}
100
+
101
+ /**
102
+ * 用工具调用标识查找唯一一条仍待响应的 interaction。
103
+ *
104
+ * @remarks
105
+ * `Runtime.respondToolInteraction` 调用它 —— 前端只有 `toolCallId`。
106
+ * 调用方必须把 `null` 当作没有安全匹配,不应猜测要处理哪条记录。
107
+ */
108
+ findPending(toolCallId: string): InteractionRecord | null {
109
+ const stored = this.options.db.interactions
110
+ .findPendingByToolCallId(toolCallId);
111
+ return stored ? toRecord(stored) : null;
112
+ }
113
+
114
+ /**
115
+ * 按 interactionId 读取一条完整记录,不存在时返回 null。
116
+ */
117
+ read(interactionId: string): InteractionRecord | null {
118
+ const stored = this.options.db.interactions.find(interactionId);
119
+ return stored ? toRecord(stored) : null;
120
+ }
121
+
122
+ /**
123
+ * 持久化一条 interaction,并暂停当前工具执行直到客户端投递响应。
124
+ *
125
+ * @remarks
126
+ * 执行适配器在 Tool 声明了 `interaction` 时调用它。
127
+ * 记录与恢复里程碑先在事务中落盘,再建立内存等待,保证重启仍能恢复 park 状态。
128
+ */
129
+ async request(
130
+ submission: TSubmission,
131
+ interaction: PiToolInteraction,
132
+ signal?: AbortSignal,
133
+ ): Promise<void> {
134
+ const pending = this.options.db.transaction(() =>
135
+ this.ensurePending(submission, interaction)
136
+ );
137
+ // 必须在 wait 之前通知:wait 会一直挂到客户端投递,之后再通知就晚了一个回合。
138
+ await this.options.onInteractionsChanged?.();
139
+ return this.wait(pending, signal);
140
+ }
141
+
142
+ /**
143
+ * 应用客户端的首次响应,结算对应 Tool 并唤醒原 Turn。
144
+ *
145
+ * @remarks
146
+ * `Runtime.respondToolInteraction` 在校验并映射出 ToolResult 后调用它。
147
+ * 不存在、已结束或提交已终态都返回 `{ ok: false }`,不抛 —— 口径对齐 `ApprovalLifecycle.decide`。
148
+ */
149
+ async respond(
150
+ interactionId: string,
151
+ response: unknown,
152
+ payload: InteractionSettlementPayload,
153
+ ): Promise<{ ok: boolean }> {
154
+ return this.settle(interactionId, {
155
+ kind: "respond",
156
+ response,
157
+ result: payload,
158
+ });
159
+ }
160
+
161
+ /**
162
+ * 在用户没有作答的情况下结束一条 interaction 并让 Turn 继续。
163
+ *
164
+ * @remarks
165
+ * park 期间收到用户消息时调用(`user_replied_freeform`),提交进终态时也调用。
166
+ * 取消同样产出稳定 ToolResult,不是 Tool 失败 —— 否则模型会收到 tool_error 并倾向重试。
167
+ */
168
+ async cancel(
169
+ interactionId: string,
170
+ reason: PiToolInteractionCancelReason,
171
+ ): Promise<{ ok: boolean }> {
172
+ return this.settle(interactionId, { kind: "cancel", reason });
173
+ }
174
+
175
+ /**
176
+ * 把某次提交下仍待响应的全部 interaction 一次性取消。
177
+ *
178
+ * @remarks
179
+ * `dispatchMessage` 发现 park 期间来了用户消息时、以及提交进终态时调用。
180
+ * 返回是否真的取消过记录,调用方据此决定要不要广播。
181
+ */
182
+ async cancelPendingForSubmission(
183
+ submissionId: string,
184
+ reason: PiToolInteractionCancelReason,
185
+ ): Promise<boolean> {
186
+ const pending = this.options.db.interactions
187
+ .listPendingForSubmission(submissionId);
188
+ if (pending.length === 0) return false;
189
+ let cancelled = false;
190
+ for (const interactionId of pending) {
191
+ const result = await this.cancel(interactionId, reason);
192
+ cancelled ||= result.ok;
193
+ }
194
+ return cancelled;
195
+ }
196
+
197
+ /**
198
+ * 在同步终态事务里把某次提交下仍待响应的 interaction 全部取消。
199
+ *
200
+ * @remarks
201
+ * `Runtime.commitTerminalOutcome` 在提交终态的 `transaction` 内调用,因此必须是同步的。
202
+ * 这里只推进状态和里程碑,**不** materialize、**不** 续跑 —— Turn 正在结束,
203
+ * 调用方负责在同一事务内调用 `materializeRecoveredToolResultsSync`,口径对齐审批的 `rejectPending`。
204
+ *
205
+ * 返回是否真的取消过记录;没有 pending 时返回 false,调用方可据此跳过 materialize。
206
+ */
207
+ cancelPendingForSubmissionSync(
208
+ submission: TSubmission,
209
+ reason: PiToolInteractionCancelReason = "submission_terminal",
210
+ ): boolean {
211
+ const pending = this.options.db.interactions
212
+ .listPendingForSubmission(submission.submissionId);
213
+ if (pending.length === 0) return false;
214
+ for (const interactionId of pending) {
215
+ const decision = this.decideRecovery(submission, {
216
+ kind: "interaction-settlement",
217
+ interactionId,
218
+ settlement: { kind: "cancel", reason },
219
+ });
220
+ this.options.applyRecoveryMutations(submission, decision.mutations);
221
+ }
222
+ return true;
223
+ }
224
+
225
+ /**
226
+ * 判断某次提交当前是否停在等待客户端响应上。
227
+ *
228
+ * @remarks
229
+ * `dispatchMessage` 用它决定要不要把 delivery 提升成 steer —— 否则 enqueue 的消息
230
+ * 要等这个 Turn 结束,而这个 Turn 正在等一个不会来的答案,死锁。
231
+ */
232
+ hasPendingForSubmission(submissionId: string): boolean {
233
+ return this.options.db.interactions
234
+ .listPendingForSubmission(submissionId).length > 0;
235
+ }
236
+
237
+ // #endregion
238
+
239
+ // #region 内部
240
+
241
+ // 作用:把响应或取消统一走同一条持久化 + 唤醒路径。
242
+ // 调用:`respond` 和 `cancel`。
243
+ // 原因:两种结局的差别只在 ToolResult 从哪来,状态推进和续跑必须完全一致。
244
+ private async settle(
245
+ interactionId: string,
246
+ settlement:
247
+ | {
248
+ kind: "respond";
249
+ response: unknown;
250
+ result: InteractionSettlementPayload;
251
+ }
252
+ | { kind: "cancel"; reason: PiToolInteractionCancelReason },
253
+ ): Promise<{ ok: boolean }> {
254
+ const stored = this.read(interactionId);
255
+ if (!stored) return { ok: false };
256
+ const submission = this.options.findSubmission(stored.submissionId);
257
+ if (!submission || isTerminalSubmissionStatus(submission.status)) {
258
+ return { ok: false };
259
+ }
260
+ if (stored.status !== "pending") return { ok: false };
261
+
262
+ const decision = this.decideRecovery(submission, {
263
+ kind: "interaction-settlement",
264
+ interactionId,
265
+ settlement,
266
+ });
267
+ if (!decision.applied) return { ok: false };
268
+ this.options.db.transaction(() => {
269
+ this.options.applyRecoveryMutations(submission, decision.mutations);
270
+ });
271
+ const persisted = this.read(interactionId);
272
+ if (!persisted || persisted.status === "pending") {
273
+ return { ok: false };
274
+ }
275
+
276
+ // 把刚写下的权威 tool-result 落成 settlement 行 —— park 住的执行链醒来后
277
+ // 只认 findToolSettlement,不读里程碑。这一步必须早于唤醒。
278
+ await this.options.materializeRecoveredToolResults(submission);
279
+ // 先重算活动状态再唤醒:唤醒会把 Turn 一路跑下去,晚通知就会让「等待回复」
280
+ // 在续跑期间多挂一段时间。
281
+ await this.options.onInteractionsChanged?.();
282
+
283
+ const waiter = this.waiters.get(interactionId);
284
+ if (waiter) {
285
+ waiter();
286
+ return { ok: true };
287
+ }
288
+ // 没有内存等待器 = DO 在 park 期间睡过一觉,原 Turn 的执行栈已经没了。
289
+ // 走持久恢复把它重新拉起来,里程碑里的 tool-result 会被当成已结算结果复用。
290
+ await this.options.resumeSubmission(submission.submissionId);
291
+ return { ok: true };
292
+ }
293
+
294
+ // 作用:读取一个 Submission 的全部恢复里程碑正文。
295
+ // 原因:持久化历史必须作为恢复决策的唯一输入。
296
+ private milestoneBodies(submissionId: string): string[] {
297
+ return this.options.db.milestones.listBodies(submissionId);
298
+ }
299
+
300
+ // 作用:请求 Pi 根据当前里程碑给出下一步。
301
+ // 原因:所有路径共用同一决策入口,避免各自改写恢复状态。
302
+ private decideRecovery(
303
+ submission: TSubmission,
304
+ command: PiRecoveryCommand = { kind: "inspect" },
305
+ ): PiRecoveryDecision {
306
+ return this.options.pi.decideRecovery({
307
+ milestoneBodies: this.milestoneBodies(submission.submissionId),
308
+ identity: {
309
+ turnId: submission.submissionId,
310
+ assemblyRevision: submission.assemblyRevision,
311
+ },
312
+ command,
313
+ now: Date.now(),
314
+ });
315
+ }
316
+
317
+ // 作用:复用或创建一条 pending interaction。
318
+ // 调用:`request` 在准入事务内调用。
319
+ // 原因:已存在记录的字段冲突必须报错,不能把响应投给另一次调用。
320
+ private ensurePending(
321
+ submission: TSubmission,
322
+ interaction: PiToolInteraction,
323
+ ): InteractionRecord {
324
+ const existing = this.read(interaction.interactionId);
325
+ if (existing) {
326
+ if (
327
+ existing.requestId !== submission.requestId ||
328
+ existing.toolCallId !== interaction.toolCallId ||
329
+ existing.toolName !== interaction.toolName ||
330
+ existing.inputJson !== interaction.inputJson
331
+ ) {
332
+ throw new Error(
333
+ `Conflicting durable Tool interaction: ${interaction.toolCallId}`,
334
+ );
335
+ }
336
+ return existing;
337
+ }
338
+ // 表行和恢复里程碑都由 record-interaction 这一条命令产出,
339
+ // 不在这里直接 insert —— 两份状态分叉正是恢复最难查的一类 bug。
340
+ const decision = this.decideRecovery(submission, {
341
+ kind: "record-interaction",
342
+ interaction,
343
+ });
344
+ this.options.applyRecoveryMutations(submission, decision.mutations);
345
+ return { ...interaction, submissionId: submission.submissionId };
346
+ }
347
+
348
+ // 作用:等待当前进程里的客户端响应。
349
+ // 原因:内存 Promise 只负责唤醒当前请求,数据库仍负责持久状态以支持重启。
350
+ private wait(
351
+ interaction: InteractionRecord,
352
+ signal?: AbortSignal,
353
+ ): Promise<void> {
354
+ if (interaction.status !== "pending") return Promise.resolve();
355
+ return new Promise((resolve, reject) => {
356
+ const abort = () => {
357
+ this.waiters.delete(interaction.interactionId);
358
+ reject(signal?.reason ?? new Error("Tool interaction aborted"));
359
+ };
360
+ if (signal?.aborted) {
361
+ abort();
362
+ return;
363
+ }
364
+ signal?.addEventListener("abort", abort, { once: true });
365
+ this.waiters.set(interaction.interactionId, () => {
366
+ signal?.removeEventListener("abort", abort);
367
+ this.waiters.delete(interaction.interactionId);
368
+ resolve();
369
+ });
370
+ });
371
+ }
372
+
373
+ // #endregion
374
+ }
375
+
376
+ // 把持久行转成流程内部使用的记录。
377
+ // find / findPending 查到记录后调用,上层因而只看 Pi 的 interaction 形状。
378
+ function toRecord(stored: StoredToolInteraction): InteractionRecord {
379
+ return {
380
+ interactionId: stored.interactionId,
381
+ submissionId: stored.submissionId,
382
+ requestId: stored.requestId,
383
+ toolCallId: stored.toolCallId,
384
+ toolName: stored.toolName,
385
+ inputJson: stored.inputJson,
386
+ status: stored.status,
387
+ createdAt: stored.createdAt,
388
+ ...(stored.respondedAt === null
389
+ ? {}
390
+ : { respondedAt: stored.respondedAt }),
391
+ ...(stored.responseJson === null
392
+ ? {}
393
+ : { responseJson: stored.responseJson }),
394
+ };
395
+ }
@@ -11,6 +11,8 @@
11
11
  * @packageDocumentation
12
12
  */
13
13
  export * from "./profile";
14
+ export * from "./runtime-assembly-view";
15
+ export * from "./runtime-config";
14
16
  export * from "../pi/message";
15
17
  export * from "./receipts";
16
18
  export * from "./state";