@springbrand/agent-runtime 0.1.3-alpha.3 → 0.1.3-alpha.5

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 (59) hide show
  1. package/package.json +3 -1
  2. package/src/adapter/cloudflare/index.ts +60 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -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 +256 -0
  10. package/src/adapter/cloudflare/universal-agent/definition.ts +71 -0
  11. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  12. package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
  13. package/src/adapter/cloudflare/universal-agent/tools.ts +74 -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/agent-tool-runtime.ts +152 -0
  17. package/src/db/agent-tool.repo.ts +27 -0
  18. package/src/db/index.ts +33 -0
  19. package/src/db/interaction.repo.ts +185 -0
  20. package/src/db/schema.ts +15 -0
  21. package/src/db/submission.repo.ts +29 -0
  22. package/src/index.ts +53 -21
  23. package/src/kernel/approval-lifecycle.ts +41 -6
  24. package/src/kernel/bindings.ts +37 -0
  25. package/src/kernel/interaction-lifecycle.ts +395 -0
  26. package/src/kernel/public-contracts.ts +2 -0
  27. package/src/kernel/recoverable-chat-agent.ts +10 -2
  28. package/src/kernel/runtime-assembly-view.ts +37 -0
  29. package/src/kernel/runtime-assembly.ts +41 -0
  30. package/src/kernel/runtime-config.ts +4 -0
  31. package/src/kernel/runtime-load.ts +102 -0
  32. package/src/kernel/state.ts +8 -1
  33. package/src/kernel/submission-lifecycle.ts +30 -0
  34. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  35. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  36. package/src/pi/message/contract.ts +7 -0
  37. package/src/pi/message/conversion.ts +9 -1
  38. package/src/pi/runtime-adapter/assembly.ts +17 -3
  39. package/src/pi/runtime-adapter/execution.ts +109 -9
  40. package/src/pi/runtime-adapter/index.ts +15 -5
  41. package/src/pi/runtime-adapter/recovery.ts +188 -1
  42. package/src/pi/tool/base.ts +79 -9
  43. package/src/pi/tool/compiler.ts +34 -0
  44. package/src/pi/tool/core.ts +13 -0
  45. package/src/pi/tool/gateway.ts +54 -0
  46. package/src/pi/tool/index.ts +1 -0
  47. package/src/pi/tool/mcp.ts +93 -64
  48. package/src/pi/tool/schedule.ts +11 -0
  49. package/src/pi/tool/subagent.ts +14 -0
  50. package/src/pi/tool/workspace-sandbox.ts +15 -0
  51. package/src/pi/turn/index.ts +20 -0
  52. package/src/pi/turn/interaction.ts +181 -0
  53. package/src/pi/turn/tool-recovery.ts +244 -1
  54. package/src/runtime-agent-context.ts +112 -0
  55. package/src/runtime-agent.ts +569 -322
  56. package/src/{plugins.ts → runtime-assembler.ts} +312 -379
  57. package/src/runtime-definition.ts +173 -0
  58. package/src/runtime.ts +572 -164
  59. package/src/tool-registry.ts +143 -0
@@ -0,0 +1,185 @@
1
+ import type { SqlTaggedTemplate } from "agents/chat";
2
+
3
+ // Tool Interaction 的统一定义见 ./index.ts。
4
+ // #region 状态与行类型
5
+ export type ToolInteractionStatus = "pending" | "responded" | "cancelled";
6
+
7
+ export interface StoredToolInteraction {
8
+ interactionId: string;
9
+ submissionId: string;
10
+ requestId: string;
11
+ toolCallId: string;
12
+ toolName: string;
13
+ inputJson: string;
14
+ status: ToolInteractionStatus;
15
+ responseJson: string | null;
16
+ createdAt: number;
17
+ respondedAt: number | null;
18
+ }
19
+
20
+ export interface NewToolInteraction {
21
+ interactionId: string;
22
+ submissionId: string;
23
+ requestId: string;
24
+ toolCallId: string;
25
+ toolName: string;
26
+ inputJson: string;
27
+ status: ToolInteractionStatus;
28
+ createdAt: number;
29
+ }
30
+
31
+ type ToolInteractionRow = {
32
+ interaction_id: string;
33
+ submission_id: string;
34
+ request_id: string;
35
+ tool_call_id: string;
36
+ tool_name: string;
37
+ input_json: string;
38
+ status: string;
39
+ response_json: string | null;
40
+ created_at: number;
41
+ responded_at: number | null;
42
+ };
43
+
44
+ // #endregion
45
+ // #region 行映射
46
+ // 把 SQLite 的 Interaction 行转成 Runtime 对象。
47
+ // find 与 findPending* 查到记录后调用,上层因而不需要理解 snake_case 列名。
48
+ // 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
49
+ function mapRow(row: ToolInteractionRow): StoredToolInteraction {
50
+ return {
51
+ interactionId: row.interaction_id,
52
+ submissionId: row.submission_id,
53
+ requestId: row.request_id,
54
+ toolCallId: row.tool_call_id,
55
+ toolName: row.tool_name,
56
+ inputJson: row.input_json,
57
+ status: row.status as ToolInteractionStatus,
58
+ responseJson: row.response_json,
59
+ createdAt: row.created_at,
60
+ respondedAt: row.responded_at,
61
+ };
62
+ }
63
+
64
+ // #endregion
65
+ // #region Repository
66
+ export class ToolInteractionRepository {
67
+ // 保存当前 Agent SQLite 的查询入口。
68
+ // RuntimeDatabase 构造时调用,InteractionLifecycle 随后共享这个 Repository。
69
+ // Interaction 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
70
+ constructor(private readonly sql: SqlTaggedTemplate) {}
71
+
72
+ // 按 interactionId 读取一条完整 Interaction,不存在时返回 null。
73
+ // InteractionLifecycle 在请求去重、响应、取消和收尾时调用,调用方必须处理 null。
74
+ // interaction_id 是主键,LIMIT 1 明确只消费一行。
75
+ find(interactionId: string): StoredToolInteraction | null {
76
+ const row = this.sql<ToolInteractionRow>`
77
+ SELECT interaction_id, submission_id, request_id,
78
+ tool_call_id, tool_name, input_json,
79
+ status, response_json, created_at, responded_at
80
+ FROM pi_tool_interactions
81
+ WHERE interaction_id = ${interactionId}
82
+ LIMIT 1
83
+ `[0];
84
+ return row ? mapRow(row) : null;
85
+ }
86
+
87
+ // 持久化一条新的 pending Interaction。
88
+ // InteractionLifecycle.ensurePending 在准入事务内确认没有同一记录后调用。
89
+ // 主键和 (submission_id, tool_call_id) 唯一约束是持久化去重防线,不能用覆盖式 INSERT 掩盖冲突。
90
+ insert(interaction: NewToolInteraction): void {
91
+ this.sql`
92
+ INSERT INTO pi_tool_interactions (
93
+ interaction_id, submission_id, request_id,
94
+ tool_call_id, tool_name, input_json,
95
+ status, created_at
96
+ ) VALUES (
97
+ ${interaction.interactionId},
98
+ ${interaction.submissionId},
99
+ ${interaction.requestId},
100
+ ${interaction.toolCallId},
101
+ ${interaction.toolName},
102
+ ${interaction.inputJson},
103
+ ${interaction.status},
104
+ ${interaction.createdAt}
105
+ )
106
+ `;
107
+ }
108
+
109
+ // 只把 pending Interaction 落成 responded 或 cancelled,并记录响应体与时间。
110
+ // InteractionLifecycle 在收到客户端响应或判定取消时调用,迟到的重复投递不应覆盖首次结果。
111
+ // WHERE status='pending' 实现首次响应胜出,不能去掉这个条件;调用方在事务后重读来确认是否真的生效。
112
+ settle(
113
+ interactionId: string,
114
+ status: Exclude<ToolInteractionStatus, "pending">,
115
+ respondedAt: number,
116
+ responseJson?: string,
117
+ ): void {
118
+ this.sql`
119
+ UPDATE pi_tool_interactions
120
+ SET status = ${status},
121
+ response_json = ${responseJson ?? null},
122
+ responded_at = ${respondedAt}
123
+ WHERE interaction_id = ${interactionId}
124
+ AND status = 'pending'
125
+ `;
126
+ }
127
+
128
+ // 按创建顺序列出某次 Submission 仍等待客户端响应的 interactionId。
129
+ // InteractionLifecycle.cancelPending 在取消或终止 Turn 时调用,随后逐条追加取消恢复事实。
130
+ // ORDER BY created_at 保留原始顺序,不能依赖 SQLite 未声明的默认行顺序。
131
+ listPendingForSubmission(submissionId: string): string[] {
132
+ return this.sql<{ interaction_id: string }>`
133
+ SELECT interaction_id
134
+ FROM pi_tool_interactions
135
+ WHERE submission_id = ${submissionId}
136
+ AND status = 'pending'
137
+ ORDER BY created_at ASC
138
+ `.map((row) => row.interaction_id);
139
+ }
140
+
141
+ // 只在某个 toolCallId 唯一匹配一条活跃 pending Interaction 时返回该记录。
142
+ // Runtime.respondToolInteraction 的入口调用它 —— 前端只有 toolCallId,不知道 submissionId。
143
+ // toolCallId 只在单个 Submission 内唯一,LIMIT 2 用最小查询识别跨 Submission 重名;
144
+ // 不能用 LIMIT 1 静默选中任意一条,那会把答案投给错的那次调用。口径与 ApprovalRepository 同名方法一致。
145
+ findPendingByToolCallId(toolCallId: string): StoredToolInteraction | null {
146
+ const matches = this.sql<ToolInteractionRow>`
147
+ SELECT interaction.interaction_id, interaction.submission_id,
148
+ interaction.request_id, interaction.tool_call_id,
149
+ interaction.tool_name, interaction.input_json,
150
+ interaction.status, interaction.response_json,
151
+ interaction.created_at, interaction.responded_at
152
+ FROM pi_tool_interactions interaction
153
+ JOIN pi_submissions submission
154
+ ON submission.submission_id = interaction.submission_id
155
+ WHERE interaction.tool_call_id = ${toolCallId}
156
+ AND interaction.status = 'pending'
157
+ AND submission.status NOT IN (
158
+ 'completed', 'aborted', 'skipped', 'error'
159
+ )
160
+ LIMIT 2
161
+ `;
162
+ const only = matches.length === 1 ? matches[0] : undefined;
163
+ return only ? mapRow(only) : null;
164
+ }
165
+
166
+ // 列出所属 Submission 仍活跃的全部 pending Interaction。
167
+ // Runtime 启动扫描调用它,用来判断哪些 park 还需要续跑依据。
168
+ // 与 findPendingByToolCallId 使用同样的 JOIN 和状态条件,修改其一时必须核对另一个。
169
+ listPendingActive(): StoredToolInteraction[] {
170
+ return this.sql<ToolInteractionRow>`
171
+ SELECT interaction.interaction_id, interaction.submission_id,
172
+ interaction.request_id, interaction.tool_call_id,
173
+ interaction.tool_name, interaction.input_json,
174
+ interaction.status, interaction.response_json,
175
+ interaction.created_at, interaction.responded_at
176
+ FROM pi_tool_interactions interaction
177
+ JOIN pi_submissions submission
178
+ ON submission.submission_id = interaction.submission_id
179
+ WHERE interaction.status = 'pending'
180
+ AND submission.status IN ('pending', 'running')
181
+ ORDER BY interaction.created_at ASC
182
+ `.map(mapRow);
183
+ }
184
+ }
185
+ // #endregion
package/src/db/schema.ts CHANGED
@@ -4,6 +4,8 @@ import type { SqlTaggedTemplate } from "agents/chat";
4
4
  // 创建当前 Runtime 需要但尚不存在的 SQLite 表。
5
5
  // AgentRuntimeKernel 构造时通过 RuntimeDatabase.initializeSchema 调用,重复启动依赖 IF NOT EXISTS 保留现有表。
6
6
  // pi_approvals 有一次显式的 risk → 四档升级;步骤均可重入,中断后下次初始化会继续。
7
+ // pi_tool_interactions 是 client-settled tool 的 pending 索引:结果的权威副本仍在 pi_tool_settlements,
8
+ // 这张表只回答「这次工具调用还等不等得到客户端响应」。
7
9
  export function initializeSchema(sql: SqlTaggedTemplate): void {
8
10
  sql`CREATE TABLE IF NOT EXISTS pi_message_ui (
9
11
  message_id TEXT PRIMARY KEY,
@@ -119,6 +121,19 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
119
121
  END`;
120
122
  sql`ALTER TABLE pi_approvals DROP COLUMN risk`;
121
123
  }
124
+ sql`CREATE TABLE IF NOT EXISTS pi_tool_interactions (
125
+ interaction_id TEXT PRIMARY KEY,
126
+ submission_id TEXT NOT NULL,
127
+ request_id TEXT NOT NULL,
128
+ tool_call_id TEXT NOT NULL,
129
+ tool_name TEXT NOT NULL,
130
+ input_json TEXT NOT NULL,
131
+ status TEXT NOT NULL,
132
+ response_json TEXT,
133
+ created_at INTEGER NOT NULL,
134
+ responded_at INTEGER,
135
+ UNIQUE (submission_id, tool_call_id)
136
+ )`;
122
137
  sql`CREATE TABLE IF NOT EXISTS pi_recovery_milestones (
123
138
  submission_id TEXT NOT NULL,
124
139
  seq INTEGER NOT NULL,
@@ -180,6 +180,17 @@ export class SubmissionRepository {
180
180
  );
181
181
  }
182
182
 
183
+ // 按创建顺序列出还在排队或运行的 Submission id。
184
+ // 换装配前的「是不是全都停在等人」判断调用它,逐条核对每一条的人机等待。
185
+ // 状态集合与 countUnfinished 必须一致,修改其一时另一个也要跟着改。
186
+ listUnfinishedIds(): string[] {
187
+ return this.sql<{ submission_id: string }>`
188
+ SELECT submission_id FROM pi_submissions
189
+ WHERE status IN ('pending', 'running')
190
+ ORDER BY created_at ASC, rowid ASC
191
+ `.map((row) => row.submission_id);
192
+ }
193
+
183
194
  findRunning(): StoredSubmission | null {
184
195
  const id = this.sql<{ submission_id: string }>`
185
196
  SELECT submission_id FROM pi_submissions
@@ -252,6 +263,24 @@ export class SubmissionRepository {
252
263
  return updated.length === 1;
253
264
  }
254
265
 
266
+ // 把一条已 pin 的 Submission 换到另一对 revision / descriptor 上。
267
+ // Runtime 在「停在人机等待期间换了装配」之后调用,必须与那次装配迁移里程碑同事务。
268
+ // 与 pinAssembly 的区别就是它要求已经 pin 过:首次 pin 走 pinAssembly,
269
+ // 这里不接受空值,避免把一次漏写的准入 pin 伪装成一次迁移。
270
+ repinAssembly(id: string, revision: string, descriptor: string): boolean {
271
+ const updated = this.sql<{ submission_id: string }>`
272
+ UPDATE pi_submissions
273
+ SET assembly_revision = ${revision},
274
+ assembly_descriptor = ${descriptor}
275
+ WHERE submission_id = ${id}
276
+ AND assembly_revision <> ''
277
+ AND assembly_descriptor <> ''
278
+ AND status IN ('pending', 'running')
279
+ RETURNING submission_id
280
+ `;
281
+ return updated.length === 1;
282
+ }
283
+
255
284
  // 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
256
285
  // SubmissionLifecycle 开始 Turn 时调用,调用方用返回值识别过期或重复的状态切换。
257
286
  // Cloudflare SQLite 查询是同步的,两条语句之间不会让其他事件交错;UPDATE 再次带旧状态条件,不能去掉这层幂等保护。
package/src/index.ts CHANGED
@@ -22,11 +22,10 @@
22
22
  * - `SubAgent` 是由父 Agent 发起、但拥有独立执行上下文的子任务执行者。
23
23
  * - `canonical transcript` 是 Pi 持久化和恢复 Turn 时使用的权威消息历史。
24
24
  * - `admission` 是提示进入 Turn 前的接纳、去重和状态登记流程。
25
- * - `AgentConfig` 是应用交给 Runtime 的一次完整装配说明。
26
- * - `AgentPlugin` 是一种能力的声明式准备单元。
27
- * - `PluginKind` 同时收窄 Plugin 可以准备的字段,并作为内部稳定合并顺序。
28
- * - `prepare` 读取业务数据或外部绑定,但不会改动正在运行的 Runtime
29
- * - `Port` 是 Runtime 调用外部能力时依赖的最小接口。
25
+ * - `defineRuntimeAgent.load` 返回 config 和已加载 Resource。
26
+ * - `defineRuntimeAgent.tools` 是 Host 第一层 Tool 注册表声明。
27
+ * - `assembleRuntimeSnapshot` 消费该输入与 hooks,产出候选 Snapshot。
28
+ * - `Port` Runtime 调用外部能力时依赖的最小接口(Definition 作者不直接装配)。
30
29
  * - `RuntimeBindings` 是本次装配选中的 Port 和可执行对象集合。
31
30
  * - `RuntimeBuilder` 是包内实现,用来校验贡献并生成候选结果。
32
31
  * - `RuntimeCandidate` 是尚未生效的候选结果和提交前检查。
@@ -38,11 +37,8 @@
38
37
  *
39
38
  * Snapshot 不允许替换已选集合,但不承诺把每个 Port 的内部对象深冻结。
40
39
  *
41
- * 正常调用顺序是 `preparemerge → validate → freeze → guard → commit`。
42
- *
43
- * 应用只创建 `AgentConfig` 和 Plugin。
44
- *
45
- * Runtime 独占校验、排序和原子提交。
40
+ * 正常调用顺序是 `assemble → validate → freeze → guard → commit`。
41
+ * Runtime 独占跨能力校验和原子提交。
46
42
  *
47
43
  * @packageDocumentation
48
44
  */
@@ -50,23 +46,44 @@ export * from "./kernel/bindings";
50
46
  export * from "./kernel/extensions";
51
47
  export * from "./kernel/profile";
52
48
  export * from "./kernel/receipts";
49
+ export * from "./kernel/runtime-config";
50
+ export type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
53
51
  export * from "./runtime-agent";
54
52
  export * from "./kernel/state";
55
53
  export * from "./lib/execution-level";
56
- export { definePlugin } from "./plugins";
57
54
  export type {
58
- AgentConfig,
59
- AgentPlugin,
60
- AgentPluginSpec,
61
- PluginKind,
62
- PluginPreparation,
63
- PreparedPlugin,
55
+ RuntimeAssemblyInput,
64
56
  RuntimeDegradation,
65
57
  RuntimeExtensionContribution,
66
58
  RuntimeProfileContribution,
67
59
  RuntimeSkillContribution,
68
60
  RuntimeToolSurfacePolicy,
69
- } from "./plugins";
61
+ } from "./runtime-assembler";
62
+ export { assembleRuntimeSnapshot, toolRegistryPiToolCandidates } from "./runtime-assembler";
63
+ export type {
64
+ PlatformToolContext,
65
+ PlatformToolRegistry,
66
+ PlatformToolSpec,
67
+ ProfileOverrides,
68
+ ResolvedConnectors,
69
+ ResolvedResources,
70
+ RuntimeAgentContext,
71
+ RuntimeAgentHooks,
72
+ RuntimeSettings,
73
+ RuntimeToolBindings,
74
+ ToolAssemblyResult,
75
+ ToolContext,
76
+ ToolRegistry,
77
+ ToolSpec,
78
+ } from "./runtime-definition";
79
+ export {
80
+ emptyPlatformToolRegistry,
81
+ emptyToolRegistry,
82
+ mergeToolRegistries,
83
+ normalizeToolAssembly,
84
+ piCandidateToToolSpec,
85
+ toolRegistryFromPiCandidates,
86
+ } from "./tool-registry";
70
87
  export type { ModelOption } from "./lib/model-catalog";
71
88
  export {
72
89
  assembleSubagentPrompt,
@@ -92,16 +109,30 @@ export type {
92
109
  SettledPiToolCall,
93
110
  } from "./pi/tool";
94
111
  export { basePiToolCandidates } from "./pi/tool";
95
- export { schedulePiToolCandidates } from "./pi/tool";
96
112
  export {
113
+ createMemoryTools,
114
+ memoryPiToolCandidate,
115
+ } from "./pi/tool";
116
+ export {
117
+ createScheduleTools,
118
+ schedulePiToolCandidates,
119
+ } from "./pi/tool";
120
+ export {
121
+ createSandboxTools,
122
+ createWorkspaceTools,
97
123
  sandboxPiToolCandidates,
98
124
  workspacePiToolCandidates,
99
125
  } from "./pi/tool";
100
- export { subagentPiToolCandidates } from "./pi/tool";
126
+ export {
127
+ createSubagentTools,
128
+ subagentPiToolCandidates,
129
+ } from "./pi/tool";
101
130
  export { skillPiToolCandidates } from "./pi/tool";
102
131
  export type { PiSkillBinding } from "./pi/tool";
103
132
  export {
104
133
  browserQuickActionPiToolCandidates,
134
+ createCodeExecutionTool,
135
+ codeExecutionPiToolCandidate,
105
136
  } from "./pi/tool";
106
137
  export {
107
138
  createWorkspaceCodeExecutionPort,
@@ -113,10 +144,11 @@ export {
113
144
  export type {
114
145
  TemporaryAgentApprovalDecision,
115
146
  TemporaryAgentApprovalRequest,
116
- TemporaryAgentExecutor,
147
+ TemporaryAgentLaunch,
117
148
  TemporaryAgentRequest,
118
149
  TemporaryAgentRunContext,
119
150
  } from "./layers/orchestration/temporary-agent/core";
151
+ export { TEMPORARY_AGENT_LAUNCH_KEY } from "./layers/orchestration/temporary-agent/core";
120
152
  export {
121
153
  temporaryAgentExtensionIsSafe,
122
154
  temporaryAgentToolAllowed,
@@ -101,7 +101,10 @@ interface ApprovalLifecycleOptions<
101
101
  export class ApprovalLifecycle<
102
102
  TSubmission extends ApprovalSubmission,
103
103
  > {
104
- private readonly waiters = new Map<string, () => void>();
104
+ private readonly waiters = new Map<
105
+ string,
106
+ { readonly release: () => void; readonly discard: () => void }
107
+ >();
105
108
 
106
109
  /**
107
110
  * 接好审批流程需要的宿主依赖。
@@ -217,6 +220,27 @@ export class ApprovalLifecycle<
217
220
  return this.wait(pending.approval, signal);
218
221
  }
219
222
 
223
+ /**
224
+ * 丢弃某条提交在内存里的全部审批等待器。
225
+ *
226
+ * @remarks
227
+ * 宿主在 park 期间换掉装配、中断内存执行器之后调用。持久审批一个字都不动:
228
+ * 被丢弃的只是「唤醒这一条执行器」的能力。
229
+ *
230
+ * 不丢弃的后果很具体:决定到来时 `decide` 会看见等待器,把 Turn 唤回那条已经
231
+ * 退场的旧执行器上,于是既拿不到新能力,也永远不会走持久续跑 —— Turn 就停在
232
+ * running 上不动了。等待器以 `PiTurnInterruptedError` 拒绝,工具编译器认得它,
233
+ * 不会为此写一条权威的错误结算。
234
+ */
235
+ discardWaiters(submissionId: string): void {
236
+ for (
237
+ const executionId of this.options.db.approvals
238
+ .listPendingForSubmission(submissionId)
239
+ ) {
240
+ this.waiters.get(executionId)?.discard();
241
+ }
242
+ }
243
+
220
244
  /**
221
245
  * 在已批准工具结果落盘后,确认对应恢复调度已经完成。
222
246
  *
@@ -364,7 +388,7 @@ export class ApprovalLifecycle<
364
388
  await this.options.onApprovalsChanged();
365
389
 
366
390
  if (waiter) {
367
- waiter();
391
+ waiter.release();
368
392
  return { ok: true };
369
393
  }
370
394
 
@@ -540,10 +564,21 @@ export class ApprovalLifecycle<
540
564
  return;
541
565
  }
542
566
  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();
567
+ this.waiters.set(approval.executionId, {
568
+ release: () => {
569
+ signal?.removeEventListener("abort", abort);
570
+ this.waiters.delete(approval.executionId);
571
+ resolve();
572
+ },
573
+ // 只把等待器摘掉,不去 settle 这个 Promise。让它继续挂着,park 住的
574
+ // 那次 execute 就永远不会往下走 —— 无论是拒绝(Pi 会把它当成工具失败,
575
+ // 接着再要一次模型输出)还是放行(工具会真的跑一遍),都会让一条本该
576
+ // 退场的执行器继续做事。它所属的 adapter 已经被中止并丢弃,这个悬着的
577
+ // Promise 随它一起变成垃圾。
578
+ discard: () => {
579
+ signal?.removeEventListener("abort", abort);
580
+ this.waiters.delete(approval.executionId);
581
+ },
547
582
  });
548
583
  });
549
584
  }
@@ -522,6 +522,41 @@ export interface RuntimeSandboxPort {
522
522
 
523
523
  // #region Runtime 服务端口
524
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
+
525
560
  /**
526
561
  * 描述一条模型可见的定时任务。
527
562
  *
@@ -911,6 +946,8 @@ export interface RuntimeSkillSourceBinding {
911
946
  export interface RuntimeBindings {
912
947
  provider: RuntimeProviderPort;
913
948
  platform: RuntimePlatformPort;
949
+ /** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
950
+ gateway?: RuntimeGatewayPort;
914
951
  workspace?: WorkspacePort;
915
952
  memory?: RuntimeMemoryPort;
916
953
  skills: RuntimeSkillBindings;