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

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 (131) hide show
  1. package/package.json +11 -9
  2. package/src/adapter/cloudflare/agent.ts +233 -179
  3. package/src/adapter/cloudflare/define-agent.ts +252 -104
  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 +21 -2
  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/activation.ts +13 -15
  15. package/src/config/agent-context.ts +1 -1
  16. package/src/config/assembler.ts +48 -31
  17. package/src/config/assembly-snapshot.ts +1 -1
  18. package/src/config/definition.ts +1 -1
  19. package/src/config/harness-runtime.ts +77 -49
  20. package/src/config/index.ts +1 -1
  21. package/src/config/model-guidance.ts +20 -0
  22. package/src/config/ports.ts +57 -7
  23. package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
  24. package/src/config/public-contracts.ts +2 -2
  25. package/src/config/runtime-config.ts +2 -1
  26. package/src/context/assembly.ts +1 -1
  27. package/src/context/extension/assembly.ts +2 -2
  28. package/src/context/memory/budget/gate.ts +19 -3
  29. package/src/conversation/harness-resume.ts +89 -238
  30. package/src/conversation/harness-stream.ts +87 -71
  31. package/src/conversation/history.ts +154 -152
  32. package/src/conversation/message/contract.ts +28 -4
  33. package/src/conversation/message/conversion.ts +46 -5
  34. package/src/conversation/message/projection.ts +61 -21
  35. package/src/conversation/session-entry.ts +29 -0
  36. package/src/conversation/state.ts +23 -2
  37. package/src/harness/governance.ts +206 -0
  38. package/src/harness/index.ts +3 -0
  39. package/src/harness/session-harness.ts +269 -445
  40. package/src/harness/submission-operation.ts +15 -40
  41. package/src/index.ts +18 -8
  42. package/src/lib/artifacts.ts +18 -0
  43. package/src/lib/execution-level.ts +13 -0
  44. package/src/lib/prompt.ts +9 -10
  45. package/src/lib/token-estimate.ts +8 -0
  46. package/src/model-protocol/openrouter.ts +41 -0
  47. package/src/models/index.ts +200 -35
  48. package/src/models/openrouter/messages.ts +13 -8
  49. package/src/models/openrouter/openai.ts +200 -0
  50. package/src/models/openrouter/protocol-response.ts +85 -0
  51. package/src/models/provider-streams.ts +45 -0
  52. package/src/session/cloudflare/migration.ts +382 -864
  53. package/src/session/cloudflare/storage.ts +13 -536
  54. package/src/session/index.ts +22 -35
  55. package/src/skills/springbrand-worker-website/index.ts +1 -1
  56. package/src/submission/admission.ts +20 -30
  57. package/src/submission/contracts.ts +42 -2
  58. package/src/submission/delivery.ts +1 -1
  59. package/src/submission/finalization.ts +36 -15
  60. package/src/submission/lifecycle.ts +28 -15
  61. package/src/submission/repository.ts +97 -49
  62. package/src/submission/steer-delivery.ts +32 -76
  63. package/src/submission/unattended.ts +40 -0
  64. package/src/telemetry/contract.ts +0 -1
  65. package/src/telemetry/coordinator.ts +1 -1
  66. package/src/telemetry/runtime-telemetry.ts +0 -4
  67. package/src/telemetry/subagent-lifecycle.ts +1 -1
  68. package/src/telemetry/turn-events.ts +35 -25
  69. package/src/tools/README.md +44 -0
  70. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  71. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  72. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  73. package/src/tools/assistant/ask-user.ts +319 -0
  74. package/src/tools/assistant/candidate.ts +15 -0
  75. package/src/tools/assistant/followups.ts +34 -0
  76. package/src/tools/assistant/index.ts +19 -0
  77. package/src/tools/assistant/memory.ts +73 -0
  78. package/src/tools/assistant/plan.ts +98 -0
  79. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  80. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  81. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  82. package/src/tools/codemode/workspace-connector.ts +198 -0
  83. package/src/tools/extension/index.ts +42 -0
  84. package/src/tools/index.ts +29 -26
  85. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  86. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  87. package/src/tools/runtime/activation.ts +40 -0
  88. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  89. package/src/tools/runtime/compiler.ts +246 -0
  90. package/src/tools/runtime/contracts.ts +140 -0
  91. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
  92. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  93. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  94. package/src/tools/runtime/result.ts +22 -0
  95. package/src/tools/runtime/unattended-error.ts +4 -0
  96. package/src/tools/sandbox/index.ts +228 -0
  97. package/src/tools/schedule/index.ts +301 -0
  98. package/src/tools/skill/contracts.ts +20 -0
  99. package/src/tools/skill/index.ts +99 -0
  100. package/src/tools/skill/resources.ts +322 -0
  101. package/src/tools/skill/scripts.ts +168 -0
  102. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  103. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  104. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  105. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  106. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  107. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
  108. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  109. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  110. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  111. package/src/tools/web/index.ts +2 -0
  112. package/src/tools/web/search/api.ts +1322 -0
  113. package/src/tools/web/search/index.ts +2 -0
  114. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  115. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  116. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  117. package/src/tools/base.ts +0 -534
  118. package/src/tools/compiler.ts +0 -327
  119. package/src/tools/schedule.ts +0 -261
  120. package/src/tools/skill.ts +0 -565
  121. package/src/tools/web-search/api.ts +0 -1318
  122. package/src/tools/web-search/index.ts +0 -2
  123. package/src/tools/workspace-state-connector.ts +0 -146
  124. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  125. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  126. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  127. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  128. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  129. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  130. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  131. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -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;
@@ -2,7 +2,7 @@ import { connectConfiguredMcpServers } from "../lib/mcp";
2
2
  import { sha256Hex } from "../lib/artifacts";
3
3
  import type { RuntimeCandidate, RuntimeSnapshot } from "./assembler";
4
4
  import type { HarnessRuntimeConfig } from "./harness-runtime";
5
- import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./pinning";
5
+ import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./prepared-runtime";
6
6
  import type { RuntimeGatewaySession, RuntimeSubmissionAdmissionHook } from "./ports";
7
7
  import { withRuntimeLoadTimeout } from "./runtime-load";
8
8
  import type { RuntimeLoadPhase } from "../conversation/state";
@@ -16,6 +16,16 @@ export interface ActiveRuntime {
16
16
  readonly admission?: RuntimeSubmissionAdmissionHook;
17
17
  }
18
18
 
19
+ /** Check the same candidate guards during admission and final activation. */
20
+ export async function verifyRuntimeCandidateGuards(
21
+ candidate: RuntimeCandidate,
22
+ phase: "admission" | "commit",
23
+ ): Promise<void> {
24
+ for (const [index, guard] of candidate.commitGuards.entries()) {
25
+ await withRuntimeLoadTimeout(`${phase}-guard:${index}`, guard);
26
+ }
27
+ }
28
+
19
29
  async function closeRuntimeGatewaySession(
20
30
  session: RuntimeGatewaySession | undefined,
21
31
  step: string,
@@ -33,7 +43,6 @@ interface RuntimeActivationOptions {
33
43
  readonly sessionId: string;
34
44
  readonly createExtensionHostBinding: PreparePiRuntimeOptions["createExtensionHostBinding"];
35
45
  advance(phase: RuntimeLoadPhase): void;
36
- isExecuting(): boolean;
37
46
  commit(runtime: ActiveRuntime): void;
38
47
  }
39
48
 
@@ -92,19 +101,10 @@ export async function activateRuntimeCandidate(
92
101
  throw error;
93
102
  }
94
103
  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
104
  const next = Object.freeze({
105
105
  snapshot: candidateSnapshot,
106
106
  revision,
107
- runtimeKey: candidate.runtimeKey ?? revision,
107
+ runtimeKey: candidate.runtimeKey ?? "default",
108
108
  pi: prepared,
109
109
  ...(gatewaySession ? { gatewaySession } : {}),
110
110
  ...(candidate.admission
@@ -114,9 +114,7 @@ export async function activateRuntimeCandidate(
114
114
  let activated = false;
115
115
  const previousGatewaySession = previous?.gatewaySession;
116
116
  try {
117
- for (const [index, guard] of candidate.commitGuards.entries()) {
118
- await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
119
- }
117
+ await verifyRuntimeCandidateGuards(candidate, "commit");
120
118
  options.pi.activate(candidateSnapshot);
121
119
  activated = true;
122
120
  options.commit(next);
@@ -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 {
@@ -14,18 +15,17 @@ export {
14
15
  readModelStreamStallDetails,
15
16
  } from "../models";
16
17
  import {
17
- pinPiRuntime,
18
+ assembleCurrentPiContext,
18
19
  preparePiRuntime,
19
- readPinnedPiRuntime,
20
20
  readPreparedPiRuntime,
21
- type PinPiRuntimeOptions,
22
- type PinnedPiRuntime,
21
+ type CurrentPiContextOptions,
23
22
  type PreparePiRuntimeOptions,
24
23
  type PreparedPiRuntime,
25
- } from "./pinning";
24
+ } from "./prepared-runtime";
25
+ import { refreshModelGuidance } from "./model-guidance";
26
26
 
27
27
  /**
28
- * Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
28
+ * Runtime 通过这个配置入口准备并读取当前 Harness 的模型、提示和工具输入。
29
29
  *
30
30
  * @remarks
31
31
  * Runtime、Pi、Turn、Tool Candidate 和 canonical transcript 等核心术语见包入口 `../../index.ts`。
@@ -35,9 +35,7 @@ import {
35
35
  * 它包含已加载 Extension、Tool candidate 和快照,但 Runtime 只能读取 revision descriptor 与 degradation。
36
36
  * - revision descriptor 是影响执行行为的已排序 JSON 描述。
37
37
  * Runtime 会对它取哈希,用来识别当前 Prepared Runtime 的基础版本。
38
- * - Pinned Runtime 是 Submission 准入时固定的描述符。
39
- * 它在 Prepared Runtime 基础上加入动态 system context、approval mode 和 base revision。
40
- * 调用方:`AgentRuntimeKernel` 在配置加载、请求准入与打开 SessionHarness 前调用这些粗粒度能力。
38
+ * 调用方:`AgentRuntimeKernel` 在配置加载与打开 SessionHarness 前调用这些粗粒度能力。
41
39
  *
42
40
  * 执行位置仍由 `SessionHarness` 拥有,消息读写和投影由 `ConversationHistory` 拥有。
43
41
  */
@@ -84,39 +82,66 @@ export class HarnessRuntimeConfig {
84
82
  );
85
83
  }
86
84
 
87
- /** Read the immutable inputs consumed by the Session AgentHarness. */
88
- harnessInputs(
85
+ interaction(prepared: PreparedPiRuntime, name: string) {
86
+ return readPreparedPiRuntime(prepared, this.owner).candidates.find(
87
+ (candidate) => candidate.tool.name === name,
88
+ )?.interaction;
89
+ }
90
+
91
+ /** Read all executable inputs from the current Prepared Runtime. */
92
+ async harnessInputs(
89
93
  prepared: PreparedPiRuntime,
90
- pinnedDescriptor: string,
91
94
  request: {
92
- readonly modelSessionId?: string;
95
+ readonly modelSessionId?: string | (() => string);
93
96
  readonly onGeneration?: PiGenerationLifecycleObserver;
97
+ readonly includeMemory?: boolean;
98
+ readonly readExtensionContext?: CurrentPiContextOptions["readExtensionContext"];
99
+ readonly assertRequestAllowed?: () => void;
100
+ readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
94
101
  } = {},
95
102
  ) {
96
- const { state, descriptor } = readPinnedPiRuntime(
103
+ const state = readPreparedPiRuntime(prepared, this.owner);
104
+ const context = await assembleCurrentPiContext({
97
105
  prepared,
98
- this.owner,
99
- pinnedDescriptor,
100
- );
101
- const pinnedModel = state.snapshot.pi.model;
106
+ includeMemory: request.includeMemory,
107
+ readExtensionContext: request.readExtensionContext,
108
+ }, this.owner);
109
+ const currentModel = state.snapshot.pi.model;
102
110
  let models!: MutableModels;
111
+ const requestStream = (observer = request.onGeneration) => {
112
+ const stream = withProviderBoundary(
113
+ this.models.streamSimple.bind(this.models), request.modelSessionId, observer,
114
+ );
115
+ return ((model, context, options) => lazyStream(model, async () => {
116
+ options?.signal?.throwIfAborted();
117
+ request.assertRequestAllowed?.();
118
+ const refreshed = await refreshModelGuidance(state.candidates, context);
119
+ options?.signal?.throwIfAborted();
120
+ request.assertRequestAllowed?.();
121
+ return stream(model, refreshed, options);
122
+ })) satisfies typeof stream;
123
+ };
103
124
  models = new Proxy(this.models, {
104
125
  get: (target, property) => {
105
126
  if (property === "getModel") {
106
127
  return (provider: string, modelId: string) =>
107
- provider === pinnedModel.provider && modelId === pinnedModel.id
108
- ? pinnedModel
128
+ provider === currentModel.provider && modelId === currentModel.id
129
+ ? currentModel
109
130
  : target.getModel(provider, modelId);
110
131
  }
111
- if (property === "streamSimple") {
112
- return withProviderBoundary(
113
- target.streamSimple.bind(target),
114
- request.modelSessionId,
115
- request.onGeneration,
116
- );
117
- }
132
+ if (property === "streamSimple") return requestStream();
118
133
  if (property === "completeSimple") {
119
- return target.completeSimple.bind(models);
134
+ const compactionModels = new Proxy(models, {
135
+ get: (base, key) => {
136
+ if (key === "streamSimple") return requestStream((event) => {
137
+ request.onGeneration?.(event);
138
+ request.onCompactionGeneration?.(event);
139
+ });
140
+ const value = Reflect.get(base, key, base);
141
+ return typeof value === "function" ? value.bind(base) : value;
142
+ },
143
+ });
144
+ return target.completeSimple.bind(compactionModels);
120
145
  }
121
146
  const value = Reflect.get(target, property, target);
122
147
  return typeof value === "function" ? value.bind(target) : value;
@@ -126,16 +151,16 @@ export class HarnessRuntimeConfig {
126
151
  models,
127
152
  model: state.snapshot.pi.model,
128
153
  thinkingLevel: state.snapshot.pi.thinkingLevel,
129
- systemPrompt: descriptor.systemPrompt,
154
+ systemPrompt: context.systemPrompt,
155
+ degradations: context.degradations,
130
156
  candidates: state.candidates,
131
157
  workspace: state.snapshot.bindings.workspace,
132
158
  platform: state.snapshot.bindings.platform,
133
159
  compaction: {
134
160
  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),
161
+ // Pi compacts above contextWindow - reserveTokens.
162
+ reserveTokens: state.snapshot.pi.model.contextWindow -
163
+ compactionThreshold(state.snapshot.pi.model),
139
164
  keepRecentTokens: Math.min(
140
165
  20_000,
141
166
  Math.floor(state.snapshot.pi.model.contextWindow * 0.15),
@@ -159,24 +184,27 @@ export class HarnessRuntimeConfig {
159
184
  }
160
185
  }
161
186
 
162
- /**
163
- * 在接受 Submission 前固定本次执行使用的动态上下文和审批模式。
164
- *
165
- * @remarks
166
- * 调用方:Runtime 的准入流程为每个新 Submission 调用,并把 descriptor 与其哈希一起持久化。
167
- *
168
- * 实现理由:memory、workspace 和 Extension context 可能随时间变化,所以把准入时的 Turn 字段保存在 descriptor 中。
169
- * 不要把 pin 推迟到 Turn 启动后,否则已持久化的 Submission 将失去可验证的执行基线。
170
- */
171
- pin(options: PinPiRuntimeOptions): Promise<PinnedPiRuntime> {
172
- return pinPiRuntime(options, this.owner);
173
- }
174
-
175
187
  }
176
188
 
177
189
  export type {
178
- PinPiRuntimeOptions,
179
- PinnedPiRuntime,
180
190
  PreparePiRuntimeOptions,
181
191
  PreparedPiRuntime,
182
- } from "./pinning";
192
+ } from "./prepared-runtime";
193
+
194
+ /**
195
+ * Input tokens above which a Turn compacts its history.
196
+ *
197
+ * OpenRouter rejects some requests well below a model's advertised window
198
+ * (seen from about 393k tokens on DeepSeek V4.1 Flash and 520k-630k on GPT-6
199
+ * Luna), before any provider is chosen. No model waits past this cap, so long
200
+ * Chats compact instead of failing.
201
+ */
202
+ export const MAX_COMPACTION_THRESHOLD_TOKENS = 300_000;
203
+
204
+ /** 15% headroom within the input budget, capped at {@link MAX_COMPACTION_THRESHOLD_TOKENS}. */
205
+ export function compactionThreshold(model: { contextWindow: number; maxTokens: number }): number {
206
+ return Math.min(
207
+ Math.floor((model.contextWindow - model.maxTokens) * 0.85),
208
+ MAX_COMPACTION_THRESHOLD_TOKENS,
209
+ );
210
+ }
@@ -7,7 +7,7 @@ export * from "./assembly-view";
7
7
  export * from "./definition";
8
8
  export * from "./degradation";
9
9
  export * from "./harness-runtime";
10
- export * from "./pinning";
10
+ export * from "./prepared-runtime";
11
11
  export * from "./ports";
12
12
  export * from "./profile";
13
13
  export * from "./runtime-config";
@@ -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
+ }