@springbrand/agent-runtime 0.2.0-alpha.36 → 0.2.0-alpha.39

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.36",
3
+ "version": "0.2.0-alpha.39",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -23,6 +23,8 @@
23
23
  "@cloudflare/think": "0.15.0",
24
24
  "@earendil-works/pi-agent-core": "0.83.0",
25
25
  "@earendil-works/pi-ai": "0.83.0",
26
+ "@types/lodash-es": "^4.17.12",
27
+ "cron-schedule": "6.0.0",
26
28
  "isomorphic-git": "1.38.6",
27
29
  "lodash-es": "^4.18.1",
28
30
  "zod": "^4.4.3"
@@ -0,0 +1,84 @@
1
+ import type { RuntimePlatformPort } from "../../kernel/bindings";
2
+ import { WorkerEntrypoint } from "cloudflare:workers";
3
+
4
+ export type DynamicWorkerObservation = {
5
+ source: string;
6
+ attributes?: Readonly<Record<string, string>>;
7
+ };
8
+
9
+ type TailProps = DynamicWorkerObservation & {
10
+ workerId: string;
11
+ mainModule: string;
12
+ };
13
+
14
+ type ExportContext = Pick<DurableObjectState, "exports">;
15
+
16
+ function attachTail(
17
+ context: ExportContext,
18
+ code: WorkerLoaderWorkerCode,
19
+ observation: DynamicWorkerObservation,
20
+ workerId: string,
21
+ ): WorkerLoaderWorkerCode {
22
+ const entrypoint = (context.exports as unknown as {
23
+ DynamicWorkerTail(options: { props: TailProps }): Fetcher;
24
+ }).DynamicWorkerTail;
25
+ const tail = entrypoint({
26
+ props: { ...observation, workerId, mainModule: code.mainModule },
27
+ });
28
+ return { ...code, tails: [...(code.tails ?? []), tail] };
29
+ }
30
+
31
+ export function observeDynamicWorkers(
32
+ context: ExportContext,
33
+ loader: WorkerLoader,
34
+ observation: DynamicWorkerObservation,
35
+ ): WorkerLoader {
36
+ return {
37
+ load: (code) =>
38
+ loader.load(attachTail(context, code, observation, crypto.randomUUID())),
39
+ get: (name, getCode) => {
40
+ const workerId = name ?? crypto.randomUUID();
41
+ return loader.get(name, async () =>
42
+ attachTail(context, await getCode(), observation, workerId)
43
+ );
44
+ },
45
+ };
46
+ }
47
+
48
+ export function observeDynamicWorkerPlatform(
49
+ context: ExportContext,
50
+ load: () => Promise<RuntimePlatformPort>,
51
+ observation: DynamicWorkerObservation,
52
+ ): () => Promise<RuntimePlatformPort> {
53
+ return async () => {
54
+ const platform = await load();
55
+ return {
56
+ ...platform,
57
+ loader: observeDynamicWorkers(context, platform.loader, observation),
58
+ };
59
+ };
60
+ }
61
+
62
+ export class DynamicWorkerTail extends WorkerEntrypoint<Cloudflare.Env, TailProps> {
63
+ tail(events: TraceItem[]): void {
64
+ for (const event of events) {
65
+ console.log({
66
+ ...this.ctx.props.attributes,
67
+ source: "dynamic-worker-tail",
68
+ dynamicWorkerSource: this.ctx.props.source,
69
+ dynamicWorkerId: this.ctx.props.workerId,
70
+ durableObjectId: event.durableObjectId ?? null,
71
+ mainModule: this.ctx.props.mainModule,
72
+ eventTimestamp: event.eventTimestamp,
73
+ scriptName: event.scriptName,
74
+ entrypoint: event.entrypoint,
75
+ outcome: event.outcome,
76
+ truncated: event.truncated,
77
+ cpuTime: event.cpuTime,
78
+ wallTime: event.wallTime,
79
+ logs: event.logs,
80
+ exceptions: event.exceptions,
81
+ });
82
+ }
83
+ }
84
+ }
@@ -14,7 +14,17 @@ export {
14
14
  export {
15
15
  assembleUniversalAgentTools,
16
16
  } from "./universal-agent/tools";
17
- export { createUniversalAgentHooks } from "./universal-agent/hooks";
17
+ export {
18
+ DynamicWorkerTail,
19
+ observeDynamicWorkerPlatform,
20
+ observeDynamicWorkers,
21
+ type DynamicWorkerObservation,
22
+ } from "./dynamic-worker-observability";
23
+ export {
24
+ isValidTimeZone,
25
+ nextCronOccurrence,
26
+ zoneOffsetMs,
27
+ } from "./scheduling/zoned-cron";
18
28
 
19
29
  export {
20
30
  CloudflareSandboxAdapter,
@@ -0,0 +1,114 @@
1
+ import { parseCronExpression } from "cron-schedule";
2
+
3
+ export function zoneOffsetMs(instant: number, timeZone: string): number {
4
+ const parts = new Intl.DateTimeFormat("en-US", {
5
+ timeZone,
6
+ hour12: false,
7
+ year: "numeric",
8
+ month: "2-digit",
9
+ day: "2-digit",
10
+ hour: "2-digit",
11
+ minute: "2-digit",
12
+ second: "2-digit",
13
+ }).formatToParts(new Date(instant));
14
+ const field = (type: Intl.DateTimeFormatPartTypes) =>
15
+ Number(parts.find((part) => part.type === type)?.value ?? 0);
16
+ const wallAsUtc = Date.UTC(
17
+ field("year"),
18
+ field("month") - 1,
19
+ field("day"),
20
+ field("hour") % 24,
21
+ field("minute"),
22
+ field("second"),
23
+ );
24
+ return wallAsUtc - Math.floor(instant / 1_000) * 1_000;
25
+ }
26
+
27
+ export function nextCronOccurrence(
28
+ cron: string,
29
+ timeZone: string,
30
+ after = Date.now(),
31
+ ): number {
32
+ const parsed = parseCronExpression(cron);
33
+ const secondPrecision = parsed.seconds.length !== 1 || parsed.seconds[0] !== 0;
34
+ const step = secondPrecision ? 1_000 : 60_000;
35
+ for (
36
+ let instant = Math.floor(after / step) * step + step;
37
+ instant <= after + 3 * 3_600_000;
38
+ instant += step
39
+ ) {
40
+ if (matches(parsed, instant + zoneOffsetMs(instant, timeZone))) return instant;
41
+ }
42
+
43
+ const wallAfter = after + zoneOffsetMs(after, timeZone);
44
+ const wallDate = new Date(wallAfter);
45
+ const firstDay = Date.UTC(
46
+ wallDate.getUTCFullYear(),
47
+ wallDate.getUTCMonth(),
48
+ wallDate.getUTCDate(),
49
+ );
50
+ for (let dayOffset = 0; dayOffset <= 366 * 5; dayOffset += 1) {
51
+ const day = firstDay + dayOffset * 86_400_000;
52
+ if (!matchesDate(parsed, new Date(day))) continue;
53
+ for (const hour of parsed.hours) {
54
+ for (const minute of parsed.minutes) {
55
+ for (const second of parsed.seconds) {
56
+ const wall = day + hour * 3_600_000 + minute * 60_000 + second * 1_000;
57
+ if (wall <= wallAfter) continue;
58
+ const resolved = wallTimeCandidates(wall, timeZone, after)[0];
59
+ if (resolved !== undefined) return resolved;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ throw new Error("Cron has no occurrence in the next five years");
65
+ }
66
+
67
+ export function isValidTimeZone(timeZone: string): boolean {
68
+ try {
69
+ new Intl.DateTimeFormat("en-US", { timeZone });
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function wallTimeCandidates(wallUtc: number, timeZone: string, after: number): number[] {
77
+ const offsets = new Set([
78
+ zoneOffsetMs(after, timeZone),
79
+ zoneOffsetMs(wallUtc - 2 * 86_400_000, timeZone),
80
+ zoneOffsetMs(wallUtc, timeZone),
81
+ zoneOffsetMs(wallUtc + 2 * 86_400_000, timeZone),
82
+ ]);
83
+ return [...offsets]
84
+ .map((offset) => wallUtc - offset)
85
+ .filter((instant) =>
86
+ instant > after && instant + zoneOffsetMs(instant, timeZone) === wallUtc
87
+ )
88
+ .sort((left, right) => left - right);
89
+ }
90
+
91
+ function matches(
92
+ cron: ReturnType<typeof parseCronExpression>,
93
+ wallUtc: number,
94
+ ): boolean {
95
+ const wall = new Date(wallUtc);
96
+ return cron.seconds.includes(wall.getUTCSeconds()) &&
97
+ cron.minutes.includes(wall.getUTCMinutes()) &&
98
+ cron.hours.includes(wall.getUTCHours()) &&
99
+ matchesDate(cron, wall);
100
+ }
101
+
102
+ function matchesDate(
103
+ cron: ReturnType<typeof parseCronExpression>,
104
+ wall: Date,
105
+ ): boolean {
106
+ if (!cron.months.includes(wall.getUTCMonth())) return false;
107
+ const daysRestricted = cron.days.length !== 31;
108
+ const weekdaysRestricted = cron.weekdays.length !== 7;
109
+ const dayMatches = cron.days.includes(wall.getUTCDate());
110
+ const weekdayMatches = cron.weekdays.includes(wall.getUTCDay());
111
+ return daysRestricted && weekdaysRestricted
112
+ ? dayMatches || weekdayMatches
113
+ : dayMatches && weekdayMatches;
114
+ }
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  * - `admission` 是提示进入 Turn 前的接纳、去重和状态登记流程。
25
25
  * - `defineRuntimeAgent.load` 返回 config 和已加载 Resource。
26
26
  * - `defineRuntimeAgent.tools` 返回 Host 的 Pi Tool Candidate 装配结果。
27
- * - `assembleRuntimeSnapshot` 消费该输入与 hooks,产出候选 Snapshot。
27
+ * - `assembleRuntimeSnapshot` 消费已加载 Resource 与 Tool,产出候选 Snapshot。
28
28
  * - `Port` 是 Runtime 调用外部能力时依赖的最小接口(Definition 作者不直接装配)。
29
29
  * - `RuntimeBindings` 是本次装配选中的 Port 和可执行对象集合。
30
30
  * - `RuntimeBuilder` 是包内实现,用来校验贡献并生成候选结果。
@@ -63,11 +63,9 @@ export type {
63
63
  } from "./runtime-assembler";
64
64
  export { assembleRuntimeSnapshot } from "./runtime-assembler";
65
65
  export type {
66
- ProfileOverrides,
67
66
  ResolvedConnectors,
68
67
  ResolvedResources,
69
68
  RuntimeAgentContext,
70
- RuntimeAgentHooks,
71
69
  RuntimeSettings,
72
70
  RuntimeToolBindings,
73
71
  ToolAssemblyResult,
@@ -104,6 +102,7 @@ export type {
104
102
  PiDeclaredToolPolicy,
105
103
  } from "./pi/tool";
106
104
  export { memoryPiToolCandidate } from "./pi/tool";
105
+ export { GET_TIME_TOOL_NAME, getTimePiToolCandidate } from "./pi/tool";
107
106
  export { schedulePiToolCandidates } from "./pi/tool";
108
107
  export {
109
108
  sandboxPiToolCandidates,
@@ -543,6 +543,25 @@ export interface CreatePreparedPiTurnOptions {
543
543
  readonly durability: PiTurnDurability;
544
544
  /** Reports paired model-generation lifecycle facts to the Runtime owner. */
545
545
  readonly onGeneration?: PiGenerationLifecycleObserver;
546
+ /** Reports Code Mode child Tool starts without adding Pi recovery state. */
547
+ readonly onNestedToolStarted?: (input: Readonly<{
548
+ parentToolCallId: string;
549
+ toolCallId: string;
550
+ toolName: string;
551
+ input: unknown;
552
+ occurredAt: number;
553
+ }>) => void;
554
+ /** Reports Code Mode child Tool outcomes without adding Pi settlements. */
555
+ readonly onNestedToolFinished?: (input: Readonly<{
556
+ parentToolCallId: string;
557
+ toolCallId: string;
558
+ toolName: string;
559
+ outcome: "completed" | "failed" | "cancelled";
560
+ durationMs: number;
561
+ output?: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
562
+ error?: unknown;
563
+ occurredAt: number;
564
+ }>) => void;
546
565
  /** Per-Submission executors for tools whose metadata is fixed at assembly time. */
547
566
  readonly toolExecutors?: Readonly<Record<
548
567
  string,
@@ -634,6 +653,7 @@ export class PreparedPiTurnAdapter {
634
653
  private readonly turn: PiTurnAdapter;
635
654
  private readonly abortController = new AbortController();
636
655
  private readonly candidates: readonly PiToolCandidate[];
656
+ private nestedToolOrdinal = 0;
637
657
  private assistantOrdinal: number;
638
658
  private readonly encoder: PiChunkEncoder;
639
659
  private readonly steerMessageIds: string[] = [];
@@ -805,12 +825,44 @@ export class PreparedPiTurnAdapter {
805
825
  }
806
826
  const execute = options.toolExecutors?.[candidate.tool.name] ??
807
827
  candidate.tool.execute;
808
- return execute(
809
- toolCallId,
828
+ if (recordRecoveryAttempt || !toolCallIdPrefix) {
829
+ return execute(toolCallId, input, signal, onUpdate);
830
+ }
831
+ const startedAt = Date.now();
832
+ const telemetryToolCallId = `${toolCallId}:${++this.nestedToolOrdinal}`;
833
+ options.onNestedToolStarted?.({
834
+ parentToolCallId: toolCallIdPrefix,
835
+ toolCallId: telemetryToolCallId,
836
+ toolName: candidate.tool.name,
810
837
  input,
811
- signal,
812
- onUpdate,
813
- );
838
+ occurredAt: startedAt,
839
+ });
840
+ try {
841
+ const output = await execute(toolCallId, input, signal, onUpdate);
842
+ const occurredAt = Date.now();
843
+ options.onNestedToolFinished?.({
844
+ parentToolCallId: toolCallIdPrefix,
845
+ toolCallId: telemetryToolCallId,
846
+ toolName: candidate.tool.name,
847
+ outcome: "completed",
848
+ durationMs: occurredAt - startedAt,
849
+ output,
850
+ occurredAt,
851
+ });
852
+ return output;
853
+ } catch (error) {
854
+ const occurredAt = Date.now();
855
+ options.onNestedToolFinished?.({
856
+ parentToolCallId: toolCallIdPrefix,
857
+ toolCallId: telemetryToolCallId,
858
+ toolName: candidate.tool.name,
859
+ outcome: signal?.aborted ? "cancelled" : "failed",
860
+ durationMs: occurredAt - startedAt,
861
+ error,
862
+ occurredAt,
863
+ });
864
+ throw error;
865
+ }
814
866
  },
815
867
  },
816
868
  });
@@ -32,6 +32,7 @@ export * from "./mcp";
32
32
  export * from "./schedule";
33
33
  export * from "./skill";
34
34
  export * from "./subagent";
35
+ export * from "./time";
35
36
  export * from "./workspace-sandbox";
36
37
  export * from "./workspace-revision";
37
38
  export * from "./web-search";
@@ -0,0 +1,31 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import type { PiToolCandidate } from "./compiler";
4
+
5
+ export const GET_TIME_TOOL_NAME = "get_time";
6
+
7
+ const parameters = Type.Object({});
8
+
9
+ /** Product-neutral current-time Tool for Host registries. */
10
+ export function getTimePiToolCandidate(owner = "utilities"): PiToolCandidate {
11
+ return {
12
+ owner,
13
+ requiredExecutionLevel: "safe",
14
+ source: "action",
15
+ summary: "Get the current server time as an ISO string.",
16
+ tool: {
17
+ name: GET_TIME_TOOL_NAME,
18
+ label: "Get Time",
19
+ parameters,
20
+ description: "Get the current server time as an ISO string.",
21
+ async execute(_toolCallId, _input, signal) {
22
+ signal?.throwIfAborted();
23
+ const details = { now: new Date().toISOString() };
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify(details) }],
26
+ details,
27
+ } satisfies AgentToolResult<typeof details>;
28
+ },
29
+ },
30
+ };
31
+ }
@@ -35,7 +35,6 @@ import {
35
35
  } from "./runtime-assembler";
36
36
  import type {
37
37
  ResolvedResources,
38
- RuntimeAgentHooks,
39
38
  ToolAssemblyResult,
40
39
  } from "./runtime-definition";
41
40
  import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
@@ -78,6 +77,7 @@ export interface LoadedRuntime<Config> {
78
77
  readonly runtimeKey: string;
79
78
  readonly config: Config;
80
79
  readonly resources: ResolvedResources;
80
+ readonly commitGuard?: () => Promise<void>;
81
81
  }
82
82
 
83
83
  export interface RuntimeConfigUpdate<Change> {
@@ -106,11 +106,6 @@ interface RuntimeAgentDefinitionBase<
106
106
  config: Config,
107
107
  ) => ToolAssemblyResult | Promise<ToolAssemblyResult>;
108
108
 
109
- readonly hooks?:
110
- | RuntimeAgentHooks<Env, Config, Command, Change>
111
- | ((context: RuntimeAgentPlanningContext<Env, Command, Change>) =>
112
- RuntimeAgentHooks<Env, Config, Command, Change>);
113
-
114
109
  /** Prepare the optional telemetry consumer for this concrete Agent facet. */
115
110
  readonly telemetry?: (
116
111
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
@@ -407,18 +402,6 @@ export function defineRuntimeAgent<
407
402
  | MutableRuntimeAgentDefinition<Env, Config, Command, Change>
408
403
  | ReadonlyRuntimeAgentDefinition<Env, Config>,
409
404
  ): RuntimeAgentClass<Env, Command, Change> {
410
- const resolveDefinitionHooks = (
411
- context: RuntimeAgentPlanningContext<Env, Command, Change>,
412
- ): RuntimeAgentHooks<Env, Config, Command, Change> | undefined => {
413
- const hooks = definition.hooks;
414
- if (!hooks) return undefined;
415
- return typeof hooks === "function"
416
- ? (hooks as (
417
- ctx: RuntimeAgentPlanningContext<Env, Command, Change>,
418
- ) => RuntimeAgentHooks<Env, Config, Command, Change>)(context)
419
- : hooks as RuntimeAgentHooks<Env, Config, Command, Change>;
420
- };
421
-
422
405
  const resolveDefinitionTelemetry = (
423
406
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
424
407
  ): AgentTelemetryBinding | undefined => {
@@ -634,7 +617,8 @@ export function defineRuntimeAgent<
634
617
  )(context, loaded.config),
635
618
  { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
636
619
  );
637
- let hooks = resolveDefinitionHooks(context);
620
+ let resources = loaded.resources;
621
+ let commitGuard = loaded.commitGuard;
638
622
  if (!this.telemetryResolved) {
639
623
  this.resolvedTelemetry = resolveDefinitionTelemetry(context);
640
624
  this.telemetryResolved = true;
@@ -643,14 +627,14 @@ export function defineRuntimeAgent<
643
627
  if (this.role === "temporary") {
644
628
  this.temporaryDispose = tools.dispose;
645
629
  tools = this.applyTemporaryToolPolicy(tools);
646
- hooks = await this.createTemporaryHooks();
630
+ resources = await this.applyTemporaryResources(resources);
631
+ commitGuard = undefined;
647
632
  }
648
633
  const candidate = await assembleRuntimeSnapshot({
649
634
  ctx: context,
650
- config: loaded.config,
651
635
  tools,
652
- resources: loaded.resources,
653
- hooks,
636
+ resources,
637
+ ...(commitGuard ? { commitGuard } : {}),
654
638
  ...(telemetry ? { telemetry } : {}),
655
639
  });
656
640
 
@@ -709,9 +693,9 @@ export function defineRuntimeAgent<
709
693
  };
710
694
  }
711
695
 
712
- private async createTemporaryHooks(): Promise<
713
- RuntimeAgentHooks<Env, Config, Command, Change>
714
- > {
696
+ private async applyTemporaryResources(
697
+ resources: ResolvedResources,
698
+ ): Promise<ResolvedResources> {
715
699
  const launch = this.temporaryLaunch ??
716
700
  await this.ctx.storage.get<TemporaryAgentLaunch<Config>>(
717
701
  TEMPORARY_AGENT_LAUNCH_KEY,
@@ -719,17 +703,27 @@ export function defineRuntimeAgent<
719
703
  if (!launch) throw new Error("Temporary Agent is not initialized");
720
704
  this.temporaryLaunch = launch;
721
705
  return {
722
- profileOverrides: () => ({
723
- systemPrompt: launch.instructions,
724
- executionLevel: "high",
725
- }),
726
- gateTool: async (request) => {
727
- if (!requiresExecutionApproval(
728
- launch.executionLevel,
729
- request.requiredExecutionLevel,
730
- )) return;
731
- await this.requestParentTemporaryApproval(launch, request);
706
+ ...resources,
707
+ settings: {
708
+ ...resources.settings,
709
+ profile: {
710
+ ...resources.settings.profile,
711
+ systemPrompt: launch.instructions,
712
+ executionLevel: "high",
713
+ },
714
+ },
715
+ platform: {
716
+ ...resources.platform,
717
+ gateTool: async (request) => {
718
+ if (!requiresExecutionApproval(
719
+ launch.executionLevel,
720
+ request.requiredExecutionLevel,
721
+ )) return;
722
+ await this.requestParentTemporaryApproval(launch, request);
723
+ },
732
724
  },
725
+ turnEvents: undefined,
726
+ admission: undefined,
733
727
  };
734
728
  }
735
729
 
@@ -7,6 +7,7 @@ import type {
7
7
  RuntimeSandboxPort,
8
8
  RuntimeSchedulePort,
9
9
  RuntimeSkillSourceBinding,
10
+ RuntimeSubmissionAdmissionHook,
10
11
  RuntimeSubagentPort,
11
12
  RuntimeTurnEventsPort,
12
13
  RuntimeBindings,
@@ -37,7 +38,6 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
37
38
  import type { PiToolCandidate } from "./pi/tool/compiler";
38
39
  import type {
39
40
  AssembleRuntimeSnapshotInput,
40
- RuntimeAgentHooks,
41
41
  RuntimeSettings,
42
42
  RuntimeExtensionContribution,
43
43
  RuntimeProfileContribution,
@@ -45,7 +45,6 @@ import type {
45
45
  RuntimeToolSurfacePolicy,
46
46
  } from "./runtime-definition";
47
47
  import type { RuntimeCodeExecutionFactory } from "./kernel/bindings";
48
- import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
49
48
  import {
50
49
  basePiToolCandidates,
51
50
  memoryPiToolCandidate,
@@ -122,8 +121,7 @@ export interface RuntimeSnapshot {
122
121
  export interface RuntimeCandidate {
123
122
  readonly snapshot: RuntimeSnapshot;
124
123
  readonly commitGuards: readonly (() => Promise<void>)[];
125
- readonly hooks?: RuntimeAgentHooks<Cloudflare.Env, unknown, never, never>;
126
- readonly turnEvents?: RuntimeTurnEventsPort;
124
+ readonly admission?: RuntimeSubmissionAdmissionHook;
127
125
  readonly telemetry?: AgentTelemetryBinding;
128
126
  }
129
127
 
@@ -690,110 +688,34 @@ export async function assembleRuntime(
690
688
  commit(candidate.snapshot);
691
689
  }
692
690
 
693
- function hooksToTurnEventsPort<
694
- Env extends Cloudflare.Env,
695
- Config,
696
- Command,
697
- Change,
698
- >(
699
- ctx: AssembleRuntimeSnapshotInput<Env, Config, Command, Change>["ctx"],
700
- hooks?: RuntimeAgentHooks<Env, Config, Command, Change>,
701
- ): RuntimeTurnEventsPort | undefined {
702
- if (!hooks) return undefined;
703
- if (
704
- !hooks.onTurnEnd &&
705
- !hooks.onModelUsage &&
706
- !hooks.onSubagentUsage &&
707
- !hooks.onLifecycleFact &&
708
- !hooks.onToolStart &&
709
- !hooks.onToolSettled &&
710
- !hooks.onApproval &&
711
- !hooks.onActivityChanged &&
712
- !hooks.onSubmissionTerminal
713
- ) {
714
- return undefined;
715
- }
716
- return {
717
- onResponse: hooks.onTurnEnd
718
- ? (messages) => hooks.onTurnEnd!(ctx, messages)
719
- : async () => undefined,
720
- ...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
721
- ...(hooks.onSubagentUsage ? { onSubagentUsage: hooks.onSubagentUsage } : {}),
722
- ...(hooks.onLifecycleFact ? { onLifecycleFact: hooks.onLifecycleFact } : {}),
723
- ...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
724
- ...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
725
- ...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
726
- ...(hooks.onActivityChanged
727
- ? { onActivityChanged: hooks.onActivityChanged }
728
- : {}),
729
- ...(hooks.onSubmissionTerminal
730
- ? { onSubmissionTerminal: hooks.onSubmissionTerminal }
731
- : {}),
732
- };
733
- }
734
-
735
691
  /**
736
- * 从 Config、Tool 和已加载 Resource 装配 Runtime 候选 Snapshot。
692
+ * 从 Tool 和已加载 Resource 装配 Runtime 候选 Snapshot。
737
693
  *
738
694
  * @internal
739
695
  */
740
696
  export async function assembleRuntimeSnapshot<
741
697
  Env extends Cloudflare.Env,
742
- Config,
743
698
  Command = never,
744
699
  Change = never,
745
700
  >(
746
- input: AssembleRuntimeSnapshotInput<Env, Config, Command, Change>,
701
+ input: AssembleRuntimeSnapshotInput<Env, Command, Change>,
747
702
  ): Promise<RuntimeCandidate> {
748
703
  const resources = input.resources;
749
704
  const toolAssembly = input.tools;
750
705
  const settings = resources.settings;
751
706
  const provider = resources.provider;
752
- let profile = settings.profile;
753
- if (input.hooks?.profileOverrides) {
754
- const overrides = await withRuntimeLoadTimeout(
755
- "profile-overrides",
756
- () => input.hooks!.profileOverrides!(
757
- input.ctx,
758
- input.config,
759
- ),
760
- );
761
- if (overrides.systemPrompt !== undefined) {
762
- profile = { ...profile, systemPrompt: overrides.systemPrompt };
763
- }
764
- if (overrides.executionLevel !== undefined) {
765
- profile = { ...profile, executionLevel: overrides.executionLevel };
766
- }
767
- }
707
+ const profile = settings.profile;
768
708
 
769
709
  const hostTools = toolAssembly.tools;
770
710
 
771
711
  const commitGuards: Array<() => Promise<void>> = [];
772
712
  if (toolAssembly.commitGuard) commitGuards.push(toolAssembly.commitGuard);
773
- if (input.hooks?.beforeCommit) {
774
- for (const guard of input.hooks.beforeCommit) {
775
- commitGuards.push(() => guard(input.ctx, input.config));
776
- }
777
- }
778
-
779
- const platform: RuntimePlatformPort = input.hooks?.gateTool
780
- ? Object.freeze({
781
- ...resources.platform,
782
- gateTool: (request: {
783
- toolCallId: string;
784
- toolName: string;
785
- input: unknown;
786
- requiredExecutionLevel: import("./lib/execution-level").ExecutionLevel;
787
- signal: AbortSignal;
788
- }) => input.hooks!.gateTool!(request),
789
- })
790
- : resources.platform;
713
+ if (input.commitGuard) commitGuards.push(input.commitGuard);
791
714
 
792
715
  const memoryProfile =
793
716
  toolAssembly.memoryProfile ?? settings.memoryProfile;
794
717
  const enabledSubagents =
795
718
  toolAssembly.enabledSubagents ?? settings.enabledSubagents;
796
- const turnEvents = hooksToTurnEventsPort(input.ctx, input.hooks);
797
719
 
798
720
  const toolPolicy: RuntimeToolSurfacePolicy | undefined =
799
721
  settings.toolPolicy || toolAssembly.surfacePolicy
@@ -828,7 +750,7 @@ export async function assembleRuntimeSnapshot<
828
750
  ...(toolPolicy ? { toolPolicy } : {}),
829
751
  enabledSubagents: [...enabledSubagents],
830
752
  provider,
831
- platform,
753
+ platform: resources.platform,
832
754
  ...(toolAssembly.bindings?.workspace
833
755
  ? { workspace: toolAssembly.bindings.workspace }
834
756
  : {}),
@@ -851,9 +773,9 @@ export async function assembleRuntimeSnapshot<
851
773
  ? { gateway: resources.connectors.gateway }
852
774
  : {}),
853
775
  extensions: resources.extensions,
854
- ...(turnEvents ? { turnEvents } : {}),
776
+ ...(resources.turnEvents ? { turnEvents: resources.turnEvents } : {}),
855
777
  ...(input.telemetry ? { telemetry: input.telemetry } : {}),
856
- commitGuards: [],
778
+ commitGuards,
857
779
  degradations: [
858
780
  ...(toolAssembly.degradations ?? []),
859
781
  ...(resources.degradations ?? []),
@@ -863,22 +785,8 @@ export async function assembleRuntimeSnapshot<
863
785
 
864
786
  const candidate = await prepareRuntimeCandidate(assemblyInput);
865
787
  return Object.freeze({
866
- snapshot: candidate.snapshot,
867
- commitGuards: Object.freeze([
868
- ...candidate.commitGuards,
869
- ...commitGuards,
870
- ]),
871
- ...(input.hooks
872
- ? {
873
- hooks: input.hooks as unknown as RuntimeAgentHooks<
874
- Cloudflare.Env,
875
- unknown,
876
- never,
877
- never
878
- >,
879
- }
880
- : {}),
881
- ...(turnEvents ? { turnEvents } : {}),
788
+ ...candidate,
789
+ ...(resources.admission ? { admission: resources.admission } : {}),
882
790
  ...(candidate.telemetry ? { telemetry: candidate.telemetry } : {}),
883
791
  });
884
792
  }
@@ -2,16 +2,11 @@ import type { SkillSource } from "agents/skills";
2
2
  import type { ExecutionLevel } from "./lib/execution-level";
3
3
  import type {
4
4
  RuntimeGatewayPort,
5
- RuntimeModelUsageEvent,
6
- RuntimeSubmissionAdmissionHook,
7
- RuntimeSubagentUsageEvent,
8
- RuntimeLifecycleFact,
9
- RuntimeEventConfirmation,
10
5
  RuntimePlatformPort,
11
6
  RuntimeProviderPort,
12
7
  RuntimeSkillScriptPolicy,
13
- RuntimeToolStartEvent,
14
- RuntimeTurnMessage,
8
+ RuntimeSubmissionAdmissionHook,
9
+ RuntimeTurnEventsPort,
15
10
  } from "./kernel/bindings";
16
11
  import type {
17
12
  RuntimeDenyPolicy,
@@ -20,12 +15,10 @@ import type {
20
15
  ThinkingEffort,
21
16
  } from "./kernel/profile";
22
17
  import type { RuntimeExtensionConfig } from "./kernel/extensions";
23
- import type { RuntimeActivityProjection } from "./kernel/state";
24
18
  import type { RuntimeDegradation } from "./kernel/degradation";
25
19
  import type { PiToolCandidate } from "./pi/tool/compiler";
26
20
  import type {
27
21
  RuntimeAgentContext,
28
- RuntimeAssemblyContext,
29
22
  RuntimeAgentPlanningContext,
30
23
  } from "./runtime-agent-context";
31
24
  import type { AgentTelemetryBinding } from "./telemetry/contract";
@@ -83,14 +76,6 @@ export interface RuntimeToolSurfacePolicy extends ToolSurfaceSelectionPolicy {
83
76
 
84
77
  export type { RuntimeAgentContext, RuntimeAssemblyContext } from "./runtime-agent-context";
85
78
 
86
- export interface GateToolRequest {
87
- readonly toolCallId: string;
88
- readonly toolName: string;
89
- readonly input: unknown;
90
- readonly requiredExecutionLevel: ExecutionLevel;
91
- readonly signal: AbortSignal;
92
- }
93
-
94
79
  export interface ResolvedConnectors {
95
80
  readonly servers: readonly RuntimeMcpServer[];
96
81
  readonly gateway?: RuntimeGatewayPort;
@@ -101,81 +86,14 @@ export interface ResolvedResources {
101
86
  readonly settings: RuntimeSettings;
102
87
  readonly provider: RuntimeProviderPort;
103
88
  readonly platform: RuntimePlatformPort;
89
+ readonly turnEvents?: RuntimeTurnEventsPort;
90
+ readonly admission?: RuntimeSubmissionAdmissionHook;
104
91
  readonly skills: readonly RuntimeSkillContribution[];
105
92
  readonly extensions: readonly RuntimeExtensionContribution[];
106
93
  readonly connectors: ResolvedConnectors;
107
94
  readonly degradations?: readonly RuntimeDegradation[];
108
95
  }
109
96
 
110
- export interface ProfileOverrides {
111
- readonly systemPrompt?: string;
112
- readonly executionLevel?: ExecutionLevel;
113
- }
114
-
115
- export interface ApprovalHookInput {
116
- readonly submissionId: string;
117
- readonly approvalExecutionId: string;
118
- }
119
-
120
- export interface SubmissionTerminalHookInput {
121
- readonly submissionId: string;
122
- readonly status: "completed" | "aborted" | "skipped" | "error";
123
- readonly error?: string;
124
- readonly resultMessageId?: string;
125
- }
126
-
127
- export interface RuntimeToolSettlementEvent {
128
- readonly eventId: string;
129
- readonly submissionId: string;
130
- readonly toolCallId: string;
131
- readonly toolName: string;
132
- readonly status: "success" | "error";
133
- }
134
-
135
- export interface RuntimeAgentHooks<
136
- Env extends Cloudflare.Env = Cloudflare.Env,
137
- Config = unknown,
138
- Command = never,
139
- Change = never,
140
- > {
141
- readonly beforeCommit?: ReadonlyArray<
142
- (
143
- ctx: RuntimeAssemblyContext<Env, Command, Change>,
144
- config: Config,
145
- ) => Promise<void>
146
- >;
147
-
148
- readonly onTurnEnd?: (
149
- ctx: RuntimeAssemblyContext<Env, Command, Change>,
150
- messages: readonly RuntimeTurnMessage[],
151
- ) => Promise<void>;
152
- readonly onModelUsage?: (event: RuntimeModelUsageEvent) => Promise<void>;
153
- readonly onSubagentUsage?: (
154
- event: RuntimeSubagentUsageEvent,
155
- ) => Promise<RuntimeEventConfirmation | void>;
156
- readonly onLifecycleFact?: (
157
- event: RuntimeLifecycleFact,
158
- ) => Promise<RuntimeEventConfirmation | void>;
159
- /** Single admission seam shared by chat, RPC, regenerate and scheduled submissions. */
160
- readonly onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
161
- readonly onToolStart?: (event: RuntimeToolStartEvent) => Promise<void>;
162
- readonly onToolSettled?: (event: RuntimeToolSettlementEvent) => Promise<void>;
163
- readonly onApproval?: (input: ApprovalHookInput) => Promise<void>;
164
- readonly onActivityChanged?: (
165
- projection: RuntimeActivityProjection,
166
- ) => Promise<void>;
167
- readonly onSubmissionTerminal?: (
168
- input: SubmissionTerminalHookInput,
169
- ) => Promise<void>;
170
-
171
- readonly gateTool?: (request: GateToolRequest) => Promise<void>;
172
-
173
- readonly profileOverrides?: (
174
- ctx: RuntimeAssemblyContext<Env, Command, Change>,
175
- config: Config,
176
- ) => ProfileOverrides | Promise<ProfileOverrides>;
177
- }
178
-
179
97
  export interface RuntimeSettings {
180
98
  readonly profile: RuntimeProfileContribution;
181
99
  readonly memoryProfile: RuntimeMemoryProfile;
@@ -185,14 +103,12 @@ export interface RuntimeSettings {
185
103
 
186
104
  export interface AssembleRuntimeSnapshotInput<
187
105
  Env extends Cloudflare.Env,
188
- Config,
189
106
  Command = never,
190
107
  Change = never,
191
108
  > {
192
109
  readonly ctx: RuntimeAgentPlanningContext<Env, Command, Change>;
193
- readonly config: Config;
194
110
  readonly tools: ToolAssemblyResult;
195
111
  readonly resources: ResolvedResources;
196
- readonly hooks?: RuntimeAgentHooks<Env, Config, Command, Change>;
112
+ readonly commitGuard?: () => Promise<void>;
197
113
  readonly telemetry?: AgentTelemetryBinding;
198
114
  }
package/src/runtime.ts CHANGED
@@ -36,7 +36,11 @@ import {
36
36
  type SubmissionReceipt,
37
37
  } from "./kernel/receipts";
38
38
  import type { ExecutionLevel } from "./lib/execution-level";
39
- import type { RuntimeGatewaySession } from "./kernel/bindings";
39
+ import type {
40
+ RuntimeGatewaySession,
41
+ RuntimeSubmissionAdmissionHook,
42
+ RuntimeTurnEventsPort,
43
+ } from "./kernel/bindings";
40
44
  import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
41
45
  import { projectRuntimeAssembly } from "./kernel/runtime-assembly";
42
46
  import type {
@@ -57,8 +61,6 @@ import {
57
61
  type RuntimeCandidate,
58
62
  type RuntimeSnapshot,
59
63
  } from "./runtime-assembler";
60
- import type { RuntimeAgentHooks } from "./runtime-definition";
61
- import type { RuntimeTurnEventsPort } from "./kernel/bindings";
62
64
  import {
63
65
  RuntimeTelemetryCoordinator,
64
66
  } from "./telemetry";
@@ -199,6 +201,14 @@ interface ActiveTurn {
199
201
  agent: PreparedPiTurnAdapter;
200
202
  }
201
203
 
204
+ interface ActiveRuntime {
205
+ readonly snapshot: RuntimeSnapshot;
206
+ readonly revision: string;
207
+ readonly pi: PreparedPiRuntime;
208
+ readonly gatewaySession?: RuntimeGatewaySession;
209
+ readonly admission?: RuntimeSubmissionAdmissionHook;
210
+ }
211
+
202
212
  interface PlannedContinuationData {
203
213
  readonly submissionId: string;
204
214
  readonly requestId: string;
@@ -414,12 +424,7 @@ export abstract class AgentRuntimeKernel<
414
424
 
415
425
  protected abstract ensureRuntimeReady(): Promise<void>;
416
426
 
417
- private runtimeSnapshot?: RuntimeSnapshot;
418
- private runtimeRevision?: string;
419
- private runtimePi?: PreparedPiRuntime;
420
- private runtimeGatewaySession?: RuntimeGatewaySession;
421
- private runtimeTurnEvents?: RuntimeTurnEventsPort;
422
- private runtimeHooks?: RuntimeAgentHooks;
427
+ private activeRuntime?: ActiveRuntime;
423
428
  private telemetryCoordinator?: RuntimeTelemetryCoordinator;
424
429
  private piAdapter?: PiRuntimeAdapter;
425
430
  private readonly transcript: PiRuntimeTranscript;
@@ -464,7 +469,7 @@ export abstract class AgentRuntimeKernel<
464
469
  return this.runtimeLoadTracker ??= new RuntimeLoadTracker({
465
470
  read: () => this.state.runtimeLoad,
466
471
  publish: (runtimeLoad) => this.setState({ ...this.state, runtimeLoad }),
467
- isAvailable: () => Boolean(this.runtimeSnapshot),
472
+ isAvailable: () => Boolean(this.activeRuntime),
468
473
  });
469
474
  }
470
475
 
@@ -715,14 +720,11 @@ export abstract class AgentRuntimeKernel<
715
720
  }
716
721
 
717
722
  private turnEventsPort(): RuntimeTurnEventsPort | undefined {
718
- return this.runtimeTurnEvents ?? this.runtimeSnapshot?.bindings.turnEvents;
723
+ return this.activeRuntime?.snapshot.bindings.turnEvents;
719
724
  }
720
725
 
721
726
  protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
722
- this.runtimeTurnEvents =
723
- candidate.turnEvents ?? candidate.snapshot.bindings.turnEvents;
724
- this.runtimeHooks = candidate.hooks;
725
- const previous = this.runtimeSnapshot;
727
+ const previous = this.activeRuntime;
726
728
  const candidateSnapshot = candidate.snapshot;
727
729
  this.runtimeLoad.advance("assembly");
728
730
  this.runtimeLoad.advance("mcp");
@@ -776,7 +778,7 @@ export abstract class AgentRuntimeKernel<
776
778
  // 没有任何工具在执行 —— 换装配对它是安全的,而且这正是「决定时才装上
777
779
  // 新能力,同一轮接着跑」所依赖的那一步。真正在执行的 Turn 仍然被挡住。
778
780
  const contested = Boolean(previous) &&
779
- revision !== this.runtimeRevision &&
781
+ revision !== previous?.revision &&
780
782
  this.submissions.isBusy();
781
783
  const parked = contested && this.db.everyUnfinishedSubmissionParked();
782
784
  if (contested && !parked) {
@@ -791,8 +793,17 @@ export abstract class AgentRuntimeKernel<
791
793
  () => this.prepareParkedSubmissionRepin(prepared, revision),
792
794
  )
793
795
  : undefined;
796
+ const next = Object.freeze({
797
+ snapshot: candidateSnapshot,
798
+ revision,
799
+ pi: prepared,
800
+ ...(gatewaySession ? { gatewaySession } : {}),
801
+ ...(candidate.admission
802
+ ? { admission: candidate.admission }
803
+ : {}),
804
+ }) satisfies ActiveRuntime;
794
805
  let activated = false;
795
- const previousGatewaySession = this.runtimeGatewaySession;
806
+ const previousGatewaySession = previous?.gatewaySession;
796
807
  try {
797
808
  for (const [index, guard] of candidate.commitGuards.entries()) {
798
809
  await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
@@ -800,14 +811,11 @@ export abstract class AgentRuntimeKernel<
800
811
  this.pi.activate(candidateSnapshot);
801
812
  activated = true;
802
813
  repin?.commit();
803
- this.runtimeSnapshot = candidateSnapshot;
804
- this.runtimeRevision = revision;
805
- this.runtimePi = prepared;
806
- this.runtimeGatewaySession = gatewaySession;
807
814
  this.telemetry.configure(candidate.telemetry);
815
+ this.activeRuntime = next;
808
816
  } catch (error) {
809
817
  repin?.abort();
810
- if (activated && previous) this.pi.activate(previous);
818
+ if (activated && previous) this.pi.activate(previous.snapshot);
811
819
  await closeRuntimeGatewaySession(gatewaySession, "gateway.close.aborted");
812
820
  throw error;
813
821
  }
@@ -967,6 +975,28 @@ export abstract class AgentRuntimeKernel<
967
975
  occurredAt: event.timestamp,
968
976
  });
969
977
  },
978
+ onNestedToolStarted: (event) => {
979
+ this.telemetry.capture("toolStarted", {
980
+ submissionId: submission.submissionId,
981
+ ...event,
982
+ });
983
+ },
984
+ onNestedToolFinished: (event) => {
985
+ this.telemetry.capture("toolFinished", {
986
+ submissionId: submission.submissionId,
987
+ parentToolCallId: event.parentToolCallId,
988
+ toolCallId: event.toolCallId,
989
+ toolName: event.toolName,
990
+ outcome: event.outcome,
991
+ durationMs: event.durationMs,
992
+ ...(event.output ? {
993
+ output: event.output,
994
+ outputBytes: json(event.output).length,
995
+ } : {}),
996
+ ...(event.error ? { error: errorText(event.error) } : {}),
997
+ occurredAt: event.occurredAt,
998
+ });
999
+ },
970
1000
  ...(toolExecutors && Object.keys(toolExecutors).length > 0
971
1001
  ? { toolExecutors }
972
1002
  : {}),
@@ -1101,22 +1131,22 @@ export abstract class AgentRuntimeKernel<
1101
1131
  try {
1102
1132
  const payload = JSON.parse(row.body) as RuntimeEventOutboxPayload;
1103
1133
  if (payload.type === "model-usage") {
1104
- if (!turnEvents.onModelUsage) continue;
1105
- await turnEvents.onModelUsage(payload.event);
1134
+ await turnEvents.onModelUsage?.(payload.event);
1106
1135
  } else if (payload.type === "tool-settlement") {
1107
- if (!turnEvents.onToolSettled) continue;
1108
- await turnEvents.onToolSettled(payload.event);
1136
+ await turnEvents.onToolSettled?.(payload.event);
1109
1137
  } else if (payload.type === "subagent-usage") {
1110
- if (!turnEvents.onSubagentUsage) continue;
1111
- const confirmation = await turnEvents.onSubagentUsage(payload.event);
1112
- if (confirmation?.confirmed !== true) {
1113
- throw new Error("Runtime event was not confirmed");
1138
+ if (turnEvents.onSubagentUsage) {
1139
+ const confirmation = await turnEvents.onSubagentUsage(payload.event);
1140
+ if (confirmation?.confirmed !== true) {
1141
+ throw new Error("Runtime event was not confirmed");
1142
+ }
1114
1143
  }
1115
1144
  } else {
1116
- if (!turnEvents.onLifecycleFact) continue;
1117
- const confirmation = await turnEvents.onLifecycleFact(payload.event);
1118
- if (confirmation?.confirmed !== true) {
1119
- throw new Error("Runtime event was not confirmed");
1145
+ if (turnEvents.onLifecycleFact) {
1146
+ const confirmation = await turnEvents.onLifecycleFact(payload.event);
1147
+ if (confirmation?.confirmed !== true) {
1148
+ throw new Error("Runtime event was not confirmed");
1149
+ }
1120
1150
  }
1121
1151
  }
1122
1152
  this.db.transaction(() =>
@@ -1320,12 +1350,12 @@ export abstract class AgentRuntimeKernel<
1320
1350
  // 调用:所有需要模型、工作区、扩展或投影绑定的运行时路径调用。
1321
1351
  // 原因:未初始化时立即失败,比在深层路径触发空值更容易定位。
1322
1352
  private assembly(): RuntimeSnapshot {
1323
- if (!this.runtimeSnapshot) {
1353
+ if (!this.activeRuntime) {
1324
1354
  throw new Error(
1325
1355
  "Runtime must be assembled before use",
1326
1356
  );
1327
1357
  }
1328
- return this.runtimeSnapshot;
1358
+ return this.activeRuntime.snapshot;
1329
1359
  }
1330
1360
 
1331
1361
  private async normalizeUIUserInput(
@@ -1377,10 +1407,14 @@ export abstract class AgentRuntimeKernel<
1377
1407
  // 调用:Session facet 的 `getRuntimeAssembly` RPC。
1378
1408
  // 原因:UI 需要读取真实装配结果,而不是上层配置声明。
1379
1409
  protected readRuntimeAssembly(): RuntimeAssemblyView {
1410
+ const runtime = this.activeRuntime;
1411
+ if (!runtime) {
1412
+ throw new Error("Runtime must be assembled before use");
1413
+ }
1380
1414
  return projectRuntimeAssembly(
1381
- this.assembly(),
1382
- this.runtimeRevision ?? null,
1383
- this.preparedPi(),
1415
+ runtime.snapshot,
1416
+ runtime.revision,
1417
+ runtime.pi,
1384
1418
  );
1385
1419
  }
1386
1420
 
@@ -1442,12 +1476,12 @@ export abstract class AgentRuntimeKernel<
1442
1476
  }
1443
1477
 
1444
1478
  private preparedPi(): PreparedPiRuntime {
1445
- if (!this.runtimePi) {
1479
+ if (!this.activeRuntime) {
1446
1480
  throw new Error(
1447
1481
  "SpringBrand Runtime must be prepared before starting a Turn",
1448
1482
  );
1449
1483
  }
1450
- return this.runtimePi;
1484
+ return this.activeRuntime.pi;
1451
1485
  }
1452
1486
 
1453
1487
  // #endregion
@@ -1474,10 +1508,11 @@ export abstract class AgentRuntimeKernel<
1474
1508
  descriptor: string;
1475
1509
  }> {
1476
1510
  await this.drainRuntimeEvents();
1477
- if (!this.runtimeRevision) {
1511
+ const runtime = this.activeRuntime;
1512
+ if (!runtime) {
1478
1513
  throw new Error("Runtime revision is not initialized");
1479
1514
  }
1480
- return this.pinAssembly(this.preparedPi(), this.runtimeRevision);
1515
+ return this.pinAssembly(runtime.pi, runtime.revision);
1481
1516
  }
1482
1517
 
1483
1518
  private async pinAssembly(
@@ -1731,7 +1766,7 @@ export abstract class AgentRuntimeKernel<
1731
1766
  "Regenerate request must match an existing user message",
1732
1767
  );
1733
1768
  }
1734
- const hook = this.runtimeHooks?.onSubmissionAdmission;
1769
+ const hook = this.activeRuntime?.admission;
1735
1770
  let existing: StoredSubmissionAdmission | null = null;
1736
1771
  if (hook) {
1737
1772
  const attemptedAt = Date.now();
@@ -223,6 +223,7 @@ export class RuntimeTelemetry {
223
223
  submissionId: string;
224
224
  toolCallId: string;
225
225
  toolName: string;
226
+ parentToolCallId?: string;
226
227
  input?: unknown;
227
228
  occurredAt?: number;
228
229
  }>): TelemetryRecordResult {
@@ -235,10 +236,9 @@ export class RuntimeTelemetry {
235
236
  ...(input.input === undefined ? {} : { input: input.input as never }),
236
237
  }, {
237
238
  ...input,
238
- parentOperationId: telemetryOperations.turn(
239
- this.resource.sessionId,
240
- input.submissionId,
241
- ),
239
+ parentOperationId: input.parentToolCallId
240
+ ? telemetryOperations.tool(input.submissionId, input.parentToolCallId)
241
+ : telemetryOperations.turn(this.resource.sessionId, input.submissionId),
242
242
  });
243
243
  }
244
244
 
@@ -246,6 +246,7 @@ export class RuntimeTelemetry {
246
246
  submissionId: string;
247
247
  toolCallId: string;
248
248
  toolName: string;
249
+ parentToolCallId?: string;
249
250
  outcome: "completed" | "failed" | "cancelled";
250
251
  durationMs?: number;
251
252
  outputBytes?: number;
@@ -268,10 +269,9 @@ export class RuntimeTelemetry {
268
269
  ...(input.error ? { error: input.error } : {}),
269
270
  }, {
270
271
  ...input,
271
- parentOperationId: telemetryOperations.turn(
272
- this.resource.sessionId,
273
- input.submissionId,
274
- ),
272
+ parentOperationId: input.parentToolCallId
273
+ ? telemetryOperations.tool(input.submissionId, input.parentToolCallId)
274
+ : telemetryOperations.turn(this.resource.sessionId, input.submissionId),
275
275
  });
276
276
  }
277
277
 
@@ -1,57 +0,0 @@
1
- import type {
2
- RuntimeSubmissionAdmissionHook,
3
- RuntimeTurnEventsPort,
4
- } from "../../../kernel/bindings";
5
- import type { RuntimeAgentHooks } from "../../../runtime-definition";
6
-
7
- export function createUniversalAgentHooks<
8
- Env extends Cloudflare.Env,
9
- Config,
10
- Command = never,
11
- Change = never,
12
- >(adapter: {
13
- guard(config: Config): Promise<void>;
14
- turnEvents(): RuntimeTurnEventsPort | Promise<RuntimeTurnEventsPort>;
15
- onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
16
- }): RuntimeAgentHooks<Env, Config, Command, Change> {
17
- const turnEvents = () => Promise.resolve(adapter.turnEvents());
18
- return {
19
- beforeCommit: [async (_context, config) => adapter.guard(config)],
20
- onTurnEnd: async (_context, messages) => {
21
- await (await turnEvents()).onResponse(messages);
22
- },
23
- onModelUsage: async (event) => {
24
- await (await turnEvents()).onModelUsage?.(event);
25
- },
26
- ...(adapter.onSubmissionAdmission
27
- ? { onSubmissionAdmission: adapter.onSubmissionAdmission }
28
- : {}),
29
- // 一个不订阅这两类事实的 Host 不能把 outbox 堵住:Runtime 只有拿到确认才
30
- // 会标记投递,而 `undefined` 表示"没投出去"。因此没有 Host 实现时这里就地确认,
31
- // 语义等同于旧版本"Host 不关心就丢弃",而不是让整条队列在队头无限重试。
32
- onSubagentUsage: async (event) => {
33
- const port = await turnEvents();
34
- return port.onSubagentUsage
35
- ? port.onSubagentUsage(event)
36
- : { confirmed: true };
37
- },
38
- onLifecycleFact: async (event) => {
39
- const port = await turnEvents();
40
- return port.onLifecycleFact
41
- ? port.onLifecycleFact(event)
42
- : { confirmed: true };
43
- },
44
- onToolSettled: async (event) => {
45
- await (await turnEvents()).onToolSettled?.(event);
46
- },
47
- onApproval: async (input) => {
48
- await (await turnEvents()).onApproval?.(input);
49
- },
50
- onActivityChanged: async (projection) => {
51
- await (await turnEvents()).onActivityChanged?.(projection);
52
- },
53
- onSubmissionTerminal: async (input) => {
54
- await (await turnEvents()).onSubmissionTerminal?.(input);
55
- },
56
- };
57
- }