@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.14

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 (50) hide show
  1. package/package.json +4 -3
  2. package/src/adapter/cloudflare/index.ts +1 -0
  3. package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
  4. package/src/adapter/cloudflare/subagent/definition.ts +60 -5
  5. package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
  6. package/src/adapter/cloudflare/universal-agent/preparation.ts +36 -2
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +9 -12
  8. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  9. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  10. package/src/db/index.ts +4 -0
  11. package/src/db/runtime-event-outbox.repo.ts +34 -1
  12. package/src/db/schema.ts +42 -0
  13. package/src/db/submission-admission.repo.ts +127 -0
  14. package/src/db/submission.repo.ts +54 -3
  15. package/src/index.ts +5 -2
  16. package/src/kernel/bindings.ts +52 -0
  17. package/src/kernel/durable-lifecycle.ts +100 -0
  18. package/src/kernel/public-contracts.ts +11 -0
  19. package/src/kernel/receipts.ts +1 -0
  20. package/src/kernel/recoverable-chat-agent.ts +0 -19
  21. package/src/kernel/subagent-runtime.ts +137 -0
  22. package/src/kernel/submission-authority.ts +114 -0
  23. package/src/kernel/submission-lifecycle.ts +35 -10
  24. package/src/layers/context/budget/gate.ts +99 -0
  25. package/src/lib/prompt.ts +7 -3
  26. package/src/pi/message/projection.ts +2 -2
  27. package/src/pi/runtime-adapter/assembly.ts +4 -3
  28. package/src/pi/runtime-adapter/execution.ts +55 -16
  29. package/src/pi/runtime-adapter/index.ts +34 -0
  30. package/src/pi/runtime-adapter/models.ts +10 -1
  31. package/src/pi/runtime-adapter/recovery.ts +17 -17
  32. package/src/pi/runtime-adapter/transcript.ts +2 -2
  33. package/src/pi/tool/base.ts +113 -27
  34. package/src/pi/tool/compiler.ts +66 -6
  35. package/src/pi/tool/core-host.ts +50 -23
  36. package/src/pi/tool/core.ts +2 -23
  37. package/src/pi/tool/index.ts +1 -0
  38. package/src/pi/tool/mcp.ts +2 -2
  39. package/src/pi/tool/skill.ts +78 -3
  40. package/src/pi/tool/subagent.ts +142 -16
  41. package/src/pi/tool/workspace-revision.ts +64 -0
  42. package/src/pi/tool/workspace-sandbox.ts +4 -4
  43. package/src/pi/turn/tool-recovery.ts +7 -7
  44. package/src/runtime-agent-context.ts +1 -0
  45. package/src/runtime-agent.ts +8 -3
  46. package/src/runtime-assembler.ts +61 -9
  47. package/src/runtime-definition.ts +12 -0
  48. package/src/runtime.ts +485 -66
  49. package/src/tool-registry.ts +10 -1
  50. package/src/workspace-versioning.ts +46 -0
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  runner as createSkillScriptRunner,
3
3
  SkillRegistry,
4
+ type SkillResource,
4
5
  type SkillScriptRequest,
5
6
  type SkillScriptRunner,
6
7
  type SkillSource,
7
8
  } from "agents/skills";
8
- import { jsonSchema, type ToolSet } from "ai";
9
+ import { jsonSchema, tool, type ToolSet } from "ai";
10
+ import { z } from "zod";
9
11
  import type {
10
12
  RuntimeSkillScriptPolicy,
11
13
  WorkspacePort,
12
14
  } from "../../kernel/bindings";
13
15
  import { aiToolToPi } from "./ai-adapter";
14
16
  import type { PiToolCandidate } from "./compiler";
17
+ import { serializeWorkspaceMutation } from "./workspace-sandbox";
15
18
 
16
19
  /** A configured Skill source and its script capabilities. */
17
20
  export interface PiSkillBinding {
@@ -103,11 +106,72 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
103
106
  activate_skill: "Activate Skill",
104
107
  read_skill_resource: "Read Skill resource",
105
108
  run_skill_script: "Run Skill script",
109
+ materialize_skill_resource: "Materialize Skill resource",
106
110
  };
107
111
 
108
112
  const SKILL_ENTRY_READ_GUIDANCE =
109
113
  "SKILL.md contains the Skill instructions; use activate_skill instead.";
110
114
 
115
+ function resourceBytes(resource: SkillResource): Uint8Array {
116
+ if ((resource.encoding ?? "text") === "text") {
117
+ return new TextEncoder().encode(resource.content);
118
+ }
119
+ const binary = atob(resource.content);
120
+ const bytes = new Uint8Array(binary.length);
121
+ for (let index = 0; index < binary.length; index++) {
122
+ bytes[index] = binary.charCodeAt(index);
123
+ }
124
+ return bytes;
125
+ }
126
+
127
+ function materializeSkillResourceTool(
128
+ bindings: readonly PiSkillBinding[],
129
+ workspace: WorkspacePort,
130
+ ) {
131
+ const names = bindings.map(({ name }) => name) as [string, ...string[]];
132
+ const byName = new Map(bindings.map((binding) => [binding.name, binding]));
133
+ return tool({
134
+ description:
135
+ "Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
136
+ "Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
137
+ "The destination is created or overwritten, including parent directories.",
138
+ inputSchema: z.object({
139
+ name: z.enum(names).describe("Activated Skill name"),
140
+ path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
141
+ destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
142
+ }),
143
+ execute: async ({ name, path, destination }, { abortSignal }) => {
144
+ abortSignal?.throwIfAborted();
145
+ const source = byName.get(name)?.source;
146
+ if (!source?.readResource) {
147
+ throw new Error(`Skill \"${name}\" has no readable resources.`);
148
+ }
149
+ const resource = await source.readResource(name, path);
150
+ if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
151
+ abortSignal?.throwIfAborted();
152
+ const bytes = resourceBytes(resource);
153
+
154
+ await serializeWorkspaceMutation(workspace, destination, async () => {
155
+ abortSignal?.throwIfAborted();
156
+ const parent = destination.replace(/\/[^/]+$/, "");
157
+ if (parent && parent !== "/") {
158
+ await workspace.mkdir(parent, { recursive: true });
159
+ }
160
+ await workspace.writeFileBytes(destination, bytes, resource.mimeType);
161
+ });
162
+
163
+ return {
164
+ name,
165
+ path: resource.path,
166
+ destination,
167
+ bytesWritten: bytes.byteLength,
168
+ encoding: resource.encoding ?? "text",
169
+ ...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
170
+ };
171
+ },
172
+ });
173
+ }
174
+
111
175
  /** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
112
176
  export async function skillPiToolCandidates(
113
177
  bindings: readonly PiSkillBinding[],
@@ -121,7 +185,15 @@ export async function skillPiToolCandidates(
121
185
  );
122
186
  await registry.load();
123
187
 
124
- return Object.entries(registry.tools()).map(([name, tool]) => {
188
+ const tools = registry.tools();
189
+ if (options.workspace) {
190
+ tools.materialize_skill_resource = materializeSkillResourceTool(
191
+ bindings,
192
+ options.workspace,
193
+ );
194
+ }
195
+
196
+ return Object.entries(tools).map(([name, tool]) => {
125
197
  const adapted = aiToolToPi(name, tool, {
126
198
  label: SKILL_TOOL_LABELS[name] ?? name,
127
199
  ...(name === "read_skill_resource"
@@ -154,7 +226,10 @@ export async function skillPiToolCandidates(
154
226
  }
155
227
  return {
156
228
  owner: "runtime-skill",
157
- requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
229
+ requiredExecutionLevel:
230
+ name === "run_skill_script" || name === "materialize_skill_resource"
231
+ ? "high"
232
+ : "safe",
158
233
  tool: adapted,
159
234
  };
160
235
  });
@@ -1,8 +1,17 @@
1
1
  import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import { z } from "zod";
3
- import type { RuntimeSubagentPort } from "../../kernel/bindings";
3
+ import type {
4
+ RuntimeSubagentLifecyclePort,
5
+ RuntimeSubagentPort,
6
+ RuntimeSubagentRunResult,
7
+ RuntimeSubagentUsageEvent,
8
+ } from "../../kernel/bindings";
4
9
  import { AGENT_TYPES } from "../../layers/orchestration/subagents/agent-types/registry";
5
10
  import { serializeOutput } from "../../lib/artifacts";
11
+ import {
12
+ isZeroSubagentUsage,
13
+ subagentTerminalStatus,
14
+ } from "../../kernel/subagent-runtime";
6
15
  import type { PiToolCandidate } from "./compiler";
7
16
  import {
8
17
  toolRegistryFromPiCandidates,
@@ -38,6 +47,61 @@ function failure(agentType: string, status: string, error?: string) {
38
47
  };
39
48
  }
40
49
 
50
+ async function runRegisteredSubagent(
51
+ lifecycle: RuntimeSubagentLifecyclePort,
52
+ subagentRunId: string,
53
+ run: () => Promise<RuntimeSubagentRunResult>,
54
+ ): Promise<RuntimeSubagentRunResult> {
55
+ let result: RuntimeSubagentRunResult;
56
+ try {
57
+ result = await run();
58
+ } catch (error) {
59
+ await lifecycle.onTerminal({
60
+ subagentRunId,
61
+ status: "failed",
62
+ zeroUsage: true,
63
+ });
64
+ throw error;
65
+ }
66
+ if (result.runId !== subagentRunId) {
67
+ await recordSubagentResult(lifecycle, {
68
+ ...result,
69
+ runId: subagentRunId,
70
+ status: "error",
71
+ });
72
+ throw new Error("SubAgent run ID changed after durable registration");
73
+ }
74
+ return result;
75
+ }
76
+
77
+ async function recordSubagentResult(
78
+ lifecycle: RuntimeSubagentLifecyclePort,
79
+ run: RuntimeSubagentRunResult,
80
+ ): Promise<void> {
81
+ const status = subagentTerminalStatus(run.status);
82
+ if (run.usage) {
83
+ const event: RuntimeSubagentUsageEvent = {
84
+ eventId: `${run.runId}:usage`,
85
+ submissionId: lifecycle.submissionId,
86
+ runId: run.runId,
87
+ subagentRunId: run.runId,
88
+ parentRunId: lifecycle.parentRunId,
89
+ ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
90
+ ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
91
+ ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
92
+ kind: "subagent",
93
+ status,
94
+ usage: run.usage,
95
+ };
96
+ await lifecycle.onUsage(event);
97
+ }
98
+ await lifecycle.onTerminal({
99
+ subagentRunId: run.runId,
100
+ status,
101
+ zeroUsage: !run.usage || isZeroSubagentUsage(run.usage),
102
+ });
103
+ }
104
+
41
105
  /**
42
106
  * 把已启用的 SubAgent 类型转换成 Pi 候选工具。
43
107
  *
@@ -53,6 +117,7 @@ function failure(agentType: string, status: string, error?: string) {
53
117
  export function subagentPiToolCandidates(
54
118
  subagents: RuntimeSubagentPort | undefined,
55
119
  enabledSubagents: readonly string[],
120
+ lifecycle?: RuntimeSubagentLifecyclePort,
56
121
  ): PiToolCandidate[] {
57
122
  if (!subagents) return [];
58
123
  const types = [...new Set(enabledSubagents)].flatMap((name) => {
@@ -79,10 +144,38 @@ export function subagentPiToolCandidates(
79
144
  // 适配层只固定类型名并统一结果语义。
80
145
  async execute(_toolCallId, input, signal) {
81
146
  signal?.throwIfAborted();
82
- const run = await subagents.run(
83
- type.name,
84
- input as Record<string, unknown>,
85
- );
147
+ const childInput = input as Record<string, unknown>;
148
+ if (lifecycle && !subagents.reserve) {
149
+ throw new Error("Durable SubAgent execution requires run reservation");
150
+ }
151
+ const reserved = lifecycle
152
+ ? await subagents.reserve!(type.name, childInput, {
153
+ parentRunId: lifecycle.parentRunId,
154
+ submissionId: lifecycle.submissionId,
155
+ toolCallId: _toolCallId,
156
+ })
157
+ : undefined;
158
+ if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
159
+ const runOptions = lifecycle
160
+ ? {
161
+ parentRunId: lifecycle.parentRunId,
162
+ submissionId: lifecycle.submissionId,
163
+ ...(reserved ? { subagentRunId: reserved.runId } : {}),
164
+ ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
165
+ ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
166
+ ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
167
+ }
168
+ : undefined;
169
+ const run = lifecycle && reserved
170
+ ? await runRegisteredSubagent(
171
+ lifecycle,
172
+ reserved.runId,
173
+ () => subagents.run(type.name, childInput, runOptions),
174
+ )
175
+ : await subagents.run(type.name, childInput);
176
+ if (lifecycle && !run.childStillRunning) {
177
+ await recordSubagentResult(lifecycle, run);
178
+ }
86
179
  if (run.status !== "completed" || run.output === undefined) {
87
180
  return result(failure(type.name, run.status, run.error));
88
181
  }
@@ -112,7 +205,7 @@ export function subagentPiToolCandidates(
112
205
  // 调用:Pi 需要子任务异步进行、不应阻塞当前 Turn 时调用。
113
206
  // 原因:先用具体 AgentType Schema 复核嵌套 input,
114
207
  // 再把 detached 和通知语义交给 Host,避免后台入口绕过类型约束。
115
- async execute(_toolCallId, value, signal) {
208
+ async execute(toolCallId, value, signal) {
116
209
  signal?.throwIfAborted();
117
210
  const input = value as z.infer<typeof parameters>;
118
211
  const parsed = AGENT_TYPES.byName[input.agentType]!.inputSchema.safeParse(
@@ -123,15 +216,47 @@ export function subagentPiToolCandidates(
123
216
  `SubAgent input failed schema validation: ${parsed.error.message}`,
124
217
  );
125
218
  }
126
- const run = await subagents.run(
127
- input.agentType,
128
- parsed.data as Record<string, unknown>,
129
- {
130
- detached: true,
131
- maxBudgetMs: BACKGROUND_MAX_BUDGET_MS,
132
- notifySource: "dispatch-background",
133
- },
134
- );
219
+ if (lifecycle && !subagents.reserve) {
220
+ throw new Error("Durable SubAgent execution requires run reservation");
221
+ }
222
+ const reserved = lifecycle
223
+ ? await subagents.reserve!(
224
+ input.agentType,
225
+ parsed.data as Record<string, unknown>,
226
+ {
227
+ parentRunId: lifecycle.parentRunId,
228
+ submissionId: lifecycle.submissionId,
229
+ toolCallId,
230
+ },
231
+ )
232
+ : undefined;
233
+ if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
234
+ const runInput = parsed.data as Record<string, unknown>;
235
+ const runOptions = {
236
+ detached: true,
237
+ maxBudgetMs: BACKGROUND_MAX_BUDGET_MS,
238
+ notifySource: "dispatch-background",
239
+ ...(lifecycle
240
+ ? {
241
+ parentRunId: lifecycle.parentRunId,
242
+ submissionId: lifecycle.submissionId,
243
+ ...(reserved ? { subagentRunId: reserved.runId } : {}),
244
+ ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
245
+ ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
246
+ ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
247
+ }
248
+ : {}),
249
+ };
250
+ const run = lifecycle && reserved
251
+ ? await runRegisteredSubagent(
252
+ lifecycle,
253
+ reserved.runId,
254
+ () => subagents.run(input.agentType, runInput, runOptions),
255
+ )
256
+ : await subagents.run(input.agentType, runInput, runOptions);
257
+ if (lifecycle && !run.childStillRunning && run.status !== "running") {
258
+ await recordSubagentResult(lifecycle, run);
259
+ }
135
260
  return result({
136
261
  runId: run.runId,
137
262
  status: run.status,
@@ -153,8 +278,9 @@ export function subagentPiToolCandidates(
153
278
  export function createSubagentTools(
154
279
  subagents: RuntimeSubagentPort | undefined,
155
280
  enabledSubagents: readonly string[],
281
+ lifecycle?: RuntimeSubagentLifecyclePort,
156
282
  ): ToolRegistry {
157
283
  return toolRegistryFromPiCandidates(
158
- subagentPiToolCandidates(subagents, enabledSubagents),
284
+ subagentPiToolCandidates(subagents, enabledSubagents, lifecycle),
159
285
  );
160
286
  }
@@ -0,0 +1,64 @@
1
+ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import type { WorkspaceRevisionRestorePort } from "../../workspace-versioning";
4
+ import { serializeOutput } from "../../lib/artifacts";
5
+ import {
6
+ toolRegistryFromPiCandidates,
7
+ type ToolRegistry,
8
+ } from "../../tool-registry";
9
+ import type { PiToolCandidate } from "./compiler";
10
+
11
+ const parameters = Type.Object({
12
+ target: Type.Union([
13
+ Type.Literal("previous"),
14
+ Type.String({
15
+ pattern: "^[0-9a-f]{40}$",
16
+ description: "A complete revision ID shown in Workspace history.",
17
+ }),
18
+ ], {
19
+ description: "Use previous for the revision immediately before the current one.",
20
+ }),
21
+ });
22
+
23
+ function result<T>(details: T): AgentToolResult<T> {
24
+ return {
25
+ content: [{ type: "text", text: serializeOutput(details).text }],
26
+ details,
27
+ };
28
+ }
29
+
30
+ export function workspaceRevisionPiToolCandidate(
31
+ versions: WorkspaceRevisionRestorePort,
32
+ ): PiToolCandidate {
33
+ const tool: AgentTool<typeof parameters> = {
34
+ name: "restore_workspace_revision",
35
+ label: "Restore Workspace revision",
36
+ description:
37
+ "Restore the configured Workspace tree to the previous revision or an exact revision. Use only when the person explicitly asks to roll back. This always asks for approval and changes only the Workspace files managed by the Host.",
38
+ parameters,
39
+ async execute(_toolCallId, input, signal) {
40
+ signal?.throwIfAborted();
41
+ return result({
42
+ status: "restored",
43
+ ...await versions.restore(input.target),
44
+ note: "The approved Workspace revision is now current.",
45
+ });
46
+ },
47
+ };
48
+ return {
49
+ owner: "workspace",
50
+ tool,
51
+ requiredExecutionLevel: "safe",
52
+ alwaysRequiresApproval: true,
53
+ source: "action",
54
+ summary: "Restore the Workspace tree",
55
+ };
56
+ }
57
+
58
+ export function createWorkspaceRevisionTools(
59
+ versions: WorkspaceRevisionRestorePort,
60
+ ): ToolRegistry {
61
+ return toolRegistryFromPiCandidates([
62
+ workspaceRevisionPiToolCandidate(versions),
63
+ ]);
64
+ }
@@ -111,7 +111,7 @@ const mutationQueues = new WeakMap<
111
111
  Map<string, Promise<void>>
112
112
  >();
113
113
 
114
- function serializeByPath<T>(
114
+ export function serializeWorkspaceMutation<T>(
115
115
  workspace: WorkspacePort,
116
116
  path: string,
117
117
  run: () => Promise<T>,
@@ -193,7 +193,7 @@ export function workspacePiToolCandidates(
193
193
  signal?.throwIfAborted();
194
194
  running(onUpdate, "Writing the Workspace file.");
195
195
 
196
- return serializeByPath(workspace, path, async () => {
196
+ return serializeWorkspaceMutation(workspace, path, async () => {
197
197
  // 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
198
198
  signal?.throwIfAborted();
199
199
  return writeTool.execute(
@@ -221,7 +221,7 @@ export function workspacePiToolCandidates(
221
221
  running(onUpdate, "Editing the Workspace file.");
222
222
  // 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
223
223
  // 比对之后被别的调用换掉,丢写照旧发生。
224
- return serializeByPath(workspace, params.path, async () => {
224
+ return serializeWorkspaceMutation(workspace, params.path, async () => {
225
225
  signal?.throwIfAborted();
226
226
  const output = await editTool.execute(
227
227
  toolCallId,
@@ -267,7 +267,7 @@ export function workspacePiToolCandidates(
267
267
  if (typeof path !== "string") {
268
268
  return removeTool.execute(toolCallId, params, signal, onUpdate);
269
269
  }
270
- return serializeByPath(workspace, path, () =>
270
+ return serializeWorkspaceMutation(workspace, path, () =>
271
271
  removeTool.execute(toolCallId, params, signal, onUpdate),
272
272
  );
273
273
  },
@@ -429,7 +429,7 @@ function assertIdentity(
429
429
  state.assemblyRevision !== milestone.assemblyRevision)
430
430
  ) {
431
431
  throw new Error(
432
- "Pi recovery milestone does not belong to the active Turn revision",
432
+ "SpringBrand recovery milestone does not belong to the active Turn revision",
433
433
  );
434
434
  }
435
435
  }
@@ -496,7 +496,7 @@ export function replayPiToolRecovery(
496
496
  const previous = tools[milestone.toolCallId];
497
497
  if (previous && !sameToolInput(previous, milestone)) {
498
498
  throw new Error(
499
- `Conflicting Pi Tool input for ${milestone.toolCallId}`,
499
+ `Conflicting SpringBrand Tool input for ${milestone.toolCallId}`,
500
500
  );
501
501
  }
502
502
  tools[milestone.toolCallId] = previous ?? {
@@ -891,7 +891,7 @@ export function applyRecoveredPiApprovalDecision(
891
891
  ): RecoveredPiApprovalDecision {
892
892
  const approval = state.approvals[input.executionId];
893
893
  if (!approval) {
894
- throw new Error(`Unknown Pi Tool approval ${input.executionId}`);
894
+ throw new Error(`Unknown SpringBrand Tool approval ${input.executionId}`);
895
895
  }
896
896
  const transition = decidePiToolApproval(
897
897
  approval,
@@ -908,7 +908,7 @@ export function applyRecoveredPiApprovalDecision(
908
908
  return { milestones: [], outcome: transition.outcome };
909
909
  }
910
910
  if (!state.turnId || !state.assemblyRevision) {
911
- throw new Error("Pi Tool approval is missing its Turn revision");
911
+ throw new Error("SpringBrand Tool approval is missing its Turn revision");
912
912
  }
913
913
 
914
914
  const identity: RecoveryIdentity = {
@@ -951,7 +951,7 @@ export function recordRecoveredPiToolInteraction(
951
951
  interaction: PiToolInteraction,
952
952
  ): PiToolRecoveryMilestone[] {
953
953
  if (!state.turnId || !state.assemblyRevision) {
954
- throw new Error("Pi Tool interaction is missing its Turn revision");
954
+ throw new Error("SpringBrand Tool interaction is missing its Turn revision");
955
955
  }
956
956
  return [{
957
957
  version: 1,
@@ -989,7 +989,7 @@ export function applyRecoveredPiToolInteractionSettlement(
989
989
  ): RecoveredPiToolInteractionSettlement {
990
990
  const interaction = state.interactions[input.interactionId];
991
991
  if (!interaction) {
992
- throw new Error(`Unknown Pi Tool interaction ${input.interactionId}`);
992
+ throw new Error(`Unknown SpringBrand Tool interaction ${input.interactionId}`);
993
993
  }
994
994
  const transition = input.kind === "respond"
995
995
  ? respondPiToolInteraction(interaction, {
@@ -1008,7 +1008,7 @@ export function applyRecoveredPiToolInteractionSettlement(
1008
1008
  return { milestones: [], outcome };
1009
1009
  }
1010
1010
  if (!state.turnId || !state.assemblyRevision) {
1011
- throw new Error("Pi Tool interaction is missing its Turn revision");
1011
+ throw new Error("SpringBrand Tool interaction is missing its Turn revision");
1012
1012
  }
1013
1013
 
1014
1014
  const identity: RecoveryIdentity = {
@@ -37,6 +37,7 @@ export interface RuntimeAgentToolResult {
37
37
  output?: unknown;
38
38
  summary?: string;
39
39
  error?: string;
40
+ childStillRunning?: boolean;
40
41
  }
41
42
 
42
43
  export type RuntimeAgentRole = "primary" | "temporary";
@@ -652,9 +652,14 @@ export function defineRuntimeAgent<
652
652
  const policy = assembly.surfacePolicy;
653
653
  return {
654
654
  ...assembly,
655
- bindings: assembly.bindings?.workspace
656
- ? { workspace: assembly.bindings.workspace }
657
- : {},
655
+ bindings: {
656
+ ...(assembly.bindings?.workspace
657
+ ? { workspace: assembly.bindings.workspace }
658
+ : {}),
659
+ ...(assembly.bindings?.codeExecution
660
+ ? { codeExecution: assembly.bindings.codeExecution }
661
+ : {}),
662
+ },
658
663
  memoryProfile: {
659
664
  enabled: false,
660
665
  memoryTokens: 2_000,
@@ -1,6 +1,5 @@
1
1
  import type { SkillSource } from "agents/skills";
2
2
  import type {
3
- RuntimeCodeExecutionPort,
4
3
  RuntimeGatewayPort,
5
4
  RuntimeMemoryPort,
6
5
  RuntimePlatformPort,
@@ -44,7 +43,11 @@ import type {
44
43
  RuntimeSkillContribution,
45
44
  RuntimeToolSurfacePolicy,
46
45
  } from "./runtime-definition";
47
- import type { ToolRegistry } from "./tool-registry";
46
+ import {
47
+ toolRegistryFromPiCandidates,
48
+ type RuntimeCodeExecutionFactory,
49
+ type ToolRegistry,
50
+ } from "./tool-registry";
48
51
  import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
49
52
  import {
50
53
  basePiToolCandidates,
@@ -52,9 +55,11 @@ import {
52
55
  } from "./pi/tool/base";
53
56
  import {
54
57
  browserQuickActionPiToolCandidates,
58
+ codeExecutionPiToolCandidate,
55
59
  } from "./pi/tool/core";
56
60
  import { skillPiToolCandidates } from "./pi/tool/skill";
57
61
  import { createWebSearch } from "./pi/tool/web-search";
62
+ import { subagentPiToolCandidates } from "./pi/tool/subagent";
58
63
  import { resolvePiModel } from "./pi/runtime-adapter/models";
59
64
 
60
65
  /** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
@@ -76,7 +81,7 @@ export interface RuntimeAssemblyInput {
76
81
  readonly provider: RuntimeProviderPort;
77
82
  readonly platform: RuntimePlatformPort;
78
83
  readonly workspace?: WorkspacePort;
79
- readonly codeExecution?: RuntimeCodeExecutionPort;
84
+ readonly codeExecution?: RuntimeCodeExecutionFactory;
80
85
  readonly sandbox?: RuntimeSandboxPort;
81
86
  readonly memory?: RuntimeMemoryPort;
82
87
  readonly hostTools: readonly PiToolCandidate[];
@@ -149,6 +154,7 @@ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
149
154
  }
150
155
 
151
156
  interface ToolSurfaceInput {
157
+ readonly codeExecution?: RuntimeCodeExecutionFactory;
152
158
  readonly platform: RuntimePlatformPort;
153
159
  readonly workspace?: WorkspacePort;
154
160
  readonly memory?: {
@@ -214,18 +220,46 @@ async function createToolSurface(
214
220
  ...staticCandidates,
215
221
  ...candidates,
216
222
  ]) {
217
- const name = requiredName(candidate.tool.name, "Pi Tool");
223
+ const name = requiredName(candidate.tool.name, "SpringBrand Tool");
218
224
  if (!visible(candidate)) continue;
219
225
  if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
220
- throw new Error(`Pi Tool ${name} execution level is invalid`);
226
+ throw new Error(`SpringBrand Tool ${name} execution level is invalid`);
221
227
  }
222
228
  if (tools.has(name)) {
223
- throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
229
+ throw new Error(`Duplicate Runtime SpringBrand Tool: ${name}`);
224
230
  }
225
231
  tools.set(name, Object.freeze({ ...candidate }));
226
232
  }
227
233
 
228
- return Object.freeze([...tools.values()]);
234
+ const finalized = [...tools.values()];
235
+ if (tools.has("execute")) {
236
+ throw new Error("SpringBrand reserved Runtime Tool name: execute");
237
+ }
238
+ if (
239
+ !input.codeExecution ||
240
+ deny.has("execute") ||
241
+ allowsTool?.("execute") === false
242
+ ) {
243
+ return Object.freeze(finalized);
244
+ }
245
+ const mergeable = finalized.filter(
246
+ (candidate) =>
247
+ !candidate.interaction &&
248
+ typeof candidate.tool.execute === "function",
249
+ );
250
+ const direct = finalized.filter(
251
+ (candidate) =>
252
+ candidate.interaction ||
253
+ typeof candidate.tool.execute !== "function",
254
+ );
255
+ return Object.freeze([
256
+ Object.freeze(codeExecutionPiToolCandidate(
257
+ input.codeExecution.create(
258
+ toolRegistryFromPiCandidates(mergeable),
259
+ ),
260
+ )),
261
+ ...direct,
262
+ ]);
229
263
  },
230
264
  }),
231
265
  extensions: input.extensions.filter(
@@ -245,7 +279,7 @@ class RuntimeBuilder {
245
279
  private platform?: RuntimePlatformPort;
246
280
  private gateway?: RuntimeGatewayPort;
247
281
  private workspace?: WorkspacePort;
248
- private codeExecution?: RuntimeCodeExecutionPort;
282
+ private codeExecution?: RuntimeCodeExecutionFactory;
249
283
  private sandbox?: RuntimeSandboxPort;
250
284
  private schedule?: RuntimeSchedulePort;
251
285
  private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
@@ -469,12 +503,19 @@ class RuntimeBuilder {
469
503
  });
470
504
  const skillSources = [...this.skillSources.values()];
471
505
  const toolSurface = await createToolSurface({
506
+ ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
472
507
  platform: this.platform,
473
508
  ...(this.workspace ? { workspace: this.workspace } : {}),
474
509
  ...(this.memoryProfile.enabled && this.memory
475
510
  ? { memory: { port: this.memory, profile: this.memoryProfile } }
476
511
  : {}),
477
- hostTools: this.hostTools,
512
+ hostTools: [
513
+ ...this.hostTools,
514
+ ...subagentPiToolCandidates(
515
+ this.subagents,
516
+ [...this.enabledSubagents],
517
+ ),
518
+ ],
478
519
  skills: skillSources,
479
520
  enabledSubagents: [...this.enabledSubagents],
480
521
  webSearch,
@@ -492,6 +533,7 @@ class RuntimeBuilder {
492
533
  ...(this.memoryProfile.enabled && this.memory
493
534
  ? { memory: this.memory }
494
535
  : {}),
536
+ ...(this.subagents ? { subagents: this.subagents } : {}),
495
537
  skills: Object.freeze({
496
538
  sources: Object.freeze(skillSources),
497
539
  }),
@@ -632,6 +674,8 @@ function hooksToTurnEventsPort<
632
674
  if (
633
675
  !hooks.onTurnEnd &&
634
676
  !hooks.onModelUsage &&
677
+ !hooks.onSubagentUsage &&
678
+ !hooks.onLifecycleFact &&
635
679
  !hooks.onToolStart &&
636
680
  !hooks.onToolSettled &&
637
681
  !hooks.onApproval &&
@@ -645,6 +689,8 @@ function hooksToTurnEventsPort<
645
689
  ? (messages) => hooks.onTurnEnd!(ctx, messages)
646
690
  : async () => undefined,
647
691
  ...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
692
+ ...(hooks.onSubagentUsage ? { onSubagentUsage: hooks.onSubagentUsage } : {}),
693
+ ...(hooks.onLifecycleFact ? { onLifecycleFact: hooks.onLifecycleFact } : {}),
648
694
  ...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
649
695
  ...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
650
696
  ...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
@@ -754,9 +800,15 @@ export async function assembleRuntimeSnapshot<
754
800
  ...(toolAssembly.bindings?.workspace
755
801
  ? { workspace: toolAssembly.bindings.workspace }
756
802
  : {}),
803
+ ...(toolAssembly.bindings?.codeExecution
804
+ ? { codeExecution: toolAssembly.bindings.codeExecution }
805
+ : {}),
757
806
  ...(memoryProfile.enabled && toolAssembly.bindings?.memory
758
807
  ? { memory: toolAssembly.bindings.memory }
759
808
  : {}),
809
+ ...(toolAssembly.bindings?.subagents
810
+ ? { subagents: toolAssembly.bindings.subagents }
811
+ : {}),
760
812
  hostTools,
761
813
  skills: resources.skills,
762
814
  connectors: resources.connectors.servers,