@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,1163 +0,0 @@
1
- import {
2
- runAgentLoopContinue,
3
- convertToLlm,
4
- type AgentEvent,
5
- type AgentMessage,
6
- type AgentLoopConfig,
7
- type ThinkingLevel,
8
- } from "@earendil-works/pi-agent-core";
9
- import type {
10
- Api,
11
- AssistantMessage,
12
- Message,
13
- Model,
14
- Models,
15
- ToolResultMessage,
16
- UserMessage,
17
- } from "@earendil-works/pi-ai";
18
- import { isContextOverflow } from "@earendil-works/pi-ai";
19
- import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
20
- import {
21
- parkPiToolApproval,
22
- parkPiToolInteraction,
23
- requiresPiToolApproval,
24
- } from "../turn";
25
- import {
26
- classifyPiTerminalStopReason,
27
- PiChunkEncoder,
28
- } from "../message";
29
- import type { UIMessageChunk } from "ai";
30
- import { ChatStreamStalledError } from "agents/chat";
31
- import {
32
- codeExecutionPiToolCandidate,
33
- compilePiTools,
34
- createPiToolGovernance,
35
- type PiToolCandidate,
36
- type PiToolInteractionSpec,
37
- type PiToolGovernance,
38
- type SettledPiToolCall,
39
- } from "../tool";
40
- import {
41
- piToolRetryPolicy,
42
- readPinnedPiRuntime,
43
- type PreparedPiRuntime,
44
- } from "./assembly";
45
- import {
46
- isRecoverableAssistantError,
47
- isOpenRouterServerToolTimeout,
48
- isModelStreamStallMessage,
49
- withProviderRetry,
50
- type PiGenerationLifecycleObserver,
51
- } from "./models";
52
- import { EXECUTION_LEVELS } from "../../lib/execution-level";
53
- import { projectToolOutputForModel } from "../../layers/context/budget/gate";
54
- import type { SpillWorkspace } from "../../lib/artifacts";
55
-
56
- // #region Single-run Pi bridge
57
-
58
- // 一片执行最多跑多少个模型回合。这是 CPU 预算边界,不是任务边界:DO 的
59
- // CPU 上限按 invocation 计,一条长回合跑在一次 invocation 里会把预算吃满并被驱逐,
60
- // 让步换来的是一次全新 invocation 和全新预算。
61
- const MAX_MODEL_TURNS_PER_SLICE = 30;
62
-
63
- // 一条 Submission 跨所有执行片最多跑多少个模型回合。这是任务边界:
64
- // 没有它,一个不收敛的工具循环会无限让步、无限唤醒、无限烧 token。
65
- export const MAX_MODEL_TURNS_PER_SUBMISSION = 300;
66
-
67
- // 预算耗尽时注入的收尾指令。跟着它一起把工具表清空,模型只能出文本,
68
- // 于是这一轮以一条正常的终止助手消息结束 —— 而不是硬判失败或继续让步。
69
- // 走 user 角色而非 assistant:多个 provider 拒绝以模型回合结尾的请求。
70
- const WRAP_UP_MESSAGE = "You have reached this run's model-turn budget. Stop " +
71
- "calling tools now. Reply with a final message that states what you " +
72
- "completed, what you did not finish, and the exact next step you would " +
73
- "take. Do not start new work.";
74
-
75
- const TOOL_FAILURE_WRAP_UP_MESSAGE = "This turn reached its tool failure limit. " +
76
- "Stop calling tools now. Reply with a final message that explains what " +
77
- "failed and the exact next step the user should take. Do not start new work.";
78
-
79
- // 收尾开始后最多再放行几个模型回合。工具已经清空,正常一轮就结束了;
80
- // 留出余量是给 steering 消息,但不能因此把本片的 CPU 上限一起取消。
81
- const MAX_WRAP_UP_TURNS = 2;
82
-
83
- export class RetryableModelError extends Error {}
84
-
85
- export class RetryableModelSessionError extends RetryableModelError {}
86
-
87
- export class ContextOverflowError extends RetryableModelError {}
88
-
89
- function prepareToolArguments(
90
- toolName: string,
91
- input: unknown,
92
- candidates: readonly PiToolCandidate[],
93
- ): unknown {
94
- const prepare = candidates.find(({ tool }) => tool.name === toolName)
95
- ?.tool.prepareArguments;
96
- if (!prepare) return input;
97
- try {
98
- return prepare(input);
99
- } catch {
100
- // Streaming inputs may still be incomplete. Preserve them so Pi can
101
- // produce the normal validation ToolResult once the call is final.
102
- return input;
103
- }
104
- }
105
-
106
- function normalizeToolCallArguments(
107
- message: AgentMessage,
108
- candidates: readonly PiToolCandidate[],
109
- ): void {
110
- if (message.role !== "assistant") return;
111
- for (const part of message.content) {
112
- if (part.type === "toolCall") {
113
- part.arguments = prepareToolArguments(
114
- part.name,
115
- part.arguments,
116
- candidates,
117
- ) as Record<string, unknown>;
118
- }
119
- }
120
- }
121
-
122
- function visibleAssistantContent(message: AssistantMessage) {
123
- return message.content.filter((part) =>
124
- part.type === "text"
125
- ? part.text.trim()
126
- : part.type === "thinking"
127
- ? part.thinking.trim()
128
- : false
129
- );
130
- }
131
-
132
- interface PiTurnAdapterOptions {
133
- readonly pi: {
134
- readonly model: Model<Api>;
135
- readonly thinkingLevel?: ThinkingLevel;
136
- };
137
- readonly models: Models;
138
- readonly modelSessionId: string;
139
- // 读取这次执行真正要交给 Pi 的 canonical transcript。
140
- // PiTurnAdapter.run 在创建 Pi Loop 前调用它,宿主应返回当时最新的 transcript。
141
- // 这里保留为延迟读取,是为了避免适配器创建后新增的恢复消息被旧快照漏掉。
142
- readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
143
- // 把工具的最终结果交回宿主持久化。
144
- // 共享工具编译器在向 Pi 返回成功或规范化失败前调用它。
145
- // 这个先落库再发布结果的顺序是恢复边界,不能改成事后异步补写。
146
- readonly settle: (
147
- call: SettledPiToolCall,
148
- ) => void | Promise<void>;
149
- readonly onGeneration?: PiGenerationLifecycleObserver;
150
- // 在模型调用前通过 AgentLoopConfig.transformContext 调整上下文。
151
- // 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
152
- readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
153
- readonly workspace?: SpillWorkspace;
154
- // 之前的执行片已经用掉的模型回合数。首次执行传 0。
155
- readonly modelTurnsConsumed: number;
156
- }
157
-
158
- /**
159
- * 收集那些结果已经由工具自己收口的 Tool 名字。
160
- *
161
- * @remarks
162
- * `run` 在建 Pi Loop 前调用,把结果交给 `projectToolResultsForModel`。
163
- *
164
- * `outputBudget: "structure"` 的语义就是“这个结果不需要 Runtime 再替它省”:read
165
- * 按页返回并在页脚写明覆盖的行号,activate_skill 返回的是模型接下来要照做的指令,
166
- * Code Mode 的结果由它自己收口。对这些再套一次叶子上限,模型看到的正文会和结果里
167
- * 写明的范围对不上 —— 页脚说 `lines 1-307`,正文却被从中间挖空。
168
- *
169
- * 其余工具仍然照常压缩:这道闸按分类放行,不是整体放宽。
170
- */
171
- function selfBoundedToolNames(
172
- candidates: readonly PiToolCandidate[],
173
- ): ReadonlySet<string> {
174
- return new Set(
175
- candidates
176
- .filter((candidate) => candidate.outputBudget?.kind === "structure")
177
- .map((candidate) => candidate.tool.name),
178
- );
179
- }
180
-
181
- function projectToolResultsForModel(
182
- messages: readonly AgentMessage[],
183
- selfBounded: ReadonlySet<string>,
184
- ): AgentMessage[] {
185
- let changed = false;
186
- const projected = messages.map((message) => {
187
- if (message.role !== "toolResult") return message;
188
- if (selfBounded.has(message.toolName)) return message;
189
- const payload = {
190
- content: message.content,
191
- details: message.details,
192
- };
193
- const next = projectToolOutputForModel(payload) as typeof payload;
194
- if (next === payload) return message;
195
- changed = true;
196
- return {
197
- ...message,
198
- content: next.content,
199
- details: next.details,
200
- };
201
- });
202
- return changed ? projected : [...messages];
203
- }
204
-
205
- function orderLateToolResultsBeforeTurnAbortMarker(
206
- messages: readonly AgentMessage[],
207
- ): AgentMessage[] {
208
- const ordered: AgentMessage[] = [];
209
- for (let index = 0; index < messages.length; index += 1) {
210
- const message = messages[index]!;
211
- const previous = ordered.at(-1);
212
- if (
213
- message.role !== "custom" ||
214
- message.customType !== "turn-aborted" ||
215
- previous?.role !== "assistant"
216
- ) {
217
- ordered.push(message);
218
- continue;
219
- }
220
- const pending = new Set(
221
- previous.content.flatMap((part) =>
222
- part.type === "toolCall" ? [part.id] : []
223
- ),
224
- );
225
- let cursor = index + 1;
226
- while (true) {
227
- const result = messages[cursor];
228
- if (result?.role !== "toolResult" || !pending.has(result.toolCallId)) {
229
- break;
230
- }
231
- ordered.push(result);
232
- cursor += 1;
233
- }
234
- ordered.push(message);
235
- index = cursor - 1;
236
- }
237
- return ordered;
238
- }
239
-
240
- class PiTurnAdapter {
241
- private readonly pendingSteers: UserMessage[] = [];
242
- private readonly governance: PiToolGovernance;
243
-
244
- // 创建一个 Turn 所需的短生命周期 Pi 执行器状态。
245
- // PreparedPiTurnAdapter 只构造一次,并把运行、重试、转向和中止都交给它。
246
- // 工具治理对象必须在这些路径间复用,因为它保存本 Turn 的重试次数和熔断状态。
247
- constructor(private readonly opts: PiTurnAdapterOptions) {
248
- this.governance = createPiToolGovernance();
249
- }
250
-
251
- // 把 Tool candidate 变成 Pi 可以执行的受治理 Tool。
252
- // 正常运行会编译 Pinned Runtime 的整份列表,恢复时只编译指定 Tool candidate。
253
- // 输出限额和持久化结算统一走共享编译器,避免恢复另开旁路。
254
- private compile(candidates: readonly PiToolCandidate[]) {
255
- return compilePiTools(candidates, {
256
- governance: this.governance,
257
- workspace: this.opts.workspace,
258
- settle: this.opts.settle,
259
- });
260
- }
261
-
262
- // 用正常 Turn 的受治理路径重新执行一次已知工具调用。
263
- // recovery effect 和审批续跑在找到原调用记录对应的 Tool candidate 后调用它。
264
- // Tool candidate 被过滤时返回 false,让调用方安全停止;实际执行仍保留结算和重试上限。
265
- async retryTool(
266
- candidate: PiToolCandidate,
267
- toolCallId: string,
268
- input: unknown,
269
- signal?: AbortSignal,
270
- ): Promise<boolean> {
271
- const [tool] = this.compile([candidate]);
272
- if (!tool) return false;
273
- signal?.throwIfAborted();
274
- await tool.execute(toolCallId, input, signal);
275
- return true;
276
- }
277
-
278
- // 把一条用户消息排入当前 Turn。
279
- // 提交仍在运行时收到扩展宿主消息,Runtime 会调用这里。
280
- // Pi Loop 创建前先缓冲消息,可覆盖异步启动窗口,同时不引入第二份 transcript 来源。
281
- steer(message: UserMessage): void {
282
- this.pendingSteers.push(message);
283
- }
284
-
285
- // 从当前 canonical transcript 继续运行 Pi,并把生命周期事件交给监听器。
286
- // Submission 的首次执行或恢复执行取得 Pinned Runtime 后,PreparedPiTurnAdapter 调用这里。
287
- // 每次运行持有独立上下文;Loop 等待事件 sink 完成后才继续。
288
- async run(
289
- opts: {
290
- readonly systemPrompt: string;
291
- readonly tools: readonly PiToolCandidate[];
292
- readonly signal?: AbortSignal;
293
- },
294
- listener: (event: AgentEvent) => void | Promise<void>,
295
- ): Promise<PiTurnRunResult> {
296
- const { systemPrompt, signal } = opts;
297
- signal?.throwIfAborted();
298
- const canonicalMessages = await this.opts.canonicalMessages();
299
- signal?.throwIfAborted();
300
- const tools = this.compile(opts.tools);
301
- const selfBounded = selfBoundedToolNames(opts.tools);
302
- // `modelTurns` 是跨执行片的累计数,`sliceTurns` 只数本片 —— 前者管任务预算,
303
- // 后者管 CPU 预算,两个边界互不替代。
304
- let modelTurns = this.opts.modelTurnsConsumed;
305
- let sliceTurns = 0;
306
- let reachedModelTurnLimit = false;
307
- let wrapUpMessage: UserMessage | undefined;
308
- let wrapUpTurns = 0;
309
- const config: AgentLoopConfig = {
310
- model: this.opts.pi.model,
311
- reasoning: this.opts.pi.thinkingLevel === "off" ? undefined : this.opts.pi.thinkingLevel,
312
- toolExecution: "parallel",
313
- convertToLlm,
314
- getSteeringMessages: async () => this.pendingSteers.splice(0, 1),
315
- // transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
316
- // OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
317
- // 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
318
- transformContext: async (messages, signal) => {
319
- const ctx = await this.opts.transformContext(messages, signal);
320
- return transformMessages(
321
- orderLateToolResultsBeforeTurnAbortMarker(
322
- projectToolResultsForModel(
323
- wrapUpMessage ? [...ctx, wrapUpMessage] : ctx,
324
- selfBounded,
325
- ),
326
- ) as Message[],
327
- this.opts.pi.model,
328
- ) as AgentMessage[];
329
- },
330
- afterToolCall: this.governance.afterToolCall,
331
- // Pi calls this only when another model turn is needed; counting belongs to turn_end.
332
- prepareNextTurn: ({ context }) => {
333
- if (wrapUpMessage) return undefined;
334
- const toolFailureLimitReached = this.governance.wrapUpRequested();
335
- if (
336
- !toolFailureLimitReached &&
337
- modelTurns < MAX_MODEL_TURNS_PER_SUBMISSION - 1
338
- ) return undefined;
339
- // Control instructions enter only the model projection, after official compaction.
340
- // Keeping them out of Loop history preserves its canonical transcript boundary.
341
- wrapUpMessage = {
342
- role: "user",
343
- content: [{
344
- type: "text",
345
- text: toolFailureLimitReached
346
- ? TOOL_FAILURE_WRAP_UP_MESSAGE
347
- : WRAP_UP_MESSAGE,
348
- }],
349
- timestamp: Date.now(),
350
- };
351
- return { context: { ...context, tools: [] } };
352
- },
353
- shouldStopAfterTurn: () => {
354
- if (signal?.aborted === true) return true;
355
- // 收尾那一轮必须跑完,否则让步会把唯一的终态机会顶掉。但「必须跑完」
356
- // 不等于「不再有上限」:Pi 在这之后还会取 steering 和 follow-up 消息,
357
- // 一直有新消息进来就会一直发模型请求,而且全在同一个 invocation 里 ——
358
- // 那正是 MAX_MODEL_TURNS_PER_SLICE 要挡的 CPU 驱逐。
359
- if (wrapUpMessage) return wrapUpTurns >= MAX_WRAP_UP_TURNS;
360
- if (this.governance.wrapUpRequested() || modelTurns >= MAX_MODEL_TURNS_PER_SUBMISSION - 1) return false;
361
- reachedModelTurnLimit = sliceTurns >= MAX_MODEL_TURNS_PER_SLICE;
362
- return reachedModelTurnLimit;
363
- },
364
- };
365
- await runAgentLoopContinue(
366
- { systemPrompt, messages: [...canonicalMessages], tools: [...tools] },
367
- config,
368
- async (event) => {
369
- await listener(event);
370
- if (event.type === "turn_end") {
371
- modelTurns += 1;
372
- sliceTurns += 1;
373
- if (wrapUpMessage) wrapUpTurns += 1;
374
- }
375
- },
376
- signal,
377
- withProviderRetry(
378
- this.opts.models.streamSimple.bind(this.opts.models),
379
- 0,
380
- this.opts.modelSessionId,
381
- this.opts.onGeneration,
382
- ),
383
- );
384
- return reachedModelTurnLimit
385
- ? { kind: "yielded", modelTurns }
386
- : { kind: "terminal", modelTurns };
387
- }
388
- }
389
-
390
- // #endregion
391
-
392
- // #region Public prepared-Turn contracts
393
-
394
- /** 表示可直接复用、无需再次执行工具的持久化结果。 */
395
- export interface PiStoredToolSettlement {
396
- readonly result: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
397
- readonly isError: boolean;
398
- }
399
-
400
- export type PiCanonicalUserInput = UserMessage;
401
- export type PiToolSettlement = SettledPiToolCall;
402
-
403
- /** 表示工具真正执行前写入的持久化输入证据。 */
404
- export interface PiToolInputRecord {
405
- readonly toolCallId: string;
406
- readonly toolName: string;
407
- readonly input: unknown;
408
- readonly retry: "idempotent" | "non-idempotent";
409
- }
410
-
411
- /**
412
- * 提供 Pi Turn 执行期间需要的持久化读写能力。
413
- *
414
- * @remarks
415
- * Runtime 为每个 Submission 实现这个端口,PreparedPiTurnAdapter 会在审批和 Tool 执行前后调用它。
416
- * 持久化留在 Pi Loop 之外,恢复流程才能从 Runtime 数据库重建决定,而不是依赖已丢失的内存状态。
417
- */
418
- export interface PiTurnDurability {
419
- /**
420
- * 查找某次工具调用已经保存的结果。
421
- *
422
- * @remarks
423
- * PreparedPiTurnAdapter 会在审批前查询一次,并在审批可能生成拒绝结果后再查询一次。
424
- * 两次读取分别防止重复执行已完成工作,以及漏掉 Turn 暂停期间产生的结果。
425
- */
426
- findToolSettlement(toolCallId: string): PiStoredToolSettlement | null;
427
- /**
428
- * 保存工具审批请求并等待决定。
429
- *
430
- * @remarks
431
- * Pinned Runtime 的 approval mode 和 Tool candidate 风险要求审批时,PreparedPiTurnAdapter 才会调用它;onCreated 在记录创建后发布回执。
432
- * 通过 onCreated 发布可保证持久化请求早于浏览器事件,调用方必须保留这个顺序。
433
- */
434
- requestToolApproval(
435
- approval: import("../turn").PiToolApproval,
436
- signal?: AbortSignal,
437
- onCreated?: () => void | Promise<void>,
438
- ): Promise<void>;
439
- /**
440
- * 保存一次客户端结算请求并等待响应。
441
- *
442
- * @remarks
443
- * Tool candidate 声明了 `interaction` 时,PreparedPiTurnAdapter 在执行前调用它,`execute` 不会被调用。
444
- * 返回后必须再读一次 settlement —— 响应、取消和跨休眠恢复三条路径都只把结果留在 settlement 里。
445
- */
446
- requestToolInteraction(
447
- interaction: import("../turn").PiToolInteraction,
448
- settle: import("../tool").PiToolInteractionSpec,
449
- signal?: AbortSignal,
450
- ): Promise<void>;
451
- /**
452
- * 记录工具输入,并返回它是否是第一次持久化尝试。
453
- *
454
- * @remarks
455
- * Tool candidate 没有已存结算结果时,PreparedPiTurnAdapter 会在真正执行前调用它。
456
- * 恢复逻辑会把返回值和重试策略一起用于阻止结果不确定的非幂等重放,因此不能混淆 true 与 false。
457
- */
458
- appendToolInput(input: PiToolInputRecord): boolean;
459
- /**
460
- * 保存受治理工具调用经过限额处理后的最终结果。
461
- *
462
- * @remarks
463
- * compilePiTools 会在返回成功或向 Pi 发布规范化失败前调用它。
464
- * 这个先写结果的边界让后续重试能看到原始结局,因此必须留在受治理包装器内部。
465
- */
466
- settleTool(call: PiToolSettlement): void | Promise<void>;
467
- }
468
-
469
- /**
470
- * 描述把一个 Submission 的 Pinned Runtime 接入 Pi 执行所需的全部依赖。
471
- *
472
- * @remarks
473
- * Submission 首次执行或恢复时,Runtime 把它传给 PiRuntimeAdapter.createTurn。
474
- * 这些回调让 transcript、持久化状态、流投影和终态仍由 Runtime 负责,避免在 Pi Loop 内复制一份。
475
- * Runtime、Submission、Turn、Prepared Runtime、Pinned Runtime、Tool candidate 和 transcript 沿用 `./index.ts` 的统一定义。
476
- */
477
- export interface CreatePreparedPiTurnOptions {
478
- readonly prepared: PreparedPiRuntime;
479
- readonly pinnedDescriptor: string;
480
- /** Session 当前的稳定 Provider 路由身份;终态 Provider 故障可在下一 Submission 前旋转它。 */
481
- readonly modelSessionId: string;
482
- readonly submission: {
483
- readonly id: string;
484
- readonly requestId: string;
485
- readonly messageId: string;
486
- readonly startedAt: number;
487
- readonly continuation: boolean;
488
- readonly assistantOrdinal: number;
489
- /** 之前的执行片已用掉的模型回合数;省略按 0 处理。 */
490
- readonly modelTurnsConsumed?: number;
491
- };
492
- /**
493
- * 在 Pi 启动前读取最新 canonical transcript。
494
- *
495
- * PiTurnAdapter.run 在创建 Pi Loop 前调用它,Runtime 应返回 transcript 当时已提交的消息。
496
- * 延迟读取可纳入适配器创建后才物化的恢复结果,不能提前固化为数组快照。
497
- */
498
- readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
499
- readonly durability: PiTurnDurability;
500
- /** Reports paired model-generation lifecycle facts to the Runtime owner. */
501
- readonly onGeneration?: PiGenerationLifecycleObserver;
502
- /** Reports Code Mode child Tool starts without adding Pi recovery state. */
503
- readonly onNestedToolStarted?: (input: Readonly<{
504
- parentToolCallId: string;
505
- toolCallId: string;
506
- toolName: string;
507
- input: unknown;
508
- occurredAt: number;
509
- }>) => void;
510
- /** Reports Code Mode child Tool outcomes without adding Pi settlements. */
511
- readonly onNestedToolFinished?: (input: Readonly<{
512
- parentToolCallId: string;
513
- toolCallId: string;
514
- toolName: string;
515
- outcome: "completed" | "failed" | "cancelled";
516
- durationMs: number;
517
- output?: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
518
- error?: unknown;
519
- occurredAt: number;
520
- }>) => void;
521
- /** Reports model-requested Tools absent from the pinned Runtime surface. */
522
- readonly onUnknownToolStarted?: (input: Readonly<{
523
- toolCallId: string;
524
- toolName: string;
525
- input: unknown;
526
- occurredAt: number;
527
- }>) => void;
528
- /** Reports the immediate Pi failure for a Tool absent from the pinned Runtime surface. */
529
- readonly onUnknownToolFinished?: (input: Readonly<{
530
- toolCallId: string;
531
- toolName: string;
532
- outcome: "failed";
533
- output: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
534
- occurredAt: number;
535
- }>) => void;
536
- /** Per-Submission executors for tools whose metadata is fixed at assembly time. */
537
- readonly toolExecutors?: Readonly<Record<
538
- string,
539
- NonNullable<PiToolCandidate["tool"]["execute"]>
540
- >>;
541
- /**
542
- * 调整 Pi 即将发送给模型的消息上下文。
543
- *
544
- * Pi Loop 在每次模型调用前使用它,Runtime 通过这里接入现有上下文处理。
545
- * 直接沿用 AgentLoopConfig 的类型可确保参数和取消信号与 Pi 保持一致。
546
- */
547
- readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
548
- /**
549
- * 在提交终止助手消息前读取已保存的取消原因。
550
- *
551
- * handleEvent 收到非工具调用的助手终止消息时调用它,Runtime 可省略该能力。
552
- * 以持久化原因为准可覆盖模型随后到达的普通完成结果,避免已取消提交被记为成功。
553
- */
554
- readonly abortReason?: () => string | undefined;
555
- /**
556
- * 保存并广播一条投影后的 Pi 流 chunk。
557
- *
558
- * 工具审批创建和 handleEvent 生成可见记录时会调用它,Runtime 决定具体存储与广播方式。
559
- * 适配器会等待该回调,以免 Pi 的后续事件越过尚未落地的前一条记录。
560
- */
561
- readonly onRecord: (
562
- chunk: UIMessageChunk,
563
- ) => void | Promise<void>;
564
- /**
565
- * 把一条完成的 Pi 消息提交到 Runtime 的 transcript。
566
- *
567
- * handleEvent 收到 user、assistant 或 toolResult 的 message_end 时调用它。
568
- * 只在 message_end 提交可避免把流式中间态当成恢复依据,调用方应保持幂等写入。
569
- */
570
- readonly onCanonicalMessage: (
571
- commit: PiCanonicalMessageCommit,
572
- ) => void | Promise<void>;
573
- /**
574
- * 记录终止助手消息给出的权威 Turn 结果。
575
- *
576
- * handleEvent 处理非 toolUse 的助手 message_end 时调用它,Runtime 用它推进提交终态。
577
- * 工具调用消息不是 Turn 终点,因此不能在 stopReason 为 toolUse 时调用。
578
- */
579
- readonly onTerminal: (
580
- terminal: PiTurnTerminalIntent,
581
- ) => void | Promise<void>;
582
- }
583
-
584
- export type PiCanonicalMessageCommit =
585
- | {
586
- readonly kind: "append-user";
587
- readonly id: string;
588
- readonly message: UserMessage;
589
- }
590
- | {
591
- readonly kind: "commit-turn";
592
- readonly ordinal: number;
593
- readonly message: AssistantMessage | ToolResultMessage;
594
- };
595
-
596
- export interface PiTurnTerminalIntent {
597
- readonly outcome: "succeeded" | "failed" | "aborted";
598
- readonly message?: string;
599
- }
600
-
601
- export interface PiPreparedTurnRunOptions {
602
- readonly signal?: AbortSignal;
603
- }
604
-
605
- export type PiTurnRunResult =
606
- /** Turn 已产生权威结果,或已经收尾。 */
607
- | { readonly kind: "terminal"; readonly modelTurns: number }
608
- /** 本片让出执行,Turn 未结束;调用方必须排出续跑。 */
609
- | { readonly kind: "yielded"; readonly modelTurns: number };
610
-
611
- // #endregion
612
-
613
- // #region Durable prepared-Turn execution
614
-
615
- /**
616
- * 执行一个 Submission 的 Pinned Runtime,同时把持久化职责交给 Runtime。
617
- *
618
- * @remarks
619
- * PiRuntimeAdapter 为实时执行、恢复和审批续跑创建本类,调用方随后使用 run()、retryTool()、steer() 或 abort()。
620
- * 构造时会核对 Prepared Runtime 的归属和 revision descriptor,避免执行过程悄悄切换到新的提示词、模型或 Tool candidate 集合。
621
- * 核心术语沿用 `./index.ts` 的统一定义。
622
- */
623
- export class PreparedPiTurnAdapter {
624
- private readonly turn: PiTurnAdapter;
625
- private readonly abortController = new AbortController();
626
- private readonly candidates: readonly PiToolCandidate[];
627
- private assistantOrdinal: number;
628
- private readonly encoder: PiChunkEncoder;
629
- private readonly steerMessageIds: string[] = [];
630
- private terminalIntent?: PiTurnTerminalIntent;
631
- private readonly contextWindow: number;
632
-
633
- /**
634
- * 把 Prepared Runtime 和 Submission 持久化端口绑定成一个可执行 Turn。
635
- *
636
- * @remarks
637
- * PiRuntimeAdapter.createTurn 是预期调用方,它会传入私有归属令牌和已配置的模型注册表。
638
- * 每个 Tool candidate 会先被包装,确保平台门禁、已有结算、审批和不确定重试检查都早于原始 execute 函数。
639
- */
640
- constructor(
641
- options: CreatePreparedPiTurnOptions,
642
- dependencies: {
643
- readonly owner: object;
644
- readonly models: Models;
645
- },
646
- ) {
647
- const { state, descriptor } = readPinnedPiRuntime(
648
- options.prepared,
649
- dependencies.owner,
650
- options.pinnedDescriptor,
651
- );
652
- this.contextWindow = state.snapshot.pi.model.contextWindow;
653
- this.assistantOrdinal = options.submission.assistantOrdinal;
654
- this.encoder = new PiChunkEncoder({
655
- messageId: options.submission.messageId,
656
- startedAt: options.submission.startedAt,
657
- turnId: options.submission.id,
658
- });
659
- const bindCandidate = (
660
- candidate: PiToolCandidate,
661
- toolCallIdPrefix?: string,
662
- recordRecoveryAttempt = true,
663
- ): PiToolCandidate => ({
664
- ...candidate,
665
- tool: {
666
- ...candidate.tool,
667
- // 只在 Runtime 的持久化和策略门禁都放行后执行一个 Tool candidate。
668
- // 实时运行由 Pi Loop 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
669
- // 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
670
- execute: async (toolCallId, input, signal, onUpdate) => {
671
- if (toolCallIdPrefix) {
672
- toolCallId = `${toolCallIdPrefix}:${toolCallId}`;
673
- }
674
- const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
675
- ? await candidate.requiredExecutionLevelForInput(input)
676
- : candidate.requiredExecutionLevel;
677
- if (!EXECUTION_LEVELS.includes(requiredExecutionLevel)) {
678
- throw new Error(
679
- `SpringBrand Tool "${candidate.tool.name}" resolved an invalid execution level`,
680
- );
681
- }
682
- await state.snapshot.bindings.platform.gateTool?.({
683
- toolCallId,
684
- toolName: candidate.tool.name,
685
- input,
686
- requiredExecutionLevel,
687
- signal: signal ?? new AbortController().signal,
688
- });
689
- const settled = options.durability.findToolSettlement(toolCallId);
690
- if (settled) {
691
- if (settled.isError) {
692
- const text = settled.result.content
693
- .flatMap((part) => part.type === "text" ? [part.text] : [])
694
- .join("");
695
- throw new Error(text || "Tool execution failed");
696
- }
697
- return settled.result;
698
- }
699
- if (
700
- candidate.alwaysRequiresApproval ||
701
- requiresPiToolApproval({
702
- executionLevel: descriptor.executionLevel,
703
- requiredExecutionLevel,
704
- })
705
- ) {
706
- const approval = parkPiToolApproval({
707
- executionId: `${options.submission.id}:${toolCallId}`,
708
- requestId: options.submission.requestId,
709
- source: candidate.source ?? "action",
710
- toolCallId,
711
- toolName: candidate.tool.name,
712
- summary:
713
- candidate.summary ??
714
- candidate.tool.label ??
715
- candidate.tool.name,
716
- executionLevel: descriptor.executionLevel,
717
- requiredExecutionLevel,
718
- inputJson: JSON.stringify(input),
719
- createdAt: Date.now(),
720
- }).approval;
721
- await options.durability.requestToolApproval(
722
- approval,
723
- signal,
724
- () => this.publishChunks(
725
- this.encoder.approvalRequested(
726
- toolCallId,
727
- approval.executionId,
728
- ),
729
- ),
730
- );
731
- const approvalSettlement = options.durability
732
- .findToolSettlement(toolCallId);
733
- await this.publishChunks(this.encoder.approvalResponded(
734
- toolCallId,
735
- approval.executionId,
736
- !approvalSettlement?.isError,
737
- approvalSettlement?.isError
738
- ? approvalSettlement.result.content
739
- .flatMap((part) => part.type === "text" ? [part.text] : [])
740
- .join("") || "Tool execution denied"
741
- : undefined,
742
- ));
743
- }
744
- const afterApproval = options.durability
745
- .findToolSettlement(toolCallId);
746
- if (afterApproval) return afterApproval.result;
747
- // 结果由客户端提供的 Tool 在这里 park,永远不走到下面的 execute。
748
- // 放在审批之后:一个 Tool 既要审批又要客户端结算时,先过许可再问用户,
749
- // 否则会先向用户要答案、再告诉他这事根本不许做。
750
- if (candidate.interaction) {
751
- // 必须先把 Tool 输入落成里程碑再 park。恢复出来的结算靠这条记录取回
752
- // `args`;缺了它,Tool 醒来返回结果时 settleTool 会拿 undefined 的 args
753
- // 去和真 args 比对,判成 "Conflicting durable Tool settlement" —— 整个
754
- // Turn 就死在这。(审批那条路不受影响:批准后 Tool 真的执行,args 在
755
- // 下面的 appendToolInput 里补上了。)
756
- options.durability.appendToolInput({
757
- toolCallId,
758
- toolName: candidate.tool.name,
759
- input,
760
- retry: piToolRetryPolicy(candidate),
761
- });
762
- const parked = parkPiToolInteraction({
763
- interactionId: `${options.submission.id}:${toolCallId}`,
764
- requestId: options.submission.requestId,
765
- toolCallId,
766
- toolName: candidate.tool.name,
767
- inputJson: JSON.stringify(input),
768
- createdAt: Date.now(),
769
- }).interaction;
770
- await options.durability.requestToolInteraction(
771
- parked,
772
- candidate.interaction,
773
- signal,
774
- );
775
- const responded = options.durability
776
- .findToolSettlement(toolCallId);
777
- if (!responded) {
778
- throw new Error(
779
- `Tool interaction produced no durable settlement: ${candidate.tool.name}`,
780
- );
781
- }
782
- return responded.result;
783
- }
784
- if (recordRecoveryAttempt) {
785
- const retry = piToolRetryPolicy(candidate);
786
- const firstAttempt = options.durability.appendToolInput({
787
- toolCallId,
788
- toolName: candidate.tool.name,
789
- input,
790
- retry,
791
- });
792
- if (!firstAttempt && retry === "non-idempotent") {
793
- throw new Error(
794
- `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
795
- );
796
- }
797
- }
798
- const execute = options.toolExecutors?.[candidate.tool.name] ??
799
- candidate.tool.execute;
800
- if (recordRecoveryAttempt || !toolCallIdPrefix) {
801
- return execute(toolCallId, input, signal, onUpdate);
802
- }
803
- const startedAt = Date.now();
804
- const telemetryToolCallId = toolCallId;
805
- options.onNestedToolStarted?.({
806
- parentToolCallId: toolCallIdPrefix,
807
- toolCallId: telemetryToolCallId,
808
- toolName: candidate.tool.name,
809
- input,
810
- occurredAt: startedAt,
811
- });
812
- try {
813
- const output = await execute(toolCallId, input, signal, onUpdate);
814
- const occurredAt = Date.now();
815
- options.onNestedToolFinished?.({
816
- parentToolCallId: toolCallIdPrefix,
817
- toolCallId: telemetryToolCallId,
818
- toolName: candidate.tool.name,
819
- outcome: "completed",
820
- durationMs: occurredAt - startedAt,
821
- output,
822
- occurredAt,
823
- });
824
- return output;
825
- } catch (error) {
826
- const occurredAt = Date.now();
827
- options.onNestedToolFinished?.({
828
- parentToolCallId: toolCallIdPrefix,
829
- toolCallId: telemetryToolCallId,
830
- toolName: candidate.tool.name,
831
- outcome: signal?.aborted ? "cancelled" : "failed",
832
- durationMs: occurredAt - startedAt,
833
- error,
834
- occurredAt,
835
- });
836
- throw error;
837
- }
838
- },
839
- },
840
- });
841
- this.candidates = state.candidates.map((candidate) => {
842
- if (candidate.tool.name !== "execute") return bindCandidate(candidate);
843
- const factory = state.snapshot.bindings.codeExecution;
844
- if (!factory) {
845
- throw new Error("Prepared Code Mode Tool requires a Runtime factory");
846
- }
847
- return bindCandidate({
848
- ...candidate,
849
- tool: {
850
- ...candidate.tool,
851
- execute: (toolCallId, input, signal, onUpdate) => {
852
- const runtimeCandidate = codeExecutionPiToolCandidate(
853
- factory.create(state.codeExecutionCandidates.map((inner) =>
854
- // Code Mode records its own connector calls. Pi persists the
855
- // non-idempotent parent execute attempt/result; recording an inner
856
- // input without a Pi settlement creates an orphan recovery
857
- // action that cannot be found on the direct Tool surface.
858
- bindCandidate(inner, toolCallId, false)
859
- )),
860
- );
861
- return runtimeCandidate.tool.execute(
862
- toolCallId,
863
- input,
864
- signal,
865
- onUpdate,
866
- );
867
- },
868
- },
869
- });
870
- });
871
- this.turn = new PiTurnAdapter({
872
- pi: {
873
- model: state.snapshot.pi.model,
874
- thinkingLevel: state.snapshot.pi.thinkingLevel,
875
- },
876
- models: dependencies.models,
877
- modelSessionId: options.modelSessionId,
878
- ...(options.onGeneration ? { onGeneration: options.onGeneration } : {}),
879
- canonicalMessages: options.canonicalMessages,
880
- // 被中断的 Turn 不写结算。它退场路上还会吐出「工具被中止」,而工具其实
881
- // 没有产生任何结果 —— 结算是单调的,落了这条假结果,重建出来的续跑会
882
- // 把一个从没跑过的工具当成跑过并失败。取消不走这里:取消要的就是终态。
883
- settle: (call) =>
884
- this.interrupted ? undefined : options.durability.settleTool(call),
885
- transformContext: options.transformContext,
886
- workspace: state.snapshot.bindings.workspace,
887
- modelTurnsConsumed: options.submission.modelTurnsConsumed ?? 0,
888
- });
889
- this.systemPrompt = descriptor.systemPrompt;
890
- this.options = options;
891
- }
892
-
893
- private readonly systemPrompt: string;
894
- private readonly options: CreatePreparedPiTurnOptions;
895
- private interrupted = false;
896
-
897
- /**
898
- * 请求取消这个 Turn。
899
- *
900
- * @remarks
901
- * 活动提交被中止时 Runtime 会调用它,包括仍在异步启动的阶段。
902
- * 同一取消信号覆盖历史读取、Loop 和工具重试。
903
- */
904
- abort(): void {
905
- this.abortController.abort();
906
- }
907
-
908
- /**
909
- * 中断这个 Turn,把它交给持久续跑接手。
910
- *
911
- * @remarks
912
- * 宿主在 park 期间换掉装配后调用。与 `abort` 的差别只有一处、但很要命:
913
- * 中断途中冒出来的工具结算一律丢弃。这条执行器的系统提示和工具表锁在它被
914
- * 创建的那一刻,接下来跑的是另一条按新装配重建的执行器,两边共用同一批
915
- * `toolCallId` —— 让退场路上的假结果先落盘,新那条就再也纠正不过来了。
916
- */
917
- interrupt(): void {
918
- this.interrupted = true;
919
- this.abort();
920
- }
921
-
922
- /**
923
- * 把一条用户消息加入当前 Turn。
924
- *
925
- * @remarks
926
- * 当前提交仍活动时收到消息,Runtime 的扩展宿主会调用它。
927
- * 启动前与运行中都进入同一队列,Loop 每次消费一条;空闲时调用方直接追加 transcript。
928
- */
929
- steer(
930
- message: UserMessage,
931
- messageId: string = crypto.randomUUID(),
932
- ): void {
933
- this.steerMessageIds.push(messageId);
934
- this.turn.steer(message);
935
- }
936
-
937
- /**
938
- * 按记录的名称重新执行 Pinned Runtime 中的 Tool candidate。
939
- *
940
- * @remarks
941
- * recovery effect 和审批通过后的续跑会传入原始调用标识与输入;返回 false 表示 Tool candidate 不存在或已被拒绝。
942
- * 先查 Pinned Runtime 的 Tool candidate 再走正常编译器,可防止恢复绕过授权、治理或持久化结算。
943
- */
944
- async retryTool(request: {
945
- readonly toolName: string;
946
- readonly toolCallId: string;
947
- readonly input: unknown;
948
- }): Promise<boolean> {
949
- const candidate = this.candidates.find(
950
- (item) => item.tool.name === request.toolName,
951
- );
952
- return candidate
953
- ? this.turn.retryTool(
954
- candidate,
955
- request.toolCallId,
956
- request.input,
957
- this.abortController.signal,
958
- )
959
- : false;
960
- }
961
-
962
- /**
963
- * 取回某个 Tool 的客户端结算声明,没有声明时返回 null。
964
- *
965
- * @remarks
966
- * `Runtime.respondToolInteraction` 在把客户端响应映射成 ToolResult 前调用它。
967
- *
968
- * 声明里含函数,无法持久化;从 Pinned Runtime 的 candidate 现取,
969
- * 可让「DO 一直醒着」和「park 期间睡过一觉」走同一条解析路径,而不是一条真解析、一条退化成缺省。
970
- */
971
- interactionSpec(toolName: string): PiToolInteractionSpec | null {
972
- return this.candidates.find(
973
- (item) => item.tool.name === toolName,
974
- )?.interaction ?? null;
975
- }
976
-
977
- /**
978
- * 运行这个 Turn,直到 Pi Loop 和所有等待中的事件回调结束。
979
- *
980
- * @remarks
981
- * Submission 首次执行和续跑时,Runtime 都会在可恢复聊天任务中调用它。
982
- * 适配器只传入 Pinned Runtime 的 system prompt 和 Tool candidate,再让所有事件经过 handleEvent,以保持 transcript 与流状态的顺序。
983
- */
984
- async run(
985
- options: PiPreparedTurnRunOptions,
986
- ): Promise<PiTurnRunResult> {
987
- this.terminalIntent = undefined;
988
- const signal = options.signal
989
- ? AbortSignal.any([options.signal, this.abortController.signal])
990
- : this.abortController.signal;
991
- const result = await this.turn.run(
992
- {
993
- systemPrompt: this.systemPrompt,
994
- tools: this.candidates,
995
- signal,
996
- },
997
- (event) => this.handleEvent(event, signal),
998
- );
999
- if (this.terminalIntent) {
1000
- await this.options.onTerminal(this.terminalIntent);
1001
- return { kind: "terminal", modelTurns: result.modelTurns };
1002
- }
1003
- return result;
1004
- }
1005
-
1006
- // 提交完成的 Pi 消息、推导终态意图,并把事件投影给可恢复流。
1007
- // Loop 等待每个事件的 Promise,canonical 提交失败时直接拒绝本次执行。
1008
- // canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
1009
- private async handleEvent(event: AgentEvent, signal: AbortSignal): Promise<void> {
1010
- if (
1011
- event.type === "tool_execution_start" &&
1012
- !this.candidates.some(({ tool }) => tool.name === event.toolName)
1013
- ) {
1014
- this.options.onUnknownToolStarted?.({
1015
- toolCallId: event.toolCallId,
1016
- toolName: event.toolName,
1017
- input: event.args,
1018
- occurredAt: Date.now(),
1019
- });
1020
- } else if (
1021
- event.type === "tool_execution_end" &&
1022
- !this.candidates.some(({ tool }) => tool.name === event.toolName)
1023
- ) {
1024
- this.options.onUnknownToolFinished?.({
1025
- toolCallId: event.toolCallId,
1026
- toolName: event.toolName,
1027
- outcome: "failed",
1028
- output: event.result,
1029
- occurredAt: Date.now(),
1030
- });
1031
- }
1032
- if (
1033
- event.type === "message_start" ||
1034
- event.type === "message_update" ||
1035
- event.type === "message_end"
1036
- ) {
1037
- normalizeToolCallArguments(event.message, this.candidates);
1038
- }
1039
- if (
1040
- event.type === "message_update" &&
1041
- event.assistantMessageEvent.type === "toolcall_end"
1042
- ) {
1043
- const call = event.assistantMessageEvent.toolCall;
1044
- call.arguments = prepareToolArguments(
1045
- call.name,
1046
- call.arguments,
1047
- this.candidates,
1048
- ) as Record<string, unknown>;
1049
- }
1050
- if (
1051
- event.type === "message_update" &&
1052
- event.assistantMessageEvent.type === "error" &&
1053
- (isContextOverflow(
1054
- event.assistantMessageEvent.error,
1055
- this.contextWindow,
1056
- ) || isModelStreamStallMessage(
1057
- event.assistantMessageEvent.error.errorMessage,
1058
- ) ||
1059
- isRecoverableAssistantError(event.assistantMessageEvent.error) ||
1060
- isOpenRouterServerToolTimeout(event.assistantMessageEvent.error))
1061
- ) {
1062
- return;
1063
- }
1064
- let projectedEvent = event;
1065
- if (event.type === "message_end") {
1066
- const message = event.message;
1067
- if (message.role === "assistant") {
1068
- if (message.stopReason === "deferred") throw new Error("Deferred Pi responses are not supported by this Runtime");
1069
- if (isContextOverflow(message, this.contextWindow)) {
1070
- throw new ContextOverflowError(
1071
- message.errorMessage ?? "Model context window overflow",
1072
- );
1073
- }
1074
- const stalled = message.stopReason === "error" &&
1075
- isModelStreamStallMessage(message.errorMessage);
1076
- const resetModelSession = isOpenRouterServerToolTimeout(message);
1077
- const retryable = isRecoverableAssistantError(message);
1078
- if (stalled || retryable || resetModelSession) {
1079
- const content = visibleAssistantContent(message);
1080
- if (!resetModelSession && content.length > 0) {
1081
- this.assistantOrdinal += 1;
1082
- await this.options.onCanonicalMessage({
1083
- kind: "commit-turn",
1084
- ordinal: this.assistantOrdinal,
1085
- message: {
1086
- ...message,
1087
- content,
1088
- },
1089
- });
1090
- }
1091
- if (stalled) {
1092
- throw new ChatStreamStalledError(message.errorMessage!);
1093
- }
1094
- if (resetModelSession) {
1095
- throw new RetryableModelSessionError(message.errorMessage);
1096
- }
1097
- throw new RetryableModelError(message.errorMessage);
1098
- }
1099
- }
1100
- if (message.role === "user") {
1101
- await this.options.onCanonicalMessage({
1102
- kind: "append-user",
1103
- id: this.steerMessageIds.shift() ?? crypto.randomUUID(),
1104
- message,
1105
- });
1106
- } else if (
1107
- message.role === "assistant" ||
1108
- message.role === "toolResult"
1109
- ) {
1110
- const abortReason = this.options.abortReason?.() ?? (signal.aborted ? "Turn aborted" : undefined);
1111
- const authoritativeMessage =
1112
- message.role === "assistant" &&
1113
- message.stopReason !== "toolUse" &&
1114
- abortReason
1115
- ? {
1116
- ...message,
1117
- content: visibleAssistantContent(message),
1118
- stopReason: "aborted" as const,
1119
- errorMessage: abortReason,
1120
- }
1121
- : message;
1122
- if (authoritativeMessage.role === "assistant") {
1123
- this.assistantOrdinal += 1;
1124
- }
1125
- await this.options.onCanonicalMessage({
1126
- kind: "commit-turn",
1127
- ordinal: this.assistantOrdinal,
1128
- message: authoritativeMessage,
1129
- });
1130
- projectedEvent = {
1131
- ...event,
1132
- message: authoritativeMessage,
1133
- };
1134
- if (
1135
- authoritativeMessage.role === "assistant" &&
1136
- authoritativeMessage.stopReason !== "toolUse"
1137
- ) {
1138
- const terminal = classifyPiTerminalStopReason(
1139
- authoritativeMessage.stopReason,
1140
- );
1141
- this.terminalIntent = {
1142
- outcome: terminal.outcome,
1143
- ...(authoritativeMessage.errorMessage
1144
- ? { message: authoritativeMessage.errorMessage }
1145
- : terminal.message
1146
- ? { message: terminal.message }
1147
- : {}),
1148
- };
1149
- }
1150
- }
1151
- }
1152
-
1153
- await this.publishChunks(this.encoder.encode(projectedEvent));
1154
- }
1155
-
1156
- private async publishChunks(chunks: readonly UIMessageChunk[]): Promise<void> {
1157
- for (const chunk of chunks) {
1158
- await this.options.onRecord(chunk);
1159
- }
1160
- }
1161
- }
1162
-
1163
- // #endregion