@springbrand/agent-runtime 0.1.0

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 (75) hide show
  1. package/package.json +28 -0
  2. package/src/db/approval.repo.ts +291 -0
  3. package/src/db/ext-context.repo.ts +34 -0
  4. package/src/db/index.ts +83 -0
  5. package/src/db/message-ui.repo.ts +39 -0
  6. package/src/db/milestone.repo.ts +96 -0
  7. package/src/db/runtime-event-outbox.repo.ts +89 -0
  8. package/src/db/schema.ts +164 -0
  9. package/src/db/settlement.repo.ts +104 -0
  10. package/src/db/steer.repo.ts +73 -0
  11. package/src/db/submission.repo.ts +323 -0
  12. package/src/index.ts +133 -0
  13. package/src/kernel/approval-lifecycle.ts +552 -0
  14. package/src/kernel/bindings.ts +898 -0
  15. package/src/kernel/degradation.ts +15 -0
  16. package/src/kernel/extensions.ts +108 -0
  17. package/src/kernel/profile.ts +116 -0
  18. package/src/kernel/public-contracts.ts +17 -0
  19. package/src/kernel/receipts.ts +124 -0
  20. package/src/kernel/recoverable-chat-agent.ts +899 -0
  21. package/src/kernel/state.ts +76 -0
  22. package/src/kernel/submission-lifecycle.ts +600 -0
  23. package/src/layers/context/budget/gate.ts +88 -0
  24. package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
  25. package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
  26. package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
  27. package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
  28. package/src/layers/orchestration/temporary-agent/core.ts +152 -0
  29. package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
  30. package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
  31. package/src/lib/artifacts.ts +54 -0
  32. package/src/lib/egress.ts +44 -0
  33. package/src/lib/execution-level.ts +27 -0
  34. package/src/lib/extension-name.ts +18 -0
  35. package/src/lib/host-actions.ts +57 -0
  36. package/src/lib/mcp.ts +86 -0
  37. package/src/lib/model-catalog.ts +7 -0
  38. package/src/lib/prompt.ts +139 -0
  39. package/src/lib/telemetry-dev.ts +44 -0
  40. package/src/pi/assembly/context.ts +510 -0
  41. package/src/pi/assembly/extensions.ts +661 -0
  42. package/src/pi/assembly/index.ts +19 -0
  43. package/src/pi/assembly/snapshot.ts +200 -0
  44. package/src/pi/message/contract.ts +8 -0
  45. package/src/pi/message/conversion.ts +73 -0
  46. package/src/pi/message/index.ts +3 -0
  47. package/src/pi/message/projection.ts +604 -0
  48. package/src/pi/runtime-adapter/assembly.ts +552 -0
  49. package/src/pi/runtime-adapter/execution.ts +683 -0
  50. package/src/pi/runtime-adapter/index.ts +232 -0
  51. package/src/pi/runtime-adapter/models.ts +243 -0
  52. package/src/pi/runtime-adapter/recovery.ts +805 -0
  53. package/src/pi/runtime-adapter/transcript.ts +825 -0
  54. package/src/pi/session/index.ts +24 -0
  55. package/src/pi/session/storage.ts +353 -0
  56. package/src/pi/tool/ai-adapter.ts +100 -0
  57. package/src/pi/tool/base.ts +110 -0
  58. package/src/pi/tool/compiler.ts +444 -0
  59. package/src/pi/tool/core-host.ts +48 -0
  60. package/src/pi/tool/core.ts +251 -0
  61. package/src/pi/tool/index.ts +32 -0
  62. package/src/pi/tool/mcp.ts +319 -0
  63. package/src/pi/tool/schedule.ts +198 -0
  64. package/src/pi/tool/skill.ts +455 -0
  65. package/src/pi/tool/subagent.ts +148 -0
  66. package/src/pi/tool/web-search/api.ts +1292 -0
  67. package/src/pi/tool/web-search/index.ts +2 -0
  68. package/src/pi/tool/web-search/web-search.ts +127 -0
  69. package/src/pi/tool/workspace-sandbox.ts +664 -0
  70. package/src/pi/turn/approval.ts +181 -0
  71. package/src/pi/turn/index.ts +62 -0
  72. package/src/pi/turn/tool-recovery.ts +792 -0
  73. package/src/plugins.ts +1024 -0
  74. package/src/runtime-agent.ts +654 -0
  75. package/src/runtime.ts +2880 -0
package/src/index.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Agent Runtime 的公共入口。
3
+ *
4
+ * @remarks
5
+ * 核心术语统一以这里为准。
6
+ *
7
+ * - `Agent` 是一个可按名寻址的 Cloudflare Agents 实例,也是持有独立存储的 Durable Object。
8
+ * - `facet` 是挂在父 Agent 下的子 Agent;它与父节点同机运行,但使用独立的 SQLite 存储。
9
+ * - `Session` 是本项目的聊天会话边界,由一个 `UniversalAgent` facet 承载。
10
+ * - `Turn` 是 Session 中从一次提示被接纳到输出终态的一次执行。
11
+ * - `Host` 是承载 Runtime 的 Cloudflare Agent,并负责提供生命周期、存储和网络边界。
12
+ * - `Kernel` 是 Runtime 内部连接 Cloudflare Agent 生命周期与 Pi 执行的核心实现。
13
+ * - `Pi` 是负责模型、Tool 循环和 canonical transcript 的执行层。
14
+ * - `Workspace` 是 Tool 可以读取或修改的受控文件系统视图。
15
+ * - `Memory` 是可跨 Turn 读取或写入的长期上下文能力。
16
+ * - `Tool` 是模型在 Turn 中可以调用的一项受控能力。
17
+ * - `Tool Candidate` 是尚未完成治理和编译的内部 Tool 定义。
18
+ * - `Tool Settlement` 是 Tool 调用完成后写回模型循环的统一结果。
19
+ * - `Governance` 是对 Tool 可见性、审批和执行条件的集中约束。
20
+ * - `Artifact` 是 Runtime 保存并对外引用的结构化执行产物。
21
+ * - `Extension` 是 Pi 在装配期加载、并在 Turn 中参与执行的扩展模块。
22
+ * - `SubAgent` 是由父 Agent 发起、但拥有独立执行上下文的子任务执行者。
23
+ * - `canonical transcript` 是 Pi 持久化和恢复 Turn 时使用的权威消息历史。
24
+ * - `admission` 是提示进入 Turn 前的接纳、去重和状态登记流程。
25
+ * - `AgentConfig` 是应用交给 Runtime 的一次完整装配说明。
26
+ * - `AgentPlugin` 是一种能力的加载与贡献单元。
27
+ * - `PluginKind` 是 Plugin 的固定类别,也是贡献时的稳定排序依据。
28
+ * - `loader` 读取业务数据或外部绑定,并返回本次装配要使用的值。
29
+ * - `prepare` 执行 `loader`,但不会改动正在运行的 Runtime。
30
+ * - `RuntimeContributionContext` 是 Plugin 写入候选配置的受限接口。
31
+ * - `Port` 是 Runtime 调用外部能力时依赖的最小接口。
32
+ * - `RuntimeBindings` 是本次装配选中的 Port 和可执行对象集合。
33
+ * - `RuntimeBuilder` 是包内实现,用来校验贡献并生成候选结果。
34
+ * - `RuntimeCandidate` 是尚未生效的候选结果和提交前检查。
35
+ * - `RuntimeSnapshot` 是一次成功提交后供 Kernel 读取的冻结装配选择。
36
+ * - `commit guard` 在最终提交前重新确认容易过期的业务条件。
37
+ * - `degradation` 表示某项可选能力不可用,但整体仍可以启动。
38
+ * - `runtime key` 标识当前 Agent 实例实际加载的是哪一份业务配置。
39
+ * - `RuntimeProfile` 是 Runtime 对当前能力、限制和退化状态的只读说明。
40
+ *
41
+ * Snapshot 不允许替换已选集合,但不承诺把每个 Port 的内部对象深冻结。
42
+ *
43
+ * 正常调用顺序是 `loader → prepare → contribute → validate → guard → commit`。
44
+ * 待确认:当前实现先校验 Plugin kind,再在 `prepare` 中调用 `loader`;这里的阶段简写是否仍应作为对外文档保留,需要与实际命名统一。
45
+ *
46
+ * 应用只创建 `AgentConfig` 和 Plugin。
47
+ *
48
+ * Runtime 独占校验、排序和原子提交。
49
+ *
50
+ * @packageDocumentation
51
+ */
52
+ export * from "./kernel/bindings";
53
+ export * from "./kernel/extensions";
54
+ export * from "./kernel/profile";
55
+ export * from "./kernel/receipts";
56
+ export * from "./runtime-agent";
57
+ export * from "./kernel/state";
58
+ export * from "./lib/execution-level";
59
+ export { definePlugin } from "./plugins";
60
+ export type {
61
+ AgentConfig,
62
+ AgentPlugin,
63
+ AgentPluginSpec,
64
+ PluginKind,
65
+ PluginLoadResult,
66
+ PreparedPlugin,
67
+ RuntimeContributionContext,
68
+ RuntimeDegradation,
69
+ RuntimeProfileContribution,
70
+ } from "./plugins";
71
+ export type { ModelOption } from "./lib/model-catalog";
72
+ export {
73
+ assembleSubagentPrompt,
74
+ assembleSystemPrompt,
75
+ } from "./lib/prompt";
76
+ export { HttpGateway } from "./lib/egress";
77
+ export { decideHostActions } from "./lib/host-actions";
78
+ export type {
79
+ HostActionPolicy,
80
+ HostActionSelection,
81
+ } from "./lib/host-actions";
82
+ export {
83
+ AGENT_TYPES,
84
+ } from "./layers/orchestration/subagents/agent-types/registry";
85
+ export type {
86
+ AgentType,
87
+ } from "./layers/orchestration/subagents/agent-types/contract";
88
+ export { withProviderRetry } from "./pi/runtime-adapter";
89
+ export { compilePiTools } from "./pi/tool";
90
+ export type {
91
+ CompilePiToolsOptions,
92
+ PiToolCandidate,
93
+ SettledPiToolCall,
94
+ } from "./pi/tool";
95
+ export { basePiToolCandidates } from "./pi/tool";
96
+ export { schedulePiToolCandidates } from "./pi/tool";
97
+ export {
98
+ sandboxPiToolCandidates,
99
+ workspacePiToolCandidates,
100
+ } from "./pi/tool";
101
+ export { subagentPiToolCandidates } from "./pi/tool";
102
+ export { skillPiToolCandidates } from "./pi/tool";
103
+ export type { PiSkillBinding } from "./pi/tool";
104
+ export {
105
+ browserQuickActionPiToolCandidates,
106
+ } from "./pi/tool";
107
+ export {
108
+ workspaceCodeExecutionPiToolCandidate,
109
+ } from "./pi/tool";
110
+ export {
111
+ assemblePiExtensions,
112
+ loadPiExtension,
113
+ } from "./pi/assembly";
114
+ export type {
115
+ TemporaryAgentApprovalDecision,
116
+ TemporaryAgentApprovalRequest,
117
+ TemporaryAgentExecutor,
118
+ TemporaryAgentRequest,
119
+ TemporaryAgentRunContext,
120
+ } from "./layers/orchestration/temporary-agent/core";
121
+ export {
122
+ bridgeTemporaryAgentToolApprovals,
123
+ temporaryAgentExtensionIsSafe,
124
+ temporaryAgentToolAllowed,
125
+ } from "./layers/orchestration/temporary-agent/runner";
126
+ export {
127
+ createTemporaryAgentWorkspace,
128
+ } from "./layers/orchestration/temporary-agent/workspace";
129
+ export {
130
+ createPiModels,
131
+ resolvePiApiKey,
132
+ resolvePiModel,
133
+ } from "./pi/runtime-adapter/models";
@@ -0,0 +1,552 @@
1
+ import {
2
+ isTerminalSubmissionStatus,
3
+ type RuntimeDatabase,
4
+ type SubmissionStatus,
5
+ } from "../db";
6
+ import type {
7
+ PiDurableMutation,
8
+ PiRecoveryCommand,
9
+ PiRecoveryDecision,
10
+ PiRuntimeAdapter,
11
+ PiToolApproval,
12
+ } from "../pi/runtime-adapter";
13
+ import type { ApprovalDecision, ApprovalReceipt } from "./receipts";
14
+
15
+ // #region 类型约定
16
+
17
+ /**
18
+ * 提供审批流程定位一次持久化提交所需的最小信息。
19
+ *
20
+ * @remarks
21
+ * `Runtime` 创建审批流程并传入自己的提交记录时使用这个约束。
22
+ * 调用方要提供稳定的提交标识、请求标识、装配版本和当前状态。
23
+ * 这里只要求审批与恢复真正需要的字段,避免审批流程依赖完整提交模型。
24
+ * `Runtime` 等核心术语沿用包入口 `../index.ts` 的定义。
25
+ */
26
+ export interface ApprovalSubmission {
27
+ readonly submissionId: string;
28
+ readonly requestId: string;
29
+ readonly assemblyRevision: string;
30
+ readonly status: SubmissionStatus;
31
+ }
32
+
33
+ /**
34
+ * 描述一次审批恢复任务重新进入 `Runtime` 时携带的数据。
35
+ *
36
+ * @remarks
37
+ * `ApprovalLifecycle` 安排恢复任务时写入它,`Runtime` 的调度回调触发时读取它。
38
+ * 调用方应原样传递这些字段,不要自行拼接恢复标识。
39
+ * 可选字段允许调度入口安全接收不完整数据,再由入口拒绝无效任务。
40
+ *
41
+ * TODO(待确认): 当前 `_piApprovalContinuation` 只读取 `submissionId` 和
42
+ * `approvalExecutionId`,`requestId` 与 `continuationKey` 尚未被消费。
43
+ */
44
+ export interface ApprovalContinuationData {
45
+ readonly submissionId: string;
46
+ readonly requestId: string;
47
+ readonly continuationKey?: string;
48
+ readonly approvalExecutionId?: string;
49
+ }
50
+
51
+ /**
52
+ * 表示已经绑定到持久化提交的完整工具审批记录。
53
+ *
54
+ * @remarks
55
+ * 审批流程读取数据库后返回它,`Runtime` 的决定与恢复入口会使用它。
56
+ * 调用方应把 `executionId` 当作审批操作标识,把 `submissionId` 用于回查提交。
57
+ * 该类型复用 Pi 的审批字段,只补充数据库归属,避免维护第二份审批模型。
58
+ */
59
+ export interface ApprovalRecord extends PiToolApproval {
60
+ readonly submissionId: string;
61
+ }
62
+
63
+ type RuntimeApprovalDecision = Exclude<
64
+ ApprovalDecision,
65
+ { decision: "allow_level" }
66
+ >;
67
+
68
+ // 宿主用这些依赖把审批状态机接入数据库、Pi 恢复计算和 Runtime 调度。
69
+ interface ApprovalLifecycleOptions<
70
+ TSubmission extends ApprovalSubmission,
71
+ > {
72
+ readonly db: RuntimeDatabase;
73
+ readonly pi: PiRuntimeAdapter;
74
+ readonly findSubmission: (submissionId: string) => TSubmission | null;
75
+ readonly applyRecoveryMutations: (
76
+ submission: TSubmission,
77
+ mutations: readonly PiDurableMutation[],
78
+ ) => boolean;
79
+ readonly materializeRecoveredToolResults: (
80
+ submission: TSubmission,
81
+ ) => Promise<PiRecoveryDecision>;
82
+ readonly scheduleContinuation: (
83
+ data: ApprovalContinuationData,
84
+ ) => Promise<void>;
85
+ readonly onApprovalsChanged: () => Promise<void>;
86
+ }
87
+
88
+ // #endregion
89
+
90
+ // #region 审批流程 API
91
+
92
+ /**
93
+ * 管理持久化工具审批从登记、等待、决定到恢复调度的完整过程。
94
+ *
95
+ * @remarks
96
+ * `Runtime` 为每个运行实例创建一个对象,并把 Pi Turn、用户决定和启动恢复都交给它。
97
+ * 调用方通过公开方法推进审批,但恢复后的 Pi Turn 仍由 `Runtime` 执行。
98
+ * 数据库记录和恢复里程碑必须在这里同步推进,否则重启后两份状态会分叉。
99
+ * `Runtime`、装配版本等核心术语沿用包入口 `../index.ts` 的定义。
100
+ */
101
+ export class ApprovalLifecycle<
102
+ TSubmission extends ApprovalSubmission,
103
+ > {
104
+ private readonly waiters = new Map<string, () => void>();
105
+
106
+ /**
107
+ * 接好审批流程需要的宿主依赖。
108
+ *
109
+ * @remarks
110
+ * `Runtime` 在构造自身时调用一次,后续方法都复用同一组数据库和恢复能力。
111
+ * 调用方应传入同一运行实例的依赖,不要跨实例共享提交查询或调度回调。
112
+ * 依赖由宿主注入,是为了让本类只协调审批,不接管 Turn 执行和外部调度。
113
+ */
114
+ constructor(
115
+ private readonly options: ApprovalLifecycleOptions<TSubmission>,
116
+ ) {}
117
+
118
+ /**
119
+ * 返回当前仍在运行的提交里有多少条待审批记录。
120
+ *
121
+ * @remarks
122
+ * `Runtime` 的恢复端口检查是否仍有人机交互等待时调用它。
123
+ * 调用方只应把结果用于判断当前是否繁忙,不要据此定位具体审批。
124
+ * 这里查询持久化活动记录而不是内存等待器,因为内存等待器会在重启时消失。
125
+ */
126
+ countPending(): number {
127
+ return this.options.db.approvals.countPendingActive();
128
+ }
129
+
130
+ /**
131
+ * 用工具调用标识查找唯一一条仍待处理的审批操作。
132
+ *
133
+ * @remarks
134
+ * `Runtime` 收到 `tool-approval` 客户端事件时调用它,再转交批准或拒绝入口。
135
+ * 调用方必须把 `null` 当作没有安全匹配,不应猜测要处理哪条记录。
136
+ * 数据库查询只在恰好一个匹配时返回标识,避免同名调用造成歧义决定。
137
+ */
138
+ findPendingExecution(toolCallId: string): string | null {
139
+ return this.options.db.approvals.findPendingByToolCallId(toolCallId);
140
+ }
141
+
142
+ /**
143
+ * 列出当前需要展示给用户的持久化审批。
144
+ *
145
+ * @remarks
146
+ * `Runtime` 广播审批状态时调用它,并与临时 Agent 的审批合并。
147
+ * 调用方应直接消费返回的 `ApprovalReceipt`,不要再从内存等待器推导状态。
148
+ * 这里只投影活动提交中的待处理记录,避免已结束 Turn 的旧审批重新出现。
149
+ */
150
+ list(): ApprovalReceipt[] {
151
+ return this.options.db.approvals.listPendingActive().map((row) => ({
152
+ executionId: row.executionId,
153
+ source: row.source as ApprovalReceipt["source"],
154
+ action: row.toolName,
155
+ summary: row.summary,
156
+ executionLevel: row.executionLevel,
157
+ requiredExecutionLevel: row.requiredExecutionLevel,
158
+ inputJson: row.inputJson,
159
+ requestId: row.requestId,
160
+ }));
161
+ }
162
+
163
+ /**
164
+ * 按审批操作标识读取一条完整的持久化审批。
165
+ *
166
+ * @remarks
167
+ * 决定审批、恢复回调和本类内部的幂等检查都会在需要最新状态时调用它。
168
+ * 调用方必须处理 `null`,因为记录可能不存在或已经被本地数据清理。
169
+ * 数据库是审批状态的来源。
170
+ * 内存等待器只负责唤醒当前请求,不能代替读取。
171
+ */
172
+ read(executionId: string): ApprovalRecord | null {
173
+ const stored = this.options.db.approvals.find(executionId);
174
+ if (!stored) return null;
175
+ return {
176
+ executionId: stored.executionId,
177
+ submissionId: stored.submissionId,
178
+ requestId: stored.requestId,
179
+ source: stored.source as ApprovalReceipt["source"],
180
+ toolCallId: stored.toolCallId,
181
+ toolName: stored.toolName,
182
+ summary: stored.summary,
183
+ executionLevel: stored.executionLevel,
184
+ requiredExecutionLevel: stored.requiredExecutionLevel,
185
+ inputJson: stored.inputJson,
186
+ status: stored.status,
187
+ createdAt: stored.createdAt,
188
+ ...(stored.decidedAt ? { decidedAt: stored.decidedAt } : {}),
189
+ ...(stored.reason ? { reason: stored.reason } : {}),
190
+ };
191
+ }
192
+
193
+ /**
194
+ * 持久化一条工具审批,并暂停当前工具执行直到有人作出决定。
195
+ *
196
+ * @remarks
197
+ * Pi Turn 在受控工具需要人工审批时调用它,可传入中止信号和首次创建回调。
198
+ * 调用方应等待返回的 Promise。
199
+ * 审批已存在时不会重复广播或调用 `onApproval`。
200
+ * 记录与恢复里程碑先在事务中落盘,再建立内存等待,保证重启仍能恢复审批。
201
+ */
202
+ async request(
203
+ submission: TSubmission,
204
+ approval: PiToolApproval,
205
+ signal?: AbortSignal,
206
+ onApproval?: (
207
+ approval: ApprovalRecord,
208
+ ) => void | Promise<void>,
209
+ ): Promise<void> {
210
+ const pending = this.options.db.transaction(() =>
211
+ this.ensurePending(submission, approval)
212
+ );
213
+ if (pending.created) {
214
+ await this.options.onApprovalsChanged();
215
+ await onApproval?.(pending.approval);
216
+ }
217
+ return this.wait(pending.approval, signal);
218
+ }
219
+
220
+ /**
221
+ * 在已批准工具结果落盘后,确认对应恢复调度已经完成。
222
+ *
223
+ * @remarks
224
+ * `Runtime.settleTool` 保存工具结果和恢复里程碑后调用它。
225
+ * 调用方只应在同一持久化事务内、确认工具结果已保存后调用。
226
+ * 提交点放在工具结果之后,避免恢复记录显示完成但实际结果尚未持久化。
227
+ */
228
+ commitContinuation(
229
+ submission: TSubmission,
230
+ toolCallId: string,
231
+ ): void {
232
+ const approval = this.read(
233
+ `${submission.submissionId}:${toolCallId}`,
234
+ );
235
+ if (!approval || approval.status === "pending") return;
236
+ const decision = this.decideRecovery(submission);
237
+ if (
238
+ decision.effect.kind !== "schedule-continuation" ||
239
+ !decision.effect.continuationKey.startsWith(
240
+ `approval:${approval.executionId}:`,
241
+ )
242
+ ) {
243
+ return;
244
+ }
245
+ const committed = this.decideRecovery(submission, {
246
+ kind: "commit-continuation",
247
+ continuationKey: decision.effect.continuationKey,
248
+ });
249
+ this.options.applyRecoveryMutations(
250
+ submission,
251
+ committed.mutations,
252
+ );
253
+ }
254
+
255
+ /**
256
+ * 把某次提交下仍待处理的全部审批统一拒绝。
257
+ *
258
+ * @remarks
259
+ * `Runtime` 提交终态时调用它,确保已经结束的 Turn 不再留下可操作审批。
260
+ * 调用方应把返回值理解为是否找到待审批记录,并负责随后广播最新状态。
261
+ * 这里通过 Pi 恢复决策写入拒绝结果,不直接改表,避免数据库与恢复里程碑分叉。
262
+ */
263
+ rejectPending(
264
+ submission: TSubmission,
265
+ reason: string,
266
+ ): boolean {
267
+ const pendingIds = this.options.db.approvals
268
+ .listPendingForSubmission(submission.submissionId);
269
+ if (pendingIds.length === 0) return false;
270
+
271
+ for (const executionId of pendingIds) {
272
+ const stored = this.read(executionId);
273
+ if (!stored) continue;
274
+ const pending = this.decideRecovery(submission, {
275
+ kind: "record-approval",
276
+ approval: stored,
277
+ });
278
+ this.options.applyRecoveryMutations(
279
+ submission,
280
+ pending.mutations,
281
+ );
282
+ const decision = this.decideRecovery(submission, {
283
+ kind: "approval-decision",
284
+ executionId: stored.executionId,
285
+ approved: false,
286
+ reason,
287
+ });
288
+ this.options.applyRecoveryMutations(
289
+ submission,
290
+ decision.mutations,
291
+ );
292
+ }
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * 接受一次批准或拒绝,并唤醒当前工具执行或安排恢复任务。
298
+ *
299
+ * @remarks
300
+ * `Runtime.approve` 和 `Runtime.reject` 收到用户决定时调用它。
301
+ * 调用方应使用审批 `executionId`,并根据 `ok` 判断本次决定是否真正生效。
302
+ * 决定及里程碑在同一事务中写入。
303
+ * 有内存等待器时直接唤醒,没有时走持久化调度。
304
+ * 不存在的标识按幂等成功处理,终态提交或重复决定则返回 `ok: false`。
305
+ */
306
+ async decide(
307
+ executionId: string,
308
+ input: RuntimeApprovalDecision,
309
+ ): Promise<{ ok: boolean }> {
310
+ const approved = input.decision === "allow_once";
311
+ const reason = input.decision === "deny" ? input.reason : undefined;
312
+ const stored = this.read(executionId);
313
+ if (!stored) return { ok: true };
314
+ const submission = this.options.findSubmission(stored.submissionId);
315
+ if (!submission || isTerminalSubmissionStatus(submission.status)) {
316
+ return { ok: false };
317
+ }
318
+ if (stored.status !== "pending") {
319
+ if (!this.waiters.has(stored.executionId)) {
320
+ await this.dispatchContinuation(
321
+ submission,
322
+ stored.executionId,
323
+ );
324
+ }
325
+ return { ok: false };
326
+ }
327
+
328
+ const pending = this.decideRecovery(submission, {
329
+ kind: "record-approval",
330
+ approval: stored,
331
+ });
332
+ const decision = this.decideRecovery(
333
+ submission,
334
+ {
335
+ kind: "approval-decision",
336
+ executionId: stored.executionId,
337
+ approved,
338
+ reason,
339
+ },
340
+ [
341
+ ...this.milestoneBodies(submission.submissionId),
342
+ ...pending.mutations.flatMap((mutation) =>
343
+ mutation.kind === "append-milestone"
344
+ ? [mutation.body]
345
+ : []
346
+ ),
347
+ ],
348
+ );
349
+ if (!decision.applied) return { ok: false };
350
+ this.options.db.transaction(() => {
351
+ this.options.applyRecoveryMutations(
352
+ submission,
353
+ [...pending.mutations, ...decision.mutations],
354
+ );
355
+ });
356
+ const persisted = this.read(stored.executionId);
357
+ if (!persisted || persisted.status === "pending") {
358
+ return { ok: false };
359
+ }
360
+ const waiter = this.waiters.get(stored.executionId);
361
+ if (!approved && waiter) {
362
+ await this.options.materializeRecoveredToolResults(submission);
363
+ }
364
+ await this.options.onApprovalsChanged();
365
+
366
+ if (waiter) {
367
+ waiter();
368
+ return { ok: true };
369
+ }
370
+
371
+ await this.dispatchContinuation(
372
+ submission,
373
+ persisted.executionId,
374
+ );
375
+ return { ok: true };
376
+ }
377
+
378
+ /**
379
+ * 为一条已决定审批安排可恢复的后续执行。
380
+ *
381
+ * @remarks
382
+ * 没有内存等待器的决定、启动扫描和通用恢复循环会调用它。
383
+ * 调用方应传入审批所属提交和对应 `executionId`;不匹配的恢复计划会安全返回。
384
+ * 顺序必须保持为先记录 pending、再调度;只有 durable ToolResult 落盘后才能记录 committed,
385
+ * 否则调度回调在 Runtime 装配失败时会失去下一次启动的补发依据。
386
+ */
387
+ async dispatchContinuation(
388
+ submission: TSubmission,
389
+ executionId: string,
390
+ ): Promise<void> {
391
+ if (isTerminalSubmissionStatus(submission.status)) return;
392
+ const decision = this.decideRecovery(submission);
393
+ if (
394
+ decision.effect.kind !== "schedule-continuation" ||
395
+ !decision.effect.continuationKey.startsWith(
396
+ `approval:${executionId}:`,
397
+ )
398
+ ) {
399
+ return;
400
+ }
401
+ this.options.db.transaction(() => {
402
+ this.options.applyRecoveryMutations(
403
+ submission,
404
+ decision.mutations,
405
+ );
406
+ });
407
+ await this.options.scheduleContinuation({
408
+ submissionId: submission.submissionId,
409
+ requestId: submission.requestId,
410
+ continuationKey: decision.effect.continuationKey,
411
+ approvalExecutionId: executionId,
412
+ });
413
+ }
414
+
415
+ /**
416
+ * 扫描持久化决定,并补发仍需要执行的审批恢复任务。
417
+ *
418
+ * @remarks
419
+ * `Runtime.onStart` 在广播当前审批后调用它。
420
+ * 调用方应等待扫描结束,再继续普通的提交恢复流程。
421
+ * 重启会丢失内存等待器,所以这里从数据库重建后续动作,并复用幂等调度路径。
422
+ */
423
+ async dispatchPendingContinuations(): Promise<void> {
424
+ for (const approval of this.options.db.approvals.listDecided()) {
425
+ const submission = this.options.findSubmission(
426
+ approval.submissionId,
427
+ );
428
+ if (submission) {
429
+ await this.dispatchContinuation(
430
+ submission,
431
+ approval.executionId,
432
+ );
433
+ }
434
+ }
435
+ }
436
+
437
+ // #endregion
438
+
439
+ // #region 恢复辅助方法
440
+
441
+ // 作用:读取一个 Submission 的全部恢复里程碑正文。
442
+ // 调用:恢复计算在每次生成决策前调用。
443
+ // 原因:持久化历史必须作为恢复决策的唯一输入。
444
+ private milestoneBodies(submissionId: string): string[] {
445
+ return this.options.db.milestones.listBodies(submissionId);
446
+ }
447
+
448
+ // 作用:请求 Pi 根据当前里程碑给出下一步。
449
+ // 调用:审批请求、决定、终态拒绝和恢复调度路径调用。
450
+ // 原因:所有路径共用同一决策入口,避免各自改写恢复状态。
451
+ private decideRecovery(
452
+ submission: TSubmission,
453
+ command: PiRecoveryCommand = { kind: "inspect" },
454
+ milestoneBodies = this.milestoneBodies(submission.submissionId),
455
+ ): PiRecoveryDecision {
456
+ return this.options.pi.decideRecovery({
457
+ milestoneBodies,
458
+ identity: {
459
+ turnId: submission.submissionId,
460
+ assemblyRevision: submission.assemblyRevision,
461
+ },
462
+ command,
463
+ now: Date.now(),
464
+ });
465
+ }
466
+
467
+ // 作用:复用或创建一条 pending Approval。
468
+ // 调用:`request` 在准入事务内调用。
469
+ // 原因:已存在记录的字段冲突必须报错,不能覆盖旧审批。
470
+ private ensurePending(
471
+ submission: TSubmission,
472
+ approval: PiToolApproval,
473
+ ): { approval: ApprovalRecord; created: boolean } {
474
+ const existing = this.read(
475
+ `${submission.submissionId}:${approval.toolCallId}`,
476
+ );
477
+ if (existing) {
478
+ if (
479
+ existing.requestId !== submission.requestId ||
480
+ existing.toolName !== approval.toolName ||
481
+ existing.executionLevel !== approval.executionLevel ||
482
+ existing.requiredExecutionLevel !==
483
+ approval.requiredExecutionLevel ||
484
+ existing.inputJson !== approval.inputJson
485
+ ) {
486
+ throw new Error(
487
+ `Conflicting durable Tool approval: ${approval.toolCallId}`,
488
+ );
489
+ }
490
+ return { approval: existing, created: false };
491
+ }
492
+
493
+ this.options.db.approvals.insert({
494
+ executionId: approval.executionId,
495
+ submissionId: submission.submissionId,
496
+ requestId: approval.requestId,
497
+ source: approval.source,
498
+ toolCallId: approval.toolCallId,
499
+ toolName: approval.toolName,
500
+ summary: approval.summary,
501
+ executionLevel: approval.executionLevel,
502
+ requiredExecutionLevel: approval.requiredExecutionLevel,
503
+ inputJson: approval.inputJson,
504
+ status: approval.status,
505
+ createdAt: approval.createdAt,
506
+ });
507
+ const decision = this.decideRecovery(submission, {
508
+ kind: "record-approval",
509
+ approval,
510
+ });
511
+ this.options.applyRecoveryMutations(
512
+ submission,
513
+ decision.mutations,
514
+ );
515
+ return {
516
+ approval: {
517
+ ...approval,
518
+ submissionId: submission.submissionId,
519
+ },
520
+ created: true,
521
+ };
522
+ }
523
+
524
+ // 作用:等待当前进程里的审批决定。
525
+ // 调用:`request` 在持久化 pending Approval 后调用。
526
+ // 原因:内存 Promise 只负责唤醒当前请求,数据库仍负责持久状态以支持重启。
527
+ private wait(
528
+ approval: ApprovalRecord,
529
+ signal?: AbortSignal,
530
+ ): Promise<void> {
531
+ if (approval.status !== "pending") return Promise.resolve();
532
+ return new Promise((resolve, reject) => {
533
+ // 清掉本地等待器并转发中止原因。AbortSignal 触发或已中止检查都会走这里。
534
+ const abort = () => {
535
+ this.waiters.delete(approval.executionId);
536
+ reject(signal?.reason ?? new Error("Tool execution aborted"));
537
+ };
538
+ if (signal?.aborted) {
539
+ abort();
540
+ return;
541
+ }
542
+ signal?.addEventListener("abort", abort, { once: true });
543
+ this.waiters.set(approval.executionId, () => {
544
+ signal?.removeEventListener("abort", abort);
545
+ this.waiters.delete(approval.executionId);
546
+ resolve();
547
+ });
548
+ });
549
+ }
550
+
551
+ // #endregion
552
+ }