@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.20

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 (125) hide show
  1. package/package.json +11 -9
  2. package/src/adapter/cloudflare/agent.ts +177 -122
  3. package/src/adapter/cloudflare/define-agent.ts +47 -26
  4. package/src/adapter/cloudflare/index.ts +18 -0
  5. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  6. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  7. package/src/adapter/cloudflare/schema.ts +15 -0
  8. package/src/adapter/cloudflare/subagent/definition.ts +25 -13
  9. package/src/adapter/cloudflare/subagent/runner.ts +43 -50
  10. package/src/adapter/cloudflare/subagent/tools.ts +4 -4
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
  13. package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
  14. package/src/config/agent-context.ts +1 -1
  15. package/src/config/assembler.ts +48 -31
  16. package/src/config/assembly-snapshot.ts +1 -1
  17. package/src/config/definition.ts +1 -1
  18. package/src/config/harness-runtime.ts +51 -13
  19. package/src/config/model-guidance.ts +20 -0
  20. package/src/config/pinning.ts +39 -5
  21. package/src/config/ports.ts +57 -7
  22. package/src/config/public-contracts.ts +2 -2
  23. package/src/context/assembly.ts +1 -1
  24. package/src/context/extension/assembly.ts +2 -2
  25. package/src/context/memory/budget/gate.ts +19 -3
  26. package/src/conversation/harness-resume.ts +89 -238
  27. package/src/conversation/harness-stream.ts +87 -71
  28. package/src/conversation/history.ts +154 -152
  29. package/src/conversation/message/contract.ts +12 -0
  30. package/src/conversation/message/conversion.ts +3 -3
  31. package/src/conversation/message/projection.ts +61 -21
  32. package/src/conversation/session-entry.ts +29 -0
  33. package/src/conversation/state.ts +23 -2
  34. package/src/harness/governance.ts +206 -0
  35. package/src/harness/index.ts +3 -0
  36. package/src/harness/session-harness.ts +255 -446
  37. package/src/harness/submission-operation.ts +15 -40
  38. package/src/index.ts +18 -8
  39. package/src/lib/artifacts.ts +18 -0
  40. package/src/lib/execution-level.ts +13 -0
  41. package/src/lib/prompt.ts +9 -10
  42. package/src/lib/token-estimate.ts +8 -0
  43. package/src/model-protocol/openrouter.ts +41 -0
  44. package/src/models/index.ts +200 -35
  45. package/src/models/openrouter/messages.ts +13 -8
  46. package/src/models/openrouter/openai.ts +200 -0
  47. package/src/models/openrouter/protocol-response.ts +85 -0
  48. package/src/models/provider-streams.ts +45 -0
  49. package/src/session/cloudflare/migration.ts +382 -864
  50. package/src/session/cloudflare/storage.ts +13 -536
  51. package/src/session/index.ts +22 -35
  52. package/src/skills/springbrand-worker-website/index.ts +1 -1
  53. package/src/submission/admission.ts +12 -1
  54. package/src/submission/contracts.ts +42 -2
  55. package/src/submission/delivery.ts +1 -1
  56. package/src/submission/finalization.ts +36 -15
  57. package/src/submission/repository.ts +64 -25
  58. package/src/submission/steer-delivery.ts +32 -76
  59. package/src/submission/unattended.ts +40 -0
  60. package/src/telemetry/coordinator.ts +1 -1
  61. package/src/telemetry/subagent-lifecycle.ts +1 -1
  62. package/src/telemetry/turn-events.ts +35 -25
  63. package/src/tools/README.md +44 -0
  64. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  65. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  66. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  67. package/src/tools/assistant/ask-user.ts +317 -0
  68. package/src/tools/assistant/candidate.ts +15 -0
  69. package/src/tools/assistant/followups.ts +34 -0
  70. package/src/tools/assistant/index.ts +19 -0
  71. package/src/tools/assistant/memory.ts +73 -0
  72. package/src/tools/assistant/plan.ts +98 -0
  73. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  74. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  75. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  76. package/src/tools/codemode/workspace-connector.ts +198 -0
  77. package/src/tools/extension/index.ts +42 -0
  78. package/src/tools/index.ts +29 -26
  79. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  80. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  81. package/src/tools/runtime/activation.ts +40 -0
  82. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  83. package/src/tools/runtime/compiler.ts +246 -0
  84. package/src/tools/runtime/contracts.ts +140 -0
  85. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
  86. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  87. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  88. package/src/tools/runtime/result.ts +22 -0
  89. package/src/tools/runtime/unattended-error.ts +4 -0
  90. package/src/tools/sandbox/index.ts +228 -0
  91. package/src/tools/schedule/index.ts +301 -0
  92. package/src/tools/skill/contracts.ts +20 -0
  93. package/src/tools/skill/index.ts +99 -0
  94. package/src/tools/skill/resources.ts +322 -0
  95. package/src/tools/skill/scripts.ts +168 -0
  96. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  97. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  98. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  99. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  100. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  101. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
  102. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  103. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  104. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  105. package/src/tools/web/index.ts +2 -0
  106. package/src/tools/web/search/api.ts +1322 -0
  107. package/src/tools/web/search/index.ts +2 -0
  108. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  109. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  110. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  111. package/src/tools/base.ts +0 -534
  112. package/src/tools/compiler.ts +0 -327
  113. package/src/tools/schedule.ts +0 -261
  114. package/src/tools/skill.ts +0 -565
  115. package/src/tools/web-search/api.ts +0 -1318
  116. package/src/tools/web-search/index.ts +0 -2
  117. package/src/tools/workspace-state-connector.ts +0 -146
  118. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  119. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  120. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  121. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  122. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  123. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  124. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  125. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -7,12 +7,12 @@ import {
7
7
  type FiberRecoveryContext,
8
8
  type FiberRecoveryResult,
9
9
  } from "agents";
10
- import type { HarnessEvent } from "@earendil-works/pi-agent-core";
10
+ import type { AgentEvent as HarnessEvent } from "@earendil-works/pi-durable";
11
11
  import type { UIMessageChunk } from "ai";
12
12
  import {
13
13
  AGENT_TYPES,
14
- } from "../../../tools/subagent-types/registry";
15
- import type { AgentType } from "../../../tools/subagent-types/contract";
14
+ } from "../../../tools/subagent/types/registry";
15
+ import type { AgentType } from "../../../tools/subagent/types/contract";
16
16
  import type {
17
17
  RuntimeProviderPort,
18
18
  WorkspacePort,
@@ -30,8 +30,8 @@ import {
30
30
  decodeSubagentCompletion,
31
31
  emptySubagentUsage,
32
32
  encodeSubagentCompletion,
33
- } from "../../../tools/subagent-runtime";
34
- import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent-authority";
33
+ } from "../../../tools/subagent/runtime";
34
+ import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent/authority";
35
35
  import { createCloudflareSubAgentTools } from "./tools";
36
36
 
37
37
  type RunStatus =
@@ -96,25 +96,27 @@ function stringifyJson(value: unknown): string {
96
96
  function progressFor(
97
97
  event: HarnessEvent,
98
98
  ): Omit<AgentToolProgressSnapshot, "at"> | null {
99
- if (event.type === "run_start" || event.type === "run_resume") {
99
+ if (event.type === "run_start") {
100
100
  return { phase: "running", message: "SubAgent started" };
101
101
  }
102
- if (event.type === "tool_start") {
102
+ if (event.type === "tool_execution_start") {
103
103
  return {
104
104
  phase: "tool",
105
105
  message: `Running ${event.toolName}`,
106
106
  };
107
107
  }
108
- if (event.type === "tool_update") {
108
+ if (event.type === "tool_execution_update") {
109
109
  return {
110
110
  phase: "tool",
111
111
  message: `${event.toolName} is still running`,
112
112
  };
113
113
  }
114
- if (event.type === "tool_end") {
114
+ if (event.type === "tool_execution_end") {
115
+ const result = event.entry?.model?.[0];
116
+ const isError = result?.role !== "toolResult" || result.isError;
115
117
  return {
116
- phase: event.isError ? "tool-error" : "tool",
117
- message: event.isError
118
+ phase: isError ? "tool-error" : "tool",
119
+ message: isError
118
120
  ? `${event.toolName} failed`
119
121
  : `${event.toolName} completed`,
120
122
  };
@@ -414,14 +416,24 @@ export abstract class CloudflareSubAgent<
414
416
  tools,
415
417
  signal,
416
418
  onEvent: (event) => {
417
- if (event.type === "message_end" && event.message.role === "assistant") {
418
- addSubagentUsage(usage, event.message.usage);
419
+ if (event.type === "message_end" && event.entry.model?.[0]?.role === "assistant") {
420
+ addSubagentUsage(usage, event.entry.model[0].usage);
419
421
  this.persistUsage(runId, usage);
420
422
  }
421
423
  publish(projection!.event(event));
422
424
  const progress = progressFor(event);
423
425
  if (progress) this.persistProgress(runId, progress);
424
426
  },
427
+ // Live frames can stop short of the last response; the canonical entries are the billed total.
428
+ onEntries: (entries) => {
429
+ const canonical = emptySubagentUsage();
430
+ for (const entry of entries) {
431
+ const message = entry.model?.[0];
432
+ if (message?.role === "assistant") addSubagentUsage(canonical, message.usage);
433
+ }
434
+ Object.assign(usage, canonical);
435
+ this.persistUsage(runId, usage);
436
+ },
425
437
  });
426
438
  publish(projection.finish({ status: "completed", endedAt: Date.now() }));
427
439
  this.completeRun(runId, output, usage);
@@ -1,18 +1,14 @@
1
- import {
2
- type AgentHarnessTool,
3
- type HarnessEvent,
4
- type Session,
5
- type StreamFn,
6
- } from "@earendil-works/pi-agent-core";
1
+ import type { StreamFn } from "@earendil-works/pi-agent-core";
2
+ import type { AgentEvent as HarnessEvent, AgentEventStream, EntryRecord, Storage as Session, ToolRegistration } from "@earendil-works/pi-durable";
7
3
  import {
8
4
  awaitWithContext,
9
5
  BACKGROUND_CONTEXT,
10
6
  withAbortSignal,
11
- } from "@earendil-works/pi-agent-core/harness/context";
7
+ } from "@earendil-works/chord/context";
12
8
  import type { Api, AssistantMessage, Model, Models } from "@earendil-works/pi-ai";
13
9
  import { z } from "zod";
14
10
  import { assembleSubagentPrompt } from "../../../lib/prompt";
15
- import type { AgentType } from "../../../tools/subagent-types/contract";
11
+ import type { AgentType } from "../../../tools/subagent/types/contract";
16
12
  import { structuredOutputStream } from "../../../models";
17
13
  import {
18
14
  openSessionHarness,
@@ -39,11 +35,24 @@ export interface CloudflareSubAgentRun {
39
35
  openSession(): Session | Promise<Session>;
40
36
  operationId?: string;
41
37
  /** Tools compiled by the package's single Harness adapter. */
42
- tools?: AgentHarnessTool<object | undefined>[];
38
+ tools?: ToolRegistration<any, any>[];
43
39
  signal?: AbortSignal;
44
40
  timeoutMs?: number;
45
- /** Native operation events for projections that must survive model retries. */
41
+ /** Native operation events for live projections; they may stop short of the final frame. */
46
42
  onEvent?: (event: HarnessEvent, signal: AbortSignal) => void | Promise<void>;
43
+ /** This operation's canonical entries after each settle or release, the source of its final usage. */
44
+ onEntries?: (entries: readonly EntryRecord[]) => void | Promise<void>;
45
+ }
46
+
47
+ /** The first failed Tool result among an operation's canonical entries. */
48
+ function toolFailureOf(entries: readonly EntryRecord[]): { name: string; error: string } | undefined {
49
+ for (const entry of entries) {
50
+ const result = entry.model?.[0];
51
+ if (result?.role !== "toolResult" || !result.isError) continue;
52
+ const diagnostics = (entry.data as { diagnostics?: Array<{ severity: string; message: string }> } | undefined)?.diagnostics;
53
+ return { name: result.toolName, error: diagnostics?.find((item) => item.severity === "error")?.message ?? toolErrorText(result) };
54
+ }
55
+ return undefined;
47
56
  }
48
57
 
49
58
  function textOf(message: AssistantMessage): string {
@@ -131,9 +140,15 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
131
140
  streamOptions: { maxRetries: 0 },
132
141
  toolExecution: "parallel",
133
142
  compaction: { enabled: false, reserveTokens: 1, keepRecentTokens: 1 },
134
- } satisfies Omit<SessionHarnessOptions<object | undefined>, "session">;
135
- let runtime: SessionHarness<object | undefined> | undefined;
143
+ } satisfies Omit<SessionHarnessOptions, "storage">;
144
+ let runtime: SessionHarness | undefined;
145
+ let events: AgentEventStream | undefined;
136
146
  let toolFailure: { name: string; error: string } | undefined;
147
+ const operationEntries = async (active: SessionHarness): Promise<EntryRecord[]> => {
148
+ const submission = await active.submissionFor(operationId, BACKGROUND_CONTEXT);
149
+ const entries = await active.entries(BACKGROUND_CONTEXT);
150
+ return submission?.entry === undefined ? [] : entries.filter((entry) => entry.id >= submission.entry!);
151
+ };
137
152
 
138
153
  let timedOut = false;
139
154
  const timeout = setTimeout(() => {
@@ -141,61 +156,43 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
141
156
  controller.abort(new Error("SubAgent run timed out"));
142
157
  }, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
143
158
  try {
144
- let settled: Awaited<ReturnType<SessionHarness<object | undefined>["run"]>>;
159
+ let settled: Awaited<ReturnType<SessionHarness["run"]>>;
145
160
  while (true) {
146
161
  signal.throwIfAborted();
147
162
  if (!runtime) {
148
163
  runtime = await openSessionHarness({
149
164
  ...options,
150
- session: await run.openSession(),
165
+ storage: await run.openSession(),
151
166
  }, context);
152
167
  if (runtime.openOperation && runtime.openOperation.operationId !== operationId) {
153
168
  throw new Error("SubAgent Session has an unrelated open operation");
154
169
  }
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
- }
170
+ events = await runtime.watch(context);
171
+ events.start(async (batch) => {
172
+ for (const event of batch) await run.onEvent?.(event, signal);
166
173
  });
167
174
  }
168
175
  const result = await runtime.run(
169
176
  !runtime.openOperation
170
177
  ? {
171
- kind: "prompt",
172
178
  operationId,
173
179
  prompt: taskPrompt(input.data, structuredStream ? undefined : outputSchema),
174
180
  }
175
181
  : undefined,
176
182
  context,
177
- { waitForRetry: true, pollDeferred: true },
178
183
  );
184
+ await events?.stop();
185
+ events = undefined;
179
186
  if (result.kind === "released") {
180
187
  await runtime.close(BACKGROUND_CONTEXT);
181
188
  runtime = undefined;
182
189
  continue;
183
190
  }
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
191
  settled = result;
192
+ // Pi's stop may drop the last frames; failures and usage come from the canonical record.
193
+ const owned = await operationEntries(runtime);
194
+ toolFailure = toolFailureOf(owned);
195
+ await run.onEntries?.(owned);
199
196
  break;
200
197
  }
201
198
  if (timedOut) throw new Error("SubAgent run timed out");
@@ -212,14 +209,9 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
212
209
  if (toolFailure) {
213
210
  throw new Error(`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`);
214
211
  }
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);
212
+ const assistant = [...await runtime.entries(context)].reverse().flatMap((entry) => entry.model ?? []).find((message) => message.role === "assistant");
213
+ if (assistant?.role !== "assistant") throw new Error("SubAgent produced no structured output");
214
+ const text = textOf(assistant);
223
215
  if (!text) throw new Error("SubAgent produced no structured output");
224
216
  let output: unknown;
225
217
  try {
@@ -239,9 +231,10 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
239
231
  } finally {
240
232
  clearTimeout(timeout);
241
233
  if (signal.aborted && runtime?.openOperation) {
242
- await runtime.requestAbort(operationId, BACKGROUND_CONTEXT).catch(() => undefined);
234
+ await runtime.abort(BACKGROUND_CONTEXT).catch(() => undefined);
243
235
  await runtime.run(undefined, BACKGROUND_CONTEXT).catch(() => undefined);
244
236
  }
237
+ await events?.stop();
245
238
  await runtime?.close(BACKGROUND_CONTEXT).catch(() => undefined);
246
239
  }
247
240
  }
@@ -1,3 +1,4 @@
1
+ import type { ToolRegistration } from "@earendil-works/pi-durable";
1
2
  import {
2
3
  DynamicWorkerExecutor,
3
4
  resolveProvider,
@@ -9,19 +10,18 @@ import {
9
10
  } from "@cloudflare/shell";
10
11
  import { stateToolsFromBackend } from "@cloudflare/shell/workers";
11
12
  import type {
12
- AgentHarnessTool,
13
13
  AgentTool,
14
14
  AgentToolResult,
15
15
  } from "@earendil-works/pi-agent-core";
16
16
  import { Type } from "@earendil-works/pi-ai";
17
17
  import type { WorkspacePort } from "../../../config/ports";
18
- import type { AgentType } from "../../../tools/subagent-types/contract";
18
+ import type { AgentType } from "../../../tools/subagent/types/contract";
19
19
  import {
20
20
  compileHarnessTools,
21
21
  type CompileHarnessToolsOptions,
22
22
  type PiToolCandidate,
23
23
  } from "../../../tools";
24
- import { workspacePiToolCandidates } from "../../../tools/workspace-sandbox";
24
+ import { workspacePiToolCandidates } from "../../../tools/workspace";
25
25
 
26
26
  const READONLY_WORKSPACE_TOOLS = new Set([
27
27
  "read",
@@ -221,7 +221,7 @@ export function createCloudflareSubAgentTools(options: {
221
221
  loader: WorkerLoader;
222
222
  outbound: Fetcher;
223
223
  settle: NonNullable<CompileHarnessToolsOptions["onSettlement"]>;
224
- }): AgentHarnessTool<object | undefined>[] {
224
+ }): ToolRegistration<any, any>[] {
225
225
  const workspace = workspacePiToolCandidates(options.workspace).filter(
226
226
  (candidate) =>
227
227
  options.profile === "capable" ||
@@ -9,16 +9,16 @@ import type {
9
9
  RuntimeSubagentPort,
10
10
  WorkspacePort,
11
11
  } from "../../../config/ports";
12
- import { decodeSubagentCompletion } from "../../../tools/subagent-runtime";
12
+ import { decodeSubagentCompletion } from "../../../tools/subagent/runtime";
13
13
  import type { RuntimeDegradation } from "../../../config/degradation";
14
14
  import type { RuntimeMemoryProfile } from "../../../config/profile";
15
- import { AGENT_TYPES } from "../../../tools/subagent-types/registry";
15
+ import { AGENT_TYPES } from "../../../tools/subagent/types/registry";
16
16
  import type { RuntimeAgentConfigContext } from "../../../config/agent-context";
17
17
  import {
18
18
  createBrowserExecutionFactory,
19
19
  createWorkspaceCodeExecutionFactory,
20
20
  type RuntimeBrowserBinding,
21
- } from "../../../tools/core-host";
21
+ } from "../../../tools/codemode/adapter";
22
22
  import {
23
23
  createCloudflareSandboxAdapter,
24
24
  type SandboxAdmission,
@@ -7,15 +7,13 @@ import type {
7
7
  } from "../../../config/ports";
8
8
  import type { RuntimeDegradation } from "../../../config/degradation";
9
9
  import type { RuntimeMemoryProfile } from "../../../config/profile";
10
- import type { WorkspaceRevisionRestorePort } from "../../../tools/workspace-versioning";
10
+ import type { WorkspaceRevisionRestorePort } from "../../../tools/workspace/versioning";
11
11
  import { schedulePiToolCandidates } from "../../../tools/schedule";
12
- import { workspaceRevisionPiToolCandidate } from "../../../tools/workspace-revision";
13
- import {
14
- sandboxPiToolCandidates,
15
- } from "../../../tools/workspace-sandbox";
12
+ import { workspaceRevisionPiToolCandidate } from "../../../tools/workspace/revision";
13
+ import { sandboxPiToolCandidates } from "../../../tools/sandbox";
16
14
  import type { RuntimeCodeExecutionFactory } from "../../../config/ports";
17
15
  import type { ToolAssemblyResult } from "../../../config/definition";
18
- import type { PiToolCandidate } from "../../../tools/compiler";
16
+ import type { PiToolCandidate } from "../../../tools/runtime/contracts";
19
17
 
20
18
  export function assembleUniversalAgentTools(options: {
21
19
  hostTools?: readonly PiToolCandidate[];
@@ -5,7 +5,7 @@ import {
5
5
  type WorkspaceRevision,
6
6
  type WorkspaceRevisionManifest,
7
7
  type WorkspaceVersionPort,
8
- } from "../../../tools/workspace-versioning";
8
+ } from "../../../tools/workspace/versioning";
9
9
  import { createWorkspaceGitFs } from "./git-fs";
10
10
 
11
11
  const PAGE_SIZE = 256;
@@ -8,7 +8,7 @@ import type { RuntimeConfigUpdateResult } from "./runtime-config";
8
8
  import type {
9
9
  TemporaryAgentRequest,
10
10
  TemporaryAgentRunContext,
11
- } from "../tools/temporary-agent/core";
11
+ } from "../tools/subagent/temporary/core";
12
12
 
13
13
  /**
14
14
  * 描述 Agent 寻址路径中的一级父子节点。
@@ -1,3 +1,4 @@
1
+ import { schedulePiToolCandidates } from "../tools/schedule";
1
2
  import type { SkillSource } from "agents/skills";
2
3
  import type {
3
4
  RuntimeGatewayPort,
@@ -36,10 +37,7 @@ import {
36
37
  type PiRuntimeAssembly,
37
38
  } from "./assembly-snapshot";
38
39
  import type { AgentTool } from "@earendil-works/pi-agent-core";
39
- import type {
40
- PiToolCandidate,
41
- ToolExposureMode,
42
- } from "../tools/compiler";
40
+ import type { PiToolCandidate, ToolExposureMode } from "../tools/runtime/contracts";
43
41
  import type {
44
42
  AssembleRuntimeSnapshotInput,
45
43
  RuntimeSettings,
@@ -50,17 +48,11 @@ import type {
50
48
  RuntimeToolSurfacePolicy,
51
49
  } from "./definition";
52
50
  import type { RuntimeCodeExecutionFactory } from "./ports";
53
- import {
54
- basePiToolCandidates,
55
- memoryPiToolCandidate,
56
- } from "../tools/base";
57
- import {
58
- BROWSER_EXECUTE_TOOL_NAME,
59
- browserExecutionPiToolCandidate,
60
- codeExecutionPiToolCandidate,
61
- } from "../tools/core";
51
+ import { basePiToolCandidates } from "../tools/assistant";
52
+ import { memoryPiToolCandidate } from "../tools/assistant/memory";
53
+ import { BROWSER_EXECUTE_TOOL_NAME, browserExecutionPiToolCandidate, codeExecutionPiToolCandidate } from "../tools/codemode";
62
54
  import { skillPiToolCandidates } from "../tools/skill";
63
- import { createWebSearch, WEB_SEARCH_MODEL } from "../tools/web-search";
55
+ import { createWebSearch, WEB_SEARCH_MODEL } from "../tools/web/search";
64
56
  import { subagentPiToolCandidates } from "../tools/subagent";
65
57
  import { resolvePiModel } from "../models";
66
58
  import {
@@ -264,6 +256,16 @@ async function createToolSurface(
264
256
  if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
265
257
  throw new Error(`SpringBrand Tool ${name} execution level is invalid`);
266
258
  }
259
+ if (candidate.unattendedExecution) {
260
+ requiredName(
261
+ candidate.unattendedExecution.capability,
262
+ `SpringBrand Tool ${name} unattended capability`,
263
+ );
264
+ requiredName(
265
+ candidate.unattendedExecution.operation,
266
+ `SpringBrand Tool ${name} unattended operation`,
267
+ );
268
+ }
267
269
  if (tools.has(name)) {
268
270
  throw new Error(`Duplicate Runtime SpringBrand Tool: ${name}`);
269
271
  }
@@ -294,17 +296,12 @@ async function createToolSurface(
294
296
  typeof candidate.tool.execute === "function",
295
297
  );
296
298
  const codeExecutionCandidates = Object.freeze([...mergeable]);
297
- const codeExecutionToolNames = new Set(
298
- codeExecutionCandidates.map(({ tool }) => tool.name),
299
- );
300
299
  const codeExecution = input.codeExecution.create(
301
300
  codeExecutionCandidates,
302
301
  );
303
- const topLevelOnly = directVisible.filter(
304
- ({ tool }) => !codeExecutionToolNames.has(tool.name),
305
- );
302
+ const directToolNames = new Set(directVisible.map(({ tool }) => tool.name));
306
303
  const routeList = (deferred: boolean) => {
307
- const matches = topLevelOnly.filter((candidate) =>
304
+ const matches = directVisible.filter((candidate) =>
308
305
  (defersTool?.(candidate.tool.name) === true) === deferred
309
306
  );
310
307
  return matches.length > 0
@@ -316,8 +313,9 @@ async function createToolSurface(
316
313
  };
317
314
  const routeGuidance = [
318
315
  "## Tool routing",
319
- "Top-level deferred Tools are not callable through `tools.*`. Use Provider Tool Search with their exact name, then call them directly.",
316
+ "For top-level deferred Tools, use Provider Tool Search with their exact name, then call them directly.",
320
317
  "Top-level immediate Tools are already available outside execute; call them directly.",
318
+ "Only Code Mode Tools listed below are callable inside execute through tools.<name>. Tools marked execute-only are not registered as top-level Tools.",
321
319
  "",
322
320
  "## Top-level deferred Tools",
323
321
  routeList(true),
@@ -325,21 +323,20 @@ async function createToolSurface(
325
323
  "## Top-level immediate Tools",
326
324
  routeList(false),
327
325
  "",
326
+ "## Code Mode Tools",
327
+ codeExecutionCandidates.length > 0
328
+ ? codeExecutionCandidates.map(({ tool }) =>
329
+ `- \`tools.${tool.name}\` — ${directToolNames.has(tool.name) ? "also available as a top-level Tool" : "execute-only"}`
330
+ ).join("\n")
331
+ : "- None.",
332
+ "",
328
333
  ].join("\n");
329
- const materializeGuidance = codeExecutionCandidates.some(
330
- ({ tool }) => tool.name === "materialize_skill_resource",
331
- )
332
- ? "Skill resource copying is available only inside execute through " +
333
- "`tools.materialize_skill_resource({ name, path, destination })`; " +
334
- "no Direct Tool is registered for it. " +
335
- "Copy multiple resources sequentially in one execute so durable replay preserves call order.\n\n"
336
- : "";
337
334
  return Object.freeze({
338
335
  candidates: Object.freeze([
339
336
  classify(codeExecutionPiToolCandidate({
340
337
  ...codeExecution,
341
338
  description:
342
- routeGuidance + "\n" + materializeGuidance + codeExecution.description,
339
+ routeGuidance + "\n" + codeExecution.description,
343
340
  })),
344
341
  ...directVisible.map(classify),
345
342
  ]),
@@ -586,6 +583,9 @@ class RuntimeBuilder {
586
583
  ? { headers: Object.freeze({ ...endpoint.headers }) }
587
584
  : {}),
588
585
  models: Object.freeze([...endpoint.models]),
586
+ ...(endpoint.openRouterProviderPins
587
+ ? { openRouterProviderPins: Object.freeze({ ...endpoint.openRouterProviderPins }) }
588
+ : {}),
589
589
  ...(endpoint.modelInputModalities
590
590
  ? {
591
591
  modelInputModalities: Object.freeze(Object.fromEntries(
@@ -595,6 +595,22 @@ class RuntimeBuilder {
595
595
  )),
596
596
  }
597
597
  : {}),
598
+ ...(endpoint.modelMetadata
599
+ ? {
600
+ modelMetadata: Object.freeze(Object.fromEntries(
601
+ Object.entries(endpoint.modelMetadata).map(([modelId, metadata]) => [
602
+ modelId,
603
+ Object.freeze({
604
+ ...metadata,
605
+ cost: Object.freeze({ ...metadata.cost }),
606
+ ...(metadata.thinkingLevelMap
607
+ ? { thinkingLevelMap: Object.freeze({ ...metadata.thinkingLevelMap }) }
608
+ : {}),
609
+ }),
610
+ ]),
611
+ )),
612
+ }
613
+ : {}),
598
614
  }),
599
615
  )),
600
616
  });
@@ -632,6 +648,7 @@ class RuntimeBuilder {
632
648
  : {}),
633
649
  hostTools: [
634
650
  ...this.hostTools,
651
+ ...(this.schedule ? schedulePiToolCandidates(this.schedule) : []),
635
652
  ...subagentPiToolCandidates(
636
653
  this.subagents,
637
654
  [...this.enabledSubagents],
@@ -18,7 +18,7 @@ import type {
18
18
  import type { RuntimeProviderPort } from "./ports";
19
19
  import { assembleSystemPrompt } from "../lib/prompt";
20
20
  import { resolvePiModel } from "../models";
21
- import type { PiToolCandidate } from "../tools/compiler";
21
+ import type { PiToolCandidate } from "../tools/runtime/contracts";
22
22
 
23
23
  /**
24
24
  * 收集完动态 Extension/MCP 候选后,一次性生成最终 Tool Surface。
@@ -17,7 +17,7 @@ import type {
17
17
  } from "./profile";
18
18
  import type { RuntimeExtensionConfig } from "../context/extension/contracts";
19
19
  import type { RuntimeDegradation } from "./degradation";
20
- import type { PiToolCandidate } from "../tools/compiler";
20
+ import type { PiToolCandidate } from "../tools/runtime/contracts";
21
21
  import type {
22
22
  RuntimeAgentContext,
23
23
  RuntimeAgentPlanningContext,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createModels,
3
+ lazyStream,
3
4
  type MutableModels,
4
5
  } from "@earendil-works/pi-ai";
5
6
  import {
@@ -23,6 +24,7 @@ import {
23
24
  type PreparePiRuntimeOptions,
24
25
  type PreparedPiRuntime,
25
26
  } from "./pinning";
27
+ import { refreshModelGuidance } from "./model-guidance";
26
28
 
27
29
  /**
28
30
  * Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
@@ -89,8 +91,10 @@ export class HarnessRuntimeConfig {
89
91
  prepared: PreparedPiRuntime,
90
92
  pinnedDescriptor: string,
91
93
  request: {
92
- readonly modelSessionId?: string;
94
+ readonly modelSessionId?: string | (() => string);
93
95
  readonly onGeneration?: PiGenerationLifecycleObserver;
96
+ readonly assertRequestAllowed?: () => void;
97
+ readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
94
98
  } = {},
95
99
  ) {
96
100
  const { state, descriptor } = readPinnedPiRuntime(
@@ -100,6 +104,19 @@ export class HarnessRuntimeConfig {
100
104
  );
101
105
  const pinnedModel = state.snapshot.pi.model;
102
106
  let models!: MutableModels;
107
+ const requestStream = (observer = request.onGeneration) => {
108
+ const stream = withProviderBoundary(
109
+ this.models.streamSimple.bind(this.models), request.modelSessionId, observer,
110
+ );
111
+ return ((model, context, options) => lazyStream(model, async () => {
112
+ options?.signal?.throwIfAborted();
113
+ request.assertRequestAllowed?.();
114
+ const refreshed = await refreshModelGuidance(state.candidates, context);
115
+ options?.signal?.throwIfAborted();
116
+ request.assertRequestAllowed?.();
117
+ return stream(model, refreshed, options);
118
+ })) satisfies typeof stream;
119
+ };
103
120
  models = new Proxy(this.models, {
104
121
  get: (target, property) => {
105
122
  if (property === "getModel") {
@@ -108,15 +125,19 @@ export class HarnessRuntimeConfig {
108
125
  ? pinnedModel
109
126
  : target.getModel(provider, modelId);
110
127
  }
111
- if (property === "streamSimple") {
112
- return withProviderBoundary(
113
- target.streamSimple.bind(target),
114
- request.modelSessionId,
115
- request.onGeneration,
116
- );
117
- }
128
+ if (property === "streamSimple") return requestStream();
118
129
  if (property === "completeSimple") {
119
- return target.completeSimple.bind(models);
130
+ const compactionModels = new Proxy(models, {
131
+ get: (base, key) => {
132
+ if (key === "streamSimple") return requestStream((event) => {
133
+ request.onGeneration?.(event);
134
+ request.onCompactionGeneration?.(event);
135
+ });
136
+ const value = Reflect.get(base, key, base);
137
+ return typeof value === "function" ? value.bind(base) : value;
138
+ },
139
+ });
140
+ return target.completeSimple.bind(compactionModels);
120
141
  }
121
142
  const value = Reflect.get(target, property, target);
122
143
  return typeof value === "function" ? value.bind(target) : value;
@@ -132,10 +153,9 @@ export class HarnessRuntimeConfig {
132
153
  platform: state.snapshot.bindings.platform,
133
154
  compaction: {
134
155
  enabled: true,
135
- // Pi compacts above contextWindow - reserveTokens. Keep the same
136
- // 15% headroom within the available input budget as the pre-Harness runtime.
137
- reserveTokens: Math.ceil(state.snapshot.pi.model.contextWindow -
138
- (state.snapshot.pi.model.contextWindow - state.snapshot.pi.model.maxTokens) * 0.85),
156
+ // Pi compacts above contextWindow - reserveTokens.
157
+ reserveTokens: state.snapshot.pi.model.contextWindow -
158
+ compactionThreshold(state.snapshot.pi.model),
139
159
  keepRecentTokens: Math.min(
140
160
  20_000,
141
161
  Math.floor(state.snapshot.pi.model.contextWindow * 0.15),
@@ -180,3 +200,21 @@ export type {
180
200
  PreparePiRuntimeOptions,
181
201
  PreparedPiRuntime,
182
202
  } from "./pinning";
203
+
204
+ /**
205
+ * Input tokens above which a Turn compacts its history.
206
+ *
207
+ * OpenRouter rejects some requests well below a model's advertised window
208
+ * (seen from about 393k tokens on DeepSeek V4.1 Flash and 520k-630k on GPT-6
209
+ * Luna), before any provider is chosen. No model waits past this cap, so long
210
+ * Chats compact instead of failing.
211
+ */
212
+ export const MAX_COMPACTION_THRESHOLD_TOKENS = 300_000;
213
+
214
+ /** 15% headroom within the input budget, capped at {@link MAX_COMPACTION_THRESHOLD_TOKENS}. */
215
+ export function compactionThreshold(model: { contextWindow: number; maxTokens: number }): number {
216
+ return Math.min(
217
+ Math.floor((model.contextWindow - model.maxTokens) * 0.85),
218
+ MAX_COMPACTION_THRESHOLD_TOKENS,
219
+ );
220
+ }
@@ -0,0 +1,20 @@
1
+ import { getCurrentTools, normalizeContext, type Context, type Tool, type TranscriptContext } from "@earendil-works/pi-ai";
2
+ import type { PiToolCandidate } from "../tools/runtime/contracts";
3
+
4
+ /** Request-only presentation; Pi remains the source of transcript and active Tool selection. */
5
+ export async function refreshModelGuidance(candidates: readonly PiToolCandidate[], context: Context): Promise<TranscriptContext> {
6
+ const transcript = normalizeContext(context);
7
+ const active = new Set(getCurrentTools(transcript.messages).map((tool) => tool.name));
8
+ const byName = new Map(candidates.map((candidate) => [candidate.tool.name, candidate]));
9
+ const guidance = new Map(await Promise.all(candidates.filter((candidate) => active.has(candidate.tool.name) && candidate.modelGuidance)
10
+ .map(async (candidate) => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
11
+ const messages = transcript.messages.map((message) => message.role !== "system" || !message.toolsAdded ? message : {
12
+ ...message,
13
+ toolsAdded: message.toolsAdded.map((tool): Tool => {
14
+ const candidate = byName.get(tool.name), prompt = guidance.get(tool.name);
15
+ return { ...tool, ...(candidate?.deferLoading ? { deferLoading: true as const } : {}), ...(prompt ? { description: prompt.description } : {}) };
16
+ }),
17
+ });
18
+ const sections = [...new Set([...guidance.values()].flatMap((value) => value.systemPrompt ? [value.systemPrompt] : []))];
19
+ return normalizeContext({ messages: sections.length ? [...messages, { role: "system", content: "", sections: { "springbrand.live-services": sections.join("\n\n") }, timestamp: 1 }] : messages });
20
+ }