@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.2
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.
- package/package.json +3 -3
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1592 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/{db → adapter/cloudflare}/schema.ts +16 -83
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +182 -0
- package/src/config/index.ts +14 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
- package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +98 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +76 -10
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +175 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +481 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/mcp.ts +1 -1
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/submission/admission.ts +275 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +62 -119
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
- package/src/tools/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
- package/src/{pi/tool → tools}/base.ts +14 -30
- package/src/tools/compiler.ts +327 -0
- package/src/{pi/tool → tools}/core-host.ts +3 -3
- package/src/{pi/tool → tools}/core.ts +3 -3
- package/src/tools/decision-gate.ts +227 -0
- package/src/{pi/tool → tools}/gateway.ts +1 -1
- package/src/tools/human-decisions.ts +127 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/interaction/index.ts +1 -0
- package/src/{pi/tool → tools}/mcp.ts +1 -1
- package/src/{pi/tool → tools}/schedule.ts +3 -3
- package/src/{pi/tool → tools}/skill.ts +4 -4
- package/src/{kernel → tools}/subagent-runtime.ts +1 -1
- package/src/{pi/tool → tools}/subagent.ts +4 -4
- package/src/tools/temporary-agent/approvals.ts +126 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
- package/src/tools/temporary-agent/core.ts +53 -0
- package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
- package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
- package/src/{pi/tool → tools}/web-search/api.ts +1 -1
- package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
- package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
- package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- /package/src/{kernel → config}/degradation.ts +0 -0
- /package/src/{kernel → config}/runtime-config.ts +0 -0
- /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- /package/src/{pi/tool → tools}/declared.ts +0 -0
- /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
- /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
- /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
- /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
- /package/src/{pi/tool → tools}/time.ts +0 -0
- /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
- /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
|
-
|
|
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 "
|
|
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 "
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
69
|
+
* 读取当前 Session entry ID 到 Submission ID 的关联。
|
|
77
70
|
*
|
|
78
|
-
* `
|
|
71
|
+
* `storedEntries` 用它补齐 Pi Session 本身不保存的 Runtime Submission 归属。
|
|
79
72
|
*/
|
|
80
|
-
|
|
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:
|
|
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
|
-
* `
|
|
201
|
+
* `AgentRuntimeKernel` 在构造期间创建它,Runtime 随后把消息投影、恢复读取和历史导入导出收口到这里。
|
|
193
202
|
*
|
|
194
203
|
* Pi Session 保存规范分支,Runtime 数据库保存 Submission、审批和用户 sidecar;分开保存可避免把 UI 字段写进模型上下文。
|
|
195
204
|
*/
|
|
196
|
-
export class
|
|
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
|
-
* `
|
|
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
|
-
}
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
|
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:
|
|
576
|
+
id: stableMessageId,
|
|
659
577
|
createdAt: entry.createdAt,
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
:
|
|
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
|
|
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 = {
|
|
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(
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
return this.
|
|
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:
|
|
861
|
-
submissionId:
|
|
819
|
+
id: stored.id,
|
|
820
|
+
submissionId: stored.submissionId,
|
|
862
821
|
message: entry.message,
|
|
863
|
-
createdAt:
|
|
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
|
|