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

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 (141) hide show
  1. package/package.json +3 -3
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1592 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  8. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  9. package/src/{db → adapter/cloudflare}/schema.ts +16 -83
  10. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  11. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  12. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  13. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  14. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
  15. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  16. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  17. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  18. package/src/config/activation.ts +140 -0
  19. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  20. package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
  21. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  22. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  23. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  24. package/src/config/harness-runtime.ts +182 -0
  25. package/src/config/index.ts +14 -0
  26. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
  27. package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
  28. package/src/{kernel → config}/profile.ts +7 -0
  29. package/src/{kernel → config}/public-contracts.ts +6 -6
  30. package/src/{kernel → config}/runtime-load.ts +1 -1
  31. package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
  32. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  33. package/src/context/extension/index.ts +3 -0
  34. package/src/context/index.ts +6 -0
  35. package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
  36. package/src/conversation/harness-resume.ts +288 -0
  37. package/src/conversation/harness-stream.ts +98 -0
  38. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  39. package/src/conversation/index.ts +6 -0
  40. package/src/{pi → conversation}/message/contract.ts +3 -0
  41. package/src/{pi → conversation}/message/conversion.ts +75 -0
  42. package/src/{pi → conversation}/message/index.ts +1 -0
  43. package/src/{pi → conversation}/message/model-input.ts +1 -1
  44. package/src/{pi → conversation}/message/projection.ts +76 -10
  45. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  46. package/src/conversation/state.ts +175 -0
  47. package/src/harness/index.ts +7 -0
  48. package/src/harness/session-harness.ts +481 -0
  49. package/src/harness/submission-operation.ts +49 -0
  50. package/src/index.ts +38 -43
  51. package/src/lib/mcp.ts +1 -1
  52. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
  53. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  54. package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
  55. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  56. package/src/session/index.ts +39 -0
  57. package/src/submission/admission.ts +275 -0
  58. package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
  59. package/src/submission/delivery.ts +212 -0
  60. package/src/submission/finalization.ts +111 -0
  61. package/src/submission/index.ts +9 -0
  62. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  63. package/src/{db/submission.repo.ts → submission/repository.ts} +62 -119
  64. package/src/submission/steer-delivery.ts +167 -0
  65. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  66. package/src/telemetry/contract.ts +12 -0
  67. package/src/telemetry/coordinator.ts +65 -0
  68. package/src/telemetry/delivery.ts +2 -2
  69. package/src/telemetry/index.ts +5 -0
  70. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  71. package/src/telemetry/recorder.ts +1 -1
  72. package/src/telemetry/runtime-telemetry.ts +12 -0
  73. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  74. package/src/telemetry/turn-events.ts +279 -0
  75. package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
  76. package/src/tools/approval/index.ts +1 -0
  77. package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
  78. package/src/{pi/tool → tools}/base.ts +14 -30
  79. package/src/tools/compiler.ts +327 -0
  80. package/src/{pi/tool → tools}/core-host.ts +3 -3
  81. package/src/{pi/tool → tools}/core.ts +3 -3
  82. package/src/tools/decision-gate.ts +227 -0
  83. package/src/{pi/tool → tools}/gateway.ts +1 -1
  84. package/src/tools/human-decisions.ts +127 -0
  85. package/src/tools/index.ts +30 -0
  86. package/src/tools/interaction/index.ts +1 -0
  87. package/src/{pi/tool → tools}/mcp.ts +1 -1
  88. package/src/{pi/tool → tools}/schedule.ts +3 -3
  89. package/src/{pi/tool → tools}/skill.ts +4 -4
  90. package/src/{kernel → tools}/subagent-runtime.ts +1 -1
  91. package/src/{pi/tool → tools}/subagent.ts +4 -4
  92. package/src/tools/temporary-agent/approvals.ts +126 -0
  93. package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
  94. package/src/tools/temporary-agent/core.ts +53 -0
  95. package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
  96. package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
  97. package/src/{pi/tool → tools}/web-search/api.ts +1 -1
  98. package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
  99. package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
  100. package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
  101. package/src/db/milestone.repo.ts +0 -104
  102. package/src/db/settlement.repo.ts +0 -104
  103. package/src/kernel/approval-lifecycle.ts +0 -638
  104. package/src/kernel/interaction-lifecycle.ts +0 -433
  105. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  106. package/src/kernel/state.ts +0 -91
  107. package/src/pi/assembly/index.ts +0 -19
  108. package/src/pi/runtime-adapter/execution.ts +0 -1163
  109. package/src/pi/runtime-adapter/index.ts +0 -293
  110. package/src/pi/runtime-adapter/recovery.ts +0 -982
  111. package/src/pi/session/index.ts +0 -24
  112. package/src/pi/tool/compiler.ts +0 -483
  113. package/src/pi/tool/index.ts +0 -38
  114. package/src/pi/turn/approval.ts +0 -181
  115. package/src/pi/turn/index.ts +0 -82
  116. package/src/pi/turn/interaction.ts +0 -181
  117. package/src/runtime.ts +0 -4760
  118. /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  119. /package/src/{kernel → config}/degradation.ts +0 -0
  120. /package/src/{kernel → config}/runtime-config.ts +0 -0
  121. /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  122. /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  123. /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  124. /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  125. /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  126. /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  127. /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  128. /package/src/{pi/tool → tools}/declared.ts +0 -0
  129. /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
  130. /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
  131. /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
  132. /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
  133. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
  134. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
  135. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
  136. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
  137. /package/src/{pi/tool → tools}/time.ts +0 -0
  138. /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
  139. /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
  140. /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
  141. /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
@@ -1,5 +1,80 @@
1
1
  import type { UserMessage } from "@earendil-works/pi-ai";
2
2
  import type { UIMessage } from "ai";
3
+ import type { RequestedCapability } from "./contract";
4
+
5
+ export function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
6
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
7
+ return [];
8
+ }
9
+ const record = metadata as Record<string, unknown>;
10
+ if (!("requestedCapabilities" in record)) return [];
11
+ if (!Array.isArray(record.requestedCapabilities)) {
12
+ throw new Error("requestedCapabilities must be an array");
13
+ }
14
+ const result: RequestedCapability[] = [];
15
+ const seen = new Set<string>();
16
+ for (const value of record.requestedCapabilities) {
17
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
18
+ throw new Error("requestedCapabilities contains an invalid capability");
19
+ }
20
+ const capability = value as Record<string, unknown>;
21
+ if (
22
+ (capability.kind !== "skill" && capability.kind !== "tool" && capability.kind !== "plan") ||
23
+ typeof capability.name !== "string" ||
24
+ !capability.name.trim() ||
25
+ typeof capability.label !== "string" ||
26
+ !capability.label.trim()
27
+ ) {
28
+ throw new Error("requestedCapabilities contains an invalid capability");
29
+ }
30
+ const key = `${capability.kind}:${capability.name}`;
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ result.push({ kind: capability.kind, name: capability.name, label: capability.label });
34
+ }
35
+ return result;
36
+ }
37
+
38
+ /** Resolve user-selected capabilities against the loaded Runtime before model input conversion. */
39
+ export function normalizeUIUserInput(
40
+ message: UIMessage & { role: "user" },
41
+ installed: {
42
+ readonly hasSkill: (name: string) => boolean;
43
+ readonly hasTool: (name: string) => boolean;
44
+ },
45
+ ): UserMessage {
46
+ const context: string[] = [];
47
+ for (const capability of requestedCapabilitiesOf(message.metadata)) {
48
+ if (capability.kind === "skill") {
49
+ if (!installed.hasSkill(capability.name)) {
50
+ throw new Error(`Requested Skill is not installed: ${capability.name}`);
51
+ }
52
+ context.push(
53
+ `requested capability: skill/${capability.name}`,
54
+ `required action: call activate_skill for "${capability.name}" before handling the task`,
55
+ );
56
+ continue;
57
+ }
58
+ if (capability.kind === "tool") {
59
+ if (!installed.hasTool(capability.name)) {
60
+ throw new Error(`Requested Tool is not installed: ${capability.name}`);
61
+ }
62
+ context.push(
63
+ `requested capability: tool/${capability.name}`,
64
+ `required action: call tool "${capability.name}" when handling the task`,
65
+ );
66
+ continue;
67
+ }
68
+ if (capability.name !== "plan") {
69
+ throw new Error(`Unknown plan capability: ${capability.name}`);
70
+ }
71
+ context.push(
72
+ "requested capability: plan/plan",
73
+ "required action: call update_plan with the complete plan before other work, then wait for user confirmation before execution",
74
+ );
75
+ }
76
+ return uiUserMessageToPi(message, context.join("\n"));
77
+ }
3
78
 
4
79
  export interface UIUserSidecar {
5
80
  readonly metadata?: unknown;
@@ -1,3 +1,4 @@
1
1
  export * from "./contract";
2
2
  export * from "./conversion";
3
3
  export * from "./projection";
4
+ export * from "./model-input";
@@ -1,5 +1,5 @@
1
1
  import type { Message, UserMessage } from "@earendil-works/pi-ai";
2
- import type { RuntimeModelInputModality } from "../../kernel/bindings";
2
+ import type { RuntimeModelInputModality } from "../../config/profile";
3
3
 
4
4
  const UNSUPPORTED_FILE_PLACEHOLDER =
5
5
  "(file omitted: model API does not support this file type)";
@@ -11,7 +11,7 @@ import type {
11
11
  UIMessage,
12
12
  UIMessageChunk,
13
13
  } from "ai";
14
- import { USER_STOP_REASON } from "../../kernel/receipts";
14
+ import { USER_STOP_REASON } from "../../submission/contracts";
15
15
 
16
16
  const MAX_OUTPUT_BYTES = 256 * 1024;
17
17
  const MAX_OUTPUT_PREVIEW = 16 * 1024;
@@ -174,6 +174,7 @@ export class PiChunkEncoder {
174
174
  private readonly messageId: string;
175
175
  private readonly startedAt?: number;
176
176
  private readonly turnId?: string;
177
+ private readonly userMessageId?: string;
177
178
  private started = false;
178
179
  private finished = false;
179
180
  private firstTurnStarted = false;
@@ -192,10 +193,12 @@ export class PiChunkEncoder {
192
193
  messageId: string;
193
194
  startedAt?: number;
194
195
  turnId?: string;
196
+ userMessageId?: string | null;
195
197
  }) {
196
198
  this.messageId = options.messageId;
197
199
  this.startedAt = options.startedAt;
198
200
  this.turnId = options.turnId;
201
+ this.userMessageId = options.userMessageId ?? undefined;
199
202
  }
200
203
 
201
204
  start(): UIMessageChunk[] {
@@ -213,6 +216,7 @@ export class PiChunkEncoder {
213
216
  createdAt: this.startedAt,
214
217
  turnStatus: "running",
215
218
  ...(this.turnId ? { turnId: this.turnId } : {}),
219
+ ...(this.userMessageId ? { userMessageId: this.userMessageId } : {}),
216
220
  },
217
221
  }),
218
222
  },
@@ -220,6 +224,54 @@ export class PiChunkEncoder {
220
224
  ];
221
225
  }
222
226
 
227
+ /** Seed this encoder and its open part IDs from one Pi-backed UI snapshot. */
228
+ restore(message: UIMessage, streaming: boolean): UIMessageChunk[] {
229
+ if (this.started) throw new Error("Cannot restore an already-started stream encoder");
230
+ this.started = true;
231
+ this.firstTurnStarted = message.parts.length === 0;
232
+ const chunks: UIMessageChunk[] = [{ type: "start", messageId: this.messageId,
233
+ ...(message.metadata === undefined ? {} : { messageMetadata: message.metadata }) }];
234
+ let contentIndex = 0;
235
+ let hasStep = false;
236
+ let lastContentIndex = message.parts.length - 1;
237
+ while (lastContentIndex >= 0 && message.parts[lastContentIndex]?.type === "step-start") lastContentIndex -= 1;
238
+ for (const [partIndex, part] of message.parts.entries()) {
239
+ if (part.type === "step-start") {
240
+ if (hasStep) { chunks.push({ type: "finish-step" }); this.stepIndex += 1; }
241
+ chunks.push({ type: "start-step" });
242
+ hasStep = true;
243
+ contentIndex = 0;
244
+ continue;
245
+ }
246
+ if (!hasStep) { chunks.push({ type: "start-step" }); hasStep = true; }
247
+ if (part.type === "text" || part.type === "reasoning") {
248
+ const kind = part.type;
249
+ const id = this.partId(kind, contentIndex);
250
+ chunks.push({ type: `${kind}-start`, id });
251
+ if (part.text) chunks.push({ type: `${kind}-delta`, id, delta: part.text });
252
+ if (streaming && partIndex === lastContentIndex) {
253
+ (kind === "text" ? this.activeText : this.activeReasoning).add(contentIndex);
254
+ } else chunks.push({ type: `${kind}-end`, id });
255
+ } else if (part.type === "dynamic-tool") {
256
+ if (part.state === "input-streaming") {
257
+ chunks.push({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName, dynamic: true });
258
+ this.toolCallIdByIndex.set(contentIndex, part.toolCallId);
259
+ this.streamingToolById.set(part.toolCallId, { toolName: part.toolName, started: true, deltas: [] });
260
+ } else {
261
+ chunks.push(...this.toolInput(part.toolCallId, part.toolName, part.input));
262
+ if (part.state === "approval-requested") chunks.push(...this.approvalRequested(part.toolCallId, part.approval.id));
263
+ else if (part.state === "output-available") chunks.push({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output, preliminary: part.preliminary, dynamic: true });
264
+ else if (part.state === "output-error") chunks.push({ type: "tool-output-error", toolCallId: part.toolCallId, errorText: part.errorText, dynamic: true });
265
+ else if (part.state === "output-denied") chunks.push({ type: "tool-output-denied", toolCallId: part.toolCallId });
266
+ }
267
+ }
268
+ contentIndex += 1;
269
+ }
270
+ if (!hasStep) chunks.push({ type: "start-step" });
271
+ else if (!streaming) chunks.push({ type: "finish-step" });
272
+ return chunks;
273
+ }
274
+
223
275
  toolInput(
224
276
  toolCallId: string,
225
277
  toolName: string,
@@ -262,7 +314,10 @@ export class PiChunkEncoder {
262
314
  ];
263
315
  }
264
316
 
265
- encode(event: AgentEvent): UIMessageChunk[] {
317
+ encode(
318
+ event: AgentEvent,
319
+ options: { readonly terminal?: boolean } = {},
320
+ ): UIMessageChunk[] {
266
321
  if (this.finished) return [];
267
322
  switch (event.type) {
268
323
  case "agent_start":
@@ -318,7 +373,7 @@ export class PiChunkEncoder {
318
373
  case "message_end":
319
374
  this.firstTurnStarted = false;
320
375
  return event.message.role === "assistant"
321
- ? this.endAssistant(event.message)
376
+ ? this.endAssistant(event.message, options.terminal !== false)
322
377
  : [];
323
378
  case "agent_end":
324
379
  case "turn_end":
@@ -468,15 +523,25 @@ export class PiChunkEncoder {
468
523
  return chunks;
469
524
  }
470
525
 
471
- private endAssistant(message: AssistantMessage): UIMessageChunk[] {
526
+ private endAssistant(message: AssistantMessage, terminal = true): UIMessageChunk[] {
472
527
  this.pendingToolByIndex.clear();
473
528
  this.toolCallIdByIndex.clear();
474
529
  const chunks = this.closeOpenParts();
475
- if (message.stopReason === "toolUse") {
530
+ if (message.stopReason === "toolUse" || !terminal) {
476
531
  chunks.push({ type: "finish-step" });
477
532
  return chunks;
478
533
  }
534
+ const outcome = classifyPiTerminalStopReason(message.stopReason);
535
+ if (outcome.outcome === "succeeded") chunks.push({ type: "finish-step" });
536
+ chunks.push(...this.finish(message));
537
+ return chunks;
538
+ }
539
+
540
+ /** Finalize the UI operation after its last model step has already closed. */
541
+ finish(message: Pick<AssistantMessage, "stopReason" | "timestamp" | "errorMessage">): UIMessageChunk[] {
542
+ if (this.finished) return [];
479
543
  this.finished = true;
544
+ const chunks: UIMessageChunk[] = [];
480
545
  const publicError = publicAssistantError(message.errorMessage);
481
546
  const terminal = classifyPiTerminalStopReason(message.stopReason);
482
547
  if (this.startedAt !== undefined) {
@@ -507,7 +572,6 @@ export class PiChunkEncoder {
507
572
  });
508
573
  return chunks;
509
574
  }
510
- chunks.push({ type: "finish-step" });
511
575
  chunks.push({
512
576
  type: "finish",
513
577
  finishReason: terminal.finishReason ?? "other",
@@ -555,7 +619,7 @@ function projectedTool(
555
619
  const questions: unknown = JSON.parse(input.questions);
556
620
  if (Array.isArray(questions)) input.questions = questions;
557
621
  } catch {
558
- // Preserve malformed legacy input for the normal unavailable state.
622
+ // Preserve malformed provider output for the normal unavailable state.
559
623
  }
560
624
  }
561
625
  const base = {
@@ -601,15 +665,16 @@ export function piAssistantToUIMessage(
601
665
  readonly id: string;
602
666
  readonly metadata?: unknown;
603
667
  readonly approvals?: Readonly<Record<string, PiToolApprovalView | undefined>>;
668
+ readonly state?: "streaming" | "done";
604
669
  },
605
670
  ): UIMessage {
606
671
  const parts = message.content.flatMap(
607
672
  (content): Array<UIMessage["parts"][number]> => {
608
673
  if (content.type === "text") {
609
- return [{ type: "text", text: content.text, state: "done" }];
674
+ return [{ type: "text", text: content.text, state: options.state ?? "done" }];
610
675
  }
611
676
  if (content.type === "thinking") {
612
- return [{ type: "reasoning", text: content.thinking, state: "done" }];
677
+ return [{ type: "reasoning", text: content.thinking, state: options.state ?? "done" }];
613
678
  }
614
679
  if (content.type === "toolCall") {
615
680
  return [projectedTool(content, options.approvals?.[content.id])];
@@ -629,6 +694,7 @@ export function piAssistantToUIMessage(
629
694
  export function applyPiToolResult(
630
695
  message: UIMessage,
631
696
  result: ToolResultMessage,
697
+ options: { readonly preliminary?: boolean } = {},
632
698
  ): UIMessage {
633
699
  return {
634
700
  ...message,
@@ -658,7 +724,7 @@ export function applyPiToolResult(
658
724
  state: "output-available",
659
725
  input: part.input,
660
726
  output: toolOutput({ content: result.content, details: result.details }),
661
- preliminary: false,
727
+ preliminary: options.preliminary ?? false,
662
728
  ...(approval ? { approval } : {}),
663
729
  };
664
730
  }),
@@ -30,6 +30,10 @@ export class MessageUiRepository {
30
30
  `;
31
31
  }
32
32
 
33
+ delete(messageId: string): void {
34
+ this.sql`DELETE FROM pi_message_ui WHERE message_id = ${messageId}`;
35
+ }
36
+
33
37
  // 删除全部 Message UI 补充数据。
34
38
  // Transcript.clear 在清空标准会话时调用,并由上层事务把两类数据一起提交。
35
39
  // 该责任曾属于 RuntimeDatabase.clearAll,现在归 Transcript 所有;不能两处分别维护清理规则。
@@ -0,0 +1,175 @@
1
+ import type { ApprovalReceipt } from "../submission/contracts";
2
+ import type { StoredSubmission } from "../submission/repository";
3
+ import type { PendingApprovalSummary } from "../tools/approval/repository";
4
+ import type { PiCanonicalUserInput } from "./message/contract";
5
+
6
+ export type RuntimeLoadPhase = "config" | "assembly" | "mcp" | "pi";
7
+
8
+ export type RuntimeActivity = "idle" | "working" | "needs-input";
9
+
10
+ export interface RuntimeActivityProjection {
11
+ activity: RuntimeActivity;
12
+ /**
13
+ * 是否仍有 Agent Tool 子运行在执行。
14
+ *
15
+ * `activity` 三态互斥,表达不了「等人回应的同时后台还在跑」。Host 的列表需要
16
+ * 这两件事各自成立,所以它是一个独立维度,而不是第四种活动值。
17
+ */
18
+ backgroundWork: boolean;
19
+ revision: number;
20
+ }
21
+
22
+ /**
23
+ * Runtime 最近一次装载尝试的公开状态。
24
+ *
25
+ * `available` 与本次尝试分开表达:强制重载失败时,旧 Runtime 仍可能继续服务。
26
+ * 该状态由 Agents SDK 的 Agent state 同步给浏览器,不包含底层异常文本。
27
+ */
28
+ export type RuntimeLoadState =
29
+ | {
30
+ status: "idle";
31
+ available: false;
32
+ }
33
+ | {
34
+ status: "loading";
35
+ phase: RuntimeLoadPhase;
36
+ available: boolean;
37
+ startedAt: number;
38
+ updatedAt: number;
39
+ }
40
+ | {
41
+ status: "ready";
42
+ available: true;
43
+ startedAt: number;
44
+ completedAt: number;
45
+ }
46
+ | {
47
+ status: "error";
48
+ phase: RuntimeLoadPhase;
49
+ available: boolean;
50
+ startedAt: number;
51
+ failedAt: number;
52
+ };
53
+
54
+ export interface RuntimeQueuedSubmission {
55
+ submissionId: string;
56
+ messageId: string;
57
+ preview: string;
58
+ position: number;
59
+ createdAt: number;
60
+ }
61
+
62
+ export interface RuntimeTurnState {
63
+ activeSubmissionId?: string;
64
+ activeRequestId?: string;
65
+ activeMessageId?: string;
66
+ steerable: boolean;
67
+ hasPendingSteer: boolean;
68
+ queued: RuntimeQueuedSubmission[];
69
+ }
70
+
71
+ /**
72
+ * 保存 Kernel 在当前会话里自己维护的审批状态。
73
+ *
74
+ * @remarks
75
+ * `RuntimeAgent` 把它作为 Agent state 的形状,并在审批记录变化时读写。
76
+ *
77
+ * 它故意不保存用户、Agent 或 Session 等业务身份,避免 Runtime 越过 Host 的归属边界。
78
+ *
79
+ * `Runtime`、`Kernel` 与 `Host` 等核心术语见 `../index.ts`。
80
+ */
81
+ export interface RuntimeState {
82
+ /** 当前实例的 Runtime 装载状态;旧状态记录没有该字段时等同 `idle`。 */
83
+ runtimeLoad?: RuntimeLoadState;
84
+ /** 供 Host 列表持久化的粗粒度活动状态;Submission 与 Approval 仍是执行真相。 */
85
+ activity?: RuntimeActivityProjection;
86
+ approvals?: ApprovalReceipt[];
87
+ turn?: RuntimeTurnState;
88
+ }
89
+
90
+ type SubmissionProjection = Pick<
91
+ StoredSubmission,
92
+ "submissionId" | "requestId" | "assistantMessageId" | "userMessageId" |
93
+ "queuedInputJson" | "createdAt"
94
+ >;
95
+
96
+ /** Rebuild client-visible activity from current facts without retaining lifecycle state. */
97
+ export function projectRuntimeState(input: {
98
+ readonly approvals: readonly PendingApprovalSummary[];
99
+ readonly temporaryApprovals: readonly ApprovalReceipt[];
100
+ readonly running: SubmissionProjection | null | undefined;
101
+ readonly pending: readonly SubmissionProjection[];
102
+ readonly hasPendingApproval: (submissionId: string) => boolean;
103
+ readonly hasPendingInteraction: (submissionId: string) => boolean;
104
+ readonly hasPendingSteer: (submissionId: string) => boolean;
105
+ readonly hasRunningAgentTools: boolean;
106
+ readonly previousState: RuntimeState;
107
+ }): { state: RuntimeState; changed: boolean } {
108
+ const approvals: ApprovalReceipt[] = [
109
+ ...input.approvals.map((approval) => ({
110
+ executionId: approval.executionId,
111
+ source: approval.source as ApprovalReceipt["source"],
112
+ action: approval.toolName,
113
+ summary: approval.summary,
114
+ executionLevel: approval.executionLevel,
115
+ requiredExecutionLevel: approval.requiredExecutionLevel,
116
+ inputJson: approval.inputJson,
117
+ requestId: approval.requestId,
118
+ })),
119
+ ...input.temporaryApprovals,
120
+ ];
121
+ const current = input.running ?? input.pending[0];
122
+ const queued = (input.running ? input.pending : input.pending.slice(1)).map(
123
+ (submission, index): RuntimeQueuedSubmission => {
124
+ let preview = "Queued message";
125
+ if (submission.queuedInputJson) {
126
+ try {
127
+ const message = JSON.parse(submission.queuedInputJson) as PiCanonicalUserInput;
128
+ preview = (typeof message.content === "string" ? message.content :
129
+ message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join(" "))
130
+ .trim().slice(0, 160) || preview;
131
+ } catch {
132
+ // The queue pump reports corrupt payloads when it activates the Submission.
133
+ }
134
+ }
135
+ return {
136
+ submissionId: submission.submissionId,
137
+ messageId: submission.userMessageId ?? submission.submissionId,
138
+ preview,
139
+ position: index + 1,
140
+ createdAt: submission.createdAt,
141
+ };
142
+ },
143
+ );
144
+ const turn: RuntimeTurnState = {
145
+ ...(current ? {
146
+ activeSubmissionId: current.submissionId,
147
+ activeMessageId: current.assistantMessageId,
148
+ ...(current.requestId ? { activeRequestId: current.requestId } : {}),
149
+ } : {}),
150
+ steerable: Boolean(current && !input.hasPendingApproval(current.submissionId)),
151
+ hasPendingSteer: Boolean(current && input.hasPendingSteer(current.submissionId)),
152
+ queued,
153
+ };
154
+ const activity: RuntimeActivity = approvals.length > 0 ||
155
+ (current && input.hasPendingInteraction(current.submissionId))
156
+ ? "needs-input"
157
+ : current || queued.length > 0 ? "working" : "idle";
158
+ const previousActivity = input.previousState.activity;
159
+ const nextActivity: RuntimeActivityProjection = previousActivity?.activity === activity &&
160
+ previousActivity.backgroundWork === input.hasRunningAgentTools
161
+ ? previousActivity
162
+ : {
163
+ activity,
164
+ backgroundWork: input.hasRunningAgentTools,
165
+ revision: (previousActivity?.revision ?? 0) + 1,
166
+ };
167
+ const changed = input.previousState.approvals === undefined ||
168
+ JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
169
+ JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
170
+ nextActivity !== previousActivity;
171
+ return {
172
+ state: changed ? { ...input.previousState, activity: nextActivity, approvals, turn } : input.previousState,
173
+ changed,
174
+ };
175
+ }
@@ -0,0 +1,7 @@
1
+ export { openSessionHarness, SessionHarness } from "./session-harness";
2
+ export type {
3
+ SessionHarnessLimits,
4
+ SessionHarnessOptions,
5
+ SessionHarnessRunResult,
6
+ } from "./session-harness";
7
+ export { prepareSubmissionOperation } from "./submission-operation";