@springbrand/agent-runtime 0.2.0-alpha.16 → 0.2.0-alpha.18

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/subagent/runner.ts +5 -1
  4. package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -2
  5. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +23 -18
  6. package/src/db/index.ts +5 -0
  7. package/src/db/schema.ts +15 -0
  8. package/src/db/telemetry-outbox.repo.ts +151 -0
  9. package/src/index.ts +1 -0
  10. package/src/kernel/approval-lifecycle.ts +35 -3
  11. package/src/kernel/bindings.ts +0 -1
  12. package/src/kernel/interaction-lifecycle.ts +35 -6
  13. package/src/layers/orchestration/temporary-agent/workspace.ts +4 -4
  14. package/src/lib/prompt.ts +22 -15
  15. package/src/pi/assembly/context.ts +2 -2
  16. package/src/pi/message/conversion.ts +13 -1
  17. package/src/pi/runtime-adapter/execution.ts +26 -26
  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 +5 -103
  22. package/src/pi/turn/tool-recovery.ts +11 -1
  23. package/src/runtime-agent.ts +24 -0
  24. package/src/runtime-assembler.ts +29 -15
  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 +11 -11
  36. package/src/lib/telemetry-dev.ts +0 -47
@@ -276,31 +276,37 @@ export function basePiToolCandidates(
276
276
  },
277
277
  },
278
278
  },
279
- candidate({
280
- name: "suggest_followups",
281
- label: "Suggest follow-ups",
282
- description:
283
- "Offer the user 2-4 optional follow-up directions after completing a substantive task (a report, an analysis, a multi-step job). Call this at most once, and it MUST be the very last thing you do: finish all of your prose FIRST, then call this tool and END the turn immediately — do NOT write any text after calling it. Do NOT call it for small talk, quick answers, or while a task is still in progress.",
284
- parameters: suggestFollowupsParameters,
285
- async execute(_toolCallId, input) {
286
- return result({ noted: true, count: input.items.length });
287
- },
288
- }),
289
- candidate({
290
- name: "update_plan",
291
- label: "Update plan",
292
- description:
293
- "Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
294
- parameters: updatePlanParameters,
295
- prepareArguments: normalizeUpdatePlanArguments,
296
- async execute(_toolCallId, input) {
297
- return result({
298
- ok: true,
299
- total: input.steps.length,
300
- done: input.steps.filter((step) => step.status === "done").length,
301
- });
302
- },
303
- }),
279
+ {
280
+ ...candidate({
281
+ name: "suggest_followups",
282
+ label: "Suggest follow-ups",
283
+ description:
284
+ "Offer the user 2-4 optional follow-up directions after completing a substantive task (a report, an analysis, a multi-step job). Call this at most once, and it MUST be the very last thing you do: finish all of your prose FIRST, then call this tool and END the turn immediately — do NOT write any text after calling it. Do NOT call it for small talk, quick answers, or while a task is still in progress.",
285
+ parameters: suggestFollowupsParameters,
286
+ async execute(_toolCallId, input) {
287
+ return result({ noted: true, count: input.items.length });
288
+ },
289
+ }),
290
+ direct: true,
291
+ },
292
+ {
293
+ ...candidate({
294
+ name: "update_plan",
295
+ label: "Update plan",
296
+ description:
297
+ "Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
298
+ parameters: updatePlanParameters,
299
+ prepareArguments: normalizeUpdatePlanArguments,
300
+ async execute(_toolCallId, input) {
301
+ return result({
302
+ ok: true,
303
+ total: input.steps.length,
304
+ done: input.steps.filter((step) => step.status === "done").length,
305
+ });
306
+ },
307
+ }),
308
+ direct: true,
309
+ },
304
310
  ...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
305
311
  ];
306
312
  }
@@ -46,7 +46,9 @@ export interface PiToolInteractionSpec {
46
46
  export interface PiToolCandidate {
47
47
  readonly owner: string;
48
48
  readonly tool: AgentTool<any, any>;
49
- /** @internal Tools hidden behind this Code Mode candidate. */
49
+ /** Keep this Tool Direct-only instead of also offering it through Code Mode. */
50
+ readonly direct?: true;
51
+ /** @internal Tools also callable through this Code Mode candidate. */
50
52
  readonly codeExecutionTools?: readonly PiToolCandidate[];
51
53
  /** Conservative maximum used in the stable Runtime descriptor. */
52
54
  readonly requiredExecutionLevel: ExecutionLevel;
@@ -78,15 +80,6 @@ export interface CompilePiToolsOptions {
78
80
  ) => void | Promise<void>;
79
81
  }
80
82
 
81
- /** 记录一次 Pi 工具调用的耗时、结果大小和溢出状态。 */
82
- export interface PiToolTelemetry {
83
- readonly tool: string;
84
- readonly ms: number;
85
- readonly ok: boolean;
86
- readonly bytes: number;
87
- readonly spill: boolean;
88
- }
89
-
90
83
  const governanceState = Symbol("PiToolGovernanceState");
91
84
 
92
85
  /** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
@@ -116,7 +109,6 @@ interface PiToolGovernanceState {
116
109
  readonly terminalCalls: Set<string>;
117
110
  totalFailures: number;
118
111
  aborted: boolean;
119
- readonly telemetry?: (event: PiToolTelemetry) => void;
120
112
  }
121
113
 
122
114
  // #endregion
@@ -163,57 +155,6 @@ function callKey(toolName: string, args: unknown): string {
163
155
  return `${toolName}\0${(hash >>> 0).toString(36)}`;
164
156
  }
165
157
 
166
- // 估算工具结果给 Durable Object 带来的字节压力。
167
- // `emitToolTelemetry()` 在每次成功、失败或阻断后调用它。
168
- // artifact_ref 已把大结果外溢,因此必须使用 details 中的原始字节数而不是小引用自身的大小。
169
- function resultPressure(result: AgentToolResult<unknown>): {
170
- bytes: number;
171
- spill: boolean;
172
- } {
173
- const details = result.details as {
174
- kind?: string;
175
- bytes?: number;
176
- } | undefined;
177
- if (
178
- details?.kind === "artifact_ref" &&
179
- typeof details.bytes === "number"
180
- ) {
181
- return { bytes: details.bytes, spill: true };
182
- }
183
- try {
184
- return {
185
- bytes: JSON.stringify(result).length,
186
- spill: false,
187
- };
188
- } catch {
189
- return { bytes: 0, spill: false };
190
- }
191
- }
192
-
193
- // 尽力把一次工具调用的计量事件交给可选遥测端口。
194
- // 受治理 execute 方法在每条终止路径上调用。
195
- // 遥测是观测而非业务逻辑,所以端口抛错必须被吞掉,不能改变工具结果。
196
- function emitToolTelemetry(
197
- state: PiToolGovernanceState | undefined,
198
- tool: string,
199
- startedAt: number,
200
- ok: boolean,
201
- result: AgentToolResult<unknown>,
202
- ): void {
203
- if (!state?.telemetry) return;
204
- const pressure = resultPressure(result);
205
- try {
206
- state.telemetry({
207
- tool,
208
- ms: Math.max(0, Math.round(performance.now() - startedAt)),
209
- ok,
210
- ...pressure,
211
- });
212
- } catch {
213
- // Observability must not change Tool execution.
214
- }
215
- }
216
-
217
158
  // Most Pi adapters keep the original Tool value in details and its serialized
218
159
  // model form in one text block. Spill that original once instead of duplicating
219
160
  // it inside an AgentToolResult JSON envelope.
@@ -313,15 +254,12 @@ function blockReason(
313
254
  *
314
255
  * 状态用 symbol 绑定到钩子对象,让编译后的工具和 Pi 后置钩子共享一份 Turn 计数而不暴露公开可变 API。
315
256
  */
316
- export function createPiToolGovernance(
317
- telemetry?: (event: PiToolTelemetry) => void,
318
- ): PiToolGovernance {
257
+ export function createPiToolGovernance(): PiToolGovernance {
319
258
  const state: PiToolGovernanceState = {
320
259
  failures: new Map(),
321
260
  terminalCalls: new Set(),
322
261
  totalFailures: 0,
323
262
  aborted: false,
324
- telemetry,
325
263
  };
326
264
  const governance: PiToolGovernance = {
327
265
  [governanceState]: state,
@@ -338,7 +276,7 @@ export function createPiToolGovernance(
338
276
  return governance;
339
277
  }
340
278
 
341
- // 为一个候选工具包上输出限额、持久化结算、重试治理和遥测。
279
+ // 为一个候选工具包上输出限额、持久化结算和重试治理。
342
280
  // `compilePiTools()` 对最终 Tool Surface 中的每个候选项调用。
343
281
  // 一个共享包装边界可确保所有工具无论成功还是失败都经过 settle,不能由各工具自行选择是否持久化。
344
282
  function governedTool(
@@ -355,7 +293,6 @@ function governedTool(
355
293
  // Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。
356
294
  // 输出限额必须发生在 settle 之前,否则 Durable Object 会持久化一份与模型最终所见不同的过大结果。
357
295
  async execute(toolCallId, args, signal, onUpdate) {
358
- const startedAt = performance.now();
359
296
  const blocked = blockReason(
360
297
  state,
361
298
  toolCallId,
@@ -380,22 +317,8 @@ function governedTool(
380
317
  source: candidate.source,
381
318
  });
382
319
  } catch (settlementFailure) {
383
- emitToolTelemetry(
384
- state,
385
- candidate.tool.name,
386
- startedAt,
387
- false,
388
- result,
389
- );
390
320
  throw settlementFailure;
391
321
  }
392
- emitToolTelemetry(
393
- state,
394
- candidate.tool.name,
395
- startedAt,
396
- false,
397
- result,
398
- );
399
322
  throw cause;
400
323
  }
401
324
 
@@ -434,13 +357,6 @@ function governedTool(
434
357
  candidate.tool.name,
435
358
  args,
436
359
  );
437
- emitToolTelemetry(
438
- state,
439
- candidate.tool.name,
440
- startedAt,
441
- false,
442
- result,
443
- );
444
360
  const boundedMessage = (result.content[0] as { text: string }).text;
445
361
  throw cause instanceof Error &&
446
362
  failure === cause &&
@@ -470,25 +386,11 @@ function governedTool(
470
386
  candidate.tool.name,
471
387
  args,
472
388
  );
473
- emitToolTelemetry(
474
- state,
475
- candidate.tool.name,
476
- startedAt,
477
- false,
478
- result,
479
- );
480
389
  throw cause;
481
390
  }
482
391
  state?.failures.delete(
483
392
  callKey(candidate.tool.name, args),
484
393
  );
485
- emitToolTelemetry(
486
- state,
487
- candidate.tool.name,
488
- startedAt,
489
- true,
490
- result,
491
- );
492
394
  return result;
493
395
  },
494
396
  };
@@ -732,7 +732,17 @@ export function planPiToolRecovery(
732
732
  }
733
733
 
734
734
  // 已结算 Tool 先补齐续跑;未结算 Tool 只有声明幂等时才自动重试。
735
- for (const tool of Object.values(state.toolCalls)) {
735
+ const tools = Object.values(state.toolCalls);
736
+ const codeModeParents = tools
737
+ .filter((tool) => tool.toolName === "execute")
738
+ .map((tool) => `${tool.toolCallId}:`);
739
+ for (const tool of tools) {
740
+ // 旧版把 Code Mode 内层调用写成独立 Pi attempt,但只有父
741
+ // execute 会产生 settlement。这些带父调用前缀的记录不是可
742
+ // 独立恢复的 Tool;父 execute 才是唯一恢复边界。
743
+ if (codeModeParents.some((prefix) => tool.toolCallId.startsWith(prefix))) {
744
+ continue;
745
+ }
736
746
  if (tool.result) {
737
747
  const key = `tool:${tool.toolCallId}:settled`;
738
748
  const plan = tool.needsContinuation
@@ -56,6 +56,7 @@ import type {
56
56
  RuntimeAgentRole,
57
57
  RuntimeAgentToolResult,
58
58
  } from "./runtime-agent-context";
59
+ import type { AgentTelemetryBinding } from "./telemetry/contract";
59
60
 
60
61
  /**
61
62
  * 本文件把应用提供的 Config Definition 与 Planner 接到 Cloudflare Agent 生命周期。
@@ -111,6 +112,11 @@ interface RuntimeAgentDefinitionBase<
111
112
  | RuntimeAgentHooks<Env, Config, Command, Change>
112
113
  | ((context: RuntimeAgentPlanningContext<Env, Command, Change>) =>
113
114
  RuntimeAgentHooks<Env, Config, Command, Change>);
115
+
116
+ /** Prepare the optional telemetry consumer for this concrete Agent facet. */
117
+ readonly telemetry?: (
118
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
119
+ ) => AgentTelemetryBinding | undefined;
114
120
  }
115
121
 
116
122
  export interface ReadonlyRuntimeAgentDefinition<
@@ -415,11 +421,23 @@ export function defineRuntimeAgent<
415
421
  : hooks as RuntimeAgentHooks<Env, Config, Command, Change>;
416
422
  };
417
423
 
424
+ const resolveDefinitionTelemetry = (
425
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
426
+ ): AgentTelemetryBinding | undefined => {
427
+ const telemetry = definition.telemetry;
428
+ if (!telemetry) return undefined;
429
+ return (telemetry as (
430
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
431
+ ) => AgentTelemetryBinding | undefined)(context);
432
+ };
433
+
418
434
  const GeneratedRuntimeAgent = {
419
435
  [definition.name]: class extends AgentToolRuntimeKernel<Env> {
420
436
  private hasLoadedRuntime = false;
421
437
  private loadedRuntimeKey?: string;
422
438
  private loading?: Promise<void>;
439
+ private telemetryResolved = false;
440
+ private resolvedTelemetry?: AgentTelemetryBinding;
423
441
  private temporaryLaunch?: TemporaryAgentLaunch<Config>;
424
442
  private temporaryDispose?: () => Promise<void>;
425
443
  private temporaryCleanup?: Promise<void>;
@@ -609,6 +627,11 @@ export function defineRuntimeAgent<
609
627
  { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
610
628
  ));
611
629
  let hooks = resolveDefinitionHooks(context);
630
+ if (!this.telemetryResolved) {
631
+ this.resolvedTelemetry = resolveDefinitionTelemetry(context);
632
+ this.telemetryResolved = true;
633
+ }
634
+ const telemetry = this.resolvedTelemetry;
612
635
  if (this.role === "temporary") {
613
636
  this.temporaryDispose = tools.dispose;
614
637
  tools = this.applyTemporaryToolPolicy(tools);
@@ -620,6 +643,7 @@ export function defineRuntimeAgent<
620
643
  tools,
621
644
  resources: loaded.resources,
622
645
  hooks,
646
+ ...(telemetry ? { telemetry } : {}),
623
647
  });
624
648
 
625
649
  await this.initCandidate(candidate);
@@ -61,6 +61,10 @@ import { skillPiToolCandidates } from "./pi/tool/skill";
61
61
  import { createWebSearch } from "./pi/tool/web-search";
62
62
  import { subagentPiToolCandidates } from "./pi/tool/subagent";
63
63
  import { resolvePiModel } from "./pi/runtime-adapter/models";
64
+ import {
65
+ normalizeAgentTelemetryBinding,
66
+ type AgentTelemetryBinding,
67
+ } from "./telemetry/contract";
64
68
 
65
69
  /** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
66
70
 
@@ -92,6 +96,7 @@ export interface RuntimeAssemblyInput {
92
96
  readonly subagents?: RuntimeSubagentPort;
93
97
  readonly schedule?: RuntimeSchedulePort;
94
98
  readonly turnEvents?: RuntimeTurnEventsPort;
99
+ readonly telemetry?: AgentTelemetryBinding;
95
100
  readonly commitGuards: readonly (() => Promise<void>)[];
96
101
  readonly degradations: readonly RuntimeDegradation[];
97
102
  }
@@ -120,6 +125,7 @@ export interface RuntimeCandidate {
120
125
  readonly commitGuards: readonly (() => Promise<void>)[];
121
126
  readonly hooks?: RuntimeAgentHooks<Cloudflare.Env, unknown, never, never>;
122
127
  readonly turnEvents?: RuntimeTurnEventsPort;
128
+ readonly telemetry?: AgentTelemetryBinding;
123
129
  }
124
130
 
125
131
  const DISABLED_MEMORY: RuntimeMemoryProfile = Object.freeze({
@@ -244,14 +250,10 @@ async function createToolSurface(
244
250
  }
245
251
  const mergeable = finalized.filter(
246
252
  (candidate) =>
253
+ !candidate.direct &&
247
254
  !candidate.interaction &&
248
255
  typeof candidate.tool.execute === "function",
249
256
  );
250
- const direct = finalized.filter(
251
- (candidate) =>
252
- candidate.interaction ||
253
- typeof candidate.tool.execute !== "function",
254
- );
255
257
  const codeExecutionTools = Object.freeze([...mergeable]);
256
258
  return Object.freeze([
257
259
  Object.freeze({
@@ -262,7 +264,7 @@ async function createToolSurface(
262
264
  ),
263
265
  codeExecutionTools,
264
266
  }),
265
- ...direct,
267
+ ...finalized,
266
268
  ]);
267
269
  },
268
270
  }),
@@ -304,6 +306,7 @@ class RuntimeBuilder {
304
306
  private readonly enabledSubagents = new Set<string>();
305
307
  private subagents?: RuntimeSubagentPort;
306
308
  private turnEvents?: RuntimeTurnEventsPort;
309
+ private telemetry?: AgentTelemetryBinding;
307
310
  private readonly commitGuards: Array<() => Promise<void>> = [];
308
311
  private readonly degradations: RuntimeDegradation[] = [];
309
312
 
@@ -336,6 +339,9 @@ class RuntimeBuilder {
336
339
  }
337
340
  this.subagents = input.subagents;
338
341
  this.turnEvents = input.turnEvents;
342
+ this.telemetry = input.telemetry
343
+ ? normalizeAgentTelemetryBinding(input.telemetry)
344
+ : undefined;
339
345
  this.commitGuards.push(...input.commitGuards);
340
346
  }
341
347
 
@@ -564,6 +570,7 @@ class RuntimeBuilder {
564
570
  commitGuards: Object.freeze([
565
571
  ...this.commitGuards,
566
572
  ]),
573
+ ...(this.telemetry ? { telemetry: this.telemetry } : {}),
567
574
  });
568
575
  }
569
576
 
@@ -621,23 +628,28 @@ export function toolRegistryPiToolCandidates(
621
628
  if (deny.has(name)) continue;
622
629
  if (allowsTool?.(name) === false) continue;
623
630
  const normalized = requiredName(name, "Platform Tool");
631
+ const {
632
+ label,
633
+ description,
634
+ parameters,
635
+ execute,
636
+ owner = "platform",
637
+ ...metadata
638
+ } = spec;
624
639
  candidates.push(Object.freeze({
625
- owner: "platform",
626
- requiredExecutionLevel: spec.requiredExecutionLevel,
627
- ...(spec.alwaysRequiresApproval
628
- ? { alwaysRequiresApproval: true }
629
- : {}),
640
+ ...metadata,
641
+ owner,
630
642
  tool: Object.freeze({
631
643
  name: normalized,
632
- label: spec.label,
633
- description: spec.description,
634
- parameters: spec.parameters,
644
+ label,
645
+ description,
646
+ parameters,
635
647
  async execute(
636
648
  toolCallId: string,
637
649
  input: unknown,
638
650
  signal?: AbortSignal,
639
651
  ) {
640
- const result = await spec.execute(input, {
652
+ const result = await execute(input, {
641
653
  toolCallId,
642
654
  signal: signal ?? new AbortController().signal,
643
655
  });
@@ -822,6 +834,7 @@ export async function assembleRuntimeSnapshot<
822
834
  : {}),
823
835
  extensions: resources.extensions,
824
836
  ...(turnEvents ? { turnEvents } : {}),
837
+ ...(input.telemetry ? { telemetry: input.telemetry } : {}),
825
838
  commitGuards: [],
826
839
  degradations: [
827
840
  ...(toolAssembly.degradations ?? []),
@@ -848,6 +861,7 @@ export async function assembleRuntimeSnapshot<
848
861
  }
849
862
  : {}),
850
863
  ...(turnEvents ? { turnEvents } : {}),
864
+ ...(candidate.telemetry ? { telemetry: candidate.telemetry } : {}),
851
865
  });
852
866
  }
853
867
 
@@ -27,6 +27,7 @@ import type {
27
27
  RuntimeAssemblyContext,
28
28
  RuntimeAgentPlanningContext,
29
29
  } from "./runtime-agent-context";
30
+ import type { AgentTelemetryBinding } from "./telemetry/contract";
30
31
 
31
32
  export interface RuntimeProfileContribution {
32
33
  readonly model: string;
@@ -184,4 +185,5 @@ export interface AssembleRuntimeSnapshotInput<
184
185
  readonly tools: import("./tool-registry").ToolAssemblyResult;
185
186
  readonly resources: ResolvedResources;
186
187
  readonly hooks?: RuntimeAgentHooks<Env, Config, Command, Change>;
188
+ readonly telemetry?: AgentTelemetryBinding;
187
189
  }