@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,18 +1,14 @@
1
- import type { SnapshotMessage } from "agents/chat";
2
1
  import type {
3
2
  AgentMessage,
4
- CustomMessage,
3
+ Entry,
4
+ LaneSnapshot,
5
5
  } from "@earendil-works/pi-agent-core";
6
6
  import type {
7
- Api,
8
7
  AssistantMessage,
9
- Model,
10
- Models,
11
8
  ToolResultMessage,
12
9
  UserMessage,
13
10
  } from "@earendil-works/pi-ai";
14
11
  import type { UIMessage } from "ai";
15
- import { compactPiContext } from "../assembly";
16
12
  import {
17
13
  applyPiToolResult,
18
14
  captureUIUserSidecar,
@@ -20,15 +16,12 @@ import {
20
16
  publicAssistantError,
21
17
  type PiToolApprovalView,
22
18
  type UIUserSidecar,
23
- } from "../message";
19
+ } from "./message";
24
20
  import { openPiSessionStorage, type CloudflareSessionStorage } from "../session";
25
- import type { PiCanonicalMessageCommit } from "./execution";
26
- import type { PiRecoveredToolSettlement } from "./recovery";
27
- import type { RuntimeModelUsageEvent } from "../../kernel/bindings";
28
21
  import {
29
22
  isModelStreamStallMessage,
30
23
  isRecoverableAssistantError,
31
- } from "./models";
24
+ } from "../models";
32
25
 
33
26
  /**
34
27
  * 本文件负责保存 Pi 的规范消息,并按浏览器或 Extension Host 的需要投影历史。
@@ -43,7 +36,7 @@ import {
43
36
  * 定义 Transcript 读写持久层所需的最小能力。
44
37
  *
45
38
  * @remarks
46
- * `PiRuntimeAdapter.createTranscript` 接收该端口,`AgentRuntimeKernel` 在构造时用 Runtime 数据库实现它。
39
+ * `AgentRuntimeKernel` 在构造 `ConversationHistory` 时用 Runtime 数据库实现该端口。
47
40
  *
48
41
  * 端口只暴露投影和旁路数据所需的操作,避免 Transcript 直接依赖具体 Repository。
49
42
  */
@@ -73,11 +66,11 @@ export interface PiTranscriptDurability {
73
66
  */
74
67
  clearUserSidecars(): void;
75
68
  /**
76
- * 读取当前消息 ID 到 Submission ID 的关联。
69
+ * 读取当前 Session entry ID 到 Submission ID 的关联。
77
70
  *
78
- * `storedMessages` 用它补齐 Pi Session 本身不保存的 Runtime Submission 归属。
71
+ * `storedEntries` 用它补齐 Pi Session 本身不保存的 Runtime Submission 归属。
79
72
  */
80
- listMessageSubmissionLinks(): Map<string, string>;
73
+ listSessionEntrySubmissionLinks(): Map<string, string>;
81
74
  /**
82
75
  * 读取一个 Submission 的浏览器投影字段。
83
76
  *
@@ -85,6 +78,7 @@ export interface PiTranscriptDurability {
85
78
  */
86
79
  findSubmissionProjection(submissionId: string): {
87
80
  assistantMessageId: string;
81
+ userMessageId?: string | null;
88
82
  createdAt: number;
89
83
  completedAt?: number | null;
90
84
  status?: unknown;
@@ -117,6 +111,13 @@ export interface StoredTranscriptMessage {
117
111
  createdAt: number;
118
112
  }
119
113
 
114
+ export interface StoredSessionEntry {
115
+ id: string;
116
+ submissionId: string | null;
117
+ entry: Entry;
118
+ createdAt: number;
119
+ }
120
+
120
121
  export interface PiCanonicalTranscriptSnapshotEntry {
121
122
  readonly id: string;
122
123
  readonly message: AgentMessage;
@@ -140,6 +141,7 @@ function userUIMessage(
140
141
  options: {
141
142
  id: string;
142
143
  createdAt: number;
144
+ turnId?: string;
143
145
  sidecar?: UIUserSidecar;
144
146
  },
145
147
  ): UIMessage {
@@ -160,12 +162,19 @@ function userUIMessage(
160
162
  return [];
161
163
  },
162
164
  );
165
+ const sidecarMetadata = options.sidecar?.metadata !== null &&
166
+ typeof options.sidecar?.metadata === "object" &&
167
+ !Array.isArray(options.sidecar.metadata)
168
+ ? options.sidecar.metadata as Record<string, unknown>
169
+ : {};
163
170
  return {
164
171
  id: options.id,
165
172
  role: "user",
166
173
  parts,
167
- metadata: options.sidecar?.metadata ?? {
174
+ metadata: {
168
175
  createdAt: message.timestamp || options.createdAt,
176
+ ...sidecarMetadata,
177
+ ...(options.turnId ? { turnId: options.turnId } : {}),
169
178
  },
170
179
  };
171
180
  }
@@ -189,11 +198,11 @@ function hostText(message: UserMessage | AssistantMessage): string {
189
198
  * 管理一个 Runtime 实例的 Pi 规范 Transcript 及其对外投影。
190
199
  *
191
200
  * @remarks
192
- * `PiRuntimeAdapter.createTranscript` 在 `AgentRuntimeKernel` 构造期间创建它,Runtime 随后把所有消息提交、恢复、压缩和历史读取收口到这里。
201
+ * `AgentRuntimeKernel` 在构造期间创建它,Runtime 随后把消息投影、恢复读取和历史导入导出收口到这里。
193
202
  *
194
203
  * Pi Session 保存规范分支,Runtime 数据库保存 Submission、审批和用户 sidecar;分开保存可避免把 UI 字段写进模型上下文。
195
204
  */
196
- export class PiRuntimeTranscript {
205
+ export class ConversationHistory {
197
206
  private readonly store: CloudflareSessionStorage;
198
207
 
199
208
  private modelBranch() {
@@ -205,17 +214,38 @@ export class PiRuntimeTranscript {
205
214
  );
206
215
  }
207
216
 
217
+ /** Resolve a browser-stable user ID to the Session entry generated by Harness. */
218
+ private storedMessageId(id: string): string {
219
+ const direct = this.store.entry(id);
220
+ if (direct?.type === "message") return id;
221
+ const submissionByMessage =
222
+ this.durability.listSessionEntrySubmissionLinks();
223
+ return this.store.branch().find((entry) => {
224
+ if (entry.type !== "message" || entry.message.role !== "user") {
225
+ return false;
226
+ }
227
+ const body = this.durability.readUserSidecar(entry.id);
228
+ if (body) {
229
+ const sidecar = JSON.parse(body) as UIUserSidecar & { id?: unknown };
230
+ if (sidecar.id === id) return true;
231
+ }
232
+ const submissionId = submissionByMessage.get(entry.id);
233
+ return submissionId !== undefined &&
234
+ this.durability.findSubmissionProjection(submissionId)?.userMessageId ===
235
+ id;
236
+ })?.id ?? id;
237
+ }
238
+
208
239
  /**
209
240
  * 用当前 Agent 的 SQLite、Runtime 持久化端口和 Pi 模型表创建 Transcript。
210
241
  *
211
- * `PiRuntimeAdapter.createTranscript` 每个 Runtime Kernel 构造时调用一次;`hasActiveTurn` 必须反映当前 Submission 生命周期。
242
+ * `AgentRuntimeKernel` 每个实例构造时调用一次;`hasActiveTurn` 必须反映当前 Submission 生命周期。
212
243
  *
213
244
  * 官方 Storage 契约由同一 DO SQLite 实现;模型目录用于压缩,活动 Turn 检查阻止并发导入。
214
245
  */
215
246
  constructor(
216
247
  storage: DurableObjectStorage,
217
248
  private readonly durability: PiTranscriptDurability,
218
- private readonly models: Models,
219
249
  private readonly hasActiveTurn: () => boolean,
220
250
  ) {
221
251
  this.store = openPiSessionStorage(storage);
@@ -252,55 +282,7 @@ export class PiRuntimeTranscript {
252
282
  return inserted;
253
283
  }
254
284
 
255
- /**
256
- * 保存一次 Turn 产生的助手消息或工具结果。
257
- *
258
- * `commitAdapterMessage` 在执行适配器发出 `commit-turn` 时调用,并传入同一 Submission 内递增的助手序号。
259
- *
260
- * 工具结果用 toolCallId 去重,助手步骤用 ordinal 区分;这两种 ID 形状还被恢复写入复用,不能单边修改。
261
- */
262
- commitTurnMessage(
263
- submissionId: string,
264
- message: AssistantMessage | ToolResultMessage,
265
- ordinal: number,
266
- ): { id: string; inserted: boolean } {
267
- const id = message.role === "toolResult"
268
- ? `${submissionId}:tool:${message.toolCallId}`
269
- : `${submissionId}:assistant:${ordinal}`;
270
- return {
271
- id,
272
- inserted: this.append(id, message, {
273
- submissionId,
274
- }),
275
- };
276
- }
277
-
278
- /**
279
- * 把执行适配器发出的规范提交路由到对应写入路径。
280
- *
281
- * `AgentRuntimeKernel` 的 `onCanonicalMessage` 回调在数据库事务内调用它;用户 steering 使用自带 ID,其余 Turn 消息使用统一生成规则。
282
- *
283
- * 分流集中在这里可让实时执行和 Transcript 的消息身份规则保持一致。
284
- */
285
- commitAdapterMessage(
286
- submissionId: string,
287
- commit: PiCanonicalMessageCommit,
288
- ): { id: string; inserted: boolean } {
289
- return commit.kind === "append-user"
290
- ? {
291
- id: commit.id,
292
- inserted: this.append(commit.id, commit.message, {
293
- submissionId,
294
- }),
295
- }
296
- : this.commitTurnMessage(
297
- submissionId,
298
- commit.message,
299
- commit.ordinal,
300
- );
301
- }
302
-
303
- /** Stable identity makes terminal recovery replay idempotent. */
285
+ /** Persist a stable marker so browser history can distinguish a user stop from other aborts. */
304
286
  appendTurnAbortedMarker(
305
287
  submissionId: string,
306
288
  timestamp: number,
@@ -318,55 +300,7 @@ export class PiRuntimeTranscript {
318
300
  }],
319
301
  display: true,
320
302
  timestamp,
321
- } satisfies CustomMessage, { submissionId });
322
- }
323
-
324
- /**
325
- * 把恢复流程重建出的工具终态补写进规范 Transcript。
326
- *
327
- * `materializeRecoveredToolResults` 在恢复决策补齐 settlement 后调用,并先按原工具调用顺序排序。
328
- *
329
- * ID 与实时工具结果使用相同的 `submissionId + toolCallId` 形状,因此重复恢复只会命中 `append` 的幂等检查。
330
- */
331
- appendRecoveredToolSettlement(
332
- submissionId: string,
333
- settlement: PiRecoveredToolSettlement,
334
- ): boolean {
335
- return this.append(
336
- `${submissionId}:tool:${settlement.message.toolCallId}`,
337
- settlement.message,
338
- {
339
- submissionId,
340
- },
341
- );
342
- }
343
-
344
- /**
345
- * 按当前规范分支中工具调用出现的顺序排列恢复结果。
346
- *
347
- * `materializeRecoveredToolResults` 在批量补写前调用;调用方应使用返回的新数组,不依赖输入数组被修改。
348
- *
349
- * 顺序从助手消息里的 toolCall ID 推导,避免恢复里程碑的读取顺序改变模型看到的工具结果顺序。
350
- */
351
- orderRecoveredToolSettlements(
352
- settlements: readonly PiRecoveredToolSettlement[],
353
- ): PiRecoveredToolSettlement[] {
354
- const callOrder = new Map<string, number>();
355
- for (const entry of this.store.branch()) {
356
- if (entry.type !== "message") continue;
357
- const message = entry.message;
358
- if (message.role !== "assistant") continue;
359
- for (const part of message.content) {
360
- if (part.type === "toolCall" && !callOrder.has(part.id)) {
361
- callOrder.set(part.id, callOrder.size);
362
- }
363
- }
364
- }
365
- return [...settlements].sort(
366
- (left, right) =>
367
- (callOrder.get(left.message.toolCallId) ?? Number.MAX_SAFE_INTEGER) -
368
- (callOrder.get(right.message.toolCallId) ?? Number.MAX_SAFE_INTEGER),
369
- );
303
+ }, { submissionId });
370
304
  }
371
305
 
372
306
  /**
@@ -382,20 +316,6 @@ export class PiRuntimeTranscript {
382
316
  );
383
317
  }
384
318
 
385
- /**
386
- * 生成可恢复流只需要的消息 ID 与角色快照。
387
- *
388
- * `executeSubmission` 启动 `runRecoverableChatFiber` 前调用,并把结果写进 Fiber 快照。
389
- *
390
- * 快照不复制消息正文,正文仍以耐久 Transcript 为准,避免维护第二份完整历史。
391
- */
392
- async snapshotMessages(): Promise<SnapshotMessage[]> {
393
- return (await this.storedMessages()).map((entry) => ({
394
- id: entry.id,
395
- role: entry.message.role,
396
- }));
397
- }
398
-
399
319
  /**
400
320
  * 按 ID 查找一条规范用户消息。
401
321
  *
@@ -404,7 +324,7 @@ export class PiRuntimeTranscript {
404
324
  * 同时检查 entry 类型和消息角色,防止其他 Session entry 或助手消息被当作 regenerate 起点。
405
325
  */
406
326
  async findUserMessage(id: string): Promise<UserMessage | undefined> {
407
- const entry = this.store.entry(id);
327
+ const entry = this.store.entry(this.storedMessageId(id));
408
328
  return entry?.type === "message" && entry.message.role === "user"
409
329
  ? entry.message
410
330
  : undefined;
@@ -414,85 +334,13 @@ export class PiRuntimeTranscript {
414
334
 
415
335
  // #region 分支、压缩与导入
416
336
 
417
- /**
418
- * 把当前 Pi 分支叶子移动到指定 Session entry。
419
- *
420
- * regenerate admission 在验证原用户消息后调用,使后续写入从该用户消息继续。
421
- *
422
- * 这里委托同步 Session 存储辅助函数写入叶子标记,不删除原分支,保留 Pi 的分支结构。
423
- */
424
- moveLeaf(id: string): void {
425
- this.store.moveBranchTip(id);
426
- }
427
-
428
- /**
429
- * 在 Pi 请求模型前按 Runtime 阈值压缩当前上下文,并把压缩 entry 写回 Session。
430
- *
431
- * `AgentRuntimeKernel.transformPiContext` 作为 Pi 的 `transformContext` 调用它,并提供本次固定的模型和取消信号。
432
- *
433
- * 方法先逐条核对调用参数与耐久分支;不一致或压缩不可用时保留原消息,避免把漂移的内存上下文提交成新的规范分支。
434
- */
435
- async compactContext(
436
- messages: AgentMessage[],
437
- options: {
438
- compactAfterTokens: number;
439
- force?: boolean;
440
- model: Model<Api>;
441
- signal?: AbortSignal;
442
- submissionId?: string;
443
- onCompactionUsage?: (event: RuntimeModelUsageEvent, didCompact: boolean) => void;
444
- },
445
- ): Promise<AgentMessage[]> {
446
- const branch = this.modelBranch();
447
- const current = branch.flatMap((entry) =>
448
- entry.type === "message" ? [entry.message] : []
449
- );
450
- if (
451
- current.length !== messages.length ||
452
- current.some((message, index) =>
453
- JSON.stringify(message) !== JSON.stringify(messages[index])
454
- )
455
- ) {
456
- if (options.force) {
457
- throw new Error("Forced context compaction requires canonical messages");
458
- }
459
- console.warn(
460
- "[runtime-compaction:degraded]",
461
- JSON.stringify({ reason: "canonical-message-mismatch" }),
462
- );
463
- return messages;
464
- }
465
- const result = await compactPiContext({
466
- branch,
467
- compactAfterTokens: options.compactAfterTokens,
468
- force: options.force,
469
- models: this.models,
470
- model: options.model,
471
- signal: options.signal,
472
- });
473
- if (result.compactionEntry || result.modelUsage) {
474
- this.durability.transaction(() => {
475
- const eventId = this.store.recordCompaction(result.compactionEntry, result.modelUsage?.usage);
476
- if (options.submissionId && options.onCompactionUsage && result.modelUsage) {
477
- options.onCompactionUsage({
478
- eventId,
479
- submissionId: options.submissionId,
480
- kind: "compaction",
481
- ...result.modelUsage,
482
- }, result.didCompact);
483
- }
484
- });
485
- }
486
- if (options.force && !result.didCompact) {
487
- throw new Error("Forced context compaction did not reduce the model input");
488
- }
489
- if (result.degraded) {
490
- console.warn(
491
- "[runtime-compaction:degraded]",
492
- JSON.stringify({ reason: "compaction-unavailable" }),
493
- );
337
+ /** Resolve the branch position before the user prompt that will be regenerated. */
338
+ regenerationTarget(id: string): string | null {
339
+ const entry = this.store.entry(this.storedMessageId(id));
340
+ if (entry?.type !== "message" || entry.message.role !== "user") {
341
+ throw new Error(`Unknown regeneration user message: ${id}`);
494
342
  }
495
- return result.messages;
343
+ return entry.parentId;
496
344
  }
497
345
 
498
346
  /**
@@ -541,13 +389,17 @@ export class PiRuntimeTranscript {
541
389
  throughMessageId: string,
542
390
  ): Promise<PiCanonicalTranscriptSnapshot> {
543
391
  const stored = await this.storedMessages();
544
- let cutoff = stored.findIndex((entry) => entry.id === throughMessageId);
392
+ const storedId = this.storedMessageId(throughMessageId);
393
+ let cutoff = stored.findIndex((entry) => entry.id === storedId);
545
394
  if (cutoff < 0) {
546
- const targetSubmissionId = stored.find((entry) =>
547
- entry.submissionId &&
548
- this.durability.findSubmissionProjection(entry.submissionId)
549
- ?.assistantMessageId === throughMessageId
550
- )?.submissionId;
395
+ const targetSubmissionId = stored.find((entry) => {
396
+ if (!entry.submissionId) return false;
397
+ const submission = this.durability.findSubmissionProjection(
398
+ entry.submissionId,
399
+ );
400
+ return submission?.assistantMessageId === throughMessageId ||
401
+ submission?.userMessageId === throughMessageId;
402
+ })?.submissionId;
551
403
  if (targetSubmissionId) {
552
404
  for (let index = stored.length - 1; index >= 0; index -= 1) {
553
405
  if (stored[index]?.submissionId === targetSubmissionId) {
@@ -606,9 +458,54 @@ export class PiRuntimeTranscript {
606
458
  * 多个助手步骤用现有的 `step-start` 保留边界,工具结果回填原 toolCall,避免浏览器维护另一套 Turn 拼装逻辑。
607
459
  */
608
460
  async browserMessages(): Promise<UIMessage[]> {
461
+ return this.projectBrowserMessages(await this.storedMessages());
462
+ }
463
+
464
+ /** Reconnect uses Pi's complete lane view, including uncommitted model/tool progress. */
465
+ async browserSnapshot(snapshot: LaneSnapshot): Promise<UIMessage[]> {
466
+ const entries = await this.storedMessages(snapshot.transcript);
467
+ const operation = snapshot.operation;
468
+ const streaming = operation?.streamingMessage;
469
+ const streamingId = streaming ? `streaming:${operation.id}` : undefined;
470
+ if (streaming && streamingId) {
471
+ entries.push({
472
+ id: streamingId,
473
+ submissionId: operation.id,
474
+ message: streaming,
475
+ createdAt: streaming.timestamp,
476
+ });
477
+ }
478
+ let messages = await this.projectBrowserMessages(entries, streamingId);
479
+ for (const tool of operation?.runningTools ?? []) {
480
+ const toolResult = tool.result;
481
+ if (!toolResult) continue;
482
+ messages = messages.map((message) => {
483
+ const metadata = message.metadata as { turnId?: string } | undefined;
484
+ if (message.role !== "assistant" || metadata?.turnId !== operation?.id) {
485
+ return message;
486
+ }
487
+ return applyPiToolResult(message, {
488
+ role: "toolResult",
489
+ toolCallId: tool.toolCallId,
490
+ toolName: tool.toolName,
491
+ ...toolResult,
492
+ content: toolResult.content ?? [],
493
+ isError: tool.status === "settled" && tool.isError,
494
+ timestamp: snapshot.operation!.startedAt,
495
+ }, { preliminary: tool.status === "running" });
496
+ });
497
+ }
498
+ return messages;
499
+ }
500
+
501
+ private async projectBrowserMessages(
502
+ entries: readonly StoredTranscriptMessage[],
503
+ streamingId?: string,
504
+ ): Promise<UIMessage[]> {
609
505
  const result: UIMessage[] = [];
610
506
  const seenAssistantSubmissions = new Set<string>();
611
- const entries = await this.storedMessages();
507
+ const seenUserSubmissions = new Set<string>();
508
+ const userSubmissionByMessageId = new Map<string, string>();
612
509
  const assistantSubmissions = new Set(entries.flatMap((entry) =>
613
510
  entry.submissionId && entry.message.role === "assistant"
614
511
  ? [entry.submissionId]
@@ -620,7 +517,8 @@ export class PiRuntimeTranscript {
620
517
  const submissionId = pendingFailedSubmissionId;
621
518
  pendingFailedSubmissionId = undefined;
622
519
  const submission = this.durability.findSubmissionProjection(submissionId);
623
- if (submission?.status !== "error") return;
520
+ if (!submission || (submission.status !== "error" &&
521
+ !(submission.status === "skipped" && submission.error))) return;
624
522
  const completedAt = submission.completedAt ?? submission.createdAt;
625
523
  result.push({
626
524
  id: submission.assistantMessageId,
@@ -628,6 +526,9 @@ export class PiRuntimeTranscript {
628
526
  parts: [],
629
527
  metadata: {
630
528
  turnId: submissionId,
529
+ ...(submission.userMessageId
530
+ ? { userMessageId: submission.userMessageId }
531
+ : {}),
631
532
  createdAt: submission.createdAt,
632
533
  completedAt,
633
534
  turnDurationMs: Math.max(
@@ -654,13 +555,33 @@ export class PiRuntimeTranscript {
654
555
  }
655
556
  if (entry.message.role === "user") {
656
557
  const body = this.durability.readUserSidecar(entry.id);
558
+ const sidecar = body
559
+ ? JSON.parse(body) as UIUserSidecar & { id?: unknown }
560
+ : undefined;
561
+ const submission = entry.submissionId
562
+ ? this.durability.findSubmissionProjection(entry.submissionId)
563
+ : null;
564
+ const alreadySeen = entry.submissionId
565
+ ? seenUserSubmissions.has(entry.submissionId)
566
+ : false;
567
+ const sidecarId = typeof sidecar?.id === "string"
568
+ ? sidecar.id
569
+ : undefined;
570
+ const stableMessageId = sidecarId
571
+ ? sidecarId
572
+ : !alreadySeen && submission?.userMessageId
573
+ ? submission.userMessageId
574
+ : entry.id;
657
575
  result.push(userUIMessage(entry.message, {
658
- id: entry.id,
576
+ id: stableMessageId,
659
577
  createdAt: entry.createdAt,
660
- sidecar: body
661
- ? JSON.parse(body) as UIUserSidecar
662
- : undefined,
578
+ turnId: entry.submissionId ?? undefined,
579
+ sidecar,
663
580
  }));
581
+ if (entry.submissionId) {
582
+ seenUserSubmissions.add(entry.submissionId);
583
+ userSubmissionByMessageId.set(stableMessageId, entry.submissionId);
584
+ }
664
585
  if (
665
586
  entry.submissionId &&
666
587
  !assistantSubmissions.has(entry.submissionId) &&
@@ -681,11 +602,18 @@ export class PiRuntimeTranscript {
681
602
  );
682
603
  const index = activeAssistant?.submissionId === entry.submissionId
683
604
  ? activeAssistant.index
684
- : undefined;
605
+ : result.findIndex((message) =>
606
+ message.role === "assistant" &&
607
+ (message.metadata as { turnId?: string } | undefined)?.turnId ===
608
+ entry.submissionId
609
+ );
685
610
  const createdAt = submission?.createdAt ?? entry.createdAt;
686
611
  const completedAt = submission?.completedAt ?? entry.createdAt;
687
612
  const metadata = {
688
613
  turnId: entry.submissionId,
614
+ ...(submission?.userMessageId
615
+ ? { userMessageId: submission.userMessageId }
616
+ : {}),
689
617
  createdAt,
690
618
  completedAt,
691
619
  turnDurationMs: Math.max(
@@ -695,7 +623,7 @@ export class PiRuntimeTranscript {
695
623
  turnStatus: "aborted",
696
624
  interruptedByUser: true,
697
625
  };
698
- if (index === undefined) {
626
+ if (index < 0) {
699
627
  seenAssistantSubmissions.add(entry.submissionId);
700
628
  result.push({
701
629
  id: submission?.assistantMessageId ?? entry.id,
@@ -703,7 +631,10 @@ export class PiRuntimeTranscript {
703
631
  parts: [],
704
632
  metadata,
705
633
  });
706
- activeAssistant = { submissionId: entry.submissionId, index: result.length - 1 };
634
+ activeAssistant = {
635
+ submissionId: entry.submissionId,
636
+ index: result.length - 1,
637
+ };
707
638
  } else {
708
639
  const previous = result[index]!;
709
640
  result[index] = {
@@ -713,6 +644,7 @@ export class PiRuntimeTranscript {
713
644
  ...metadata,
714
645
  },
715
646
  };
647
+ activeAssistant = { submissionId: entry.submissionId, index };
716
648
  }
717
649
  // A stream may settle after the stop marker. Keep its existing UI identity
718
650
  // until the next user message rather than creating a second assistant.
@@ -736,6 +668,9 @@ export class PiRuntimeTranscript {
736
668
  : submission?.assistantMessageId ?? entry.id,
737
669
  metadata: {
738
670
  turnId: entry.submissionId,
671
+ ...(submission?.userMessageId
672
+ ? { userMessageId: submission.userMessageId }
673
+ : {}),
739
674
  createdAt: submission?.createdAt ?? entry.createdAt,
740
675
  completedAt,
741
676
  ...(completedAt === undefined
@@ -752,6 +687,7 @@ export class PiRuntimeTranscript {
752
687
  : {}),
753
688
  },
754
689
  approvals: this.approvalViews(entry.submissionId),
690
+ state: entry.id === streamingId ? "streaming" : "done",
755
691
  });
756
692
  if (existingIndex === undefined) {
757
693
  activeAssistant = {
@@ -793,6 +729,28 @@ export class PiRuntimeTranscript {
793
729
  }
794
730
  }
795
731
  flushFailedSubmission();
732
+ for (let index = 0; index < result.length; index += 1) {
733
+ const message = result[index]!;
734
+ if (message.role !== "assistant") continue;
735
+ const metadata = message.metadata as {
736
+ turnId?: string;
737
+ turnStatus?: string;
738
+ } | undefined;
739
+ if (metadata?.turnStatus !== "aborted" || !metadata.turnId) continue;
740
+ let lastSteerIndex = index;
741
+ for (let candidate = index + 1; candidate < result.length; candidate += 1) {
742
+ const following = result[candidate]!;
743
+ if (
744
+ following.role === "user" &&
745
+ userSubmissionByMessageId.get(following.id) === metadata.turnId
746
+ ) {
747
+ lastSteerIndex = candidate;
748
+ }
749
+ }
750
+ if (lastSteerIndex === index) continue;
751
+ result.splice(index, 1);
752
+ result.splice(lastSteerIndex, 0, message);
753
+ }
796
754
  return result;
797
755
  }
798
756
 
@@ -851,20 +809,35 @@ export class PiRuntimeTranscript {
851
809
  *
852
810
  * UI uses the message event time; Pi entry timestamps describe storage commits.
853
811
  */
854
- async storedMessages(): Promise<StoredTranscriptMessage[]> {
855
- const submissionByMessage =
856
- this.durability.listMessageSubmissionLinks();
857
- return this.store.branch().flatMap((entry) => {
812
+ async storedMessages(
813
+ snapshotEntries: readonly Entry[] = this.store.branch(),
814
+ ): Promise<StoredTranscriptMessage[]> {
815
+ return (await this.storedEntries(snapshotEntries)).flatMap((stored) => {
816
+ const { entry } = stored;
858
817
  if (entry.type !== "message") return [];
859
818
  return [{
860
- id: entry.id,
861
- submissionId: submissionByMessage.get(entry.id) ?? null,
819
+ id: stored.id,
820
+ submissionId: stored.submissionId,
862
821
  message: entry.message,
863
- createdAt: entry.message.timestamp,
822
+ createdAt: stored.createdAt,
864
823
  }];
865
824
  });
866
825
  }
867
826
 
827
+ /** Read every Session entry with its recoverable Submission association. */
828
+ async storedEntries(
829
+ snapshotEntries: readonly Entry[] = this.store.branch(),
830
+ ): Promise<StoredSessionEntry[]> {
831
+ const submissionByEntry =
832
+ this.durability.listSessionEntrySubmissionLinks();
833
+ return snapshotEntries.map((entry) => ({
834
+ id: entry.id,
835
+ submissionId: submissionByEntry.get(entry.id) ?? null,
836
+ entry,
837
+ createdAt: entry.timestamp,
838
+ }));
839
+ }
840
+
868
841
  // #endregion
869
842
  }
870
843
 
@@ -0,0 +1,6 @@
1
+ export * from "./history";
2
+ export * from "./harness-stream";
3
+ export * from "./harness-resume";
4
+ export * from "./message";
5
+ export * from "./repository";
6
+ export * from "./state";
@@ -1,4 +1,7 @@
1
1
  import type { UIMessage } from "ai";
2
+ import type { UserMessage } from "@earendil-works/pi-ai";
3
+
4
+ export type PiCanonicalUserInput = UserMessage;
2
5
 
3
6
  export type UIChatTrigger = "submit-message" | "regenerate-message";
4
7