@springbrand/agent-runtime 0.2.0-alpha.15 → 0.2.0-alpha.17

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/sandbox/adapter.ts +61 -36
  3. package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -2
  4. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +23 -18
  5. package/src/db/index.ts +5 -0
  6. package/src/db/schema.ts +15 -0
  7. package/src/db/telemetry-outbox.repo.ts +151 -0
  8. package/src/index.ts +1 -0
  9. package/src/kernel/approval-lifecycle.ts +35 -3
  10. package/src/kernel/bindings.ts +2 -1
  11. package/src/kernel/interaction-lifecycle.ts +35 -6
  12. package/src/layers/orchestration/temporary-agent/workspace.ts +4 -4
  13. package/src/lib/prompt.ts +22 -15
  14. package/src/pi/assembly/context.ts +2 -2
  15. package/src/pi/message/conversion.ts +13 -1
  16. package/src/pi/runtime-adapter/assembly.ts +1 -0
  17. package/src/pi/runtime-adapter/execution.ts +63 -27
  18. package/src/pi/runtime-adapter/models.ts +144 -44
  19. package/src/pi/tool/ai-adapter.ts +2 -2
  20. package/src/pi/tool/base.ts +31 -25
  21. package/src/pi/tool/compiler.ts +6 -102
  22. package/src/pi/turn/tool-recovery.ts +11 -1
  23. package/src/runtime-agent.ts +24 -0
  24. package/src/runtime-assembler.ts +38 -19
  25. package/src/runtime-definition.ts +2 -0
  26. package/src/runtime.ts +362 -20
  27. package/src/telemetry/contract.ts +389 -0
  28. package/src/telemetry/coordinator.ts +143 -0
  29. package/src/telemetry/delivery.ts +138 -0
  30. package/src/telemetry/ids.ts +60 -0
  31. package/src/telemetry/index.ts +7 -0
  32. package/src/telemetry/recorder.ts +61 -0
  33. package/src/telemetry/runtime-telemetry.ts +484 -0
  34. package/src/telemetry/sanitize.ts +97 -0
  35. package/src/tool-registry.ts +18 -11
  36. package/src/lib/telemetry-dev.ts +0 -47
@@ -71,6 +71,8 @@ interface InteractionLifecycleOptions<
71
71
  * 没有这个回调,park 的那一刻不会有人去重算 —— 侧栏要等下一次广播才翻牌。
72
72
  */
73
73
  readonly onInteractionsChanged?: () => Promise<void>;
74
+ readonly onTelemetryRequested?: (interaction: InteractionRecord) => void;
75
+ readonly onTelemetrySettled?: (interaction: InteractionRecord) => void;
74
76
  }
75
77
 
76
78
  // #endregion
@@ -131,9 +133,17 @@ export class InteractionLifecycle<
131
133
  interaction: PiToolInteraction,
132
134
  signal?: AbortSignal,
133
135
  ): Promise<void> {
134
- const pending = this.options.db.transaction(() =>
135
- this.ensurePending(submission, interaction)
136
- );
136
+ const pending = this.options.db.transaction(() => {
137
+ const value = this.ensurePending(submission, interaction);
138
+ if (value.created) {
139
+ try {
140
+ this.options.onTelemetryRequested?.(value.interaction);
141
+ } catch {
142
+ // Observability must not change interaction durability.
143
+ }
144
+ }
145
+ return value.interaction;
146
+ });
137
147
  // 必须在 wait 之前通知:wait 会一直挂到客户端投递,之后再通知就晚了一个回合。
138
148
  await this.options.onInteractionsChanged?.();
139
149
  return this.wait(pending, signal);
@@ -218,6 +228,14 @@ export class InteractionLifecycle<
218
228
  settlement: { kind: "cancel", reason },
219
229
  });
220
230
  this.options.applyRecoveryMutations(submission, decision.mutations);
231
+ const settled = this.read(interactionId);
232
+ if (settled) {
233
+ try {
234
+ this.options.onTelemetrySettled?.(settled);
235
+ } catch {
236
+ // Observability must not change terminal cleanup.
237
+ }
238
+ }
221
239
  }
222
240
  return true;
223
241
  }
@@ -267,6 +285,14 @@ export class InteractionLifecycle<
267
285
  if (!decision.applied) return { ok: false };
268
286
  this.options.db.transaction(() => {
269
287
  this.options.applyRecoveryMutations(submission, decision.mutations);
288
+ const settled = this.read(interactionId);
289
+ if (settled && settled.status !== "pending") {
290
+ try {
291
+ this.options.onTelemetrySettled?.(settled);
292
+ } catch {
293
+ // Observability must not change interaction settlement.
294
+ }
295
+ }
270
296
  });
271
297
  const persisted = this.read(interactionId);
272
298
  if (!persisted || persisted.status === "pending") {
@@ -320,7 +346,7 @@ export class InteractionLifecycle<
320
346
  private ensurePending(
321
347
  submission: TSubmission,
322
348
  interaction: PiToolInteraction,
323
- ): InteractionRecord {
349
+ ): { interaction: InteractionRecord; created: boolean } {
324
350
  const existing = this.read(interaction.interactionId);
325
351
  if (existing) {
326
352
  if (
@@ -333,7 +359,7 @@ export class InteractionLifecycle<
333
359
  `Conflicting durable Tool interaction: ${interaction.toolCallId}`,
334
360
  );
335
361
  }
336
- return existing;
362
+ return { interaction: existing, created: false };
337
363
  }
338
364
  // 表行和恢复里程碑都由 record-interaction 这一条命令产出,
339
365
  // 不在这里直接 insert —— 两份状态分叉正是恢复最难查的一类 bug。
@@ -342,7 +368,10 @@ export class InteractionLifecycle<
342
368
  interaction,
343
369
  });
344
370
  this.options.applyRecoveryMutations(submission, decision.mutations);
345
- return { ...interaction, submissionId: submission.submissionId };
371
+ return {
372
+ interaction: { ...interaction, submissionId: submission.submissionId },
373
+ created: true,
374
+ };
346
375
  }
347
376
 
348
377
  // 作用:等待当前进程里的客户端响应。
@@ -3,7 +3,7 @@ import type {
3
3
  WorkspacePort,
4
4
  } from "../../../kernel/bindings";
5
5
 
6
- const MEMORY_ROOT = "/shared/memories";
6
+ const MEMORY_ROOT = "/userspace/memories";
7
7
 
8
8
  // 作用:把 Workspace 路径统一成以 `/` 开头的正斜杠形式。
9
9
  // 调用:所有临时 Agent 路径在检查 Memory 边界前都经过这里。
@@ -21,7 +21,7 @@ function normalize(path: string): string {
21
21
 
22
22
  // 作用:判断一条路径是否指向共享 Memory 根目录或其子项。
23
23
  // 调用:`allowed` 拦截直接访问,`visible` 过滤目录和 glob 结果。
24
- // 原因:同时比较根路径和带 `/` 的子路径前缀,避免误伤 `/shared/memories-old`。
24
+ // 原因:同时比较根路径和带 `/` 的子路径前缀,避免误伤 `/userspace/memories-old`。
25
25
  function isMemoryPath(path: string): boolean {
26
26
  const normalized = normalize(path);
27
27
  return (
@@ -32,12 +32,12 @@ function isMemoryPath(path: string): boolean {
32
32
 
33
33
  // 作用:返回可交给底层 Workspace 的归一化路径。
34
34
  // 调用:临时 Agent Workspace 外观的每个路径参数都必须先经过它。
35
- // 原因:在一个共享边界拒绝 `/shared/memories`,比在每个上层 Tool 里分别检查更不容易漏掉新调用方。
35
+ // 原因:在一个共享边界拒绝 `/userspace/memories`,比在每个上层 Tool 里分别检查更不容易漏掉新调用方。
36
36
  function allowed(path: string): string {
37
37
  const normalized = normalize(path);
38
38
  if (isMemoryPath(normalized)) {
39
39
  throw new Error(
40
- "/shared/memories is not accessible to temporary agents",
40
+ "/userspace/memories is not accessible to temporary agents",
41
41
  );
42
42
  }
43
43
  return normalized;
package/src/lib/prompt.ts CHANGED
@@ -24,9 +24,10 @@ export const SYSTEM_PROMPT =
24
24
 
25
25
  // Explains how to reach capabilities instead of listing unsupported substitutes.
26
26
  export const RUNTIME =
27
- "Runtime: this agent runs on Cloudflare Workers. The execute Code Mode Dynamic Worker is your " +
28
- "instrument — you write code there and it runs with outbound network access (fetch), your " +
29
- "workspace filesystem (state.*), and your other tools (tools.*). Use it for raw or customized HTTP " +
27
+ "Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
28
+ "instrument — code there runs with outbound network access (fetch), your " +
29
+ "workspace filesystem (state.*), and your other tools (tools.* when that connector is available). Put repeated or multi-step work " +
30
+ "in one execute instead of making consecutive top-level Tool calls. Use it for raw or customized HTTP " +
30
31
  "requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
31
32
  "JavaScript — the sandbox evaluates " +
32
33
  "it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
@@ -52,28 +53,34 @@ export const PLANNING =
52
53
 
53
54
  // Tool-selection guidance mirrors the actual approval and network boundaries.
54
55
  export const TOOLS =
55
- "Tools: Prefer the dedicated workspace file tools (read/write/edit/list/find/grep) for file work " +
56
- "they run without approval. For web tasks, use web_search for web discovery, current facts, cited research, " +
57
- "and public URL analysis. Use execute Code Mode only for raw or customized network requests, structured " +
56
+ "Tools: When execute is present, call a top-level Tool directly only for a single standalone Tool call or when that Tool is unavailable " +
57
+ "inside execute. Any operation that needs the same Tool more than once, two or more related file, Skill, Extension, MCP, or Host Tool calls, " +
58
+ "or branching or repetition MUST use one execute. Do not make repeated top-level calls for that work. Inside execute, use state.* for " +
59
+ "workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent non-CDP calls. " +
60
+ "Tools available only at the top level must stay Direct. " +
61
+ "For web tasks, use web_search for web discovery, current facts, cited research, " +
62
+ "and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
58
63
  "API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
59
- "searches. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
64
+ "searches. When execute is absent, use the actually exposed Direct Tools. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
60
65
  "package managers, system commands, builds, tests and background processes; its filesystem is temporary, " +
61
- "persistent inputs are copied in automatically on first use, /shared is read-only, and only explicitly " +
66
+ "persistent inputs are copied in automatically on first use, /userspace is read-only, and only explicitly " +
62
67
  "published /workspace outputs survive. Use Code Mode for computation, multi-step data work, and the " +
63
68
  "raw or customized network cases described above; every response and failure it sees is visible to you. " +
64
69
  "bash is a shell over the workspace filesystem only " +
65
70
  "(no network, no system utilities) and is approval-gated — don't reach for it to read files or fetch. " +
66
- "For more than three related changes in one file, use one write or bash operation instead of serial edits; " +
67
- "do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
71
+ "When execute is present, make related file changes in one execute with state.*; when execute is absent, use one write or bash operation instead of serial edits. " +
72
+ "Do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
68
73
  "prioritize verification, saving durable results, and the final response. " +
69
- "When tool calls are independent, issue them in one turn so they run in parallel. When you reference " +
74
+ "When related Tool calls can run independently and execute is present, run them inside that execute rather than as parallel top-level calls; " +
75
+ "only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
70
76
  "code, cite it as file_path:line_number.";
71
77
 
72
78
  // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
73
79
  export const FILES =
74
80
  "Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
75
- "'<file>.md' (structured markdown). Policy: to summarize/quote/search, read or grep the .md; " +
76
- "to compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
81
+ "'<file>.md' (structured markdown). Policy: to summarize/quote/search one file, read or grep the .md; when execute is present, " +
82
+ "read or search multiple files through state.* in one execute instead of repeated top-level file Tool calls. " +
83
+ "To compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
77
84
  "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
78
85
  "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
79
86
  "zip archives; unknown types), parse the original in the sandbox with JS.";
@@ -93,9 +100,9 @@ export const INTERACTION =
93
100
  export const MEMORY =
94
101
  "Memory conventions: memory/preferences context blocks are writable working memory " +
95
102
  "for this Session only. Cold memory is managed explicitly by the user and is shared " +
96
- "across Sessions under /shared/memories/. Runtime access to that directory is read-only: " +
103
+ "across Sessions under /userspace/memories/. Runtime access to that directory is read-only: " +
97
104
  "never create, edit, move, or delete its files. To recall long-tail information, check " +
98
- "the memory_index block first, then read the listed entry under /shared/memories/ when relevant.";
105
+ "the memory_index block first, then read the listed entry under /userspace/memories/ when relevant.";
99
106
 
100
107
  /**
101
108
  * 把 Agent 个性和 Runtime 的固定行为说明组成主 Agent 系统提示词。
@@ -41,7 +41,7 @@ const COMPACTION_RETRY: RetryPolicy = {
41
41
  baseDelayMs: 500,
42
42
  };
43
43
 
44
- const COLD_MEMORY_INDEX = "/shared/memories/MEMORY.md";
44
+ const COLD_MEMORY_INDEX = "/userspace/memories/MEMORY.md";
45
45
  const TRUNCATION_MARKER = "\n[truncated to memory budget]";
46
46
 
47
47
  function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
@@ -350,7 +350,7 @@ async function readMemory(
350
350
  blocks.push(
351
351
  renderBlock(
352
352
  "MEMORY_INDEX",
353
- "Index of user-managed cold memory under /shared/memories; entries are read on demand",
353
+ "Index of user-managed cold memory under /userspace/memories; entries are read on demand",
354
354
  index,
355
355
  ),
356
356
  );
@@ -21,7 +21,19 @@ function attachmentText(
21
21
  part: Extract<UIMessage["parts"][number], { type: "file" }>,
22
22
  ): string {
23
23
  const name = part.filename?.trim() || "attachment";
24
- return `[Attachment: ${name} (${part.mediaType}) ${part.url}]`;
24
+ let ref: string | null = null;
25
+ try {
26
+ const path = part.url.startsWith("/")
27
+ ? new URL(part.url, "https://attachment.invalid").pathname
28
+ : new URL(part.url).pathname;
29
+ const leaf = path.split("/").filter(Boolean).at(-1);
30
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(leaf ?? "")) {
31
+ ref = leaf!;
32
+ }
33
+ } catch {
34
+ // Preserve the existing canonical form for non-platform file URLs.
35
+ }
36
+ return `[Attachment: ${name} (${part.mediaType}) ${ref ? `ref=${ref}` : part.url}]`;
25
37
  }
26
38
 
27
39
  function userContent(
@@ -208,6 +208,7 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
208
208
  "bind_resource",
209
209
  "delete",
210
210
  "edit",
211
+ "execute",
211
212
  "find",
212
213
  "get_time",
213
214
  "grep",
@@ -25,13 +25,13 @@ import { PiChunkEncoder } from "../message";
25
25
  import type { UIMessageChunk } from "ai";
26
26
  import { ChatStreamStalledError } from "agents/chat";
27
27
  import {
28
+ codeExecutionPiToolCandidate,
28
29
  compilePiTools,
29
30
  createPiToolGovernance,
30
31
  normalizeUpdatePlanArguments,
31
32
  type PiToolCandidate,
32
33
  type PiToolInteractionSpec,
33
34
  type PiToolGovernance,
34
- type PiToolTelemetry,
35
35
  type SettledPiToolCall,
36
36
  } from "../tool";
37
37
  import {
@@ -44,10 +44,12 @@ import {
44
44
  isModelStreamStallMessage,
45
45
  resolvePiApiKey,
46
46
  withProviderRetry,
47
+ type PiGenerationLifecycleObserver,
47
48
  } from "./models";
48
49
  import { EXECUTION_LEVELS } from "../../lib/execution-level";
49
50
  import { projectToolOutputForModel } from "../../layers/context/budget/gate";
50
51
  import type { SpillWorkspace } from "../../lib/artifacts";
52
+ import { toolRegistryFromPiCandidates } from "../../tool-registry";
51
53
 
52
54
  // #region Single-run Pi bridge
53
55
 
@@ -128,10 +130,7 @@ interface PiTurnAdapterOptions {
128
130
  readonly settle: (
129
131
  call: SettledPiToolCall,
130
132
  ) => void | Promise<void>;
131
- // 上报工具执行耗时、结果大小和成败。
132
- // 工具治理层在每次执行结束时调用它,宿主可选择不提供。
133
- // 该回调只负责观测,工具治理层会吞掉它的异常以免改变执行结果。
134
- readonly onToolTelemetry?: (event: PiToolTelemetry) => void;
133
+ readonly onGeneration?: PiGenerationLifecycleObserver;
135
134
  // 在模型调用前调整 Pi 的消息上下文。
136
135
  // PiCore 通过 AgentOptions.transformContext 调用它,Runtime 用它接入现有上下文处理。
137
136
  // 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
@@ -171,7 +170,7 @@ class PiTurnAdapter {
171
170
  // PreparedPiTurnAdapter 只构造一次,并把运行、重试、转向和中止都交给它。
172
171
  // 工具治理对象必须在这些路径间复用,因为它保存本 Turn 的重试次数和熔断状态。
173
172
  constructor(private readonly opts: PiTurnAdapterOptions) {
174
- this.governance = createPiToolGovernance(opts.onToolTelemetry);
173
+ this.governance = createPiToolGovernance();
175
174
  }
176
175
 
177
176
  // 把 Tool candidate 变成 Pi 可以执行的受治理 Tool。
@@ -240,6 +239,7 @@ class PiTurnAdapter {
240
239
  this.opts.models.streamSimple.bind(this.opts.models),
241
240
  0,
242
241
  this.opts.modelSessionId,
242
+ this.opts.onGeneration,
243
243
  ),
244
244
  getApiKey: () => this.opts.apiKey,
245
245
  // transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
@@ -402,18 +402,13 @@ export interface CreatePreparedPiTurnOptions {
402
402
  */
403
403
  readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
404
404
  readonly durability: PiTurnDurability;
405
+ /** Reports paired model-generation lifecycle facts to the Runtime owner. */
406
+ readonly onGeneration?: PiGenerationLifecycleObserver;
405
407
  /** Per-Submission executors for tools whose metadata is fixed at assembly time. */
406
408
  readonly toolExecutors?: Readonly<Record<
407
409
  string,
408
410
  NonNullable<PiToolCandidate["tool"]["execute"]>
409
411
  >>;
410
- /**
411
- * 上报受治理工具的耗时、结果大小和成败。
412
- *
413
- * 工具治理层在执行结束时调用它,Runtime 可用它发出工具遥测事件。
414
- * 该回调只负责观测,工具治理层会隔离它的异常,避免遥测改变工具结果。
415
- */
416
- readonly onToolTelemetry?: (event: PiToolTelemetry) => void;
417
412
  /**
418
413
  * 调整 Pi 即将发送给模型的消息上下文。
419
414
  *
@@ -527,7 +522,11 @@ export class PreparedPiTurnAdapter {
527
522
  messageId: options.submission.messageId,
528
523
  startedAt: options.submission.startedAt,
529
524
  });
530
- this.candidates = state.candidates.map((candidate) => ({
525
+ const bindCandidate = (
526
+ candidate: PiToolCandidate,
527
+ toolCallIdPrefix?: string,
528
+ recordRecoveryAttempt = true,
529
+ ): PiToolCandidate => ({
531
530
  ...candidate,
532
531
  tool: {
533
532
  ...candidate.tool,
@@ -535,6 +534,9 @@ export class PreparedPiTurnAdapter {
535
534
  // 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
536
535
  // 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
537
536
  execute: async (toolCallId, input, signal, onUpdate) => {
537
+ if (toolCallIdPrefix) {
538
+ toolCallId = `${toolCallIdPrefix}:${toolCallId}`;
539
+ }
538
540
  const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
539
541
  ? await candidate.requiredExecutionLevelForInput(input)
540
542
  : candidate.requiredExecutionLevel;
@@ -645,17 +647,19 @@ export class PreparedPiTurnAdapter {
645
647
  }
646
648
  return responded.result;
647
649
  }
648
- const retry = piToolRetryPolicy(candidate);
649
- const firstAttempt = options.durability.appendToolInput({
650
- toolCallId,
651
- toolName: candidate.tool.name,
652
- input,
653
- retry,
654
- });
655
- if (!firstAttempt && retry === "non-idempotent") {
656
- throw new Error(
657
- `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
658
- );
650
+ if (recordRecoveryAttempt) {
651
+ const retry = piToolRetryPolicy(candidate);
652
+ const firstAttempt = options.durability.appendToolInput({
653
+ toolCallId,
654
+ toolName: candidate.tool.name,
655
+ input,
656
+ retry,
657
+ });
658
+ if (!firstAttempt && retry === "non-idempotent") {
659
+ throw new Error(
660
+ `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
661
+ );
662
+ }
659
663
  }
660
664
  const execute = options.toolExecutors?.[candidate.tool.name] ??
661
665
  candidate.tool.execute;
@@ -667,7 +671,39 @@ export class PreparedPiTurnAdapter {
667
671
  );
668
672
  },
669
673
  },
670
- }));
674
+ });
675
+ this.candidates = state.candidates.map((candidate) => {
676
+ if (!candidate.codeExecutionTools) return bindCandidate(candidate);
677
+ const factory = state.snapshot.bindings.codeExecution;
678
+ if (!factory) {
679
+ throw new Error("Prepared Code Mode Tool requires a Runtime factory");
680
+ }
681
+ return bindCandidate({
682
+ ...candidate,
683
+ tool: {
684
+ ...candidate.tool,
685
+ execute: (toolCallId, input, signal, onUpdate) => {
686
+ const runtimeCandidate = codeExecutionPiToolCandidate(
687
+ factory.create(toolRegistryFromPiCandidates(
688
+ candidate.codeExecutionTools!.map((inner) =>
689
+ // Code Mode owns replay of its connector calls. Pi persists
690
+ // only the parent execute attempt/result; recording an inner
691
+ // input without a Pi settlement creates an orphan recovery
692
+ // action that cannot be found on the direct Tool surface.
693
+ bindCandidate(inner, toolCallId, false)
694
+ ),
695
+ )),
696
+ );
697
+ return runtimeCandidate.tool.execute(
698
+ toolCallId,
699
+ input,
700
+ signal,
701
+ onUpdate,
702
+ );
703
+ },
704
+ },
705
+ });
706
+ });
671
707
  this.turn = new PiTurnAdapter({
672
708
  pi: {
673
709
  model: state.snapshot.pi.model,
@@ -679,13 +715,13 @@ export class PreparedPiTurnAdapter {
679
715
  state.snapshot.pi.model.id,
680
716
  ),
681
717
  modelSessionId: options.submission.requestId,
718
+ ...(options.onGeneration ? { onGeneration: options.onGeneration } : {}),
682
719
  canonicalMessages: options.canonicalMessages,
683
720
  // 被中断的 Turn 不写结算。它退场路上还会吐出「工具被中止」,而工具其实
684
721
  // 没有产生任何结果 —— 结算是单调的,落了这条假结果,重建出来的续跑会
685
722
  // 把一个从没跑过的工具当成跑过并失败。取消不走这里:取消要的就是终态。
686
723
  settle: (call) =>
687
724
  this.interrupted ? undefined : options.durability.settleTool(call),
688
- onToolTelemetry: options.onToolTelemetry,
689
725
  transformContext: options.transformContext,
690
726
  workspace: state.snapshot.bindings.workspace,
691
727
  });