@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
@@ -19,6 +19,7 @@ import {
19
19
  isTerminalSubmissionStatus,
20
20
  SubmissionLifecycle,
21
21
  SubmissionAdmission,
22
+ ADMISSION_ATTEMPT_WINDOW_MS,
22
23
  SubmissionDelivery,
23
24
  SubmissionFinalizer,
24
25
  projectSubmissionReceipt,
@@ -33,6 +34,9 @@ import {
33
34
  type SubmissionReceipt,
34
35
  type SubmissionExecutionMode,
35
36
  type SubmissionHandle,
37
+ type StoredRuntimeKey,
38
+ type RuntimeKeyRewrite,
39
+ type RuntimeUnattendedExecutionGrant,
36
40
  } from "../../submission";
37
41
  import type { ExecutionLevel } from "../../lib/execution-level";
38
42
  import {
@@ -42,7 +46,6 @@ import {
42
46
  RuntimeLoadTracker,
43
47
  prepareRuntimeCandidate,
44
48
  projectRuntimeAssembly,
45
- type PreparedPiRuntime,
46
49
  type RuntimeAssemblyInput,
47
50
  type RuntimeAssemblyView,
48
51
  type RuntimeCandidate,
@@ -66,13 +69,15 @@ import {
66
69
  import type { UIMessage, UIMessageChunk } from "ai";
67
70
  import type {
68
71
  AssistantMessage,
72
+ JsonObject,
69
73
  } from "@earendil-works/pi-ai";
70
- import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core/harness/context";
74
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
71
75
  import { openCloudflareSession } from "../../session";
72
- import { openSessionHarness, prepareSubmissionOperation } from "../../harness";
76
+ import { ModelRecovery, openSessionHarness, prepareSubmissionOperation } from "../../harness";
73
77
  import type { SessionHarness } from "../../harness";
74
78
  import {
75
79
  compileHarnessTools,
80
+ harnessToolActivation,
76
81
  decodeSubagentCompletion,
77
82
  HarnessDecisionGate,
78
83
  HarnessHumanDecisions,
@@ -85,7 +90,10 @@ import {
85
90
  type TemporaryAgentRequest,
86
91
  type TemporaryAgentRunContext,
87
92
  } from "../../tools";
88
- import { sha256Hex } from "../../lib/artifacts";
93
+ import {
94
+ ASK_USER_TOOL_NAME,
95
+ askUserPiToolCandidate,
96
+ } from "../../tools/assistant/ask-user";
89
97
  import { RuntimeDatabase } from "./database";
90
98
 
91
99
  /**
@@ -98,6 +106,10 @@ import { RuntimeDatabase } from "./database";
98
106
  // #region Runtime 内部数据与基础工具
99
107
 
100
108
  const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
109
+ /** 定时 Run 未结束时拒绝普通输入的统一原因,服务端与客户端状态共用同一事实。 */
110
+ const SCHEDULED_INPUT_LOCKED_MESSAGE =
111
+ "This scheduled run is still in progress. Wait for it to finish before sending a message.";
112
+
101
113
  interface ActiveTurn {
102
114
  submissionId: string;
103
115
  requestId: string;
@@ -149,12 +161,22 @@ export abstract class AgentRuntimeKernel<
149
161
  };
150
162
 
151
163
  protected abstract ensureRuntimeReady(): Promise<void>;
152
- /** Runs only before the first pin of a newly activated Submission. */
164
+ /** Prepare per-Submission Host state before first execution. */
153
165
  protected prepareRuntimeForSubmission(_submissionId: string): Promise<void> {
154
166
  return this.ensureRuntimeReady();
155
167
  }
156
168
 
157
- protected ensureRuntimeVersion(_runtimeKey: string): Promise<void> {
169
+ /** Runs before a new Submission records its Runtime key. */
170
+ protected async prepareRuntimeForAdmission(): Promise<{
171
+ runtimeKey: string;
172
+ admission?: ActiveRuntime["admission"];
173
+ }> {
174
+ await this.ensureRuntimeReady();
175
+ const runtime = this.requireRuntime();
176
+ return { runtimeKey: runtime.runtimeKey, admission: runtime.admission };
177
+ }
178
+
179
+ protected ensureRuntimeVersion(_runtimeKey: string, _force = false): Promise<void> {
158
180
  return this.ensureRuntimeReady();
159
181
  }
160
182
  protected get submissionContext() {
@@ -164,6 +186,37 @@ export abstract class AgentRuntimeKernel<
164
186
  };
165
187
  }
166
188
 
189
+ /** Narrow storage seam for Host-validated, one-time Agent identity upgrades. */
190
+ protected savedRuntimeKeys(): readonly StoredRuntimeKey[] {
191
+ return this.db.submissions.listRuntimeKeys();
192
+ }
193
+
194
+ protected rewriteSavedRuntimeKeys(rewrites: readonly RuntimeKeyRewrite[]): void {
195
+ this.db.submissions.rewriteRuntimeKeys(rewrites);
196
+ }
197
+
198
+ /** One Runtime-owned fact for Host lifecycle guards; includes async preparation. */
199
+ protected hasUnfinishedRuntimeWork(): boolean {
200
+ return this.submissions.isBusy() ||
201
+ this.db.submissionAdmissions.countPending(Date.now() - ADMISSION_ATTEMPT_WINDOW_MS) > 0 ||
202
+ this.db.agentTools.hasRunning() ||
203
+ Boolean(
204
+ this.state.turn?.activeSubmissionId ||
205
+ this.state.approvals?.length ||
206
+ (this.state.activity?.activity && this.state.activity.activity !== "idle"),
207
+ );
208
+ }
209
+
210
+ /** True while a Submission owns this Chat's active execution slice. */
211
+ protected isSubmissionExecuting(): boolean {
212
+ return this.submissions.currentActive() !== undefined;
213
+ }
214
+
215
+ /** External Runtime changes share the same serial queue as Submission execution. */
216
+ protected runConfigControl<T>(run: () => Promise<T>): Promise<T> {
217
+ return this.submissions.runConfigControl(run);
218
+ }
219
+
167
220
  /** Let the Host prepare a validated client response before Tool settlement. */
168
221
  protected prepareToolInteractionResponse(
169
222
  input: ToolInteractionResponsePreparation,
@@ -173,7 +226,7 @@ export abstract class AgentRuntimeKernel<
173
226
 
174
227
  private activeRuntime?: ActiveRuntime;
175
228
  private telemetryCoordinator?: RuntimeTelemetryCoordinator;
176
- private activeHarness?: SessionHarness<object | undefined>;
229
+ private activeHarness?: SessionHarness;
177
230
  private piAdapter?: HarnessRuntimeConfig;
178
231
  private readonly transcript: ConversationHistory;
179
232
  private conversationBridge?: HarnessConversationBridge;
@@ -306,7 +359,6 @@ export abstract class AgentRuntimeKernel<
306
359
  createExtensionHostBinding: (permissions, labels) =>
307
360
  this.createExtensionHostBinding(permissions, labels),
308
361
  advance: (phase) => this.runtimeLoad.advance(phase),
309
- isExecuting: () => Boolean(this.submissions.currentActive()),
310
362
  commit: (runtime) => {
311
363
  this.telemetry.configure(candidate.telemetry);
312
364
  this.activeRuntime = runtime;
@@ -358,14 +410,14 @@ export abstract class AgentRuntimeKernel<
358
410
  runtime: Awaited<ReturnType<typeof openSessionHarness>>,
359
411
  submissionId: string,
360
412
  ): Promise<void> {
361
- const watch = await runtime.lane.watch(BACKGROUND_CONTEXT);
362
- try {
363
- this.db.transaction(() =>
364
- this.db.submissions.associateSessionEntries(submissionId, watch.snapshot)
365
- );
366
- } finally {
367
- watch.unsubscribe();
368
- }
413
+ const native = await runtime.submissionFor(submissionId, BACKGROUND_CONTEXT);
414
+ if (!native || native.entry === undefined) return;
415
+ const entries = await runtime.entries();
416
+ const start = entries.findIndex((entry) => entry.id === native.entry);
417
+ if (start < 0) throw new Error("Native Submission entry is missing from its conversation");
418
+ this.db.transaction(() => {
419
+ for (const entry of entries.slice(start)) this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
420
+ });
369
421
  }
370
422
 
371
423
  /**
@@ -390,7 +442,7 @@ export abstract class AgentRuntimeKernel<
390
442
  }
391
443
  await this.broadcastApprovals();
392
444
  const running = this.db.submissions.findRunning() as StoredSubmission | null;
393
- if (running) {
445
+ if (running && !running.unattendedFailure) {
394
446
  if (
395
447
  this.db.approvals.listPendingForSubmission(running.submissionId).length > 0 ||
396
448
  this.db.interactions.listPendingForSubmission(running.submissionId).length > 0
@@ -504,52 +556,7 @@ export abstract class AgentRuntimeKernel<
504
556
 
505
557
  // #endregion
506
558
 
507
- // #region Submission 准入与配置固定
508
-
509
- /** Pin dynamic Context and execution policy before admission. */
510
- private async admissionPin(fresh: boolean, submissionId: string): Promise<{
511
- revision: string;
512
- descriptor: string;
513
- }> {
514
- if (fresh) await this.prepareRuntimeForSubmission(submissionId);
515
- else await this.ensureRuntimeReady();
516
- return this.currentAssemblyPin();
517
- }
518
-
519
- // initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
520
- private async currentAssemblyPin(): Promise<{
521
- revision: string;
522
- descriptor: string;
523
- }> {
524
- await this.runtimeTurnEvents.drain();
525
- const runtime = this.activeRuntime;
526
- if (!runtime) {
527
- throw new Error("Runtime revision is not initialized");
528
- }
529
- return this.pinAssembly(runtime.pi, runtime.revision);
530
- }
531
-
532
- private async pinAssembly(
533
- prepared: PreparedPiRuntime,
534
- baseRevision: string,
535
- ): Promise<{ revision: string; descriptor: string }> {
536
- const pinned = await this.pi.pin({
537
- prepared,
538
- baseRevision,
539
- readExtensionContext: ({ label }) =>
540
- this._hostGetContext(label),
541
- });
542
- if (pinned.degradations.length > 0) {
543
- console.warn(
544
- "[runtime-context:degraded]",
545
- json({ degradations: pinned.degradations }),
546
- );
547
- }
548
- return {
549
- revision: await sha256Hex(pinned.descriptor),
550
- descriptor: pinned.descriptor,
551
- };
552
- }
559
+ // #region Submission 准入与当前配置
553
560
 
554
561
  private get admission(): SubmissionAdmission {
555
562
  return this.submissionAdmission ??= new SubmissionAdmission({
@@ -557,10 +564,7 @@ export abstract class AgentRuntimeKernel<
557
564
  steers: this.db.steers,
558
565
  submissions: this.db.submissions,
559
566
  transaction: (run) => this.db.transaction(run),
560
- prepareRuntime: async () => {
561
- await this.ensureRuntimeReady();
562
- return { runtimeKey: this.activeRuntime!.runtimeKey, admission: this.activeRuntime?.admission };
563
- },
567
+ prepareRuntime: () => this.prepareRuntimeForAdmission(),
564
568
  findUserMessage: (id) => this.transcript.findUserMessage(id),
565
569
  onAdmitted: (submission, hostAccepted) =>
566
570
  this.runtimeTurnEvents.submissionAdmitted(submission, hostAccepted),
@@ -615,11 +619,16 @@ export abstract class AgentRuntimeKernel<
615
619
  releaseAbortedHarness: async () => {
616
620
  const runtime = this.activeHarness;
617
621
  if (!runtime) return;
618
- await runtime.lane.waitForIdle(BACKGROUND_CONTEXT);
619
- if (this.activeHarness === runtime) this.activeHarness = undefined;
620
- await runtime.close(BACKGROUND_CONTEXT);
622
+ await runtime.conversation.waitForIdle(BACKGROUND_CONTEXT);
623
+ // The execution owner closes after transport projections settle.
624
+
625
+ },
626
+ cancelQueued: async (id) => {
627
+ const runtime = this.activeHarness;
628
+ if (!runtime) return undefined;
629
+ const result = await runtime.harness.abortSubmission(Number(id) as import("@earendil-works/pi-durable").SubmissionId, BACKGROUND_CONTEXT, runtime.conversation.id);
630
+ return { kind: result === "already_placed" || result === "settled" ? "already_consumed" as const : "cancelled" as const };
621
631
  },
622
- cancelQueued: async (id) => this.activeHarness?.lane.cancelQueued(id, BACKGROUND_CONTEXT),
623
632
  submit: (message, options) => this.submitMessage(message, options),
624
633
  waitUntil: (promise) => this.ctx.waitUntil(promise),
625
634
  });
@@ -761,20 +770,43 @@ export abstract class AgentRuntimeKernel<
761
770
  ): Promise<StoredSubmission> {
762
771
  const continuation = mode !== "fresh";
763
772
  const submissionId = submission.submissionId;
764
- submission = await this.admission.pin(
773
+ submission = await this.admission.prepareCurrentRuntime(
765
774
  submission,
766
- () => this.admissionPin(mode === "fresh", submissionId),
767
- mode === "fresh" ? undefined : (key) => this.ensureRuntimeVersion(key),
775
+ (key) => this.ensureRuntimeVersion(key, true),
768
776
  );
777
+ if (!continuation) await this.prepareRuntimeForSubmission(submissionId);
778
+ await this.runtimeTurnEvents.drain();
769
779
  const activeRuntime = this.requireRuntime();
770
- const harnessInput = this.pi.harnessInputs(
780
+ // A fresh provider Session per Submission keeps one failing upstream route from pinning later turns.
781
+ const recovery = new ModelRecovery(submissionId);
782
+ const harnessInput = await this.pi.harnessInputs(
771
783
  activeRuntime.pi,
772
- submission.assemblyDescriptor,
773
784
  {
774
- modelSessionId: submissionId,
785
+ modelSessionId: () => recovery.sessionId(),
786
+ assertRequestAllowed: () => {
787
+ const current = this.db.submissions.find(submissionId);
788
+ if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
789
+ },
775
790
  onGeneration: (event) => this.telemetry.generation(submissionId, event),
791
+ onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
792
+ includeMemory: submission.origin !== "scheduled",
793
+ readExtensionContext: ({ label }) => this._hostGetContext(label),
776
794
  },
777
795
  );
796
+ if (harnessInput.degradations.length > 0) {
797
+ console.warn("[runtime-context:degraded]", json({ degradations: harnessInput.degradations }));
798
+ }
799
+ const prepareToolInput = (toolName: string, input: unknown): unknown => {
800
+ const prepare = harnessInput.candidates.find(({ tool }) =>
801
+ tool.name === toolName
802
+ )?.tool.prepareArguments;
803
+ if (!prepare) return input;
804
+ try {
805
+ return prepare(input);
806
+ } catch {
807
+ return input;
808
+ }
809
+ };
778
810
  let runtime!: Awaited<ReturnType<typeof openSessionHarness>>;
779
811
  let publishApprovalRequest = async (
780
812
  _toolCallId: string,
@@ -785,6 +817,8 @@ export abstract class AgentRuntimeKernel<
785
817
  submissionId,
786
818
  requestId: submission.requestId,
787
819
  executionLevel: activeRuntime.snapshot.profile.executionLevel,
820
+ origin: submission.origin,
821
+ scheduledMaxExecutionLevel: submission.scheduledMaxExecutionLevel ?? undefined,
788
822
  },
789
823
  candidates: harnessInput.candidates,
790
824
  telemetry: this.telemetry,
@@ -812,11 +846,14 @@ export abstract class AgentRuntimeKernel<
812
846
  }
813
847
  },
814
848
  gateTool: harnessInput.platform.gateTool,
849
+ onUnattendedFailure: (reason) => {
850
+ this.db.submissions.recordUnattendedFailure(submissionId, reason);
851
+ },
852
+ unattendedFailure: () => this.db.submissions.find(submissionId)?.unattendedFailure,
815
853
  });
816
854
  runtime = await openSessionHarness({
817
- session: openCloudflareSession(this.ctx.storage),
855
+ storage: openCloudflareSession(this.ctx.storage),
818
856
  models: harnessInput.models,
819
- modelSessionId: submissionId,
820
857
  model: harnessInput.model,
821
858
  thinkingLevel: harnessInput.thinkingLevel,
822
859
  systemPrompt: harnessInput.systemPrompt,
@@ -824,30 +861,29 @@ export abstract class AgentRuntimeKernel<
824
861
  workspace: harnessInput.workspace,
825
862
  authorize: gate.authorize.bind(gate),
826
863
  resolveReplay: gate.resolveReplay.bind(gate),
864
+ resolveConfirmation: gate.resolveConfirmation.bind(gate),
827
865
  }),
866
+ ...harnessToolActivation(harnessInput.candidates),
828
867
  modelToolResultExemptions: harnessInput.candidates
829
868
  .filter(({ outputBudget }) => outputBudget?.kind === "structure")
830
869
  .map(({ tool }) => tool.name),
831
870
  streamOptions: { maxRetries: 0 },
832
871
  compaction: harnessInput.compaction,
833
872
  toolExecution: "parallel",
873
+ recovery,
874
+ beforeTool: (call, _api, context) => gate.beforeTool({ toolName: call.name, toolCallId: call.id, args: call.arguments }, context),
875
+ afterResponse: (message) => {
876
+ for (const part of message.content) if (part.type === "toolCall") part.arguments = prepareToolInput(part.name, part.arguments) as JsonObject;
877
+ },
834
878
  }, BACKGROUND_CONTEXT);
835
879
  this.activeHarness = runtime;
836
- let stream: ReturnType<typeof bindHarnessStream> | undefined;
880
+ let stream: Awaited<ReturnType<typeof bindHarnessStream>> | undefined;
837
881
  let deactivate = () => {};
882
+ let released = false;
838
883
  try {
839
- runtime.harness.hooks.on("before_tool", gate.beforeTool);
840
- runtime.harness.hooks.on("before_drive", async (event, context) => {
841
- if (this.db.submissions.find(submissionId)?.abortReason) {
842
- await runtime.requestAbort(event.runId, context);
843
- }
844
- });
845
884
 
846
- if (submission.abortReason && runtime.openOperation) {
847
- await runtime.requestAbort(
848
- runtime.openOperation.operationId,
849
- BACKGROUND_CONTEXT,
850
- );
885
+ if ((submission.abortReason || submission.unattendedFailure) && runtime.openOperation) {
886
+ await runtime.abort(BACKGROUND_CONTEXT);
851
887
  }
852
888
 
853
889
  await this.associateHarnessEntries(runtime, submissionId);
@@ -858,26 +894,10 @@ export abstract class AgentRuntimeKernel<
858
894
  (id) => this.transcript.regenerationTarget(id),
859
895
  BACKGROUND_CONTEXT,
860
896
  );
861
- if (preparation.kind === "settled") {
862
- const record = preparation.outcome;
863
- const terminal = await this.submissions.finish(
864
- submission,
865
- record.status === "completed" || record.status === "declined"
866
- ? "succeeded" : record.status === "aborted" ? "aborted" : "failed",
867
- record.error?.message,
868
- );
869
- this.broadcast(json({
870
- type: MessageType.CF_AGENT_CHAT_MESSAGES,
871
- messages: await this.getMessages(),
872
- }));
873
- return terminal;
874
- }
875
-
876
897
  if (submission.status === "pending") {
877
898
  this.db.submissions.transition(submissionId, "running", ["pending"]);
878
899
  this.telemetry.capture("turnStarted", {
879
900
  submissionId,
880
- assemblyRevision: submission.assemblyRevision,
881
901
  });
882
902
  submission = this.db.submissions.find(submissionId)!;
883
903
  }
@@ -886,70 +906,58 @@ export abstract class AgentRuntimeKernel<
886
906
  steers: this.db.steers,
887
907
  messageUi: this.db.messageUi,
888
908
  transaction: (run) => this.db.transaction(run),
889
- enqueue: async (message) => {
890
- const queued = await runtime.lane.steer(message, undefined, BACKGROUND_CONTEXT);
891
- if (!queued.ok) throw queued.error;
892
- return queued.value.entryId;
909
+ enqueue: async (steer) => {
910
+ const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
911
+ const queued = await runtime.conversation.submit({ type: "input", content: message.content, requestId: `steer:${steer.messageId}`, whenBusy: "steer" }, BACKGROUND_CONTEXT);
912
+ return queued.status(BACKGROUND_CONTEXT);
893
913
  },
914
+ find: (messageId) => runtime.submissionFor(`steer:${messageId}`, BACKGROUND_CONTEXT),
894
915
  });
895
916
  const uiMessageBody = submission.queuedUiMessageJson;
896
917
  let initialUserSidecarPersisted = (await this.transcript.storedMessages())
897
918
  .some((entry) =>
898
919
  entry.submissionId === submissionId && entry.message.role === "user"
899
920
  );
900
- runtime.harness.events.on("entry_added", (event) => {
901
- const entry = event.entry;
921
+ const unsubscribeEntries = runtime.harness.subscribeCommits((publication) => {
902
922
  this.db.transaction(() => {
903
- this.db.submissions.linkSessionEntry(entry.id, submissionId);
904
- if (entry.type === "message" && entry.message.role === "user") {
905
- if (!steerDelivery.settle(entry.id) && !initialUserSidecarPersisted && uiMessageBody) {
906
- this.db.messageUi.upsert(entry.id, uiMessageBody);
923
+ for (const change of publication.changes) {
924
+ if (change.type === "submission") steerDelivery.settleRecord(change.value);
925
+ if (change.type !== "entry" || change.value.conversationId !== runtime.conversation.id) continue;
926
+ const entry = change.value;
927
+ this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
928
+ if (entry.kind === "pi.user" && !initialUserSidecarPersisted && uiMessageBody) {
929
+ this.db.messageUi.upsert(String(entry.id), uiMessageBody);
907
930
  initialUserSidecarPersisted = true;
908
931
  }
909
932
  }
910
933
  });
911
-
912
934
  });
913
- const pendingWatch = await runtime.lane.watch(BACKGROUND_CONTEXT);
914
- try {
915
- await steerDelivery.reconcile(submissionId, pendingWatch.snapshot);
916
- } finally {
917
- pendingWatch.unsubscribe();
918
- }
919
-
920
- this.runtimeTurnEvents.observe(runtime.harness, submissionId, harnessInput.model);
921
- stream = bindHarnessStream(runtime.harness.events, {
935
+ await this.runtimeTurnEvents.observe(runtime, submissionId, harnessInput.model);
936
+ stream = await bindHarnessStream(runtime, {
922
937
  messageId: submission.assistantMessageId,
923
938
  userMessageId: submission.userMessageId ?? undefined,
924
939
  startedAt: submission.createdAt,
925
940
  turnId: submissionId,
941
+ prepareToolInput,
926
942
  publish: async (chunks) => {
927
- for (const chunk of chunks) {
928
- this.conversation.broadcastResponse({
929
- id: submission.requestId,
930
- body: json(chunk),
931
- done: false,
932
- continuation,
933
- });
934
- }
943
+ for (const chunk of chunks) this.conversation.broadcastResponse({ id: submission.requestId, body: json(chunk), done: false, continuation });
935
944
  },
936
945
  });
946
+ deactivate = unsubscribeEntries;
947
+
937
948
  publishApprovalRequest = stream.publishApprovalRequest;
938
- this.conversation.activate(runtime.lane, submission);
949
+ this.conversation.activate(runtime, submission);
939
950
 
940
951
  const control = {
941
952
  abort: async () => {
942
953
  if (runtime.openOperation) {
943
- await runtime.requestAbort(
944
- runtime.openOperation.operationId,
945
- BACKGROUND_CONTEXT,
946
- );
954
+ await runtime.abort(BACKGROUND_CONTEXT);
947
955
  }
948
956
  },
949
957
  interrupt: () => void runtime.close(BACKGROUND_CONTEXT),
950
958
  steer: async (_message: PiCanonicalUserInput, messageId: string) => {
951
959
  const steer = this.db.steers.findByMessageId(messageId);
952
- if (steer) await steerDelivery.queue(steer);
960
+ if (steer && runtime.openOperation) await steerDelivery.queue(steer);
953
961
  },
954
962
  };
955
963
  const turn: ActiveTurn = {
@@ -960,36 +968,28 @@ export abstract class AgentRuntimeKernel<
960
968
  continuation,
961
969
  agent: control,
962
970
  };
963
- deactivate = this.submissions.activate(submission, turn);
964
- const outcome = await runtime.run(preparation.request, BACKGROUND_CONTEXT, {
965
- pollDeferred: !preparation.request,
966
- });
971
+ const deactivateTurn = this.submissions.activate(submission, turn);
972
+ const unsubscribeEntriesAndTurn = deactivate;
973
+ deactivate = () => { unsubscribeEntriesAndTurn(); deactivateTurn(); };
974
+ await runtime.admit(preparation, BACKGROUND_CONTEXT);
975
+ await steerDelivery.reconcile(submissionId);
976
+ const outcome = await runtime.run(undefined, BACKGROUND_CONTEXT);
967
977
  this.db.submissions.clearQueuedPayload(submissionId);
968
978
  if (outcome.kind === "released") {
969
- if (
970
- outcome.reason === "slice" ||
971
- outcome.reason === "budget" ||
972
- outcome.reason === "tool-failure"
973
- ) {
979
+ if (outcome.reason === "slice") {
974
980
  await this.schedule(0, "_piHarnessContinuation", { submissionId }, {
975
981
  idempotent: true,
976
982
  });
977
983
  }
978
- return this.db.submissions.find(submissionId)!;
979
- }
980
- if (outcome.kind === "waiting") {
981
- await this.schedule(
982
- outcome.reason === "retry"
983
- ? Math.max(0, Math.ceil((outcome.notBefore - Date.now()) / 1000))
984
- : Math.max(0, Math.ceil((outcome.deferred.pollAfterMs ?? 1_000) / 1000)),
985
- "_piHarnessContinuation",
986
- { submissionId },
987
- { idempotent: true },
988
- );
984
+ released = true;
989
985
  return this.db.submissions.find(submissionId)!;
990
986
  }
991
987
  const record = outcome.outcome;
992
- const status = record.status === "completed" || record.status === "declined"
988
+ const unattendedFailure = this.db.submissions.find(submissionId)
989
+ ?.unattendedFailure;
990
+ const status = unattendedFailure
991
+ ? "failed"
992
+ : record.status === "completed" || record.status === "declined"
993
993
  ? "succeeded"
994
994
  : record.status === "aborted"
995
995
  ? "aborted"
@@ -998,7 +998,7 @@ export abstract class AgentRuntimeKernel<
998
998
  const terminal = await this.submissions.finish(
999
999
  submission,
1000
1000
  status,
1001
- record.error?.message,
1001
+ unattendedFailure ?? record.error?.message,
1002
1002
  );
1003
1003
  const projectedOutcome = projectSubmissionOutcome(record, terminal);
1004
1004
  await stream.flushTerminal(projectedOutcome);
@@ -1015,13 +1015,21 @@ export abstract class AgentRuntimeKernel<
1015
1015
  }));
1016
1016
  throw error;
1017
1017
  } finally {
1018
- await this.conversation.release(runtime.lane).catch((error) => {
1018
+ await this.conversation.release(runtime).catch((error) => {
1019
1019
  console.warn("[runtime-stream:degraded]", errorText(error));
1020
1020
  });
1021
- stream?.unsubscribe();
1021
+ await stream?.unsubscribe();
1022
1022
  deactivate();
1023
1023
  if (this.activeHarness === runtime) this.activeHarness = undefined;
1024
1024
  await runtime.close(BACKGROUND_CONTEXT).catch(() => undefined);
1025
+ if (released) {
1026
+ // Pi closes the Harness on release and drops undelivered stream frames; the
1027
+ // canonical transcript gives a paused question or approval its complete input.
1028
+ this.broadcast(json({
1029
+ type: MessageType.CF_AGENT_CHAT_MESSAGES,
1030
+ messages: await this.getMessages(),
1031
+ }));
1032
+ }
1025
1033
  }
1026
1034
  }
1027
1035
 
@@ -1055,7 +1063,7 @@ export abstract class AgentRuntimeKernel<
1055
1063
  /** Start the native resume handshake from Pi's current lane snapshot. */
1056
1064
  async onConnect(connection: Connection): Promise<void> {
1057
1065
  this.conversation.disconnect(connection);
1058
- await this.conversation.resume(connection);
1066
+ await this.conversation.connect(connection);
1059
1067
  }
1060
1068
 
1061
1069
  async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise<void> {
@@ -1152,10 +1160,8 @@ export abstract class AgentRuntimeKernel<
1152
1160
 
1153
1161
  if (event.type === "clear") {
1154
1162
  if (this.submissions.isBusy()) return;
1155
- this.db.transaction(() => {
1156
- this.transcript.clear();
1157
- this.db.clearAll();
1158
- });
1163
+ await this.transcript.clear();
1164
+ this.db.transaction(() => this.db.clearAll());
1159
1165
  await this.broadcastApprovals();
1160
1166
  this.broadcast(json({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
1161
1167
  return;
@@ -1297,7 +1303,7 @@ export abstract class AgentRuntimeKernel<
1297
1303
  await this.broadcastApprovals();
1298
1304
  return;
1299
1305
  }
1300
- this.transcript.append(crypto.randomUUID(), message);
1306
+ await this.transcript.append(crypto.randomUUID(), message);
1301
1307
  }
1302
1308
 
1303
1309
  async _hostGetSessionInfo(): Promise<{ messageCount: number }> {
@@ -1320,7 +1326,36 @@ export abstract class AgentRuntimeKernel<
1320
1326
  async addCanonicalSnapshot(
1321
1327
  snapshot: PiCanonicalTranscriptSnapshot,
1322
1328
  ): Promise<void> {
1323
- this.transcript.importSnapshot(snapshot);
1329
+ await this.transcript.importSnapshot(snapshot);
1330
+ }
1331
+
1332
+ /**
1333
+ * 本 Session 是否有一条未结束的定时 Submission。
1334
+ *
1335
+ * 定时 Run 未结束时独占输入:普通消息在准入前被拒绝,不排队等它结束。
1336
+ * 定时注入本身走 `submitScheduledPrompt`,不经过这道闸。
1337
+ */
1338
+ private get scheduledInputLocked(): boolean {
1339
+ return this.db.submissions.hasUnfinishedScheduled();
1340
+ }
1341
+
1342
+ private rejectedOrdinaryInputReceipt(): SubmissionReceipt {
1343
+ return {
1344
+ submissionId: crypto.randomUUID(),
1345
+ status: "skipped",
1346
+ accepted: false,
1347
+ error: SCHEDULED_INPUT_LOCKED_MESSAGE,
1348
+ retryable: false,
1349
+ createdAt: Date.now(),
1350
+ };
1351
+ }
1352
+
1353
+ private rejectInputLocked(): MessageDispatchReceipt {
1354
+ return {
1355
+ kind: "rejected",
1356
+ code: "input_locked",
1357
+ message: SCHEDULED_INPUT_LOCKED_MESSAGE,
1358
+ };
1324
1359
  }
1325
1360
 
1326
1361
  /** Admit text and return immediately; completion continues in the Durable Object. */
@@ -1328,6 +1363,7 @@ export abstract class AgentRuntimeKernel<
1328
1363
  text: string,
1329
1364
  options?: { idempotencyKey?: string },
1330
1365
  ): Promise<SubmissionReceipt> {
1366
+ if (this.scheduledInputLocked) return this.rejectedOrdinaryInputReceipt();
1331
1367
  const submitted = await this.submitMessage({
1332
1368
  role: "user",
1333
1369
  content: [{ type: "text", text }],
@@ -1357,17 +1393,24 @@ export abstract class AgentRuntimeKernel<
1357
1393
  }
1358
1394
 
1359
1395
  async dispatchMessage(message: UIMessage, delivery: MessageDelivery): Promise<MessageDispatchReceipt> {
1396
+ if (this.scheduledInputLocked) return this.rejectInputLocked();
1360
1397
  return this.delivery.dispatch(message, delivery);
1361
1398
  }
1362
1399
 
1363
1400
  async steerQueuedSubmission(submissionId: string): Promise<MessageDispatchReceipt> {
1401
+ if (this.scheduledInputLocked) return this.rejectInputLocked();
1364
1402
  return this.delivery.steerQueued(submissionId);
1365
1403
  }
1366
1404
 
1367
1405
  /** Admit scheduled work after a bounded stable wait, otherwise return skipped. */
1368
1406
  async submitScheduledPrompt(
1369
1407
  prompt: string,
1370
- options?: { idempotencyKey?: string },
1408
+ options?: {
1409
+ idempotencyKey?: string;
1410
+ scheduledMaxExecutionLevel?: ExecutionLevel;
1411
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
1412
+ unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
1413
+ },
1371
1414
  ): Promise<SubmissionReceipt> {
1372
1415
  const requestId = crypto.randomUUID();
1373
1416
  const submitted = await this.submitMessageWhenStable(
@@ -1379,6 +1422,8 @@ export abstract class AgentRuntimeKernel<
1379
1422
  {
1380
1423
  requestId,
1381
1424
  idempotencyKey: options?.idempotencyKey,
1425
+ origin: "scheduled",
1426
+ scheduledMaxExecutionLevel: options?.scheduledMaxExecutionLevel,
1382
1427
  },
1383
1428
  SCHEDULED_STABLE_TIMEOUT_MS,
1384
1429
  );
@@ -1463,7 +1508,7 @@ export abstract class AgentRuntimeKernel<
1463
1508
  summary: approval.summary,
1464
1509
  executionLevel: approval.executionLevel,
1465
1510
  requiredExecutionLevel: approval.requiredExecutionLevel,
1466
- inputJson: approval.inputJson,
1511
+ inputJson: approval.confirmationJson ?? approval.inputJson,
1467
1512
  requestId: approval.requestId,
1468
1513
  };
1469
1514
  }
@@ -1479,11 +1524,19 @@ export abstract class AgentRuntimeKernel<
1479
1524
  if (!submission || isTerminalSubmissionStatus(submission.status)) {
1480
1525
  return { ok: false };
1481
1526
  }
1482
- await this.ensureRuntimeReady();
1483
- const spec = this.pi.harnessInputs(
1484
- this.requireRuntime().pi,
1485
- submission.assemblyDescriptor,
1486
- ).candidates.find(({ tool }) => tool.name === pending.toolName)?.interaction;
1527
+ const active = this.submissions.currentActive();
1528
+ if (active && active.submissionId !== submission.submissionId) {
1529
+ return { ok: false };
1530
+ }
1531
+ // ask_user has a Config-independent validator. A parked answer must not
1532
+ // replace the Gateway; the continuation loads current Config instead.
1533
+ const askUser = pending.toolName === ASK_USER_TOOL_NAME;
1534
+ if (!askUser && !active) {
1535
+ await this.ensureRuntimeVersion(submission.runtimeKey, true);
1536
+ }
1537
+ const spec = askUser
1538
+ ? askUserPiToolCandidate().interaction
1539
+ : this.pi.interaction(this.requireRuntime().pi, pending.toolName);
1487
1540
  if (!spec) return { ok: false };
1488
1541
 
1489
1542
  const accepted = await this.humanDecisions.respond(
@@ -1573,6 +1626,7 @@ export abstract class AgentRuntimeKernel<
1573
1626
  hasPendingInteraction: (id) => this.humanDecisions.isWaitingForInteraction(id),
1574
1627
  hasPendingSteer: (id) => this.db.steers.listForSubmission(id).length > 0,
1575
1628
  hasRunningAgentTools: this.db.agentTools.hasRunning(),
1629
+ hasUnfinishedScheduled: this.db.submissions.hasUnfinishedScheduled(),
1576
1630
  previousState: this.state,
1577
1631
  });
1578
1632
  if (changed) this.setState(state);