@springbrand/agent-runtime 0.2.0-alpha.68 → 0.2.0-alpha.69

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.
@@ -1,4 +1,4 @@
1
- import type { SnapshotMessage, SqlTaggedTemplate } from "agents/chat";
1
+ import type { SnapshotMessage } from "agents/chat";
2
2
  import type {
3
3
  AgentMessage,
4
4
  CustomMessage,
@@ -21,12 +21,7 @@ import {
21
21
  type PiToolApprovalView,
22
22
  type UIUserSidecar,
23
23
  } from "../message";
24
- import {
25
- appendSessionMessageSync,
26
- clearSessionSync,
27
- PiTranscriptStore,
28
- moveSessionLeafSync,
29
- } from "../session";
24
+ import { openPiSessionStorage, type CloudflareSessionStorage } from "../session";
30
25
  import type { PiCanonicalMessageCommit } from "./execution";
31
26
  import type { PiRecoveredToolSettlement } from "./recovery";
32
27
  import type { RuntimeModelUsageEvent } from "../../kernel/bindings";
@@ -199,7 +194,7 @@ function hostText(message: UserMessage | AssistantMessage): string {
199
194
  * Pi Session 保存规范分支,Runtime 数据库保存 Submission、审批和用户 sidecar;分开保存可避免把 UI 字段写进模型上下文。
200
195
  */
201
196
  export class PiRuntimeTranscript {
202
- private readonly store: PiTranscriptStore;
197
+ private readonly store: CloudflareSessionStorage;
203
198
 
204
199
  private modelBranch() {
205
200
  return this.store.branch().filter((entry) =>
@@ -215,15 +210,15 @@ export class PiRuntimeTranscript {
215
210
  *
216
211
  * `PiRuntimeAdapter.createTranscript` 每个 Runtime Kernel 构造时调用一次;`hasActiveTurn` 必须反映当前 Submission 生命周期。
217
212
  *
218
- * Session 统一包住 `DoSqliteSessionStorage`,模型表只用于压缩,活动 Turn 检查只用于阻止并发导入。
213
+ * 官方 Storage 契约由同一 DO SQLite 实现;模型目录用于压缩,活动 Turn 检查阻止并发导入。
219
214
  */
220
215
  constructor(
221
- private readonly sql: SqlTaggedTemplate,
216
+ storage: DurableObjectStorage,
222
217
  private readonly durability: PiTranscriptDurability,
223
218
  private readonly models: Models,
224
219
  private readonly hasActiveTurn: () => boolean,
225
220
  ) {
226
- this.store = new PiTranscriptStore(sql);
221
+ this.store = openPiSessionStorage(storage);
227
222
  }
228
223
 
229
224
  // #region 规范消息写入
@@ -240,19 +235,15 @@ export class PiRuntimeTranscript {
240
235
  message: AgentMessage,
241
236
  options: {
242
237
  submissionId?: string;
243
- createdAt?: number;
244
238
  userMessage?: UIMessage & { role: "user" };
245
239
  } = {},
246
240
  ): boolean {
247
- const inserted = appendSessionMessageSync(this.sql, {
241
+ const inserted = this.store.appendMessage({
248
242
  id,
249
243
  message,
250
244
  ...(options.submissionId
251
245
  ? { submissionId: options.submissionId }
252
246
  : {}),
253
- ...(options.createdAt === undefined
254
- ? {}
255
- : { createdAt: options.createdAt }),
256
247
  });
257
248
  if (inserted && options.userMessage) {
258
249
  const sidecar = captureUIUserSidecar(options.userMessage);
@@ -280,7 +271,6 @@ export class PiRuntimeTranscript {
280
271
  id,
281
272
  inserted: this.append(id, message, {
282
273
  submissionId,
283
- createdAt: message.timestamp,
284
274
  }),
285
275
  };
286
276
  }
@@ -301,7 +291,6 @@ export class PiRuntimeTranscript {
301
291
  id: commit.id,
302
292
  inserted: this.append(commit.id, commit.message, {
303
293
  submissionId,
304
- createdAt: commit.message.timestamp,
305
294
  }),
306
295
  }
307
296
  : this.commitTurnMessage(
@@ -329,7 +318,7 @@ export class PiRuntimeTranscript {
329
318
  }],
330
319
  display: true,
331
320
  timestamp,
332
- } satisfies CustomMessage, { submissionId, createdAt: timestamp });
321
+ } satisfies CustomMessage, { submissionId });
333
322
  }
334
323
 
335
324
  /**
@@ -348,7 +337,6 @@ export class PiRuntimeTranscript {
348
337
  settlement.message,
349
338
  {
350
339
  submissionId,
351
- createdAt: settlement.message.timestamp,
352
340
  },
353
341
  );
354
342
  }
@@ -434,7 +422,7 @@ export class PiRuntimeTranscript {
434
422
  * 这里委托同步 Session 存储辅助函数写入叶子标记,不删除原分支,保留 Pi 的分支结构。
435
423
  */
436
424
  moveLeaf(id: string): void {
437
- moveSessionLeafSync(this.sql, id);
425
+ this.store.moveBranchTip(id);
438
426
  }
439
427
 
440
428
  /**
@@ -452,7 +440,7 @@ export class PiRuntimeTranscript {
452
440
  model: Model<Api>;
453
441
  signal?: AbortSignal;
454
442
  submissionId?: string;
455
- onCompactionPersisted?: (event: RuntimeModelUsageEvent) => void;
443
+ onCompactionUsage?: (event: RuntimeModelUsageEvent, didCompact: boolean) => void;
456
444
  },
457
445
  ): Promise<AgentMessage[]> {
458
446
  const branch = this.modelBranch();
@@ -465,6 +453,9 @@ export class PiRuntimeTranscript {
465
453
  JSON.stringify(message) !== JSON.stringify(messages[index])
466
454
  )
467
455
  ) {
456
+ if (options.force) {
457
+ throw new Error("Forced context compaction requires canonical messages");
458
+ }
468
459
  console.warn(
469
460
  "[runtime-compaction:degraded]",
470
461
  JSON.stringify({ reason: "canonical-message-mismatch" }),
@@ -479,29 +470,22 @@ export class PiRuntimeTranscript {
479
470
  model: options.model,
480
471
  signal: options.signal,
481
472
  });
482
- if (options.force && !result.didCompact) {
483
- throw new Error("Forced context compaction did not reduce the model input");
484
- }
485
- if (result.compactionEntry) {
486
- // 整对象写入:assembly 侧已经造好了完整的 compaction 条目,这里不再拆成位置
487
- // 参数。旧写法拆 5 个参数喂 pi 的 Session.appendCompaction,pi 0.81 把它加到
488
- // 7 个参数(usage / retainedTail)之后,多出来的会被静默丢掉且编译器不报。
473
+ if (result.compactionEntry || result.modelUsage) {
489
474
  this.durability.transaction(() => {
490
- const eventId = this.store.appendCompaction(result.compactionEntry!);
491
- if (
492
- options.submissionId &&
493
- options.onCompactionPersisted &&
494
- result.modelUsage
495
- ) {
496
- options.onCompactionPersisted({
475
+ const eventId = this.store.recordCompaction(result.compactionEntry, result.modelUsage?.usage);
476
+ if (options.submissionId && options.onCompactionUsage && result.modelUsage) {
477
+ options.onCompactionUsage({
497
478
  eventId,
498
479
  submissionId: options.submissionId,
499
480
  kind: "compaction",
500
481
  ...result.modelUsage,
501
- });
482
+ }, result.didCompact);
502
483
  }
503
484
  });
504
485
  }
486
+ if (options.force && !result.didCompact) {
487
+ throw new Error("Forced context compaction did not reduce the model input");
488
+ }
505
489
  if (result.degraded) {
506
490
  console.warn(
507
491
  "[runtime-compaction:degraded]",
@@ -532,7 +516,6 @@ export class PiRuntimeTranscript {
532
516
  }
533
517
  const appended = this.append(entry.id, entry.message, {
534
518
  submissionId: currentSubmissionId,
535
- createdAt: entry.createdAt,
536
519
  ...(entry.message.role === "user" && entry.userSidecar
537
520
  ? {
538
521
  userMessage: {
@@ -607,7 +590,7 @@ export class PiRuntimeTranscript {
607
590
  * 两类 Transcript 数据必须一起删除,否则复用消息 ID 时可能重新读到旧附件或 UI 元数据。
608
591
  */
609
592
  clear(): void {
610
- clearSessionSync(this.sql);
593
+ this.store.clear();
611
594
  this.durability.clearUserSidecars();
612
595
  }
613
596
 
@@ -720,6 +703,7 @@ export class PiRuntimeTranscript {
720
703
  parts: [],
721
704
  metadata,
722
705
  });
706
+ activeAssistant = { submissionId: entry.submissionId, index: result.length - 1 };
723
707
  } else {
724
708
  const previous = result[index]!;
725
709
  result[index] = {
@@ -730,7 +714,8 @@ export class PiRuntimeTranscript {
730
714
  },
731
715
  };
732
716
  }
733
- activeAssistant = undefined;
717
+ // A stream may settle after the stop marker. Keep its existing UI identity
718
+ // until the next user message rather than creating a second assistant.
734
719
  continue;
735
720
  }
736
721
  if (entry.message.role === "assistant") {
@@ -787,7 +772,10 @@ export class PiRuntimeTranscript {
787
772
  { type: "step-start" },
788
773
  ...projected.parts,
789
774
  ],
790
- metadata: projected.metadata,
775
+ metadata: {
776
+ ...(previous.metadata as Record<string, unknown> | undefined),
777
+ ...(projected.metadata as Record<string, unknown> | undefined),
778
+ },
791
779
  };
792
780
  }
793
781
  continue;
@@ -861,21 +849,18 @@ export class PiRuntimeTranscript {
861
849
  *
862
850
  * 浏览器投影、Host 历史、Snapshot 以及 Turn 启动时的助手序号计算都从这里读取,调用方无需再查关联表。
863
851
  *
864
- * Session entry 的 ISO 时间优先,解析失败才回退到消息时间;非消息 entry 在这一层统一排除。
852
+ * UI uses the message event time; Pi entry timestamps describe storage commits.
865
853
  */
866
854
  async storedMessages(): Promise<StoredTranscriptMessage[]> {
867
855
  const submissionByMessage =
868
856
  this.durability.listMessageSubmissionLinks();
869
857
  return this.store.branch().flatMap((entry) => {
870
858
  if (entry.type !== "message") return [];
871
- const parsedTimestamp = Date.parse(entry.timestamp);
872
859
  return [{
873
860
  id: entry.id,
874
861
  submissionId: submissionByMessage.get(entry.id) ?? null,
875
862
  message: entry.message,
876
- createdAt: Number.isFinite(parsedTimestamp)
877
- ? parsedTimestamp
878
- : entry.message.timestamp,
863
+ createdAt: entry.message.timestamp,
879
864
  }];
880
865
  });
881
866
  }
@@ -1,24 +1,24 @@
1
- import type { SqlTaggedTemplate } from "agents/chat";
2
- import {
3
- PiTranscriptStore,
4
- appendSessionMessageSync,
5
- clearSessionSync,
6
- moveSessionLeafSync,
7
- } from "./storage";
1
+ import { migrateLegacyPiSession } from "./migration";
2
+ import { CloudflareSessionStorage } from "./storage";
8
3
 
9
- export const piSession = Object.freeze({
10
- appendMessage: appendSessionMessageSync,
11
- clear: clearSessionSync,
12
- /**
13
- * 建库并返回回合事实源。
14
- *
15
- * 这里不再构造 pi 的 `Session` —— 会话所有权归这个 Durable Object,
16
- * pi 只在需要压缩时被当作纯函数调用。理由见 `PiTranscriptStore` 的注释。
17
- */
18
- create(sql: SqlTaggedTemplate) {
19
- return { store: new PiTranscriptStore(sql) };
20
- },
21
- moveLeaf: moveSessionLeafSync,
22
- });
4
+ export { CloudflareSessionStorage } from "./storage";
23
5
 
24
- export * from "./storage";
6
+ /** Open only after the startup transaction has installed the current database format. */
7
+ export function openPiSessionStorage(
8
+ storage: DurableObjectStorage,
9
+ ): CloudflareSessionStorage {
10
+ return storage.transactionSync(() => {
11
+ const tables = new Set(
12
+ storage.sql
13
+ .exec<{ name: string }>(
14
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('pi_storage_state', 'pi_session_entries')",
15
+ )
16
+ .toArray()
17
+ .map((row) => row.name),
18
+ );
19
+ if (!tables.has("pi_storage_state") && tables.has("pi_session_entries")) {
20
+ migrateLegacyPiSession(storage, { dryRun: false });
21
+ }
22
+ return new CloudflareSessionStorage(storage);
23
+ });
24
+ }