@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-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 (172) hide show
  1. package/package.json +7 -7
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1682 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/index.ts +18 -0
  8. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  9. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  10. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  11. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  12. package/src/{db → adapter/cloudflare}/schema.ts +28 -80
  13. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  14. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  15. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  16. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  17. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
  18. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  19. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  20. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  21. package/src/config/activation.ts +140 -0
  22. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  23. package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
  24. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  25. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  26. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  27. package/src/config/harness-runtime.ts +190 -0
  28. package/src/config/index.ts +14 -0
  29. package/src/config/model-guidance.ts +21 -0
  30. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
  31. package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
  32. package/src/{kernel → config}/profile.ts +7 -0
  33. package/src/{kernel → config}/public-contracts.ts +6 -6
  34. package/src/{kernel → config}/runtime-load.ts +1 -1
  35. package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
  36. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  37. package/src/context/extension/index.ts +3 -0
  38. package/src/context/index.ts +6 -0
  39. package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
  40. package/src/conversation/harness-resume.ts +288 -0
  41. package/src/conversation/harness-stream.ts +99 -0
  42. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  43. package/src/conversation/index.ts +6 -0
  44. package/src/{pi → conversation}/message/contract.ts +3 -0
  45. package/src/{pi → conversation}/message/conversion.ts +75 -0
  46. package/src/{pi → conversation}/message/index.ts +1 -0
  47. package/src/{pi → conversation}/message/model-input.ts +1 -1
  48. package/src/{pi → conversation}/message/projection.ts +113 -30
  49. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  50. package/src/conversation/state.ts +196 -0
  51. package/src/harness/index.ts +7 -0
  52. package/src/harness/session-harness.ts +562 -0
  53. package/src/harness/submission-operation.ts +49 -0
  54. package/src/index.ts +38 -43
  55. package/src/lib/artifacts.ts +18 -0
  56. package/src/lib/execution-level.ts +13 -0
  57. package/src/lib/mcp.ts +1 -1
  58. package/src/lib/prompt.ts +7 -6
  59. package/src/lib/token-estimate.ts +8 -0
  60. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
  61. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  62. package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
  63. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  64. package/src/session/index.ts +39 -0
  65. package/src/skills/springbrand-worker-website/index.ts +1 -1
  66. package/src/submission/admission.ts +286 -0
  67. package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
  68. package/src/submission/delivery.ts +212 -0
  69. package/src/submission/finalization.ts +111 -0
  70. package/src/submission/index.ts +9 -0
  71. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  72. package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
  73. package/src/submission/steer-delivery.ts +167 -0
  74. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  75. package/src/submission/unattended.ts +40 -0
  76. package/src/telemetry/contract.ts +12 -0
  77. package/src/telemetry/coordinator.ts +65 -0
  78. package/src/telemetry/delivery.ts +2 -2
  79. package/src/telemetry/index.ts +5 -0
  80. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  81. package/src/telemetry/recorder.ts +1 -1
  82. package/src/telemetry/runtime-telemetry.ts +12 -0
  83. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  84. package/src/telemetry/turn-events.ts +279 -0
  85. package/src/tools/README.md +44 -0
  86. package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
  87. package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
  88. package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
  89. package/src/tools/assistant/ask-user.ts +317 -0
  90. package/src/tools/assistant/candidate.ts +15 -0
  91. package/src/tools/assistant/followups.ts +34 -0
  92. package/src/tools/assistant/index.ts +19 -0
  93. package/src/tools/assistant/memory.ts +73 -0
  94. package/src/tools/assistant/plan.ts +98 -0
  95. package/src/{pi/tool → tools/assistant}/time.ts +1 -1
  96. package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
  97. package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
  98. package/src/tools/codemode/workspace-connector.ts +198 -0
  99. package/src/tools/extension/index.ts +42 -0
  100. package/src/tools/index.ts +32 -0
  101. package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
  102. package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
  103. package/src/tools/runtime/approval/index.ts +1 -0
  104. package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
  105. package/src/tools/runtime/compiler.ts +264 -0
  106. package/src/tools/runtime/contracts.ts +117 -0
  107. package/src/tools/runtime/decision-gate.ts +337 -0
  108. package/src/tools/runtime/human-decisions.ts +127 -0
  109. package/src/tools/runtime/interaction/index.ts +1 -0
  110. package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
  111. package/src/tools/runtime/result.ts +22 -0
  112. package/src/tools/runtime/unattended-error.ts +4 -0
  113. package/src/tools/sandbox/index.ts +228 -0
  114. package/src/tools/schedule/index.ts +301 -0
  115. package/src/tools/skill/contracts.ts +20 -0
  116. package/src/tools/skill/index.ts +99 -0
  117. package/src/tools/skill/resources.ts +322 -0
  118. package/src/tools/skill/scripts.ts +168 -0
  119. package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
  120. package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
  121. package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
  122. package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
  123. package/src/tools/subagent/temporary/approvals.ts +123 -0
  124. package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
  125. package/src/tools/subagent/temporary/core.ts +53 -0
  126. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
  127. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
  128. package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
  129. package/src/tools/web/index.ts +2 -0
  130. package/src/tools/web/search/api.ts +1320 -0
  131. package/src/tools/web/search/index.ts +2 -0
  132. package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
  133. package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
  134. package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
  135. package/src/db/milestone.repo.ts +0 -104
  136. package/src/db/settlement.repo.ts +0 -104
  137. package/src/kernel/approval-lifecycle.ts +0 -638
  138. package/src/kernel/interaction-lifecycle.ts +0 -433
  139. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  140. package/src/kernel/state.ts +0 -91
  141. package/src/pi/assembly/index.ts +0 -19
  142. package/src/pi/runtime-adapter/execution.ts +0 -1163
  143. package/src/pi/runtime-adapter/index.ts +0 -293
  144. package/src/pi/runtime-adapter/recovery.ts +0 -982
  145. package/src/pi/session/index.ts +0 -24
  146. package/src/pi/tool/base.ts +0 -550
  147. package/src/pi/tool/compiler.ts +0 -483
  148. package/src/pi/tool/index.ts +0 -38
  149. package/src/pi/tool/schedule.ts +0 -261
  150. package/src/pi/tool/skill.ts +0 -565
  151. package/src/pi/tool/web-search/api.ts +0 -1318
  152. package/src/pi/tool/web-search/index.ts +0 -2
  153. package/src/pi/tool/workspace-state-connector.ts +0 -146
  154. package/src/pi/turn/approval.ts +0 -181
  155. package/src/pi/turn/index.ts +0 -82
  156. package/src/pi/turn/interaction.ts +0 -181
  157. package/src/runtime.ts +0 -4760
  158. package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  159. package/src/{kernel → config}/degradation.ts +0 -0
  160. package/src/{kernel → config}/runtime-config.ts +0 -0
  161. package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  162. package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  163. package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  164. package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  165. package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  166. package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  167. package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  168. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
  169. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
  170. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
  171. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
  172. /package/src/{workspace-versioning.ts → tools/workspace/versioning.ts} +0 -0
@@ -1,6 +1,6 @@
1
- import type { RuntimeMcpServer } from "../../kernel/profile";
2
- import type { PiToolCandidate } from "./compiler";
3
- import { createPiDeclaredToolCandidate } from "./declared";
1
+ import type { RuntimeMcpServer } from "../../config/profile";
2
+ import { createPiDeclaredToolCandidate } from "../adapters/declared";
3
+ import type { PiToolCandidate } from "../runtime/contracts";
4
4
 
5
5
  interface McpCallResult {
6
6
  content: unknown[];
@@ -34,7 +34,7 @@ interface PiMcpCandidatePolicy {
34
34
  * 单个 Server 的客户端,Server 是对外提供工具的服务。连接持久化、OAuth 和
35
35
  * 恢复仍由 Agents SDK 的 MCP Client 管理,本接口只读取和调用。
36
36
  *
37
- * `Host` 与 `Pi` 等项目核心术语见 `../../index.ts`。
37
+ * `Host` 与 `Pi` 等项目核心术语见 `src/index.ts`。
38
38
  */
39
39
  export interface PiMcpHost {
40
40
  /**
@@ -284,7 +284,7 @@ export function createPiMcpToolCandidate(
284
284
  * 远端 Server 提供的 annotations 只是提示,不是本地审批边界,所以所有动态
285
285
  * MCP 工具继续标为高风险,不能据此绕过审批。
286
286
  *
287
- * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
287
+ * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `src/index.ts`。
288
288
  */
289
289
  export async function createPiMcpToolCandidates(
290
290
  host: PiMcpHost,
@@ -0,0 +1 @@
1
+ export * from "./repository";
@@ -1,8 +1,7 @@
1
1
  import type { SqlTaggedTemplate } from "agents/chat";
2
- import type { ExecutionLevel } from "../lib/execution-level";
2
+ import type { ExecutionLevel } from "../../../lib/execution-level";
3
3
 
4
4
  // Approval 的统一定义见 ./index.ts。
5
- // #region 状态与行类型
6
5
  export type ApprovalStatus = "pending" | "approved" | "rejected";
7
6
 
8
7
  export interface StoredApproval {
@@ -16,6 +15,7 @@ export interface StoredApproval {
16
15
  executionLevel: ExecutionLevel;
17
16
  requiredExecutionLevel: ExecutionLevel;
18
17
  inputJson: string;
18
+ confirmationJson?: string | null;
19
19
  status: ApprovalStatus;
20
20
  createdAt: number;
21
21
  decidedAt: number | null;
@@ -33,6 +33,7 @@ export interface NewApproval {
33
33
  executionLevel: ExecutionLevel;
34
34
  requiredExecutionLevel: ExecutionLevel;
35
35
  inputJson: string;
36
+ confirmationJson?: string | null;
36
37
  status: ApprovalStatus;
37
38
  createdAt: number;
38
39
  }
@@ -46,6 +47,7 @@ export interface PendingApprovalSummary {
46
47
  executionLevel: ExecutionLevel;
47
48
  requiredExecutionLevel: ExecutionLevel;
48
49
  inputJson: string;
50
+ confirmationJson?: string | null;
49
51
  requestId: string;
50
52
  }
51
53
 
@@ -55,11 +57,6 @@ export interface ApprovalView {
55
57
  status: ApprovalStatus;
56
58
  }
57
59
 
58
- export interface DecidedApprovalRef {
59
- executionId: string;
60
- submissionId: string;
61
- }
62
-
63
60
  type ApprovalRow = {
64
61
  execution_id: string;
65
62
  submission_id: string;
@@ -71,14 +68,13 @@ type ApprovalRow = {
71
68
  execution_level: string;
72
69
  required_level: string;
73
70
  input_json: string;
71
+ confirmation_json: string | null;
74
72
  status: string;
75
73
  created_at: number;
76
74
  decided_at: number | null;
77
75
  reason: string | null;
78
76
  };
79
77
 
80
- // #endregion
81
- // #region 行映射
82
78
  // 把 SQLite 的 Approval 行转成 Runtime 对象。
83
79
  // find 查到记录后调用,上层因而不需要理解 snake_case 列名。
84
80
  // 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
@@ -94,6 +90,7 @@ function mapRow(row: ApprovalRow): StoredApproval {
94
90
  executionLevel: row.execution_level as ExecutionLevel,
95
91
  requiredExecutionLevel: row.required_level as ExecutionLevel,
96
92
  inputJson: row.input_json,
93
+ confirmationJson: row.confirmation_json,
97
94
  status: row.status as ApprovalStatus,
98
95
  createdAt: row.created_at,
99
96
  decidedAt: row.decided_at,
@@ -101,22 +98,20 @@ function mapRow(row: ApprovalRow): StoredApproval {
101
98
  };
102
99
  }
103
100
 
104
- // #endregion
105
- // #region Repository
106
101
  export class ApprovalRepository {
107
102
  // 保存当前 Agent SQLite 的查询入口。
108
- // RuntimeDatabase 构造时调用,ApprovalLifecycle 和 Transcript 随后共享这个 Repository。
103
+ // RuntimeDatabase 构造时调用,HarnessDecisionGate 和 Transcript 随后共享这个 Repository。
109
104
  // Approval 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
110
- constructor(private readonly sql: SqlTaggedTemplate) {}
105
+ constructor(private readonly sql: SqlTaggedTemplate) { }
111
106
 
112
107
  // 按 executionId 读取一条完整 Approval,不存在时返回 null。
113
- // ApprovalLifecycle 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
108
+ // HarnessDecisionGate 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
114
109
  // execution_id 是主键,LIMIT 1 明确只消费一行;不能改成按非唯一字段随意取首行。
115
110
  find(executionId: string): StoredApproval | null {
116
111
  const row = this.sql<ApprovalRow>`
117
112
  SELECT execution_id, submission_id, request_id, source,
118
113
  tool_call_id, tool_name, summary,
119
- execution_level, required_level, input_json,
114
+ execution_level, required_level, input_json, confirmation_json,
120
115
  status, created_at, decided_at, reason
121
116
  FROM pi_approvals
122
117
  WHERE execution_id = ${executionId}
@@ -126,14 +121,14 @@ export class ApprovalRepository {
126
121
  }
127
122
 
128
123
  // 持久化一条新的 Approval。
129
- // ApprovalLifecycle.ensurePending 在准入事务内确认没有同一记录后调用。
124
+ // HarnessDecisionGate 在 `before_tool` 事务内确认没有同一记录后调用。
130
125
  // 主键和 submission_id/tool_call_id 唯一约束是持久化去重防线,不能用覆盖式 INSERT 掩盖冲突审批。
131
126
  insert(approval: NewApproval): void {
132
127
  this.sql`
133
128
  INSERT INTO pi_approvals (
134
129
  execution_id, submission_id, request_id, source,
135
130
  tool_call_id, tool_name, summary,
136
- execution_level, required_level, input_json,
131
+ execution_level, required_level, input_json, confirmation_json,
137
132
  status, created_at
138
133
  ) VALUES (
139
134
  ${approval.executionId},
@@ -146,6 +141,7 @@ export class ApprovalRepository {
146
141
  ${approval.executionLevel},
147
142
  ${approval.requiredExecutionLevel},
148
143
  ${approval.inputJson},
144
+ ${approval.confirmationJson ?? null},
149
145
  ${approval.status},
150
146
  ${approval.createdAt}
151
147
  )
@@ -153,7 +149,7 @@ export class ApprovalRepository {
153
149
  }
154
150
 
155
151
  // 只把 pending Approval 写成指定决策,并记录时间和原因。
156
- // applyPiRecoveryMutations 在处理用户决策或恢复命令时调用,迟到的重复决策不应覆盖首次结果。
152
+ // Runtime 处理用户决策时调用,迟到的重复决策不应覆盖首次结果。
157
153
  // WHERE status='pending' 实现首次决策胜出,不能去掉这个条件;调用方在事务后重读来确认是否真的生效。
158
154
  decide(
159
155
  executionId: string,
@@ -172,7 +168,7 @@ export class ApprovalRepository {
172
168
  }
173
169
 
174
170
  // 按创建顺序列出某次 Submission 仍待决策的 executionId。
175
- // ApprovalLifecycle.rejectPending 在终止 Turn 时调用,随后为每条待决策记录追加拒绝恢复事实。
171
+ // Runtime 终止 Turn 时调用,保证没有待决策记录泊留在终态 Submission 上。
176
172
  // ORDER BY created_at 保留决策的原始顺序,不能依赖 SQLite 未声明的默认行顺序。
177
173
  listPendingForSubmission(submissionId: string): string[] {
178
174
  return this.sql<{ execution_id: string }>`
@@ -185,7 +181,7 @@ export class ApprovalRepository {
185
181
  }
186
182
 
187
183
  // 列出所属 Submission 仍活跃的全部待决策 Approval 摘要。
188
- // ApprovalLifecycle.list 和广播流程在生成用户可见审批列表时调用。
184
+ // Runtime 广播流程在生成用户可见审批列表时调用。
189
185
  // JOIN Submission 是为了隐藏已终止 Turn 的孤立 pending 行,不能只按 Approval 自身状态暴露给用户。
190
186
  listPendingActive(): PendingApprovalSummary[] {
191
187
  return this.sql<{
@@ -197,12 +193,13 @@ export class ApprovalRepository {
197
193
  execution_level: string;
198
194
  required_level: string;
199
195
  input_json: string;
196
+ confirmation_json: string | null;
200
197
  request_id: string;
201
198
  }>`
202
199
  SELECT approval.execution_id, approval.tool_call_id, approval.source,
203
200
  approval.tool_name, approval.summary,
204
201
  approval.execution_level, approval.required_level,
205
- approval.input_json, approval.request_id
202
+ approval.input_json, approval.confirmation_json, approval.request_id
206
203
  FROM pi_approvals approval
207
204
  JOIN pi_submissions submission
208
205
  ON submission.submission_id = approval.submission_id
@@ -217,28 +214,13 @@ export class ApprovalRepository {
217
214
  summary: row.summary,
218
215
  executionLevel: row.execution_level as ExecutionLevel,
219
216
  requiredExecutionLevel: row.required_level as ExecutionLevel,
220
- inputJson: row.input_json,
217
+ inputJson: row.confirmation_json ?? row.input_json,
221
218
  requestId: row.request_id,
222
219
  }));
223
220
  }
224
221
 
225
- // 按决策时间列出所有已批准或已拒绝的 Approval 引用。
226
- // ApprovalLifecycle.dispatchPendingContinuations 在 Runtime 启动时调用,用恢复决策判断哪些记录仍需要继续。
227
- // 这里故意不预先猜测“已分派”,幂等性由 Recovery Milestone 决定;不能用内存队列替代跨重启扫描。
228
- listDecided(): DecidedApprovalRef[] {
229
- return this.sql<{ execution_id: string; submission_id: string }>`
230
- SELECT execution_id, submission_id
231
- FROM pi_approvals
232
- WHERE status IN ('approved', 'rejected')
233
- ORDER BY decided_at ASC
234
- `.map((row) => ({
235
- executionId: row.execution_id,
236
- submissionId: row.submission_id,
237
- }));
238
- }
239
-
240
222
  // 计算所属 Submission 仍活跃的 pending Approval 数量。
241
- // ApprovalLifecycle.countPending 在报告交互阻塞和 Runtime 稳定状态时调用。
223
+ // Runtime 在报告交互阻塞和稳定状态时调用。
242
224
  // 与 listPendingActive 使用同样的 JOIN 和状态条件才能保证计数和用户可见列表一致,修改其一时必须核对另一个。
243
225
  countPendingActive(): number {
244
226
  return (
@@ -254,7 +236,7 @@ export class ApprovalRepository {
254
236
  }
255
237
 
256
238
  // 只在某个 toolCallId 唯一匹配一条活跃 pending Approval 时返回 executionId。
257
- // Runtime 处理工具审批决策时通过 ApprovalLifecycle 调用,返回 null 表示没有或存在歧义。
239
+ // Runtime 处理工具审批决策时通过 HarnessDecisionGate 调用,返回 null 表示没有或存在歧义。
258
240
  // toolCallId 只在单个 Submission 内唯一,LIMIT 2 用最小查询识别跨 Submission 重名;不能用 LIMIT 1 静默选中任意 Approval。
259
241
  findPendingByToolCallId(toolCallId: string): string | null {
260
242
  const matches = this.sql<{ execution_id: string }>`
@@ -274,7 +256,7 @@ export class ApprovalRepository {
274
256
 
275
257
  // 列出某次 Submission 的审批 ID、工具调用 ID 和状态。
276
258
  // Transcript 投影浏览器消息时调用,用这些精简字段补齐工具调用视图。
277
- // 这里不返回风险、输入等内容,避免 Transcript 和 ApprovalLifecycle 共享过宽的数据契约。
259
+ // 这里不返回风险、输入等内容,避免 Transcript 和 HarnessDecisionGate 共享过宽的数据契约。
278
260
  listApprovalViews(submissionId: string): ApprovalView[] {
279
261
  return this.sql<{
280
262
  execution_id: string;
@@ -291,4 +273,3 @@ export class ApprovalRepository {
291
273
  }));
292
274
  }
293
275
  }
294
- // #endregion
@@ -0,0 +1,264 @@
1
+ import type { AgentHarnessTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { boundDurableToolOutput, spillDurableToolOutput } from "../../context/memory/budget/gate";
3
+ import { artifactId, serializeOutput, sha256Hex, type ArtifactRef, type SpillWorkspace } from "../../lib/artifacts";
4
+ import type { CompileHarnessToolsOptions, PiToolCandidate } from "./contracts";
5
+ import { UnattendedExecutionError } from "./unattended-error";
6
+
7
+ // DO SQLite rows also contain arguments and transcript/recovery envelopes.
8
+ // Keep each serialized result below half the 2 MiB row limit.
9
+ const DURABLE_TOOL_RESULT_MAX_BYTES = 1024 * 1024;
10
+
11
+ const MEDIA_CAPTION_MAX_CHARS = 4 * 1024;
12
+
13
+
14
+
15
+ // 把任意失败值收敛为可安全显示的简短消息。
16
+ // 受治理工具在工具执行或结算抛错时调用它。
17
+ // 读取错误自身也可能抛错,因此最外层必须保留固定回退文本。
18
+ function errorMessage(error: unknown): string {
19
+ try {
20
+ if (error instanceof Error) {
21
+ return error.message || error.name;
22
+ }
23
+ return String(error);
24
+ } catch {
25
+ return "Tool execution failed";
26
+ }
27
+ }
28
+
29
+ // Most Pi adapters keep the original Tool value in details and its serialized
30
+ // model form in one text block. Spill that original once instead of duplicating
31
+ // it inside an AgentToolResult JSON envelope.
32
+ function spillPayload(result: AgentToolResult<unknown>): unknown {
33
+ const text = result.content.length === 1 && result.content[0]?.type === "text"
34
+ ? result.content[0].text
35
+ : undefined;
36
+ if (text !== undefined && serializeOutput(result.details).text === text) {
37
+ return result.details;
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function artifactResult(
43
+ result: AgentToolResult<unknown>,
44
+ ref: ArtifactRef,
45
+ ): AgentToolResult<unknown> {
46
+ const presentation = result.details !== null && typeof result.details === "object"
47
+ ? (result.details as Record<string, unknown>).presentation
48
+ : undefined;
49
+ let captionRemaining = MEDIA_CAPTION_MAX_CHARS;
50
+ const mediaContent = result.content.some((part) => part.type !== "text")
51
+ ? result.content.flatMap((part): AgentToolResult<unknown>["content"] => {
52
+ if (part.type !== "text") return [part];
53
+ const text = part.text.slice(0, captionRemaining);
54
+ captionRemaining -= text.length;
55
+ return text ? [{ ...part, text }] : [];
56
+ })
57
+ : [];
58
+ return {
59
+ ...result,
60
+ content: [{
61
+ type: "text",
62
+ text: [
63
+ ref.note,
64
+ `Path: ${ref.path}`,
65
+ `Bytes: ${ref.bytes}`,
66
+ `Preview:\n${ref.preview}`,
67
+ ].join("\n"),
68
+ }, ...mediaContent],
69
+ details: presentation === undefined ? ref : { ...ref, presentation },
70
+ };
71
+ }
72
+
73
+ async function unavailableArtifactResult(
74
+ result: AgentToolResult<unknown>,
75
+ ): Promise<AgentToolResult<unknown>> {
76
+ const { text, ext } = serializeOutput(spillPayload(result));
77
+ const hash = await sha256Hex(text);
78
+ const ref: ArtifactRef = {
79
+ kind: "artifact_ref",
80
+ artifactId: artifactId(hash),
81
+ path: `/scratch/tool-output/${hash.slice(0, 24)}.${ext}`,
82
+ bytes: new TextEncoder().encode(text).byteLength,
83
+ hash,
84
+ workspaceRevision: "unavailable",
85
+ availability: "unavailable",
86
+ preview: text.slice(0, 600),
87
+ note:
88
+ "The Tool output was too large to keep inline, and the Workspace artifact " +
89
+ "could not be verified. The evidence is unavailable; do not treat the preview as complete.",
90
+ };
91
+ return {
92
+ ...result,
93
+ content: [{
94
+ type: "text",
95
+ text: [
96
+ ref.note,
97
+ `Artifact ID: ${ref.artifactId}`,
98
+ `Path: ${ref.path}`,
99
+ `Bytes: ${ref.bytes}`,
100
+ `Hash: ${ref.hash}`,
101
+ `Preview:\n${ref.preview}`,
102
+ ].join("\n"),
103
+ }],
104
+ details: ref,
105
+ };
106
+ }
107
+
108
+ async function durableResult(
109
+ result: AgentToolResult<unknown>,
110
+ candidate: PiToolCandidate,
111
+ workspace: SpillWorkspace | undefined,
112
+ ): Promise<AgentToolResult<unknown>> {
113
+ const spill = (candidate.outputBudget ?? { kind: "spill" }).kind === "spill";
114
+ const serialized = serializeOutput(spillPayload(result));
115
+ let ref = spill
116
+ ? await spillDurableToolOutput(spillPayload(result), { workspace })
117
+ : null;
118
+ if (spill && serialized.text.length > 32 * 1024 && !ref) {
119
+ return unavailableArtifactResult(result);
120
+ }
121
+ const bounded = ref
122
+ ? artifactResult(result, ref)
123
+ : boundDurableToolOutput(result) as AgentToolResult<unknown>;
124
+ const bytes = new TextEncoder().encode(JSON.stringify(bounded)).byteLength;
125
+ if (bytes <= DURABLE_TOOL_RESULT_MAX_BYTES) return bounded;
126
+
127
+ // Binary blocks cannot be truncated, and this boundary has no image resizer.
128
+ // Preserve the original where possible and fail before any SQLite write.
129
+ if (!spill) ref = await spillDurableToolOutput(spillPayload(result), { workspace });
130
+ throw new Error(
131
+ `Tool output exceeds the 1 MiB durable result limit (${bytes} bytes). ` +
132
+ "Return a smaller result, such as lower-resolution images or fewer media items; the original cannot be included inline. " +
133
+ (ref ? `Original output saved to Workspace: ${ref.path}` : "The original output could not be saved to Workspace."),
134
+ );
135
+ }
136
+
137
+ async function untilAborted<T>(
138
+ signal: AbortSignal | undefined,
139
+ run: () => T | Promise<T>,
140
+ ): Promise<T> {
141
+ // ponytail: 这会立即释放 Turn,但无法杀死不配合的进程内副作用;需要物理终止的 Tool 必须放进可销毁的隔离执行器。
142
+ signal?.throwIfAborted();
143
+ if (!signal) return run();
144
+ let onAbort!: () => void;
145
+ const aborted = new Promise<never>((_resolve, reject) => {
146
+ onAbort = () => reject(
147
+ signal.reason ?? new DOMException("Aborted", "AbortError"),
148
+ );
149
+ signal.addEventListener("abort", onAbort, { once: true });
150
+ });
151
+ try {
152
+ return await Promise.race([run(), aborted]);
153
+ } finally {
154
+ signal.removeEventListener("abort", onAbort);
155
+ }
156
+ }
157
+
158
+
159
+
160
+ /**
161
+ * Convert the configured candidate surface once at the AgentHarness boundary.
162
+ * Schema, argument preparation, progress, cancellation, replay classification,
163
+ * and output bounds stay attached to the original Tool declaration.
164
+ */
165
+ export function compileHarnessTools(
166
+ candidates: readonly PiToolCandidate[],
167
+ options: CompileHarnessToolsOptions = {},
168
+ ): AgentHarnessTool<object | undefined>[] {
169
+ return candidates.map((candidate) => {
170
+ const source = candidate.tool;
171
+ const tool = source.constrainedSampling === false
172
+ ? source
173
+ : {
174
+ ...source,
175
+ constrainedSampling: source.constrainedSampling ?? {
176
+ type: "json_schema" as const,
177
+ strict: "prefer" as const,
178
+ },
179
+ };
180
+ return {
181
+ ...tool,
182
+ replay: candidate.retry === "idempotent" &&
183
+ candidate.requiredExecutionLevel === "safe" &&
184
+ !candidate.alwaysRequiresApproval &&
185
+ !candidate.requiredExecutionLevelForInput &&
186
+ !candidate.interaction
187
+ ? "safe" as const
188
+ : "never" as const,
189
+ ...(candidate.deferLoading ? { deferLoading: true as const } : {}),
190
+ async execute(
191
+ toolCallId,
192
+ args,
193
+ onUpdate,
194
+ _toolContext,
195
+ _invocation,
196
+ context,
197
+ ) {
198
+ const signal = context.abortSignal ?? new AbortController().signal;
199
+ let result: AgentToolResult<unknown>;
200
+ try {
201
+ await options.authorize?.(candidate, args, {
202
+ toolCallId,
203
+ signal,
204
+ });
205
+ const replay = await options.resolveReplay?.(
206
+ candidate,
207
+ toolCallId,
208
+ args,
209
+ );
210
+ if (replay) return durableResult(replay, candidate, options.workspace);
211
+ result = await untilAborted(
212
+ signal,
213
+ async () => durableResult(
214
+ await (candidate.confirmation
215
+ ? (() => {
216
+ const confirmed = options.resolveConfirmation?.(candidate, toolCallId, args);
217
+ if (!confirmed) throw new Error("User confirmation is required");
218
+ return candidate.confirmation.execute(confirmed.prepared, { intentId: confirmed.intentId, signal });
219
+ })()
220
+ : source.execute(toolCallId, args, signal, (update) => onUpdate(update))),
221
+ candidate,
222
+ options.workspace,
223
+ ),
224
+ );
225
+ } catch (cause) {
226
+ const message = errorMessage(cause);
227
+ const failed = boundDurableToolOutput({
228
+ content: [{ type: "text", text: message }],
229
+ details: {},
230
+ }) as AgentToolResult<unknown>;
231
+ await options.onSettlement?.({
232
+ toolCallId,
233
+ toolName: source.name,
234
+ args,
235
+ result: failed,
236
+ isError: true,
237
+ cause,
238
+ summary: candidate.summary,
239
+ source: candidate.source,
240
+ });
241
+ if (cause instanceof UnattendedExecutionError) {
242
+ return { ...failed, terminate: true };
243
+ }
244
+ throw cause instanceof Error && cause.message === message
245
+ ? cause
246
+ : new Error(message, { cause });
247
+ }
248
+ await options.onSettlement?.({
249
+ toolCallId,
250
+ toolName: source.name,
251
+ args,
252
+ result,
253
+ isError: result.details !== null &&
254
+ typeof result.details === "object" &&
255
+ (result.details as { availability?: unknown }).availability ===
256
+ "unavailable",
257
+ summary: candidate.summary,
258
+ source: candidate.source,
259
+ });
260
+ return result;
261
+ },
262
+ } satisfies AgentHarnessTool<object | undefined>;
263
+ });
264
+ }
@@ -0,0 +1,117 @@
1
+ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { ApprovalReceipt } from "../../submission/contracts";
3
+ import { type BudgetPolicy } from "../../context/memory/budget/gate";
4
+ import { type SpillWorkspace } from "../../lib/artifacts";
5
+ import { type ExecutionLevel } from "../../lib/execution-level";
6
+
7
+ // 本文件沿用 `src/index.ts` 入口定义的 Tool Candidate 和 Tool Settlement 术语。
8
+
9
+
10
+ /**
11
+ * 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
12
+ *
13
+ * 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
14
+ * 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
15
+ *
16
+ * 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
17
+ * 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
18
+ * 会让下一次模型调用非法。
19
+ */
20
+ export interface PiToolInteractionSpec {
21
+ /** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
22
+ readonly validateResponse: (input: unknown, response: unknown) => boolean;
23
+ /** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
24
+ readonly settle?: (
25
+ input: unknown,
26
+ response: unknown,
27
+ ) => Omit<AgentToolResult<unknown>, "terminate">;
28
+ }
29
+
30
+ /** Trusted Tool metadata used to evaluate a scheduled task grant. */
31
+ export interface PiToolUnattendedExecutionSpec {
32
+ readonly capability: string;
33
+ readonly operation: string;
34
+ readonly objectForInput?: (
35
+ input: unknown,
36
+ ) => unknown | Promise<unknown>;
37
+ }
38
+
39
+ /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
40
+ export type ToolExposureMode = "direct" | "codemode" | "both";
41
+
42
+ /** Host-prepared confirmation travels through the existing durable approval. */
43
+ export interface PiToolConfirmationSpec {
44
+ prepare(input: unknown, context: { intentId: string }): Promise<unknown>;
45
+ execute(prepared: unknown, context: { intentId: string; signal: AbortSignal }): Promise<AgentToolResult<unknown>>;
46
+ }
47
+
48
+ export interface PiToolCandidate {
49
+ /** Presentation only, resolved at the model boundary; never changes approved execution inputs. */
50
+ readonly modelGuidance?: () => Promise<{ description: string; systemPrompt?: string }>;
51
+ readonly confirmation?: PiToolConfirmationSpec;
52
+ readonly owner: string;
53
+ readonly tool: AgentTool<any, any>;
54
+ /** @internal Send the complete schema but hide it until provider Tool Search finds it. */
55
+ readonly deferLoading?: true;
56
+ /** Omitted means visible both directly and through Code Mode. */
57
+ readonly exposureMode?: ToolExposureMode;
58
+ /** Conservative maximum used in the stable Runtime descriptor. */
59
+ readonly requiredExecutionLevel: ExecutionLevel;
60
+ /** Trusted parameter-level policy, evaluated before approval or dispatch. */
61
+ readonly requiredExecutionLevelForInput?: (
62
+ input: unknown,
63
+ ) => ExecutionLevel | Promise<ExecutionLevel>;
64
+ /**
65
+ * 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
66
+ *
67
+ * 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
68
+ * 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
69
+ * 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
70
+ */
71
+ readonly alwaysRequiresApproval?: boolean;
72
+ /** Declares the concrete grant identity derived from actual Tool input. */
73
+ readonly unattendedExecution?: PiToolUnattendedExecutionSpec;
74
+ /** Explicitly forbids this Tool from unattended Scheduled Runs. */
75
+ readonly unattendedMode?: "forbidden";
76
+ /** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
77
+ readonly retry?: "idempotent" | "non-idempotent";
78
+ readonly summary?: string;
79
+ readonly source?: ApprovalReceipt["source"];
80
+ readonly interaction?: PiToolInteractionSpec;
81
+ /** Large payloads spill by default; protocol-shaped results opt into structure. */
82
+ readonly outputBudget?: BudgetPolicy;
83
+ }
84
+
85
+ export interface CompileHarnessToolsOptions {
86
+ readonly workspace?: SpillWorkspace;
87
+ readonly resolveConfirmation?: (candidate: PiToolCandidate, toolCallId: string, args: unknown) => { prepared: unknown; intentId: string };
88
+ /** Recheck live authorization at the effect boundary, including safe replay. */
89
+ readonly authorize?: (
90
+ candidate: PiToolCandidate,
91
+ args: unknown,
92
+ invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
93
+ ) => void | Promise<void>;
94
+ /** Return an already decided interaction result without invoking the handler. */
95
+ readonly resolveReplay?: (
96
+ candidate: PiToolCandidate,
97
+ toolCallId: string,
98
+ args: unknown,
99
+ ) => AgentToolResult<unknown> | undefined | Promise<AgentToolResult<unknown> | undefined>;
100
+ /** Optional business projection; Pi remains the authoritative Tool result. */
101
+ readonly onSettlement?: (
102
+ call: SettledPiToolCall,
103
+ ) => void | Promise<void>;
104
+ }
105
+
106
+ /** 描述一次已进入持久化结算边界的 Pi 工具调用。 */
107
+ export interface SettledPiToolCall {
108
+ readonly toolCallId: string;
109
+ readonly toolName: string;
110
+ readonly args: unknown;
111
+ readonly result: AgentToolResult<unknown>;
112
+ readonly isError: boolean;
113
+ /** Diagnostic-only original error; callers must not persist or expose it. */
114
+ readonly cause?: unknown;
115
+ readonly summary?: string;
116
+ readonly source?: ApprovalReceipt["source"];
117
+ }