@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.2",
3
+ "version": "0.3.0-alpha.20",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -13,28 +13,30 @@
13
13
  ".": "./src/index.ts",
14
14
  "./adapter/cloudflare": "./src/adapter/cloudflare/index.ts",
15
15
  "./contracts": "./src/config/public-contracts.ts",
16
- "./declared-tool": "./src/tools/declared.ts",
16
+ "./declared-tool": "./src/tools/adapters/declared.ts",
17
17
  "./models": "./src/lib/model-catalog.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@cloudflare/codemode": "0.5.1",
20
+ "@cloudflare/codemode": "0.5.2",
21
21
  "@cloudflare/sandbox": "0.12.4",
22
22
  "@cloudflare/shell": "0.4.3",
23
- "@cloudflare/think": "0.17.0",
24
- "@earendil-works/pi-agent-core": "0.85.1",
25
- "@earendil-works/pi-ai": "0.85.1",
23
+ "@cloudflare/think": "0.18.0",
24
+ "@earendil-works/pi-agent-core": "1.1.0",
25
+ "@earendil-works/pi-ai": "1.1.0",
26
26
  "@types/lodash-es": "^4.17.12",
27
27
  "cron-schedule": "6.0.0",
28
28
  "isomorphic-git": "1.38.6",
29
29
  "lodash-es": "^4.18.1",
30
- "zod": "^4.4.3"
30
+ "zod": "^4.4.3",
31
+ "@earendil-works/pi-durable": "1.1.0",
32
+ "@earendil-works/chord": "1.1.0"
31
33
  },
32
34
  "peerDependencies": {
33
- "agents": "^0.22.0",
35
+ "agents": "^0.23.0",
34
36
  "ai": "^7.0.0"
35
37
  },
36
38
  "devDependencies": {
37
- "agents": "0.22.0",
39
+ "agents": "0.23.0",
38
40
  "ai": "7.0.56"
39
41
  }
40
42
  }
@@ -33,6 +33,7 @@ import {
33
33
  type SubmissionReceipt,
34
34
  type SubmissionExecutionMode,
35
35
  type SubmissionHandle,
36
+ type RuntimeUnattendedExecutionGrant,
36
37
  } from "../../submission";
37
38
  import type { ExecutionLevel } from "../../lib/execution-level";
38
39
  import {
@@ -66,13 +67,15 @@ import {
66
67
  import type { UIMessage, UIMessageChunk } from "ai";
67
68
  import type {
68
69
  AssistantMessage,
70
+ JsonObject,
69
71
  } from "@earendil-works/pi-ai";
70
- import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core/harness/context";
72
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
71
73
  import { openCloudflareSession } from "../../session";
72
- import { openSessionHarness, prepareSubmissionOperation } from "../../harness";
74
+ import { ModelRecovery, openSessionHarness, prepareSubmissionOperation } from "../../harness";
73
75
  import type { SessionHarness } from "../../harness";
74
76
  import {
75
77
  compileHarnessTools,
78
+ harnessToolActivation,
76
79
  decodeSubagentCompletion,
77
80
  HarnessDecisionGate,
78
81
  HarnessHumanDecisions,
@@ -98,6 +101,10 @@ import { RuntimeDatabase } from "./database";
98
101
  // #region Runtime 内部数据与基础工具
99
102
 
100
103
  const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
104
+ /** 定时 Run 未结束时拒绝普通输入的统一原因,服务端与客户端状态共用同一事实。 */
105
+ const SCHEDULED_INPUT_LOCKED_MESSAGE =
106
+ "This scheduled run is still in progress. Wait for it to finish before sending a message.";
107
+
101
108
  interface ActiveTurn {
102
109
  submissionId: string;
103
110
  requestId: string;
@@ -149,11 +156,16 @@ export abstract class AgentRuntimeKernel<
149
156
  };
150
157
 
151
158
  protected abstract ensureRuntimeReady(): Promise<void>;
152
- /** Runs only before the first pin of a newly activated Submission. */
159
+ /** Prepare per-Submission Host state before the first assembly pin. */
153
160
  protected prepareRuntimeForSubmission(_submissionId: string): Promise<void> {
154
161
  return this.ensureRuntimeReady();
155
162
  }
156
163
 
164
+ /** Runs before a new Submission records its Runtime key. */
165
+ protected prepareRuntimeForAdmission(): Promise<void> {
166
+ return this.ensureRuntimeReady();
167
+ }
168
+
157
169
  protected ensureRuntimeVersion(_runtimeKey: string): Promise<void> {
158
170
  return this.ensureRuntimeReady();
159
171
  }
@@ -173,7 +185,7 @@ export abstract class AgentRuntimeKernel<
173
185
 
174
186
  private activeRuntime?: ActiveRuntime;
175
187
  private telemetryCoordinator?: RuntimeTelemetryCoordinator;
176
- private activeHarness?: SessionHarness<object | undefined>;
188
+ private activeHarness?: SessionHarness;
177
189
  private piAdapter?: HarnessRuntimeConfig;
178
190
  private readonly transcript: ConversationHistory;
179
191
  private conversationBridge?: HarnessConversationBridge;
@@ -358,14 +370,14 @@ export abstract class AgentRuntimeKernel<
358
370
  runtime: Awaited<ReturnType<typeof openSessionHarness>>,
359
371
  submissionId: string,
360
372
  ): 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
- }
373
+ const native = await runtime.submissionFor(submissionId, BACKGROUND_CONTEXT);
374
+ if (!native || native.entry === undefined) return;
375
+ const entries = await runtime.entries();
376
+ const start = entries.findIndex((entry) => entry.id === native.entry);
377
+ if (start < 0) throw new Error("Native Submission entry is missing from its conversation");
378
+ this.db.transaction(() => {
379
+ for (const entry of entries.slice(start)) this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
380
+ });
369
381
  }
370
382
 
371
383
  /**
@@ -390,7 +402,7 @@ export abstract class AgentRuntimeKernel<
390
402
  }
391
403
  await this.broadcastApprovals();
392
404
  const running = this.db.submissions.findRunning() as StoredSubmission | null;
393
- if (running) {
405
+ if (running && !running.unattendedFailure) {
394
406
  if (
395
407
  this.db.approvals.listPendingForSubmission(running.submissionId).length > 0 ||
396
408
  this.db.interactions.listPendingForSubmission(running.submissionId).length > 0
@@ -506,18 +518,21 @@ export abstract class AgentRuntimeKernel<
506
518
 
507
519
  // #region Submission 准入与配置固定
508
520
 
509
- /** Pin dynamic Context and execution policy before admission. */
521
+ /** Pin dynamic Context and execution policy before the first assembly is used. */
510
522
  private async admissionPin(fresh: boolean, submissionId: string): Promise<{
511
523
  revision: string;
512
524
  descriptor: string;
513
525
  }> {
514
526
  if (fresh) await this.prepareRuntimeForSubmission(submissionId);
515
- else await this.ensureRuntimeReady();
516
- return this.currentAssemblyPin();
527
+ const submission = this.db.submissions.find(submissionId);
528
+ if (!submission) {
529
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
530
+ }
531
+ return this.currentAssemblyPin(submission.origin !== "scheduled");
517
532
  }
518
533
 
519
534
  // initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
520
- private async currentAssemblyPin(): Promise<{
535
+ private async currentAssemblyPin(includeMemory = true): Promise<{
521
536
  revision: string;
522
537
  descriptor: string;
523
538
  }> {
@@ -526,16 +541,18 @@ export abstract class AgentRuntimeKernel<
526
541
  if (!runtime) {
527
542
  throw new Error("Runtime revision is not initialized");
528
543
  }
529
- return this.pinAssembly(runtime.pi, runtime.revision);
544
+ return this.pinAssembly(runtime.pi, runtime.revision, includeMemory);
530
545
  }
531
546
 
532
547
  private async pinAssembly(
533
548
  prepared: PreparedPiRuntime,
534
549
  baseRevision: string,
550
+ includeMemory: boolean,
535
551
  ): Promise<{ revision: string; descriptor: string }> {
536
552
  const pinned = await this.pi.pin({
537
553
  prepared,
538
554
  baseRevision,
555
+ includeMemory,
539
556
  readExtensionContext: ({ label }) =>
540
557
  this._hostGetContext(label),
541
558
  });
@@ -558,7 +575,7 @@ export abstract class AgentRuntimeKernel<
558
575
  submissions: this.db.submissions,
559
576
  transaction: (run) => this.db.transaction(run),
560
577
  prepareRuntime: async () => {
561
- await this.ensureRuntimeReady();
578
+ await this.prepareRuntimeForAdmission();
562
579
  return { runtimeKey: this.activeRuntime!.runtimeKey, admission: this.activeRuntime?.admission };
563
580
  },
564
581
  findUserMessage: (id) => this.transcript.findUserMessage(id),
@@ -615,11 +632,16 @@ export abstract class AgentRuntimeKernel<
615
632
  releaseAbortedHarness: async () => {
616
633
  const runtime = this.activeHarness;
617
634
  if (!runtime) return;
618
- await runtime.lane.waitForIdle(BACKGROUND_CONTEXT);
619
- if (this.activeHarness === runtime) this.activeHarness = undefined;
620
- await runtime.close(BACKGROUND_CONTEXT);
635
+ await runtime.conversation.waitForIdle(BACKGROUND_CONTEXT);
636
+ // The execution owner closes after transport projections settle.
637
+
638
+ },
639
+ cancelQueued: async (id) => {
640
+ const runtime = this.activeHarness;
641
+ if (!runtime) return undefined;
642
+ const result = await runtime.harness.abortSubmission(Number(id) as import("@earendil-works/pi-durable").SubmissionId, BACKGROUND_CONTEXT, runtime.conversation.id);
643
+ return { kind: result === "already_placed" || result === "settled" ? "already_consumed" as const : "cancelled" as const };
621
644
  },
622
- cancelQueued: async (id) => this.activeHarness?.lane.cancelQueued(id, BACKGROUND_CONTEXT),
623
645
  submit: (message, options) => this.submitMessage(message, options),
624
646
  waitUntil: (promise) => this.ctx.waitUntil(promise),
625
647
  });
@@ -764,17 +786,35 @@ export abstract class AgentRuntimeKernel<
764
786
  submission = await this.admission.pin(
765
787
  submission,
766
788
  () => this.admissionPin(mode === "fresh", submissionId),
767
- mode === "fresh" ? undefined : (key) => this.ensureRuntimeVersion(key),
789
+ (key) => this.ensureRuntimeVersion(key),
768
790
  );
769
791
  const activeRuntime = this.requireRuntime();
792
+ // A fresh provider Session per Submission keeps one failing upstream route from pinning later turns.
793
+ const recovery = new ModelRecovery(submissionId);
770
794
  const harnessInput = this.pi.harnessInputs(
771
795
  activeRuntime.pi,
772
796
  submission.assemblyDescriptor,
773
797
  {
774
- modelSessionId: submissionId,
775
- onGeneration: (event) => this.telemetry.generation(submissionId, event),
798
+ modelSessionId: () => recovery.sessionId(),
799
+ assertRequestAllowed: () => {
800
+ const current = this.db.submissions.find(submissionId);
801
+ if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
802
+ },
803
+ onGeneration: (event) => this.telemetry.generation(submissionId, event),
804
+ onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
776
805
  },
777
806
  );
807
+ const prepareToolInput = (toolName: string, input: unknown): unknown => {
808
+ const prepare = harnessInput.candidates.find(({ tool }) =>
809
+ tool.name === toolName
810
+ )?.tool.prepareArguments;
811
+ if (!prepare) return input;
812
+ try {
813
+ return prepare(input);
814
+ } catch {
815
+ return input;
816
+ }
817
+ };
778
818
  let runtime!: Awaited<ReturnType<typeof openSessionHarness>>;
779
819
  let publishApprovalRequest = async (
780
820
  _toolCallId: string,
@@ -785,6 +825,8 @@ export abstract class AgentRuntimeKernel<
785
825
  submissionId,
786
826
  requestId: submission.requestId,
787
827
  executionLevel: activeRuntime.snapshot.profile.executionLevel,
828
+ origin: submission.origin,
829
+ scheduledMaxExecutionLevel: submission.scheduledMaxExecutionLevel ?? undefined,
788
830
  },
789
831
  candidates: harnessInput.candidates,
790
832
  telemetry: this.telemetry,
@@ -812,11 +854,14 @@ export abstract class AgentRuntimeKernel<
812
854
  }
813
855
  },
814
856
  gateTool: harnessInput.platform.gateTool,
857
+ onUnattendedFailure: (reason) => {
858
+ this.db.submissions.recordUnattendedFailure(submissionId, reason);
859
+ },
860
+ unattendedFailure: () => this.db.submissions.find(submissionId)?.unattendedFailure,
815
861
  });
816
862
  runtime = await openSessionHarness({
817
- session: openCloudflareSession(this.ctx.storage),
863
+ storage: openCloudflareSession(this.ctx.storage),
818
864
  models: harnessInput.models,
819
- modelSessionId: submissionId,
820
865
  model: harnessInput.model,
821
866
  thinkingLevel: harnessInput.thinkingLevel,
822
867
  systemPrompt: harnessInput.systemPrompt,
@@ -824,30 +869,29 @@ export abstract class AgentRuntimeKernel<
824
869
  workspace: harnessInput.workspace,
825
870
  authorize: gate.authorize.bind(gate),
826
871
  resolveReplay: gate.resolveReplay.bind(gate),
872
+ resolveConfirmation: gate.resolveConfirmation.bind(gate),
827
873
  }),
874
+ ...harnessToolActivation(harnessInput.candidates),
828
875
  modelToolResultExemptions: harnessInput.candidates
829
876
  .filter(({ outputBudget }) => outputBudget?.kind === "structure")
830
877
  .map(({ tool }) => tool.name),
831
878
  streamOptions: { maxRetries: 0 },
832
879
  compaction: harnessInput.compaction,
833
880
  toolExecution: "parallel",
881
+ recovery,
882
+ beforeTool: (call, _api, context) => gate.beforeTool({ toolName: call.name, toolCallId: call.id, args: call.arguments }, context),
883
+ afterResponse: (message) => {
884
+ for (const part of message.content) if (part.type === "toolCall") part.arguments = prepareToolInput(part.name, part.arguments) as JsonObject;
885
+ },
834
886
  }, BACKGROUND_CONTEXT);
835
887
  this.activeHarness = runtime;
836
- let stream: ReturnType<typeof bindHarnessStream> | undefined;
888
+ let stream: Awaited<ReturnType<typeof bindHarnessStream>> | undefined;
837
889
  let deactivate = () => {};
890
+ let released = false;
838
891
  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
892
 
846
- if (submission.abortReason && runtime.openOperation) {
847
- await runtime.requestAbort(
848
- runtime.openOperation.operationId,
849
- BACKGROUND_CONTEXT,
850
- );
893
+ if ((submission.abortReason || submission.unattendedFailure) && runtime.openOperation) {
894
+ await runtime.abort(BACKGROUND_CONTEXT);
851
895
  }
852
896
 
853
897
  await this.associateHarnessEntries(runtime, submissionId);
@@ -858,21 +902,6 @@ export abstract class AgentRuntimeKernel<
858
902
  (id) => this.transcript.regenerationTarget(id),
859
903
  BACKGROUND_CONTEXT,
860
904
  );
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
905
  if (submission.status === "pending") {
877
906
  this.db.submissions.transition(submissionId, "running", ["pending"]);
878
907
  this.telemetry.capture("turnStarted", {
@@ -886,70 +915,58 @@ export abstract class AgentRuntimeKernel<
886
915
  steers: this.db.steers,
887
916
  messageUi: this.db.messageUi,
888
917
  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;
918
+ enqueue: async (steer) => {
919
+ const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
920
+ const queued = await runtime.conversation.submit({ type: "input", content: message.content, requestId: `steer:${steer.messageId}`, whenBusy: "steer" }, BACKGROUND_CONTEXT);
921
+ return queued.status(BACKGROUND_CONTEXT);
893
922
  },
923
+ find: (messageId) => runtime.submissionFor(`steer:${messageId}`, BACKGROUND_CONTEXT),
894
924
  });
895
925
  const uiMessageBody = submission.queuedUiMessageJson;
896
926
  let initialUserSidecarPersisted = (await this.transcript.storedMessages())
897
927
  .some((entry) =>
898
928
  entry.submissionId === submissionId && entry.message.role === "user"
899
929
  );
900
- runtime.harness.events.on("entry_added", (event) => {
901
- const entry = event.entry;
930
+ const unsubscribeEntries = runtime.harness.subscribeCommits((publication) => {
902
931
  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);
932
+ for (const change of publication.changes) {
933
+ if (change.type === "submission") steerDelivery.settleRecord(change.value);
934
+ if (change.type !== "entry" || change.value.conversationId !== runtime.conversation.id) continue;
935
+ const entry = change.value;
936
+ this.db.submissions.linkSessionEntry(String(entry.id), submissionId);
937
+ if (entry.kind === "pi.user" && !initialUserSidecarPersisted && uiMessageBody) {
938
+ this.db.messageUi.upsert(String(entry.id), uiMessageBody);
907
939
  initialUserSidecarPersisted = true;
908
940
  }
909
941
  }
910
942
  });
911
-
912
943
  });
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, {
944
+ await this.runtimeTurnEvents.observe(runtime, submissionId, harnessInput.model);
945
+ stream = await bindHarnessStream(runtime, {
922
946
  messageId: submission.assistantMessageId,
923
947
  userMessageId: submission.userMessageId ?? undefined,
924
948
  startedAt: submission.createdAt,
925
949
  turnId: submissionId,
950
+ prepareToolInput,
926
951
  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
- }
952
+ for (const chunk of chunks) this.conversation.broadcastResponse({ id: submission.requestId, body: json(chunk), done: false, continuation });
935
953
  },
936
954
  });
955
+ deactivate = unsubscribeEntries;
956
+
937
957
  publishApprovalRequest = stream.publishApprovalRequest;
938
- this.conversation.activate(runtime.lane, submission);
958
+ this.conversation.activate(runtime, submission);
939
959
 
940
960
  const control = {
941
961
  abort: async () => {
942
962
  if (runtime.openOperation) {
943
- await runtime.requestAbort(
944
- runtime.openOperation.operationId,
945
- BACKGROUND_CONTEXT,
946
- );
963
+ await runtime.abort(BACKGROUND_CONTEXT);
947
964
  }
948
965
  },
949
966
  interrupt: () => void runtime.close(BACKGROUND_CONTEXT),
950
967
  steer: async (_message: PiCanonicalUserInput, messageId: string) => {
951
968
  const steer = this.db.steers.findByMessageId(messageId);
952
- if (steer) await steerDelivery.queue(steer);
969
+ if (steer && runtime.openOperation) await steerDelivery.queue(steer);
953
970
  },
954
971
  };
955
972
  const turn: ActiveTurn = {
@@ -960,36 +977,28 @@ export abstract class AgentRuntimeKernel<
960
977
  continuation,
961
978
  agent: control,
962
979
  };
963
- deactivate = this.submissions.activate(submission, turn);
964
- const outcome = await runtime.run(preparation.request, BACKGROUND_CONTEXT, {
965
- pollDeferred: !preparation.request,
966
- });
980
+ const deactivateTurn = this.submissions.activate(submission, turn);
981
+ const unsubscribeEntriesAndTurn = deactivate;
982
+ deactivate = () => { unsubscribeEntriesAndTurn(); deactivateTurn(); };
983
+ await runtime.admit(preparation, BACKGROUND_CONTEXT);
984
+ await steerDelivery.reconcile(submissionId);
985
+ const outcome = await runtime.run(undefined, BACKGROUND_CONTEXT);
967
986
  this.db.submissions.clearQueuedPayload(submissionId);
968
987
  if (outcome.kind === "released") {
969
- if (
970
- outcome.reason === "slice" ||
971
- outcome.reason === "budget" ||
972
- outcome.reason === "tool-failure"
973
- ) {
988
+ if (outcome.reason === "slice") {
974
989
  await this.schedule(0, "_piHarnessContinuation", { submissionId }, {
975
990
  idempotent: true,
976
991
  });
977
992
  }
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
- );
993
+ released = true;
989
994
  return this.db.submissions.find(submissionId)!;
990
995
  }
991
996
  const record = outcome.outcome;
992
- const status = record.status === "completed" || record.status === "declined"
997
+ const unattendedFailure = this.db.submissions.find(submissionId)
998
+ ?.unattendedFailure;
999
+ const status = unattendedFailure
1000
+ ? "failed"
1001
+ : record.status === "completed" || record.status === "declined"
993
1002
  ? "succeeded"
994
1003
  : record.status === "aborted"
995
1004
  ? "aborted"
@@ -998,7 +1007,7 @@ export abstract class AgentRuntimeKernel<
998
1007
  const terminal = await this.submissions.finish(
999
1008
  submission,
1000
1009
  status,
1001
- record.error?.message,
1010
+ unattendedFailure ?? record.error?.message,
1002
1011
  );
1003
1012
  const projectedOutcome = projectSubmissionOutcome(record, terminal);
1004
1013
  await stream.flushTerminal(projectedOutcome);
@@ -1015,13 +1024,21 @@ export abstract class AgentRuntimeKernel<
1015
1024
  }));
1016
1025
  throw error;
1017
1026
  } finally {
1018
- await this.conversation.release(runtime.lane).catch((error) => {
1027
+ await this.conversation.release(runtime).catch((error) => {
1019
1028
  console.warn("[runtime-stream:degraded]", errorText(error));
1020
1029
  });
1021
- stream?.unsubscribe();
1030
+ await stream?.unsubscribe();
1022
1031
  deactivate();
1023
1032
  if (this.activeHarness === runtime) this.activeHarness = undefined;
1024
1033
  await runtime.close(BACKGROUND_CONTEXT).catch(() => undefined);
1034
+ if (released) {
1035
+ // Pi closes the Harness on release and drops undelivered stream frames; the
1036
+ // canonical transcript gives a paused question or approval its complete input.
1037
+ this.broadcast(json({
1038
+ type: MessageType.CF_AGENT_CHAT_MESSAGES,
1039
+ messages: await this.getMessages(),
1040
+ }));
1041
+ }
1025
1042
  }
1026
1043
  }
1027
1044
 
@@ -1055,7 +1072,7 @@ export abstract class AgentRuntimeKernel<
1055
1072
  /** Start the native resume handshake from Pi's current lane snapshot. */
1056
1073
  async onConnect(connection: Connection): Promise<void> {
1057
1074
  this.conversation.disconnect(connection);
1058
- await this.conversation.resume(connection);
1075
+ await this.conversation.connect(connection);
1059
1076
  }
1060
1077
 
1061
1078
  async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise<void> {
@@ -1152,10 +1169,8 @@ export abstract class AgentRuntimeKernel<
1152
1169
 
1153
1170
  if (event.type === "clear") {
1154
1171
  if (this.submissions.isBusy()) return;
1155
- this.db.transaction(() => {
1156
- this.transcript.clear();
1157
- this.db.clearAll();
1158
- });
1172
+ await this.transcript.clear();
1173
+ this.db.transaction(() => this.db.clearAll());
1159
1174
  await this.broadcastApprovals();
1160
1175
  this.broadcast(json({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
1161
1176
  return;
@@ -1297,7 +1312,7 @@ export abstract class AgentRuntimeKernel<
1297
1312
  await this.broadcastApprovals();
1298
1313
  return;
1299
1314
  }
1300
- this.transcript.append(crypto.randomUUID(), message);
1315
+ await this.transcript.append(crypto.randomUUID(), message);
1301
1316
  }
1302
1317
 
1303
1318
  async _hostGetSessionInfo(): Promise<{ messageCount: number }> {
@@ -1320,7 +1335,36 @@ export abstract class AgentRuntimeKernel<
1320
1335
  async addCanonicalSnapshot(
1321
1336
  snapshot: PiCanonicalTranscriptSnapshot,
1322
1337
  ): Promise<void> {
1323
- this.transcript.importSnapshot(snapshot);
1338
+ await this.transcript.importSnapshot(snapshot);
1339
+ }
1340
+
1341
+ /**
1342
+ * 本 Session 是否有一条未结束的定时 Submission。
1343
+ *
1344
+ * 定时 Run 未结束时独占输入:普通消息在准入前被拒绝,不排队等它结束。
1345
+ * 定时注入本身走 `submitScheduledPrompt`,不经过这道闸。
1346
+ */
1347
+ private get scheduledInputLocked(): boolean {
1348
+ return this.db.submissions.hasUnfinishedScheduled();
1349
+ }
1350
+
1351
+ private rejectedOrdinaryInputReceipt(): SubmissionReceipt {
1352
+ return {
1353
+ submissionId: crypto.randomUUID(),
1354
+ status: "skipped",
1355
+ accepted: false,
1356
+ error: SCHEDULED_INPUT_LOCKED_MESSAGE,
1357
+ retryable: false,
1358
+ createdAt: Date.now(),
1359
+ };
1360
+ }
1361
+
1362
+ private rejectInputLocked(): MessageDispatchReceipt {
1363
+ return {
1364
+ kind: "rejected",
1365
+ code: "input_locked",
1366
+ message: SCHEDULED_INPUT_LOCKED_MESSAGE,
1367
+ };
1324
1368
  }
1325
1369
 
1326
1370
  /** Admit text and return immediately; completion continues in the Durable Object. */
@@ -1328,6 +1372,7 @@ export abstract class AgentRuntimeKernel<
1328
1372
  text: string,
1329
1373
  options?: { idempotencyKey?: string },
1330
1374
  ): Promise<SubmissionReceipt> {
1375
+ if (this.scheduledInputLocked) return this.rejectedOrdinaryInputReceipt();
1331
1376
  const submitted = await this.submitMessage({
1332
1377
  role: "user",
1333
1378
  content: [{ type: "text", text }],
@@ -1357,17 +1402,24 @@ export abstract class AgentRuntimeKernel<
1357
1402
  }
1358
1403
 
1359
1404
  async dispatchMessage(message: UIMessage, delivery: MessageDelivery): Promise<MessageDispatchReceipt> {
1405
+ if (this.scheduledInputLocked) return this.rejectInputLocked();
1360
1406
  return this.delivery.dispatch(message, delivery);
1361
1407
  }
1362
1408
 
1363
1409
  async steerQueuedSubmission(submissionId: string): Promise<MessageDispatchReceipt> {
1410
+ if (this.scheduledInputLocked) return this.rejectInputLocked();
1364
1411
  return this.delivery.steerQueued(submissionId);
1365
1412
  }
1366
1413
 
1367
1414
  /** Admit scheduled work after a bounded stable wait, otherwise return skipped. */
1368
1415
  async submitScheduledPrompt(
1369
1416
  prompt: string,
1370
- options?: { idempotencyKey?: string },
1417
+ options?: {
1418
+ idempotencyKey?: string;
1419
+ scheduledMaxExecutionLevel?: ExecutionLevel;
1420
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
1421
+ unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
1422
+ },
1371
1423
  ): Promise<SubmissionReceipt> {
1372
1424
  const requestId = crypto.randomUUID();
1373
1425
  const submitted = await this.submitMessageWhenStable(
@@ -1379,6 +1431,8 @@ export abstract class AgentRuntimeKernel<
1379
1431
  {
1380
1432
  requestId,
1381
1433
  idempotencyKey: options?.idempotencyKey,
1434
+ origin: "scheduled",
1435
+ scheduledMaxExecutionLevel: options?.scheduledMaxExecutionLevel,
1382
1436
  },
1383
1437
  SCHEDULED_STABLE_TIMEOUT_MS,
1384
1438
  );
@@ -1463,7 +1517,7 @@ export abstract class AgentRuntimeKernel<
1463
1517
  summary: approval.summary,
1464
1518
  executionLevel: approval.executionLevel,
1465
1519
  requiredExecutionLevel: approval.requiredExecutionLevel,
1466
- inputJson: approval.inputJson,
1520
+ inputJson: approval.confirmationJson ?? approval.inputJson,
1467
1521
  requestId: approval.requestId,
1468
1522
  };
1469
1523
  }
@@ -1573,6 +1627,7 @@ export abstract class AgentRuntimeKernel<
1573
1627
  hasPendingInteraction: (id) => this.humanDecisions.isWaitingForInteraction(id),
1574
1628
  hasPendingSteer: (id) => this.db.steers.listForSubmission(id).length > 0,
1575
1629
  hasRunningAgentTools: this.db.agentTools.hasRunning(),
1630
+ hasUnfinishedScheduled: this.db.submissions.hasUnfinishedScheduled(),
1576
1631
  previousState: this.state,
1577
1632
  });
1578
1633
  if (changed) this.setState(state);