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

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,3 +1,5 @@
1
+ import { summarizeModelUsage } from "../../model-usage";
2
+ import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/pi-agent-core/harness/context";
1
3
  import {
2
4
  buildSessionContext,
3
5
  compact as compactPiTranscript,
@@ -7,7 +9,7 @@ import {
7
9
  prepareCompaction,
8
10
  type AgentMessage,
9
11
  type CompactionEntry,
10
- type SessionTreeEntry,
12
+ type Entry,
11
13
  } from "@earendil-works/pi-agent-core";
12
14
  import type {
13
15
  AssistantMessage,
@@ -45,31 +47,9 @@ const COMPACTION_RETRY: RetryPolicy = {
45
47
  const COLD_MEMORY_INDEX = "/userspace/memories/MEMORY.md";
46
48
  const TRUNCATION_MARKER = "\n[truncated to memory budget]";
47
49
 
48
- function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
49
- const first = responses[0];
50
- if (!first) return undefined;
51
-
52
- const total = structuredClone(first.usage);
53
- for (const response of responses.slice(1)) {
54
- const usage = response.usage;
55
- total.input += usage.input;
56
- total.output += usage.output;
57
- total.cacheRead += usage.cacheRead;
58
- total.cacheWrite += usage.cacheWrite;
59
- if (usage.cacheWrite1h !== undefined) {
60
- total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
61
- }
62
- if (usage.reasoning !== undefined) {
63
- total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
64
- }
65
- total.totalTokens += usage.totalTokens;
66
- total.cost.input += usage.cost.input;
67
- total.cost.output += usage.cost.output;
68
- total.cost.cacheRead += usage.cost.cacheRead;
69
- total.cost.cacheWrite += usage.cost.cacheWrite;
70
- total.cost.total += usage.cost.total;
71
- }
72
- return total;
50
+ function compactionUsage(responses: readonly AssistantMessage[], requested: Model<any>) {
51
+ const [first, ...remaining] = responses;
52
+ return first ? { modelUsage: summarizeModelUsage(requested, [first, ...remaining]) } : {};
73
53
  }
74
54
 
75
55
  // #region Transcript compaction
@@ -85,7 +65,7 @@ function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
85
65
  * 核心术语见包入口 `index.ts`。
86
66
  */
87
67
  export async function compactPiContext(input: Readonly<{
88
- branch: readonly SessionTreeEntry[];
68
+ branch: readonly Entry[];
89
69
  compactAfterTokens: number;
90
70
  force?: boolean;
91
71
  models: Models;
@@ -95,7 +75,7 @@ export async function compactPiContext(input: Readonly<{
95
75
  messages: AgentMessage[];
96
76
  compactionEntry?: Omit<
97
77
  CompactionEntry,
98
- "id" | "parentId" | "timestamp"
78
+ "id" | "parentId" | "seq" | "timestamp"
99
79
  >;
100
80
  modelUsage?: {
101
81
  api: string;
@@ -108,12 +88,14 @@ export async function compactPiContext(input: Readonly<{
108
88
  didCompact: boolean;
109
89
  degraded: boolean;
110
90
  }> {
91
+ const responses: AssistantMessage[] = [];
111
92
  const branch = [...input.branch];
93
+ const context = input.signal ? withAbortSignal(input.signal, BACKGROUND_CONTEXT) : BACKGROUND_CONTEXT;
112
94
  let currentMessages = branch.flatMap((entry) =>
113
95
  entry.type === "message" ? [entry.message] : []
114
96
  );
115
97
  try {
116
- currentMessages = buildSessionContext(branch).messages;
98
+ currentMessages = await buildSessionContext(branch, undefined, context);
117
99
  const settings = DEFAULT_COMPACTION_SETTINGS;
118
100
  if (
119
101
  !settings.enabled ||
@@ -133,6 +115,7 @@ export async function compactPiContext(input: Readonly<{
133
115
  messages: currentMessages,
134
116
  didCompact: false,
135
117
  degraded: true,
118
+ ...compactionUsage(responses, input.model),
136
119
  };
137
120
  }
138
121
  if (!preparation.value) {
@@ -143,7 +126,6 @@ export async function compactPiContext(input: Readonly<{
143
126
  };
144
127
  }
145
128
 
146
- const responses: AssistantMessage[] = [];
147
129
  const models = {
148
130
  completeSimple: async (
149
131
  model: Model<any>,
@@ -152,6 +134,7 @@ export async function compactPiContext(input: Readonly<{
152
134
  ) => {
153
135
  const response = await input.models.completeSimple(model, context, options);
154
136
  responses.push(response);
137
+ input.signal?.throwIfAborted();
155
138
  return response;
156
139
  },
157
140
  } as Models;
@@ -160,46 +143,38 @@ export async function compactPiContext(input: Readonly<{
160
143
  models,
161
144
  input.model,
162
145
  undefined,
163
- input.signal,
164
146
  undefined,
165
147
  COMPACTION_RETRY,
148
+ undefined,
149
+ context,
166
150
  );
167
151
  if (!compacted.ok) {
168
152
  return {
169
153
  messages: currentMessages,
170
154
  didCompact: false,
171
155
  degraded: true,
156
+ ...compactionUsage(responses, input.model),
172
157
  };
173
158
  }
174
159
 
175
160
  const compactionEntry = {
176
161
  type: "compaction",
162
+ fromHook: false,
177
163
  ...compacted.value,
178
164
  } satisfies Omit<
179
165
  CompactionEntry,
180
- "id" | "parentId" | "timestamp"
166
+ "id" | "parentId" | "seq" | "timestamp"
181
167
  >;
182
168
  const previewEntry: CompactionEntry = {
183
169
  ...compactionEntry,
184
170
  id: `compaction-preview:${branch[branch.length - 1]?.id ?? "root"}`,
185
171
  parentId: branch[branch.length - 1]?.id ?? null,
186
- timestamp: new Date().toISOString(),
172
+ timestamp: Date.now(),
173
+ seq: (branch.at(-1)?.seq ?? 0) + 1,
187
174
  };
188
- const firstResponse = responses[0];
189
- const responseModel = firstResponse?.responseModel && responses.every(
190
- (response) => response.responseModel === firstResponse.responseModel,
191
- )
192
- ? firstResponse.responseModel
193
- : undefined;
194
- const stopReason = firstResponse && responses.every(
195
- (response) => response.stopReason === firstResponse.stopReason,
196
- )
197
- ? firstResponse.stopReason
198
- : undefined;
199
- const usage = sumUsage(responses);
200
- const messages = buildSessionContext(
201
- [...branch, previewEntry],
202
- ).messages;
175
+ const messages = await buildSessionContext(
176
+ [...branch, previewEntry], undefined, context,
177
+ );
203
178
  if (
204
179
  input.force &&
205
180
  messages.reduce((total, message) => total + estimateTokens(message), 0) >=
@@ -212,23 +187,13 @@ export async function compactPiContext(input: Readonly<{
212
187
  messages: currentMessages,
213
188
  didCompact: false,
214
189
  degraded: true,
190
+ ...compactionUsage(responses, input.model),
215
191
  };
216
192
  }
217
193
  return {
218
194
  messages,
219
195
  compactionEntry,
220
- ...(firstResponse && usage
221
- ? {
222
- modelUsage: {
223
- api: firstResponse.api,
224
- provider: firstResponse.provider,
225
- model: firstResponse.model,
226
- ...(responseModel ? { responseModel } : {}),
227
- ...(stopReason ? { stopReason } : {}),
228
- usage,
229
- },
230
- }
231
- : {}),
196
+ ...compactionUsage(responses, input.model),
232
197
  didCompact: true,
233
198
  degraded: false,
234
199
  };
@@ -237,6 +202,7 @@ export async function compactPiContext(input: Readonly<{
237
202
  messages: currentMessages,
238
203
  didCompact: false,
239
204
  degraded: true,
205
+ ...compactionUsage(responses, input.model),
240
206
  };
241
207
  }
242
208
  }
@@ -51,6 +51,7 @@ interface PiAssemblySnapshot {
51
51
  readonly bindings: {
52
52
  readonly platform: Pick<RuntimePlatformPort, "loader">;
53
53
  readonly workspace?: RuntimeBindings["workspace"];
54
+ readonly mcp?: RuntimeBindings["mcp"];
54
55
  };
55
56
  }
56
57
 
@@ -390,10 +391,13 @@ async function preparePiAssembly(
390
391
  ...(options.gatewaySession
391
392
  ? createPiGatewayToolCandidates(options.gatewaySession)
392
393
  : []),
393
- ...createPiMcpToolCandidates(
394
+ ...await createPiMcpToolCandidates(
394
395
  options.mcpHost,
395
- snapshot.profile.mcpServers,
396
+ snapshot.profile.mcpServers.filter(server => !server.hostManaged),
396
397
  ),
398
+ ...(snapshot.bindings.mcp
399
+ ? await createPiMcpToolCandidates(snapshot.bindings.mcp, snapshot.bindings.mcp.servers)
400
+ : []),
397
401
  ]);
398
402
  return Object.freeze({
399
403
  loaded: extensions.loaded,
@@ -1,9 +1,9 @@
1
1
  import {
2
- Agent as PiCore,
2
+ runAgentLoopContinue,
3
3
  convertToLlm,
4
4
  type AgentEvent,
5
5
  type AgentMessage,
6
- type AgentOptions,
6
+ type AgentLoopConfig,
7
7
  type ThinkingLevel,
8
8
  } from "@earendil-works/pi-agent-core";
9
9
  import type {
@@ -114,7 +114,7 @@ interface PiTurnAdapterOptions {
114
114
  readonly models: Models;
115
115
  readonly modelSessionId: string;
116
116
  // 读取这次执行真正要交给 Pi 的 canonical transcript。
117
- // PiTurnAdapter.run 在创建 PiCore 前调用它,宿主应返回当时最新的 transcript。
117
+ // PiTurnAdapter.run 在创建 Pi Loop 前调用它,宿主应返回当时最新的 transcript。
118
118
  // 这里保留为延迟读取,是为了避免适配器创建后新增的恢复消息被旧快照漏掉。
119
119
  readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
120
120
  // 把工具的最终结果交回宿主持久化。
@@ -124,10 +124,9 @@ interface PiTurnAdapterOptions {
124
124
  call: SettledPiToolCall,
125
125
  ) => void | Promise<void>;
126
126
  readonly onGeneration?: PiGenerationLifecycleObserver;
127
- // 在模型调用前调整 Pi 的消息上下文。
128
- // PiCore 通过 AgentOptions.transformContext 调用它,Runtime 用它接入现有上下文处理。
127
+ // 在模型调用前通过 AgentLoopConfig.transformContext 调整上下文。
129
128
  // 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
130
- readonly transformContext: NonNullable<AgentOptions["transformContext"]>;
129
+ readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
131
130
  readonly workspace?: SpillWorkspace;
132
131
  // 之前的执行片已经用掉的模型回合数。首次执行传 0。
133
132
  readonly modelTurnsConsumed: number;
@@ -137,7 +136,7 @@ interface PiTurnAdapterOptions {
137
136
  * 收集那些结果已经由工具自己收口的 Tool 名字。
138
137
  *
139
138
  * @remarks
140
- * `run` 在建 PiCore 前调用,把结果交给 `projectToolResultsForModel`。
139
+ * `run` 在建 Pi Loop 前调用,把结果交给 `projectToolResultsForModel`。
141
140
  *
142
141
  * `outputBudget: "structure"` 的语义就是“这个结果不需要 Runtime 再替它省”:read
143
142
  * 按页返回并在页脚写明覆盖的行号,activate_skill 返回的是模型接下来要照做的指令,
@@ -216,8 +215,6 @@ function orderLateToolResultsBeforeTurnAbortMarker(
216
215
  }
217
216
 
218
217
  class PiTurnAdapter {
219
- private piInstance?: PiCore;
220
- private abortRequested = false;
221
218
  private readonly pendingSteers: UserMessage[] = [];
222
219
  private readonly governance: PiToolGovernance;
223
220
 
@@ -255,25 +252,16 @@ class PiTurnAdapter {
255
252
  return true;
256
253
  }
257
254
 
258
- // 在 Pi 运行实例可用后尽快停止这个 Turn。
259
- // Runtime 可能在 run() 之前或期间发出取消,因此尚未创建 PiCore 时先记住请求。
260
- // canonical transcript 读取是异步的,其间没有可中止的 Pi 实例,所以不能删掉启动前标记。
261
- abort(): void {
262
- if (this.piInstance) this.piInstance.abort();
263
- else this.abortRequested = true;
264
- }
265
-
266
255
  // 把一条用户消息排入当前 Turn。
267
256
  // 提交仍在运行时收到扩展宿主消息,Runtime 会调用这里。
268
- // PiCore 创建前先缓冲消息,可覆盖异步启动窗口,同时不引入第二份 transcript 来源。
257
+ // Pi Loop 创建前先缓冲消息,可覆盖异步启动窗口,同时不引入第二份 transcript 来源。
269
258
  steer(message: UserMessage): void {
270
- if (this.piInstance) this.piInstance.steer(message);
271
- else this.pendingSteers.push(message);
259
+ this.pendingSteers.push(message);
272
260
  }
273
261
 
274
262
  // 从当前 canonical transcript 继续运行 Pi,并把生命周期事件交给监听器。
275
263
  // Submission 的首次执行或恢复执行取得 Pinned Runtime 后,PreparedPiTurnAdapter 调用这里。
276
- // 每次新建 PiCore 可隔离运行状态;等待订阅回调并在 finally 清理可保住 Pi 的事件屏障语义。
264
+ // 每次运行持有独立上下文;Loop 等待事件 sink 完成后才继续。
277
265
  async run(
278
266
  opts: {
279
267
  readonly systemPrompt: string;
@@ -285,6 +273,7 @@ class PiTurnAdapter {
285
273
  const { systemPrompt, signal } = opts;
286
274
  signal?.throwIfAborted();
287
275
  const canonicalMessages = await this.opts.canonicalMessages();
276
+ signal?.throwIfAborted();
288
277
  const tools = this.compile(opts.tools);
289
278
  const selfBounded = selfBoundedToolNames(opts.tools);
290
279
  // `modelTurns` 是跨执行片的累计数,`sliceTurns` 只数本片 —— 前者管任务预算,
@@ -292,16 +281,14 @@ class PiTurnAdapter {
292
281
  let modelTurns = this.opts.modelTurnsConsumed;
293
282
  let sliceTurns = 0;
294
283
  let reachedModelTurnLimit = false;
295
- let wrappingUp = false;
284
+ let wrapUpMessage: UserMessage | undefined;
296
285
  let wrapUpTurns = 0;
297
- const pi = new PiCore({
286
+ const config: AgentLoopConfig = {
287
+ model: this.opts.pi.model,
288
+ reasoning: this.opts.pi.thinkingLevel === "off" ? undefined : this.opts.pi.thinkingLevel,
289
+ toolExecution: "parallel",
298
290
  convertToLlm,
299
- streamFn: withProviderRetry(
300
- this.opts.models.streamSimple.bind(this.opts.models),
301
- 0,
302
- this.opts.modelSessionId,
303
- this.opts.onGeneration,
304
- ),
291
+ getSteeringMessages: async () => this.pendingSteers.splice(0, 1),
305
292
  // transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
306
293
  // OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
307
294
  // 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
@@ -309,50 +296,36 @@ class PiTurnAdapter {
309
296
  const ctx = await this.opts.transformContext(messages, signal);
310
297
  return transformMessages(
311
298
  orderLateToolResultsBeforeTurnAbortMarker(
312
- projectToolResultsForModel(ctx, selfBounded),
299
+ projectToolResultsForModel(
300
+ wrapUpMessage ? [...ctx, wrapUpMessage] : ctx,
301
+ selfBounded,
302
+ ),
313
303
  ) as Message[],
314
304
  this.opts.pi.model,
315
305
  ) as AgentMessage[];
316
306
  },
317
307
  afterToolCall: this.governance.afterToolCall,
318
- // Pi 在每个 turn_end 之后先调 prepareNextTurn、再调 shouldStopAfterTurn,
319
- // 所以回合计数记在这里,停止判据只读不写。
320
- // 用 `WithContext` 变体:只有它拿得到当前上下文,也就拿得到工具表和消息。
321
- prepareNextTurnWithContext: ({ context }) => {
322
- modelTurns += 1;
323
- sliceTurns += 1;
324
- if (wrappingUp) {
325
- wrapUpTurns += 1;
326
- return undefined;
327
- }
308
+ // Pi calls this only when another model turn is needed; counting belongs to turn_end.
309
+ prepareNextTurn: ({ context }) => {
310
+ if (wrapUpMessage) return undefined;
328
311
  const toolFailureLimitReached = this.governance.wrapUpRequested();
329
312
  if (
330
313
  !toolFailureLimitReached &&
331
314
  modelTurns < MAX_MODEL_TURNS_PER_SUBMISSION - 1
332
315
  ) return undefined;
333
- // 预算见底或工具熔断:清空工具表并注入收尾指令,逼出一条真正的
334
- // 终止助手消息。直接判失败的话用户拿不到任何交代。
335
- // 这条指令不进 newMessages,因此不会写进 transcript —— 它是控制指令,不是历史。
336
- wrappingUp = true;
337
- return {
338
- context: {
339
- ...context,
340
- tools: [],
341
- messages: [
342
- ...context.messages,
343
- {
344
- role: "user" as const,
345
- content: [{
346
- type: "text" as const,
347
- text: toolFailureLimitReached
348
- ? TOOL_FAILURE_WRAP_UP_MESSAGE
349
- : WRAP_UP_MESSAGE,
350
- }],
351
- timestamp: Date.now(),
352
- },
353
- ],
354
- },
316
+ // Control instructions enter only the model projection, after official compaction.
317
+ // Keeping them out of Loop history preserves its canonical transcript boundary.
318
+ wrapUpMessage = {
319
+ role: "user",
320
+ content: [{
321
+ type: "text",
322
+ text: toolFailureLimitReached
323
+ ? TOOL_FAILURE_WRAP_UP_MESSAGE
324
+ : WRAP_UP_MESSAGE,
325
+ }],
326
+ timestamp: Date.now(),
355
327
  };
328
+ return { context: { ...context, tools: [] } };
356
329
  },
357
330
  shouldStopAfterTurn: () => {
358
331
  if (signal?.aborted === true) return true;
@@ -360,46 +333,34 @@ class PiTurnAdapter {
360
333
  // 不等于「不再有上限」:Pi 在这之后还会取 steering 和 follow-up 消息,
361
334
  // 一直有新消息进来就会一直发模型请求,而且全在同一个 invocation 里 ——
362
335
  // 那正是 MAX_MODEL_TURNS_PER_SLICE 要挡的 CPU 驱逐。
363
- if (wrappingUp) return wrapUpTurns >= MAX_WRAP_UP_TURNS;
336
+ if (wrapUpMessage) return wrapUpTurns >= MAX_WRAP_UP_TURNS;
337
+ if (this.governance.wrapUpRequested() || modelTurns >= MAX_MODEL_TURNS_PER_SUBMISSION - 1) return false;
364
338
  reachedModelTurnLimit = sliceTurns >= MAX_MODEL_TURNS_PER_SLICE;
365
339
  return reachedModelTurnLimit;
366
340
  },
367
- initialState: {
368
- model: this.opts.pi.model,
369
- systemPrompt,
370
- messages: [...canonicalMessages],
371
- tools: [...tools],
372
- ...(this.opts.pi.thinkingLevel !== undefined
373
- ? { thinkingLevel: this.opts.pi.thinkingLevel }
374
- : {}),
341
+ };
342
+ await runAgentLoopContinue(
343
+ { systemPrompt, messages: [...canonicalMessages], tools: [...tools] },
344
+ config,
345
+ async (event) => {
346
+ await listener(event);
347
+ if (event.type === "turn_end") {
348
+ modelTurns += 1;
349
+ sliceTurns += 1;
350
+ if (wrapUpMessage) wrapUpTurns += 1;
351
+ }
375
352
  },
376
- });
377
-
378
- this.piInstance = pi;
379
- for (const message of this.pendingSteers.splice(0)) {
380
- pi.steer(message);
381
- }
382
- const unsubscribe = pi.subscribe(listener);
383
- // 把调用方的 AbortSignal 转发给只存在于本方法内的 Pi 运行实例。
384
- // continue() 尚未结束时信号监听器会调用它,所有退出路径都在 finally 中移除监听。
385
- // addEventListener 和 removeEventListener 必须使用同一函数引用,因此这里保留稳定的局部函数。
386
- const abort = (): void => pi.abort();
387
- signal?.addEventListener("abort", abort, { once: true });
388
- try {
389
- const running = pi.continue();
390
- if (this.abortRequested) {
391
- this.abortRequested = false;
392
- pi.abort();
393
- }
394
- await running;
395
- return reachedModelTurnLimit
396
- ? { kind: "yielded", modelTurns }
397
- : { kind: "terminal", modelTurns };
398
- } finally {
399
- signal?.removeEventListener("abort", abort);
400
- unsubscribe();
401
- if (this.piInstance === pi) this.piInstance = undefined;
402
- }
353
+ signal,
354
+ withProviderRetry(
355
+ this.opts.models.streamSimple.bind(this.opts.models),
356
+ 0,
357
+ this.opts.modelSessionId,
358
+ this.opts.onGeneration,
359
+ ),
360
+ );
361
+ return reachedModelTurnLimit
362
+ ? { kind: "yielded", modelTurns }
363
+ : { kind: "terminal", modelTurns };
403
364
  }
404
365
  }
405
366
 
@@ -429,7 +390,7 @@ export interface PiToolInputRecord {
429
390
  *
430
391
  * @remarks
431
392
  * Runtime 为每个 Submission 实现这个端口,PreparedPiTurnAdapter 会在审批和 Tool 执行前后调用它。
432
- * 持久化留在 PiCore 之外,恢复流程才能从 Runtime 数据库重建决定,而不是依赖已丢失的内存状态。
393
+ * 持久化留在 Pi Loop 之外,恢复流程才能从 Runtime 数据库重建决定,而不是依赖已丢失的内存状态。
433
394
  */
434
395
  export interface PiTurnDurability {
435
396
  /**
@@ -487,7 +448,7 @@ export interface PiTurnDurability {
487
448
  *
488
449
  * @remarks
489
450
  * Submission 首次执行或恢复时,Runtime 把它传给 PiRuntimeAdapter.createTurn。
490
- * 这些回调让 transcript、持久化状态、流投影和终态仍由 Runtime 负责,避免在 PiCore 内复制一份。
451
+ * 这些回调让 transcript、持久化状态、流投影和终态仍由 Runtime 负责,避免在 Pi Loop 内复制一份。
491
452
  * Runtime、Submission、Turn、Prepared Runtime、Pinned Runtime、Tool candidate 和 transcript 沿用 `./index.ts` 的统一定义。
492
453
  */
493
454
  export interface CreatePreparedPiTurnOptions {
@@ -508,7 +469,7 @@ export interface CreatePreparedPiTurnOptions {
508
469
  /**
509
470
  * 在 Pi 启动前读取最新 canonical transcript。
510
471
  *
511
- * PiTurnAdapter.run 在创建 PiCore 前调用它,Runtime 应返回 transcript 当时已提交的消息。
472
+ * PiTurnAdapter.run 在创建 Pi Loop 前调用它,Runtime 应返回 transcript 当时已提交的消息。
512
473
  * 延迟读取可纳入适配器创建后才物化的恢复结果,不能提前固化为数组快照。
513
474
  */
514
475
  readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
@@ -557,10 +518,10 @@ export interface CreatePreparedPiTurnOptions {
557
518
  /**
558
519
  * 调整 Pi 即将发送给模型的消息上下文。
559
520
  *
560
- * PiCore 在每次模型调用前使用它,Runtime 通过这里接入现有上下文处理。
561
- * 直接沿用 AgentOptions 的类型可确保参数和取消信号与 Pi 保持一致。
521
+ * Pi Loop 在每次模型调用前使用它,Runtime 通过这里接入现有上下文处理。
522
+ * 直接沿用 AgentLoopConfig 的类型可确保参数和取消信号与 Pi 保持一致。
562
523
  */
563
- readonly transformContext: NonNullable<AgentOptions["transformContext"]>;
524
+ readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
564
525
  /**
565
526
  * 在提交终止助手消息前读取已保存的取消原因。
566
527
  *
@@ -681,7 +642,7 @@ export class PreparedPiTurnAdapter {
681
642
  tool: {
682
643
  ...candidate.tool,
683
644
  // 只在 Runtime 的持久化和策略门禁都放行后执行一个 Tool candidate。
684
- // 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
645
+ // 实时运行由 Pi Loop 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
685
646
  // 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
686
647
  execute: async (toolCallId, input, signal, onUpdate) => {
687
648
  if (toolCallIdPrefix) {
@@ -915,11 +876,10 @@ export class PreparedPiTurnAdapter {
915
876
  *
916
877
  * @remarks
917
878
  * 活动提交被中止时 Runtime 会调用它,包括仍在异步启动的阶段。
918
- * 转交给 PiTurnAdapter 可保留启动前取消标记;run() 之前还没有可直接调用的 PiCore。
879
+ * 同一取消信号覆盖历史读取、Loop 和工具重试。
919
880
  */
920
881
  abort(): void {
921
882
  this.abortController.abort();
922
- this.turn.abort();
923
883
  }
924
884
 
925
885
  /**
@@ -941,7 +901,7 @@ export class PreparedPiTurnAdapter {
941
901
  *
942
902
  * @remarks
943
903
  * 当前提交仍活动时收到消息,Runtime 的扩展宿主会调用它。
944
- * 转交后,启动前缓冲和 PiCore 转向队列共用一条路径;只有没有活动 Turn 时调用方才应直接追加 transcript。
904
+ * 启动前与运行中都进入同一队列,Loop 每次消费一条;空闲时调用方直接追加 transcript。
945
905
  */
946
906
  steer(
947
907
  message: UserMessage,
@@ -992,7 +952,7 @@ export class PreparedPiTurnAdapter {
992
952
  }
993
953
 
994
954
  /**
995
- * 运行这个 Turn,直到 PiCore 和所有等待中的事件回调结束。
955
+ * 运行这个 Turn,直到 Pi Loop 和所有等待中的事件回调结束。
996
956
  *
997
957
  * @remarks
998
958
  * Submission 首次执行和续跑时,Runtime 都会在可恢复聊天任务中调用它。
@@ -1002,13 +962,16 @@ export class PreparedPiTurnAdapter {
1002
962
  options: PiPreparedTurnRunOptions,
1003
963
  ): Promise<PiTurnRunResult> {
1004
964
  this.terminalIntent = undefined;
965
+ const signal = options.signal
966
+ ? AbortSignal.any([options.signal, this.abortController.signal])
967
+ : this.abortController.signal;
1005
968
  const result = await this.turn.run(
1006
969
  {
1007
970
  systemPrompt: this.systemPrompt,
1008
971
  tools: this.candidates,
1009
- signal: options.signal,
972
+ signal,
1010
973
  },
1011
- (event) => this.handleEvent(event),
974
+ (event) => this.handleEvent(event, signal),
1012
975
  );
1013
976
  if (this.terminalIntent) {
1014
977
  await this.options.onTerminal(this.terminalIntent);
@@ -1018,9 +981,9 @@ export class PreparedPiTurnAdapter {
1018
981
  }
1019
982
 
1020
983
  // 提交完成的 Pi 消息、推导终态意图,并把事件投影给可恢复流。
1021
- // PiTurnAdapter 会对每个 AgentEvent 调用它,PiCore 会等待该 Promise 后再越过订阅事件屏障。
984
+ // Loop 等待每个事件的 Promise,canonical 提交失败时直接拒绝本次执行。
1022
985
  // canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
1023
- private async handleEvent(event: AgentEvent): Promise<void> {
986
+ private async handleEvent(event: AgentEvent, signal: AbortSignal): Promise<void> {
1024
987
  if (
1025
988
  event.type === "tool_execution_start" &&
1026
989
  !this.candidates.some(({ tool }) => tool.name === event.toolName)
@@ -1068,6 +1031,7 @@ export class PreparedPiTurnAdapter {
1068
1031
  if (event.type === "message_end") {
1069
1032
  const message = event.message;
1070
1033
  if (message.role === "assistant") {
1034
+ if (message.stopReason === "deferred") throw new Error("Deferred Pi responses are not supported by this Runtime");
1071
1035
  if (isContextOverflow(message, this.contextWindow)) {
1072
1036
  throw new ContextOverflowError(
1073
1037
  message.errorMessage ?? "Model context window overflow",
@@ -1109,7 +1073,7 @@ export class PreparedPiTurnAdapter {
1109
1073
  message.role === "assistant" ||
1110
1074
  message.role === "toolResult"
1111
1075
  ) {
1112
- const abortReason = this.options.abortReason?.();
1076
+ const abortReason = this.options.abortReason?.() ?? (signal.aborted ? "Turn aborted" : undefined);
1113
1077
  const authoritativeMessage =
1114
1078
  message.role === "assistant" &&
1115
1079
  message.stopReason !== "toolUse" &&
@@ -98,12 +98,12 @@ export class PiRuntimeAdapter {
98
98
  * 不要把 RuntimeDatabase 直接传进 Pi 层;持久化职责应继续通过这里的窄回调提供。
99
99
  */
100
100
  createTranscript(options: {
101
- readonly sql: SqlTaggedTemplate;
101
+ readonly storage: DurableObjectStorage;
102
102
  readonly durability: PiTranscriptDurability;
103
103
  readonly hasActiveTurn: () => boolean;
104
104
  }): PiRuntimeTranscript {
105
105
  return new PiRuntimeTranscript(
106
- options.sql,
106
+ options.storage,
107
107
  options.durability,
108
108
  this.models,
109
109
  options.hasActiveTurn,
@@ -224,8 +224,7 @@ export class PiRuntimeAdapter {
224
224
  * @remarks
225
225
  * 调用方:Runtime 在创建 Turn 前调用一次,并把结果作为 `createTurn` 的 `toolExecutors` 传入。
226
226
  *
227
- * 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool 层,而 accountId、
228
- * rateVersion 和 slot identity 这些计费事实属于 Submission。两者在这里汇合:Pi 提供工具,
227
+ * 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool 层,而父子执行身份属于 Submission。两者在这里汇合:Pi 提供工具,
229
228
  * Runtime 提供本次 Submission 的 lifecycle 端口,因此 Runtime 不需要认识 Tool 模块本身。
230
229
  * 没有可用 SubAgent 类型时返回 `undefined`,让 Turn 继续使用装配期的无 lifecycle 执行器。
231
230
  */