@springbrand/agent-runtime 0.1.3-alpha.7 → 0.2.0-alpha.13

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 (40) 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 +34 -0
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +7 -7
  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 +3 -0
  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/subagent-runtime.ts +137 -0
  21. package/src/kernel/submission-authority.ts +114 -0
  22. package/src/kernel/submission-lifecycle.ts +17 -5
  23. package/src/lib/prompt.ts +3 -0
  24. package/src/pi/runtime-adapter/assembly.ts +2 -1
  25. package/src/pi/runtime-adapter/execution.ts +8 -1
  26. package/src/pi/runtime-adapter/index.ts +33 -0
  27. package/src/pi/runtime-adapter/models.ts +9 -0
  28. package/src/pi/tool/core-host.ts +5 -1
  29. package/src/pi/tool/core.ts +1 -10
  30. package/src/pi/tool/index.ts +1 -0
  31. package/src/pi/tool/mcp.ts +2 -2
  32. package/src/pi/tool/schedule.ts +30 -18
  33. package/src/pi/tool/subagent.ts +142 -16
  34. package/src/pi/tool/workspace-revision.ts +64 -0
  35. package/src/runtime-agent-context.ts +1 -0
  36. package/src/runtime-assembler.ts +16 -1
  37. package/src/runtime-definition.ts +12 -0
  38. package/src/runtime.ts +427 -46
  39. package/src/tool-registry.ts +2 -0
  40. package/src/workspace-versioning.ts +46 -0
@@ -234,7 +234,8 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
234
234
  export function piToolRetryPolicy(
235
235
  candidate: PiToolCandidate,
236
236
  ): "idempotent" | "non-idempotent" {
237
- return IDEMPOTENT_TOOL_NAMES.has(candidate.tool.name)
237
+ return IDEMPOTENT_TOOL_NAMES.has(candidate.tool.name) ||
238
+ candidate.owner.startsWith("subagent:")
238
239
  ? "idempotent"
239
240
  : "non-idempotent";
240
241
  }
@@ -371,6 +371,11 @@ export interface CreatePreparedPiTurnOptions {
371
371
  */
372
372
  readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
373
373
  readonly durability: PiTurnDurability;
374
+ /** Per-Submission executors for tools whose metadata is fixed at assembly time. */
375
+ readonly toolExecutors?: Readonly<Record<
376
+ string,
377
+ NonNullable<PiToolCandidate["tool"]["execute"]>
378
+ >>;
374
379
  /**
375
380
  * 上报受治理工具的耗时、结果大小和成败。
376
381
  *
@@ -617,7 +622,9 @@ export class PreparedPiTurnAdapter {
617
622
  `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
618
623
  );
619
624
  }
620
- return candidate.tool.execute(
625
+ const execute = options.toolExecutors?.[candidate.tool.name] ??
626
+ candidate.tool.execute;
627
+ return execute(
621
628
  toolCallId,
622
629
  input,
623
630
  signal,
@@ -33,6 +33,11 @@ import {
33
33
  PiRuntimeTranscript,
34
34
  type PiTranscriptDurability,
35
35
  } from "./transcript";
36
+ import { subagentPiToolCandidates } from "../tool/subagent";
37
+ import type {
38
+ RuntimeSubagentLifecyclePort,
39
+ RuntimeSubagentPort,
40
+ } from "../../kernel/bindings";
36
41
  import type { SqlTaggedTemplate } from "agents/chat";
37
42
 
38
43
  /**
@@ -201,6 +206,34 @@ export class PiRuntimeAdapter {
201
206
  * 实现理由:构造时会校验 Prepared Runtime 的 owner 和 pinned descriptor 格式,再建立本 Turn 独享的 Tool governance 与执行状态。
202
207
  * 不要跨 Turn 复用返回对象;abort、steer、assistant ordinal 和 Tool governance 都是单次执行状态。
203
208
  */
209
+ /**
210
+ * 为一个已准入的 Submission 生成绑定 durable lifecycle 的 SubAgent Tool 执行器。
211
+ *
212
+ * @remarks
213
+ * 调用方:Runtime 在创建 Turn 前调用一次,并把结果作为 `createTurn` 的 `toolExecutors` 传入。
214
+ *
215
+ * 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool 层,而 accountId、
216
+ * rateVersion 和 slot identity 这些计费事实属于 Submission。两者在这里汇合:Pi 提供工具,
217
+ * Runtime 提供本次 Submission 的 lifecycle 端口,因此 Runtime 不需要认识 Tool 模块本身。
218
+ * 没有可用 SubAgent 类型时返回 `undefined`,让 Turn 继续使用装配期的无 lifecycle 执行器。
219
+ */
220
+ subagentToolExecutors(
221
+ subagents: RuntimeSubagentPort,
222
+ enabledSubagents: readonly string[],
223
+ lifecycle: RuntimeSubagentLifecyclePort,
224
+ ): CreatePreparedPiTurnOptions["toolExecutors"] {
225
+ const entries = subagentPiToolCandidates(
226
+ subagents,
227
+ enabledSubagents,
228
+ lifecycle,
229
+ ).flatMap((candidate) =>
230
+ candidate.tool.execute
231
+ ? [[candidate.tool.name, candidate.tool.execute] as const]
232
+ : []
233
+ );
234
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
235
+ }
236
+
204
237
  createTurn(
205
238
  options: CreatePreparedPiTurnOptions,
206
239
  ): PreparedPiTurnAdapter {
@@ -513,6 +513,7 @@ function configuredModel(
513
513
  ...catalogHeaders,
514
514
  ...endpoint.headers,
515
515
  };
516
+ const openRouterProviderPin = endpoint.openRouterProviderPins?.[modelId];
516
517
  return {
517
518
  ...metadata,
518
519
  api: apiFor(endpoint.protocol),
@@ -524,6 +525,14 @@ function configuredModel(
524
525
  ...compat,
525
526
  sendSessionAffinityHeaders: true,
526
527
  sessionAffinityFormat: "openrouter" as const,
528
+ ...(openRouterProviderPin
529
+ ? {
530
+ openRouterRouting: {
531
+ order: [openRouterProviderPin],
532
+ allow_fallbacks: false,
533
+ },
534
+ }
535
+ : {}),
527
536
  },
528
537
  }
529
538
  : {}),
@@ -36,11 +36,15 @@ export function createWorkspaceCodeExecutionPort(options: {
36
36
  ),
37
37
  name: "execute",
38
38
  });
39
- const execute = tool.execute;
39
+ const { description, execute } = tool;
40
+ if (typeof description !== "string" || description.length === 0) {
41
+ throw new Error("Think createExecuteRuntime returned a tool without a description");
42
+ }
40
43
  if (typeof execute !== "function") {
41
44
  throw new Error("Think createExecuteRuntime returned a non-executable tool");
42
45
  }
43
46
  return {
47
+ description,
44
48
  execute: (input) => Promise.resolve(execute(input, {
45
49
  toolCallId: "execute",
46
50
  messages: [],
@@ -125,16 +125,7 @@ export function codeExecutionPiToolCandidate(
125
125
  const tool: AgentTool<typeof executeParameters> = {
126
126
  name: "execute",
127
127
  label: "Execute JavaScript",
128
- description:
129
- "Run JavaScript in a short-lived Code Mode Dynamic Worker and return a value. This is your " +
130
- "raw-network and tool-composition instrument, not a general web-search tool. Call fetch directly " +
131
- "when you need a raw response, custom headers/method/body, a structured API, or content that " +
132
- "web_search could not retrieve. Check res.status; when several known endpoints are necessary, " +
133
- "fetch them in one invocation. Also available: " +
134
- "state.* for your workspace filesystem (readFile/writeFile/glob/searchFiles/replaceInFiles, " +
135
- "each taking one object argument), and codemode.step(name, fn) to run a side-effecting block " +
136
- "exactly once so it survives replay. Write plain JavaScript — TypeScript type annotations are " +
137
- "a syntax error. Use return for the result and console.log for notes.",
128
+ description: runtime.description,
138
129
  parameters: executeParameters,
139
130
  // 把模型提供的 JavaScript 交给 Codemode Runtime 执行。
140
131
  // Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
@@ -30,4 +30,5 @@ export * from "./schedule";
30
30
  export * from "./skill";
31
31
  export * from "./subagent";
32
32
  export * from "./workspace-sandbox";
33
+ export * from "./workspace-revision";
33
34
  export * from "./web-search";
@@ -59,7 +59,8 @@ export interface PiMcpHost {
59
59
  * 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
60
60
  */
61
61
  callTool(
62
- ...args: Parameters<MCPClientManager["callTool"]>
62
+ params: Parameters<MCPClientManager["callTool"]>[0],
63
+ options?: { signal?: AbortSignal },
63
64
  ): Promise<unknown>;
64
65
  };
65
66
  }
@@ -333,7 +334,6 @@ export function createPiMcpToolCandidates(
333
334
  name: mcpTool.name,
334
335
  arguments: args,
335
336
  },
336
- undefined,
337
337
  { signal },
338
338
  ),
339
339
  {
@@ -94,7 +94,10 @@ function result<T>(details: T): AgentToolResult<T> {
94
94
  function candidate<T extends TSchema>(
95
95
  tool: AgentTool<T>,
96
96
  options: Partial<
97
- Pick<PiToolCandidate, "owner" | "requiredExecutionLevel" | "summary">
97
+ Pick<
98
+ PiToolCandidate,
99
+ "alwaysRequiresApproval" | "owner" | "requiredExecutionLevel" | "summary"
100
+ >
98
101
  > = {},
99
102
  ): PiToolCandidate {
100
103
  return {
@@ -102,6 +105,9 @@ function candidate<T extends TSchema>(
102
105
  requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
103
106
  source: "action",
104
107
  tool,
108
+ ...(options.alwaysRequiresApproval
109
+ ? { alwaysRequiresApproval: true }
110
+ : {}),
105
111
  ...(options.summary ? { summary: options.summary } : {}),
106
112
  };
107
113
  }
@@ -110,24 +116,30 @@ export function schedulePiToolCandidates(
110
116
  schedule: RuntimeSchedulePort,
111
117
  ): PiToolCandidate[] {
112
118
  return [
113
- candidate({
114
- name: "schedule",
115
- label: "Schedule prompt",
116
- description:
117
- "Schedule a prompt to run LATER, autonomously. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
118
- parameters: scheduleParameters,
119
- async execute(_toolCallId, input, signal) {
120
- signal?.throwIfAborted();
121
- const spec: ScheduleSpec = {
122
- trigger: input.trigger,
123
- job: { kind: "prompt", prompt: input.prompt },
124
- label: input.label,
125
- ...(input.timezone ? { tz: input.timezone } : {}),
126
- };
127
- const { id } = await schedule.create(spec);
128
- return result({ scheduled: true, id });
119
+ candidate(
120
+ {
121
+ name: "schedule",
122
+ label: "Schedule prompt",
123
+ description:
124
+ "Schedule a prompt to run LATER, autonomously. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
125
+ parameters: scheduleParameters,
126
+ async execute(_toolCallId, input, signal) {
127
+ signal?.throwIfAborted();
128
+ const spec: ScheduleSpec = {
129
+ trigger: input.trigger,
130
+ job: { kind: "prompt", prompt: input.prompt },
131
+ label: input.label,
132
+ ...(input.timezone ? { tz: input.timezone } : {}),
133
+ };
134
+ const { id } = await schedule.create(spec);
135
+ return result({ scheduled: true, id });
136
+ },
129
137
  },
130
- }),
138
+ {
139
+ alwaysRequiresApproval: true,
140
+ summary: "Create a scheduled task",
141
+ },
142
+ ),
131
143
  candidate({
132
144
  name: "list_schedules",
133
145
  label: "List schedules",
@@ -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
+ }
@@ -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";
@@ -55,6 +55,7 @@ import {
55
55
  } from "./pi/tool/core";
56
56
  import { skillPiToolCandidates } from "./pi/tool/skill";
57
57
  import { createWebSearch } from "./pi/tool/web-search";
58
+ import { subagentPiToolCandidates } from "./pi/tool/subagent";
58
59
  import { resolvePiModel } from "./pi/runtime-adapter/models";
59
60
 
60
61
  /** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
@@ -474,7 +475,13 @@ class RuntimeBuilder {
474
475
  ...(this.memoryProfile.enabled && this.memory
475
476
  ? { memory: { port: this.memory, profile: this.memoryProfile } }
476
477
  : {}),
477
- hostTools: this.hostTools,
478
+ hostTools: [
479
+ ...this.hostTools,
480
+ ...subagentPiToolCandidates(
481
+ this.subagents,
482
+ [...this.enabledSubagents],
483
+ ),
484
+ ],
478
485
  skills: skillSources,
479
486
  enabledSubagents: [...this.enabledSubagents],
480
487
  webSearch,
@@ -492,6 +499,7 @@ class RuntimeBuilder {
492
499
  ...(this.memoryProfile.enabled && this.memory
493
500
  ? { memory: this.memory }
494
501
  : {}),
502
+ ...(this.subagents ? { subagents: this.subagents } : {}),
495
503
  skills: Object.freeze({
496
504
  sources: Object.freeze(skillSources),
497
505
  }),
@@ -632,6 +640,8 @@ function hooksToTurnEventsPort<
632
640
  if (
633
641
  !hooks.onTurnEnd &&
634
642
  !hooks.onModelUsage &&
643
+ !hooks.onSubagentUsage &&
644
+ !hooks.onLifecycleFact &&
635
645
  !hooks.onToolStart &&
636
646
  !hooks.onToolSettled &&
637
647
  !hooks.onApproval &&
@@ -645,6 +655,8 @@ function hooksToTurnEventsPort<
645
655
  ? (messages) => hooks.onTurnEnd!(ctx, messages)
646
656
  : async () => undefined,
647
657
  ...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
658
+ ...(hooks.onSubagentUsage ? { onSubagentUsage: hooks.onSubagentUsage } : {}),
659
+ ...(hooks.onLifecycleFact ? { onLifecycleFact: hooks.onLifecycleFact } : {}),
648
660
  ...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
649
661
  ...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
650
662
  ...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
@@ -757,6 +769,9 @@ export async function assembleRuntimeSnapshot<
757
769
  ...(memoryProfile.enabled && toolAssembly.bindings?.memory
758
770
  ? { memory: toolAssembly.bindings.memory }
759
771
  : {}),
772
+ ...(toolAssembly.bindings?.subagents
773
+ ? { subagents: toolAssembly.bindings.subagents }
774
+ : {}),
760
775
  hostTools,
761
776
  skills: resources.skills,
762
777
  connectors: resources.connectors.servers,
@@ -3,6 +3,10 @@ import type { ExecutionLevel } from "./lib/execution-level";
3
3
  import type {
4
4
  RuntimeGatewayPort,
5
5
  RuntimeModelUsageEvent,
6
+ RuntimeSubmissionAdmissionHook,
7
+ RuntimeSubagentUsageEvent,
8
+ RuntimeLifecycleFact,
9
+ RuntimeEventConfirmation,
6
10
  RuntimePlatformPort,
7
11
  RuntimeProviderPort,
8
12
  RuntimeSkillScriptPolicy,
@@ -136,6 +140,14 @@ export interface RuntimeAgentHooks<
136
140
  messages: readonly RuntimeTurnMessage[],
137
141
  ) => Promise<void>;
138
142
  readonly onModelUsage?: (event: RuntimeModelUsageEvent) => Promise<void>;
143
+ readonly onSubagentUsage?: (
144
+ event: RuntimeSubagentUsageEvent,
145
+ ) => Promise<RuntimeEventConfirmation | void>;
146
+ readonly onLifecycleFact?: (
147
+ event: RuntimeLifecycleFact,
148
+ ) => Promise<RuntimeEventConfirmation | void>;
149
+ /** Single admission seam shared by chat, RPC, regenerate and scheduled submissions. */
150
+ readonly onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
139
151
  readonly onToolStart?: (event: RuntimeToolStartEvent) => Promise<void>;
140
152
  readonly onToolSettled?: (event: RuntimeToolSettlementEvent) => Promise<void>;
141
153
  readonly onApproval?: (input: ApprovalHookInput) => Promise<void>;