@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.10

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 (172) hide show
  1. package/package.json +7 -7
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1682 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/index.ts +18 -0
  8. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  9. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  10. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  11. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  12. package/src/{db → adapter/cloudflare}/schema.ts +28 -80
  13. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  14. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  15. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  16. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  17. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
  18. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  19. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  20. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  21. package/src/config/activation.ts +140 -0
  22. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  23. package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
  24. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  25. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  26. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  27. package/src/config/harness-runtime.ts +190 -0
  28. package/src/config/index.ts +14 -0
  29. package/src/config/model-guidance.ts +21 -0
  30. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
  31. package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
  32. package/src/{kernel → config}/profile.ts +7 -0
  33. package/src/{kernel → config}/public-contracts.ts +6 -6
  34. package/src/{kernel → config}/runtime-load.ts +1 -1
  35. package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
  36. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  37. package/src/context/extension/index.ts +3 -0
  38. package/src/context/index.ts +6 -0
  39. package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
  40. package/src/conversation/harness-resume.ts +288 -0
  41. package/src/conversation/harness-stream.ts +99 -0
  42. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  43. package/src/conversation/index.ts +6 -0
  44. package/src/{pi → conversation}/message/contract.ts +3 -0
  45. package/src/{pi → conversation}/message/conversion.ts +75 -0
  46. package/src/{pi → conversation}/message/index.ts +1 -0
  47. package/src/{pi → conversation}/message/model-input.ts +1 -1
  48. package/src/{pi → conversation}/message/projection.ts +113 -30
  49. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  50. package/src/conversation/state.ts +196 -0
  51. package/src/harness/index.ts +7 -0
  52. package/src/harness/session-harness.ts +562 -0
  53. package/src/harness/submission-operation.ts +49 -0
  54. package/src/index.ts +38 -43
  55. package/src/lib/artifacts.ts +18 -0
  56. package/src/lib/execution-level.ts +13 -0
  57. package/src/lib/mcp.ts +1 -1
  58. package/src/lib/prompt.ts +7 -6
  59. package/src/lib/token-estimate.ts +8 -0
  60. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
  61. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  62. package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
  63. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  64. package/src/session/index.ts +39 -0
  65. package/src/skills/springbrand-worker-website/index.ts +1 -1
  66. package/src/submission/admission.ts +286 -0
  67. package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
  68. package/src/submission/delivery.ts +212 -0
  69. package/src/submission/finalization.ts +111 -0
  70. package/src/submission/index.ts +9 -0
  71. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  72. package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
  73. package/src/submission/steer-delivery.ts +167 -0
  74. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  75. package/src/submission/unattended.ts +40 -0
  76. package/src/telemetry/contract.ts +12 -0
  77. package/src/telemetry/coordinator.ts +65 -0
  78. package/src/telemetry/delivery.ts +2 -2
  79. package/src/telemetry/index.ts +5 -0
  80. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  81. package/src/telemetry/recorder.ts +1 -1
  82. package/src/telemetry/runtime-telemetry.ts +12 -0
  83. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  84. package/src/telemetry/turn-events.ts +279 -0
  85. package/src/tools/README.md +44 -0
  86. package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
  87. package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
  88. package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
  89. package/src/tools/assistant/ask-user.ts +317 -0
  90. package/src/tools/assistant/candidate.ts +15 -0
  91. package/src/tools/assistant/followups.ts +34 -0
  92. package/src/tools/assistant/index.ts +19 -0
  93. package/src/tools/assistant/memory.ts +73 -0
  94. package/src/tools/assistant/plan.ts +98 -0
  95. package/src/{pi/tool → tools/assistant}/time.ts +1 -1
  96. package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
  97. package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
  98. package/src/tools/codemode/workspace-connector.ts +198 -0
  99. package/src/tools/extension/index.ts +42 -0
  100. package/src/tools/index.ts +32 -0
  101. package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
  102. package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
  103. package/src/tools/runtime/approval/index.ts +1 -0
  104. package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
  105. package/src/tools/runtime/compiler.ts +264 -0
  106. package/src/tools/runtime/contracts.ts +117 -0
  107. package/src/tools/runtime/decision-gate.ts +337 -0
  108. package/src/tools/runtime/human-decisions.ts +127 -0
  109. package/src/tools/runtime/interaction/index.ts +1 -0
  110. package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
  111. package/src/tools/runtime/result.ts +22 -0
  112. package/src/tools/runtime/unattended-error.ts +4 -0
  113. package/src/tools/sandbox/index.ts +228 -0
  114. package/src/tools/schedule/index.ts +301 -0
  115. package/src/tools/skill/contracts.ts +20 -0
  116. package/src/tools/skill/index.ts +99 -0
  117. package/src/tools/skill/resources.ts +322 -0
  118. package/src/tools/skill/scripts.ts +168 -0
  119. package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
  120. package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
  121. package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
  122. package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
  123. package/src/tools/subagent/temporary/approvals.ts +123 -0
  124. package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
  125. package/src/tools/subagent/temporary/core.ts +53 -0
  126. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
  127. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
  128. package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
  129. package/src/tools/web/index.ts +2 -0
  130. package/src/tools/web/search/api.ts +1320 -0
  131. package/src/tools/web/search/index.ts +2 -0
  132. package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
  133. package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
  134. package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
  135. package/src/db/milestone.repo.ts +0 -104
  136. package/src/db/settlement.repo.ts +0 -104
  137. package/src/kernel/approval-lifecycle.ts +0 -638
  138. package/src/kernel/interaction-lifecycle.ts +0 -433
  139. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  140. package/src/kernel/state.ts +0 -91
  141. package/src/pi/assembly/index.ts +0 -19
  142. package/src/pi/runtime-adapter/execution.ts +0 -1163
  143. package/src/pi/runtime-adapter/index.ts +0 -293
  144. package/src/pi/runtime-adapter/recovery.ts +0 -982
  145. package/src/pi/session/index.ts +0 -24
  146. package/src/pi/tool/base.ts +0 -550
  147. package/src/pi/tool/compiler.ts +0 -483
  148. package/src/pi/tool/index.ts +0 -38
  149. package/src/pi/tool/schedule.ts +0 -261
  150. package/src/pi/tool/skill.ts +0 -565
  151. package/src/pi/tool/web-search/api.ts +0 -1318
  152. package/src/pi/tool/web-search/index.ts +0 -2
  153. package/src/pi/tool/workspace-state-connector.ts +0 -146
  154. package/src/pi/turn/approval.ts +0 -181
  155. package/src/pi/turn/index.ts +0 -82
  156. package/src/pi/turn/interaction.ts +0 -181
  157. package/src/runtime.ts +0 -4760
  158. package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  159. package/src/{kernel → config}/degradation.ts +0 -0
  160. package/src/{kernel → config}/runtime-config.ts +0 -0
  161. package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  162. package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  163. package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  164. package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  165. package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  166. package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  167. package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  168. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
  169. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
  170. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
  171. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
  172. /package/src/{workspace-versioning.ts → tools/workspace/versioning.ts} +0 -0
@@ -1,20 +1,24 @@
1
1
  import {
2
- runAgentLoopContinue,
3
- convertToLlm,
4
- type AgentMessage,
5
- type AgentEvent,
6
- type AgentTool,
2
+ type AgentHarnessTool,
3
+ type HarnessEvent,
4
+ type Session,
7
5
  type StreamFn,
8
6
  } from "@earendil-works/pi-agent-core";
9
- import type {
10
- Api,
11
- AssistantMessage,
12
- Model,
13
- } from "@earendil-works/pi-ai";
7
+ import {
8
+ awaitWithContext,
9
+ BACKGROUND_CONTEXT,
10
+ withAbortSignal,
11
+ } from "@earendil-works/pi-agent-core/harness/context";
12
+ import type { Api, AssistantMessage, Model, Models } from "@earendil-works/pi-ai";
14
13
  import { z } from "zod";
15
14
  import { assembleSubagentPrompt } from "../../../lib/prompt";
16
- import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
17
- import { structuredOutputStream } from "../../../pi/runtime-adapter/models";
15
+ import type { AgentType } from "../../../tools/subagent/types/contract";
16
+ import { structuredOutputStream } from "../../../models";
17
+ import {
18
+ openSessionHarness,
19
+ type SessionHarness,
20
+ type SessionHarnessOptions,
21
+ } from "../../../harness";
18
22
 
19
23
  const RECURSIVE_TOOLS = new Set([
20
24
  "run_agent",
@@ -30,17 +34,21 @@ export interface CloudflareSubAgentRun {
30
34
  input: unknown;
31
35
  model: Model<Api>;
32
36
  streamFn: StreamFn;
33
- tools: AgentTool[];
37
+ models?: Models;
38
+ /** A fresh Pi Session facade is required after each Harness release. */
39
+ openSession(): Session | Promise<Session>;
40
+ operationId?: string;
41
+ /** Tools compiled by the package's single Harness adapter. */
42
+ tools?: AgentHarnessTool<object | undefined>[];
34
43
  signal?: AbortSignal;
35
44
  timeoutMs?: number;
36
- onEvent?: (event: AgentEvent, signal: AbortSignal) => void | Promise<void>;
45
+ /** Native operation events for projections that must survive model retries. */
46
+ onEvent?: (event: HarnessEvent, signal: AbortSignal) => void | Promise<void>;
37
47
  }
38
48
 
39
49
  function textOf(message: AssistantMessage): string {
40
- return message.content
41
- .flatMap((part) => (part.type === "text" ? [part.text] : []))
42
- .join("")
43
- .trim();
50
+ return message.content.flatMap((part) => part.type === "text" ? [part.text] : [])
51
+ .join("").trim();
44
52
  }
45
53
 
46
54
  function structuredText(text: string): string {
@@ -49,30 +57,22 @@ function structuredText(text: string): string {
49
57
 
50
58
  function toolErrorText(result: unknown): string {
51
59
  if (
52
- typeof result === "object" &&
53
- result !== null &&
60
+ typeof result === "object" && result !== null &&
54
61
  Array.isArray((result as { content?: unknown }).content)
55
62
  ) {
56
- const text = (result as { content: unknown[] }).content
57
- .flatMap((part) =>
58
- typeof part === "object" &&
59
- part !== null &&
63
+ const text = (result as { content: unknown[] }).content.flatMap((part) =>
64
+ typeof part === "object" && part !== null &&
60
65
  (part as { type?: unknown }).type === "text" &&
61
66
  typeof (part as { text?: unknown }).text === "string"
62
- ? [(part as { text: string }).text]
63
- : [],
64
- )
65
- .join("")
66
- .trim();
67
+ ? [(part as { text: string }).text]
68
+ : []
69
+ ).join("").trim();
67
70
  if (text) return text;
68
71
  }
69
72
  return "tool execution failed";
70
73
  }
71
74
 
72
- function taskPrompt(
73
- input: unknown,
74
- outputSchema?: Record<string, unknown>,
75
- ): string {
75
+ function taskPrompt(input: unknown, outputSchema?: Record<string, unknown>): string {
76
76
  return [
77
77
  "Complete this bounded task.",
78
78
  `Input:\n${JSON.stringify(input)}`,
@@ -82,112 +82,166 @@ function taskPrompt(
82
82
  ].join("\n\n");
83
83
  }
84
84
 
85
- export async function runCloudflareSubAgent(
86
- run: CloudflareSubAgentRun,
87
- ): Promise<unknown> {
85
+ function harnessModels(run: CloudflareSubAgentRun, streamFn: StreamFn): Models {
86
+ const base = run.models;
87
+ return new Proxy(base ?? {} as Models, {
88
+ get(target, property) {
89
+ if (property === "getModel") {
90
+ return (provider: string, modelId: string) =>
91
+ provider === run.model.provider && modelId === run.model.id
92
+ ? run.model
93
+ : base?.getModel(provider, modelId);
94
+ }
95
+ if (property === "streamSimple") return streamFn;
96
+ const value = Reflect.get(target, property, target);
97
+ return typeof value === "function" ? value.bind(target) : value;
98
+ },
99
+ });
100
+ }
101
+
102
+ /** Execute one bounded mechanism SubAgent through the official AgentHarness. */
103
+ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise<unknown> {
88
104
  const input = run.type.inputSchema.safeParse(run.input);
89
105
  if (!input.success) {
90
- throw new Error(
91
- `SubAgent input failed schema validation: ${input.error.message}`,
92
- );
106
+ throw new Error(`SubAgent input failed schema validation: ${input.error.message}`);
93
107
  }
94
- for (const tool of run.tools) {
108
+ const declaredTools = run.tools ?? [];
109
+ for (const tool of declaredTools) {
95
110
  if (RECURSIVE_TOOLS.has(tool.name)) {
96
111
  throw new Error(`SubAgent cannot use recursive tool "${tool.name}"`);
97
112
  }
98
113
  }
99
114
  if (run.signal?.aborted) throw new Error("SubAgent run aborted");
100
- const { $schema: _, ...outputSchema } = z.toJSONSchema(run.type.outputSchema);
101
- const constrainedStream = structuredOutputStream(
102
- run.streamFn,
103
- run.model,
104
- outputSchema,
105
- );
106
115
 
107
- // 这里没有审批闸,也装不了:到这一步工具已经被 compilePiTools 编译成 AgentTool,
108
- // requiredExecutionLevel 只挂在编译前的 PiToolCandidate 上,这里读不到。「子 agent 不许挂需审批的工具」
109
- // 这条不变量由装配处 createCloudflareSubAgentTools 的启动期断言强制执行。
116
+ const { $schema: _, ...outputSchema } = z.toJSONSchema(run.type.outputSchema);
117
+ const structuredStream = structuredOutputStream(run.streamFn, run.model, outputSchema);
118
+ const streamFn = structuredStream ?? run.streamFn;
119
+ const operationId = run.operationId ?? crypto.randomUUID();
110
120
  const controller = new AbortController();
111
- const signal = run.signal ? AbortSignal.any([run.signal, controller.signal]) : controller.signal;
112
- const messages: AgentMessage[] = [{
113
- role: "user",
114
- content: taskPrompt(input.data, constrainedStream ? undefined : outputSchema),
115
- timestamp: Date.now(),
116
- }];
121
+ const signal = run.signal
122
+ ? AbortSignal.any([run.signal, controller.signal])
123
+ : controller.signal;
124
+ const context = withAbortSignal(signal, BACKGROUND_CONTEXT);
125
+ const options = {
126
+ models: harnessModels(run, streamFn),
127
+ model: run.model,
128
+ thinkingLevel: run.type.model === "main" ? "medium" : "off",
129
+ systemPrompt: assembleSubagentPrompt(run.type.persona),
130
+ tools: declaredTools,
131
+ streamOptions: { maxRetries: 0 },
132
+ toolExecution: "parallel",
133
+ compaction: { enabled: false, reserveTokens: 1, keepRecentTokens: 1 },
134
+ } satisfies Omit<SessionHarnessOptions<object | undefined>, "session">;
135
+ let runtime: SessionHarness<object | undefined> | undefined;
117
136
  let toolFailure: { name: string; error: string } | undefined;
118
- const emit = async (event: AgentEvent) => {
119
- if (event.type === "tool_execution_end" && event.isError) {
120
- toolFailure ??= {
121
- name: event.toolName,
122
- error: toolErrorText(event.result),
123
- };
124
- }
125
- await run.onEvent?.(event, signal);
126
- };
127
137
 
128
138
  let timedOut = false;
129
139
  const timeout = setTimeout(() => {
130
140
  timedOut = true;
131
- controller.abort();
141
+ controller.abort(new Error("SubAgent run timed out"));
132
142
  }, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
133
-
134
- let newMessages: AgentMessage[];
135
143
  try {
136
- newMessages = await runAgentLoopContinue(
137
- { systemPrompt: assembleSubagentPrompt(run.type.persona), messages, tools: [...run.tools] },
138
- { model: run.model, reasoning: run.type.model === "main" ? "medium" : undefined, toolExecution: "parallel", convertToLlm },
139
- emit,
140
- signal,
141
- constrainedStream ?? run.streamFn,
142
- );
143
- } finally {
144
- clearTimeout(timeout);
145
- }
146
-
147
- if (timedOut) throw new Error("SubAgent run timed out");
148
- if (run.signal?.aborted) throw new Error("SubAgent run aborted");
149
-
150
- const assistant = newMessages
151
- .slice()
152
- .reverse()
153
- .find(
154
- (message): message is AssistantMessage => message.role === "assistant",
155
- );
156
- if (!assistant) throw new Error("SubAgent produced no structured output");
157
- if (assistant.stopReason === "deferred") throw new Error("SubAgent deferred responses are not supported");
158
- if (assistant.stopReason === "aborted") {
159
- throw new Error("SubAgent run aborted");
160
- }
161
- if (assistant.stopReason === "error") {
162
- if (toolFailure) {
144
+ let settled: Awaited<ReturnType<SessionHarness<object | undefined>["run"]>>;
145
+ while (true) {
146
+ signal.throwIfAborted();
147
+ if (!runtime) {
148
+ runtime = await openSessionHarness({
149
+ ...options,
150
+ session: await run.openSession(),
151
+ }, context);
152
+ if (runtime.openOperation && runtime.openOperation.operationId !== operationId) {
153
+ throw new Error("SubAgent Session has an unrelated open operation");
154
+ }
155
+ const harness = runtime.harness;
156
+ const onEvent = run.onEvent;
157
+ if (onEvent) {
158
+ for (const type of ["run_start", "run_resume", "run_end", "turn_start", "message_start", "message_update", "message_end", "tool_start", "tool_update", "tool_end"] as const) {
159
+ harness.events.on(type, (event) => onEvent(event, signal));
160
+ }
161
+ }
162
+ harness.events.on("tool_end", (event) => {
163
+ if (event.isError) {
164
+ toolFailure ??= { name: event.toolName, error: toolErrorText(event.result) };
165
+ }
166
+ });
167
+ }
168
+ const result = await runtime.run(
169
+ !runtime.openOperation
170
+ ? {
171
+ kind: "prompt",
172
+ operationId,
173
+ prompt: taskPrompt(input.data, structuredStream ? undefined : outputSchema),
174
+ }
175
+ : undefined,
176
+ context,
177
+ { waitForRetry: true, pollDeferred: true },
178
+ );
179
+ if (result.kind === "released") {
180
+ await runtime.close(BACKGROUND_CONTEXT);
181
+ runtime = undefined;
182
+ continue;
183
+ }
184
+ if (result.kind === "waiting") {
185
+ const delay = result.reason === "deferred"
186
+ ? result.deferred.pollAfterMs ?? 1_000
187
+ : Math.max(0, result.notBefore - Date.now());
188
+ let timer: ReturnType<typeof setTimeout> | undefined;
189
+ try {
190
+ await awaitWithContext(new Promise<void>((resolve) => {
191
+ timer = setTimeout(resolve, delay);
192
+ }), context);
193
+ } finally {
194
+ clearTimeout(timer);
195
+ }
196
+ continue;
197
+ }
198
+ settled = result;
199
+ break;
200
+ }
201
+ if (timedOut) throw new Error("SubAgent run timed out");
202
+ if (run.signal?.aborted || settled.outcome.status === "aborted") {
203
+ throw new Error("SubAgent run aborted");
204
+ }
205
+ if (settled.outcome.status === "failed") {
163
206
  throw new Error(
164
- `SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`,
207
+ toolFailure
208
+ ? `SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`
209
+ : `SubAgent model failed: ${settled.outcome.error?.message ?? "unknown provider error"}`,
165
210
  );
166
211
  }
167
- throw new Error(
168
- `SubAgent model failed: ${assistant.errorMessage ?? "unknown provider error"}`,
169
- );
170
- }
171
- if (toolFailure) {
172
- throw new Error(
173
- `SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`,
174
- );
175
- }
176
-
177
- const text = textOf(assistant);
178
- if (!text) throw new Error("SubAgent produced no structured output");
179
-
180
- let output: unknown;
181
- try {
182
- output = JSON.parse(structuredText(text));
183
- } catch {
184
- throw new Error("SubAgent output is not valid JSON");
185
- }
186
- const result = run.type.outputSchema.safeParse(output);
187
- if (!result.success) {
188
- throw new Error(
189
- `SubAgent output failed schema validation: ${result.error.message}`,
190
- );
212
+ if (toolFailure) {
213
+ throw new Error(`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`);
214
+ }
215
+ const assistant = (await runtime.lane.findEntries(
216
+ { type: "message", order: "newestFirst" },
217
+ context,
218
+ )).find((entry) => entry.type === "message" && entry.message.role === "assistant");
219
+ if (!assistant || assistant.type !== "message" || assistant.message.role !== "assistant") {
220
+ throw new Error("SubAgent produced no structured output");
221
+ }
222
+ const text = textOf(assistant.message);
223
+ if (!text) throw new Error("SubAgent produced no structured output");
224
+ let output: unknown;
225
+ try {
226
+ output = JSON.parse(structuredText(text));
227
+ } catch {
228
+ throw new Error("SubAgent output is not valid JSON");
229
+ }
230
+ const result = run.type.outputSchema.safeParse(output);
231
+ if (!result.success) {
232
+ throw new Error(`SubAgent output failed schema validation: ${result.error.message}`);
233
+ }
234
+ return result.data;
235
+ } catch (error) {
236
+ if (timedOut) throw new Error("SubAgent run timed out");
237
+ if (run.signal?.aborted) throw new Error("SubAgent run aborted");
238
+ throw error;
239
+ } finally {
240
+ clearTimeout(timeout);
241
+ if (signal.aborted && runtime?.openOperation) {
242
+ await runtime.requestAbort(operationId, BACKGROUND_CONTEXT).catch(() => undefined);
243
+ await runtime.run(undefined, BACKGROUND_CONTEXT).catch(() => undefined);
244
+ }
245
+ await runtime?.close(BACKGROUND_CONTEXT).catch(() => undefined);
191
246
  }
192
- return result.data;
193
247
  }
@@ -9,18 +9,19 @@ import {
9
9
  } from "@cloudflare/shell";
10
10
  import { stateToolsFromBackend } from "@cloudflare/shell/workers";
11
11
  import type {
12
+ AgentHarnessTool,
12
13
  AgentTool,
13
14
  AgentToolResult,
14
15
  } from "@earendil-works/pi-agent-core";
15
16
  import { Type } from "@earendil-works/pi-ai";
16
- import type { WorkspacePort } from "../../../kernel/bindings";
17
- import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
17
+ import type { WorkspacePort } from "../../../config/ports";
18
+ import type { AgentType } from "../../../tools/subagent/types/contract";
18
19
  import {
19
- compilePiTools,
20
- type CompilePiToolsOptions,
20
+ compileHarnessTools,
21
+ type CompileHarnessToolsOptions,
21
22
  type PiToolCandidate,
22
- } from "../../../pi/tool";
23
- import { workspacePiToolCandidates } from "../../../pi/tool/workspace-sandbox";
23
+ } from "../../../tools";
24
+ import { workspacePiToolCandidates } from "../../../tools/workspace";
24
25
 
25
26
  const READONLY_WORKSPACE_TOOLS = new Set([
26
27
  "read",
@@ -183,14 +184,12 @@ function fetchCandidate(options: {
183
184
  }
184
185
 
185
186
  /**
186
- * SubAgent 链路上没有审批通道:主链的审批闸在 Runtime 的 execution 适配器里
187
- * (requiresPiToolApproval → park → 等用户裁决),而子 Agent 直接调用 `runAgentLoopContinue`
188
- * 直接跑,parked 的请求没有任何界面能落地、也没有人能批。
187
+ * SubAgent 链路上没有人工审批通道,因此只允许 safe Tool 进入它的 Harness。
189
188
  *
190
189
  * 只有 `safe` Tool 可以挂到这条没有审批通道的链路;更高档位必须在装配时失败关闭。
191
190
  *
192
191
  * 所以把这个不变量钉在装配处:要的是「装不上」,不是「跑起来才炸」。而且这里是唯一还能
193
- * 检查的位置 —— 档位挂在 PiToolCandidate 上,compilePiTools 之后只剩 AgentTool。
192
+ * 检查必须在 Candidate 被编译为 AgentHarnessTool 之前完成。
194
193
  */
195
194
  function assertSafeExecutionLevel(
196
195
  candidates: readonly PiToolCandidate[],
@@ -221,8 +220,8 @@ export function createCloudflareSubAgentTools(options: {
221
220
  workspace: WorkspacePort;
222
221
  loader: WorkerLoader;
223
222
  outbound: Fetcher;
224
- settle: CompilePiToolsOptions["settle"];
225
- }): AgentTool[] {
223
+ settle: NonNullable<CompileHarnessToolsOptions["onSettlement"]>;
224
+ }): AgentHarnessTool<object | undefined>[] {
226
225
  const workspace = workspacePiToolCandidates(options.workspace).filter(
227
226
  (candidate) =>
228
227
  options.profile === "capable" ||
@@ -245,7 +244,7 @@ export function createCloudflareSubAgentTools(options: {
245
244
  );
246
245
  assertSafeExecutionLevel(mounted);
247
246
 
248
- return compilePiTools(mounted, {
249
- settle: options.settle,
247
+ return compileHarnessTools(mounted, {
248
+ onSettlement: options.settle,
250
249
  });
251
250
  }
@@ -8,17 +8,17 @@ import type {
8
8
  RuntimePlatformPort,
9
9
  RuntimeSubagentPort,
10
10
  WorkspacePort,
11
- } from "../../../kernel/bindings";
12
- import { decodeSubagentCompletion } from "../../../kernel/subagent-runtime";
13
- import type { RuntimeDegradation } from "../../../kernel/degradation";
14
- import type { RuntimeMemoryProfile } from "../../../kernel/profile";
15
- import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
16
- import type { RuntimeAgentConfigContext } from "../../../runtime-agent-context";
11
+ } from "../../../config/ports";
12
+ import { decodeSubagentCompletion } from "../../../tools/subagent/runtime";
13
+ import type { RuntimeDegradation } from "../../../config/degradation";
14
+ import type { RuntimeMemoryProfile } from "../../../config/profile";
15
+ import { AGENT_TYPES } from "../../../tools/subagent/types/registry";
16
+ import type { RuntimeAgentConfigContext } from "../../../config/agent-context";
17
17
  import {
18
18
  createBrowserExecutionFactory,
19
19
  createWorkspaceCodeExecutionFactory,
20
20
  type RuntimeBrowserBinding,
21
- } from "../../../pi/tool/core-host";
21
+ } from "../../../tools/codemode/adapter";
22
22
  import {
23
23
  createCloudflareSandboxAdapter,
24
24
  type SandboxAdmission,
@@ -4,18 +4,16 @@ import type {
4
4
  RuntimeSchedulePort,
5
5
  RuntimeSubagentPort,
6
6
  WorkspacePort,
7
- } from "../../../kernel/bindings";
8
- import type { RuntimeDegradation } from "../../../kernel/degradation";
9
- import type { RuntimeMemoryProfile } from "../../../kernel/profile";
10
- import type { WorkspaceRevisionRestorePort } from "../../../workspace-versioning";
11
- import { schedulePiToolCandidates } from "../../../pi/tool/schedule";
12
- import { workspaceRevisionPiToolCandidate } from "../../../pi/tool/workspace-revision";
13
- import {
14
- sandboxPiToolCandidates,
15
- } from "../../../pi/tool/workspace-sandbox";
16
- import type { RuntimeCodeExecutionFactory } from "../../../kernel/bindings";
17
- import type { ToolAssemblyResult } from "../../../runtime-definition";
18
- import type { PiToolCandidate } from "../../../pi/tool/compiler";
7
+ } from "../../../config/ports";
8
+ import type { RuntimeDegradation } from "../../../config/degradation";
9
+ import type { RuntimeMemoryProfile } from "../../../config/profile";
10
+ import type { WorkspaceRevisionRestorePort } from "../../../tools/workspace/versioning";
11
+ import { schedulePiToolCandidates } from "../../../tools/schedule";
12
+ import { workspaceRevisionPiToolCandidate } from "../../../tools/workspace/revision";
13
+ import { sandboxPiToolCandidates } from "../../../tools/sandbox";
14
+ import type { RuntimeCodeExecutionFactory } from "../../../config/ports";
15
+ import type { ToolAssemblyResult } from "../../../config/definition";
16
+ import type { PiToolCandidate } from "../../../tools/runtime/contracts";
19
17
 
20
18
  export function assembleUniversalAgentTools(options: {
21
19
  hostTools?: readonly PiToolCandidate[];
@@ -27,7 +27,7 @@ import type {
27
27
  import type {
28
28
  WorkspaceFileInfo,
29
29
  WorkspacePort,
30
- } from "../../../kernel/bindings";
30
+ } from "../../../config/ports";
31
31
 
32
32
  const PAGE_SIZE = 256;
33
33
 
@@ -5,7 +5,7 @@ import type {
5
5
  WorkspaceStreamWriteOptions,
6
6
  WorkspaceQuota,
7
7
  WorkspaceUsage,
8
- } from "../../../kernel/bindings";
8
+ } from "../../../config/ports";
9
9
  import type {
10
10
  WorkspaceConditionalWriteResult,
11
11
  WorkspaceFileVersion,
@@ -1,11 +1,11 @@
1
1
  import * as git from "isomorphic-git";
2
- import type { WorkspacePort } from "../../../kernel/bindings";
2
+ import type { WorkspacePort } from "../../../config/ports";
3
3
  import {
4
4
  WorkspaceRevisionNotFoundError,
5
5
  type WorkspaceRevision,
6
6
  type WorkspaceRevisionManifest,
7
7
  type WorkspaceVersionPort,
8
- } from "../../../workspace-versioning";
8
+ } from "../../../tools/workspace/versioning";
9
9
  import { createWorkspaceGitFs } from "./git-fs";
10
10
 
11
11
  const PAGE_SIZE = 256;
@@ -0,0 +1,140 @@
1
+ import { connectConfiguredMcpServers } from "../lib/mcp";
2
+ import { sha256Hex } from "../lib/artifacts";
3
+ import type { RuntimeCandidate, RuntimeSnapshot } from "./assembler";
4
+ import type { HarnessRuntimeConfig } from "./harness-runtime";
5
+ import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./pinning";
6
+ import type { RuntimeGatewaySession, RuntimeSubmissionAdmissionHook } from "./ports";
7
+ import { withRuntimeLoadTimeout } from "./runtime-load";
8
+ import type { RuntimeLoadPhase } from "../conversation/state";
9
+
10
+ export interface ActiveRuntime {
11
+ readonly snapshot: RuntimeSnapshot;
12
+ readonly revision: string;
13
+ readonly runtimeKey: string;
14
+ readonly pi: PreparedPiRuntime;
15
+ readonly gatewaySession?: RuntimeGatewaySession;
16
+ readonly admission?: RuntimeSubmissionAdmissionHook;
17
+ }
18
+
19
+ async function closeRuntimeGatewaySession(
20
+ session: RuntimeGatewaySession | undefined,
21
+ step: string,
22
+ ): Promise<void> {
23
+ if (!session) return;
24
+ await withRuntimeLoadTimeout(step, () => session.close()).catch(
25
+ () => undefined,
26
+ );
27
+ }
28
+
29
+ interface RuntimeActivationOptions {
30
+ readonly previous?: ActiveRuntime;
31
+ readonly pi: HarnessRuntimeConfig;
32
+ readonly mcpHost: PreparePiRuntimeOptions["mcpHost"] & Parameters<typeof connectConfiguredMcpServers>[0];
33
+ readonly sessionId: string;
34
+ readonly createExtensionHostBinding: PreparePiRuntimeOptions["createExtensionHostBinding"];
35
+ advance(phase: RuntimeLoadPhase): void;
36
+ isExecuting(): boolean;
37
+ commit(runtime: ActiveRuntime): void;
38
+ }
39
+
40
+ /** Prepare optional connections and atomically install a validated Runtime candidate. */
41
+ export async function activateRuntimeCandidate(
42
+ candidate: RuntimeCandidate,
43
+ options: RuntimeActivationOptions,
44
+ ): Promise<void> {
45
+ const previous = options.previous;
46
+ const candidateSnapshot = candidate.snapshot;
47
+ options.advance("assembly");
48
+ options.advance("mcp");
49
+ await connectConfiguredMcpServers(
50
+ options.mcpHost,
51
+ candidateSnapshot.profile.mcpServers.filter(server => !server.hostManaged),
52
+ );
53
+ const gatewayDegradations = [];
54
+ let gatewaySession: RuntimeGatewaySession | undefined;
55
+ if (candidateSnapshot.bindings.gateway) {
56
+ try {
57
+ gatewaySession = await withRuntimeLoadTimeout(
58
+ "gateway.open",
59
+ () => candidateSnapshot.bindings.gateway!.open(
60
+ `${options.sessionId}:${crypto.randomUUID()}`,
61
+ ),
62
+ {
63
+ onLateResult: (session) =>
64
+ closeRuntimeGatewaySession(session, "gateway.close.late"),
65
+ },
66
+ );
67
+ } catch {
68
+ gatewayDegradations.push({
69
+ capability: "connector" as const,
70
+ reason: "unavailable" as const,
71
+ detail: "Connector Gateway MCP",
72
+ });
73
+ }
74
+ }
75
+
76
+ options.advance("pi");
77
+ let prepared: PreparedPiRuntime;
78
+ try {
79
+ prepared = await options.pi.prepare({
80
+ snapshot: candidateSnapshot,
81
+ mcpHost: options.mcpHost,
82
+ gatewaySession,
83
+ additionalDegradations: gatewayDegradations,
84
+ createExtensionHostBinding: (permissions, ownContextLabels) =>
85
+ options.createExtensionHostBinding(
86
+ permissions,
87
+ ownContextLabels,
88
+ ),
89
+ });
90
+ } catch (error) {
91
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.failed");
92
+ throw error;
93
+ }
94
+ const revision = await sha256Hex(prepared.revisionDescriptor);
95
+ const contested = Boolean(previous) &&
96
+ revision !== previous?.revision &&
97
+ options.isExecuting();
98
+ if (contested) {
99
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
100
+ throw new Error(
101
+ "Cannot reload Runtime while a revision-pinned SpringBrand Turn is active",
102
+ );
103
+ }
104
+ const next = Object.freeze({
105
+ snapshot: candidateSnapshot,
106
+ revision,
107
+ runtimeKey: candidate.runtimeKey ?? revision,
108
+ pi: prepared,
109
+ ...(gatewaySession ? { gatewaySession } : {}),
110
+ ...(candidate.admission
111
+ ? { admission: candidate.admission }
112
+ : {}),
113
+ }) satisfies ActiveRuntime;
114
+ let activated = false;
115
+ const previousGatewaySession = previous?.gatewaySession;
116
+ try {
117
+ for (const [index, guard] of candidate.commitGuards.entries()) {
118
+ await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
119
+ }
120
+ options.pi.activate(candidateSnapshot);
121
+ activated = true;
122
+ options.commit(next);
123
+ } catch (error) {
124
+ if (activated && previous) options.pi.activate(previous.snapshot);
125
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.aborted");
126
+ throw error;
127
+ }
128
+ await closeRuntimeGatewaySession(
129
+ previousGatewaySession,
130
+ "gateway.close.previous",
131
+ );
132
+
133
+ const degradations = prepared.degradations;
134
+ if (degradations.length > 0) {
135
+ console.warn(
136
+ "[runtime-load:degraded]",
137
+ JSON.stringify({ degradations }),
138
+ );
139
+ }
140
+ }