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

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 (172) hide show
  1. package/package.json +7 -7
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1682 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/index.ts +18 -0
  8. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  9. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  10. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  11. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  12. package/src/{db → adapter/cloudflare}/schema.ts +28 -80
  13. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  14. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  15. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  16. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  17. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
  18. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  19. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  20. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  21. package/src/config/activation.ts +140 -0
  22. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  23. package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
  24. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  25. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  26. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  27. package/src/config/harness-runtime.ts +190 -0
  28. package/src/config/index.ts +14 -0
  29. package/src/config/model-guidance.ts +21 -0
  30. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
  31. package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
  32. package/src/{kernel → config}/profile.ts +7 -0
  33. package/src/{kernel → config}/public-contracts.ts +6 -6
  34. package/src/{kernel → config}/runtime-load.ts +1 -1
  35. package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
  36. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  37. package/src/context/extension/index.ts +3 -0
  38. package/src/context/index.ts +6 -0
  39. package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
  40. package/src/conversation/harness-resume.ts +288 -0
  41. package/src/conversation/harness-stream.ts +99 -0
  42. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  43. package/src/conversation/index.ts +6 -0
  44. package/src/{pi → conversation}/message/contract.ts +3 -0
  45. package/src/{pi → conversation}/message/conversion.ts +75 -0
  46. package/src/{pi → conversation}/message/index.ts +1 -0
  47. package/src/{pi → conversation}/message/model-input.ts +1 -1
  48. package/src/{pi → conversation}/message/projection.ts +113 -30
  49. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  50. package/src/conversation/state.ts +196 -0
  51. package/src/harness/index.ts +7 -0
  52. package/src/harness/session-harness.ts +562 -0
  53. package/src/harness/submission-operation.ts +49 -0
  54. package/src/index.ts +38 -43
  55. package/src/lib/artifacts.ts +18 -0
  56. package/src/lib/execution-level.ts +13 -0
  57. package/src/lib/mcp.ts +1 -1
  58. package/src/lib/prompt.ts +7 -6
  59. package/src/lib/token-estimate.ts +8 -0
  60. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
  61. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  62. package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
  63. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  64. package/src/session/index.ts +39 -0
  65. package/src/skills/springbrand-worker-website/index.ts +1 -1
  66. package/src/submission/admission.ts +286 -0
  67. package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
  68. package/src/submission/delivery.ts +212 -0
  69. package/src/submission/finalization.ts +111 -0
  70. package/src/submission/index.ts +9 -0
  71. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  72. package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
  73. package/src/submission/steer-delivery.ts +167 -0
  74. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  75. package/src/submission/unattended.ts +40 -0
  76. package/src/telemetry/contract.ts +12 -0
  77. package/src/telemetry/coordinator.ts +65 -0
  78. package/src/telemetry/delivery.ts +2 -2
  79. package/src/telemetry/index.ts +5 -0
  80. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  81. package/src/telemetry/recorder.ts +1 -1
  82. package/src/telemetry/runtime-telemetry.ts +12 -0
  83. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  84. package/src/telemetry/turn-events.ts +279 -0
  85. package/src/tools/README.md +44 -0
  86. package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
  87. package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
  88. package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
  89. package/src/tools/assistant/ask-user.ts +317 -0
  90. package/src/tools/assistant/candidate.ts +15 -0
  91. package/src/tools/assistant/followups.ts +34 -0
  92. package/src/tools/assistant/index.ts +19 -0
  93. package/src/tools/assistant/memory.ts +73 -0
  94. package/src/tools/assistant/plan.ts +98 -0
  95. package/src/{pi/tool → tools/assistant}/time.ts +1 -1
  96. package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
  97. package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
  98. package/src/tools/codemode/workspace-connector.ts +198 -0
  99. package/src/tools/extension/index.ts +42 -0
  100. package/src/tools/index.ts +32 -0
  101. package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
  102. package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
  103. package/src/tools/runtime/approval/index.ts +1 -0
  104. package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
  105. package/src/tools/runtime/compiler.ts +264 -0
  106. package/src/tools/runtime/contracts.ts +117 -0
  107. package/src/tools/runtime/decision-gate.ts +337 -0
  108. package/src/tools/runtime/human-decisions.ts +127 -0
  109. package/src/tools/runtime/interaction/index.ts +1 -0
  110. package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
  111. package/src/tools/runtime/result.ts +22 -0
  112. package/src/tools/runtime/unattended-error.ts +4 -0
  113. package/src/tools/sandbox/index.ts +228 -0
  114. package/src/tools/schedule/index.ts +301 -0
  115. package/src/tools/skill/contracts.ts +20 -0
  116. package/src/tools/skill/index.ts +99 -0
  117. package/src/tools/skill/resources.ts +322 -0
  118. package/src/tools/skill/scripts.ts +168 -0
  119. package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
  120. package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
  121. package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
  122. package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
  123. package/src/tools/subagent/temporary/approvals.ts +123 -0
  124. package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
  125. package/src/tools/subagent/temporary/core.ts +53 -0
  126. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
  127. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
  128. package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
  129. package/src/tools/web/index.ts +2 -0
  130. package/src/tools/web/search/api.ts +1320 -0
  131. package/src/tools/web/search/index.ts +2 -0
  132. package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
  133. package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
  134. package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
  135. package/src/db/milestone.repo.ts +0 -104
  136. package/src/db/settlement.repo.ts +0 -104
  137. package/src/kernel/approval-lifecycle.ts +0 -638
  138. package/src/kernel/interaction-lifecycle.ts +0 -433
  139. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  140. package/src/kernel/state.ts +0 -91
  141. package/src/pi/assembly/index.ts +0 -19
  142. package/src/pi/runtime-adapter/execution.ts +0 -1163
  143. package/src/pi/runtime-adapter/index.ts +0 -293
  144. package/src/pi/runtime-adapter/recovery.ts +0 -982
  145. package/src/pi/session/index.ts +0 -24
  146. package/src/pi/tool/base.ts +0 -550
  147. package/src/pi/tool/compiler.ts +0 -483
  148. package/src/pi/tool/index.ts +0 -38
  149. package/src/pi/tool/schedule.ts +0 -261
  150. package/src/pi/tool/skill.ts +0 -565
  151. package/src/pi/tool/web-search/api.ts +0 -1318
  152. package/src/pi/tool/web-search/index.ts +0 -2
  153. package/src/pi/tool/workspace-state-connector.ts +0 -146
  154. package/src/pi/turn/approval.ts +0 -181
  155. package/src/pi/turn/index.ts +0 -82
  156. package/src/pi/turn/interaction.ts +0 -181
  157. package/src/runtime.ts +0 -4760
  158. package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  159. package/src/{kernel → config}/degradation.ts +0 -0
  160. package/src/{kernel → config}/runtime-config.ts +0 -0
  161. package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  162. package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  163. package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  164. package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  165. package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  166. package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  167. package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  168. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
  169. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
  170. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
  171. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
  172. /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;
@@ -75,6 +75,35 @@ function safeInput(value: unknown): Record<string, unknown> {
75
75
  return isRecord(safe) ? safe : {};
76
76
  }
77
77
 
78
+ export type ToolInputPreparer = (
79
+ toolName: string,
80
+ input: unknown,
81
+ ) => unknown;
82
+
83
+ function safeToolInput(
84
+ toolName: string,
85
+ value: unknown,
86
+ prepare?: ToolInputPreparer,
87
+ ): Record<string, unknown> {
88
+ if (prepare) {
89
+ try {
90
+ value = prepare(toolName, value);
91
+ } catch {
92
+ // Preserve invalid provider output for the normal Tool validation path.
93
+ }
94
+ }
95
+ const input = safeInput(value);
96
+ if (toolName === "ask_user" && typeof input.questions === "string") {
97
+ try {
98
+ const questions: unknown = JSON.parse(input.questions);
99
+ if (Array.isArray(questions)) input.questions = questions;
100
+ } catch {
101
+ // Preserve malformed provider output for the normal unavailable state.
102
+ }
103
+ }
104
+ return input;
105
+ }
106
+
78
107
  function contentText(result: unknown): string | undefined {
79
108
  if (!isRecord(result) || !Array.isArray(result.content)) return undefined;
80
109
  const text = result.content.flatMap((part) =>
@@ -174,6 +203,8 @@ export class PiChunkEncoder {
174
203
  private readonly messageId: string;
175
204
  private readonly startedAt?: number;
176
205
  private readonly turnId?: string;
206
+ private readonly userMessageId?: string;
207
+ private readonly prepareToolInput?: ToolInputPreparer;
177
208
  private started = false;
178
209
  private finished = false;
179
210
  private firstTurnStarted = false;
@@ -192,10 +223,14 @@ export class PiChunkEncoder {
192
223
  messageId: string;
193
224
  startedAt?: number;
194
225
  turnId?: string;
226
+ userMessageId?: string | null;
227
+ prepareToolInput?: ToolInputPreparer;
195
228
  }) {
196
229
  this.messageId = options.messageId;
197
230
  this.startedAt = options.startedAt;
198
231
  this.turnId = options.turnId;
232
+ this.userMessageId = options.userMessageId ?? undefined;
233
+ this.prepareToolInput = options.prepareToolInput;
199
234
  }
200
235
 
201
236
  start(): UIMessageChunk[] {
@@ -213,6 +248,7 @@ export class PiChunkEncoder {
213
248
  createdAt: this.startedAt,
214
249
  turnStatus: "running",
215
250
  ...(this.turnId ? { turnId: this.turnId } : {}),
251
+ ...(this.userMessageId ? { userMessageId: this.userMessageId } : {}),
216
252
  },
217
253
  }),
218
254
  },
@@ -220,12 +256,60 @@ export class PiChunkEncoder {
220
256
  ];
221
257
  }
222
258
 
259
+ /** Seed this encoder and its open part IDs from one Pi-backed UI snapshot. */
260
+ restore(message: UIMessage, streaming: boolean): UIMessageChunk[] {
261
+ if (this.started) throw new Error("Cannot restore an already-started stream encoder");
262
+ this.started = true;
263
+ this.firstTurnStarted = message.parts.length === 0;
264
+ const chunks: UIMessageChunk[] = [{ type: "start", messageId: this.messageId,
265
+ ...(message.metadata === undefined ? {} : { messageMetadata: message.metadata }) }];
266
+ let contentIndex = 0;
267
+ let hasStep = false;
268
+ let lastContentIndex = message.parts.length - 1;
269
+ while (lastContentIndex >= 0 && message.parts[lastContentIndex]?.type === "step-start") lastContentIndex -= 1;
270
+ for (const [partIndex, part] of message.parts.entries()) {
271
+ if (part.type === "step-start") {
272
+ if (hasStep) { chunks.push({ type: "finish-step" }); this.stepIndex += 1; }
273
+ chunks.push({ type: "start-step" });
274
+ hasStep = true;
275
+ contentIndex = 0;
276
+ continue;
277
+ }
278
+ if (!hasStep) { chunks.push({ type: "start-step" }); hasStep = true; }
279
+ if (part.type === "text" || part.type === "reasoning") {
280
+ const kind = part.type;
281
+ const id = this.partId(kind, contentIndex);
282
+ chunks.push({ type: `${kind}-start`, id });
283
+ if (part.text) chunks.push({ type: `${kind}-delta`, id, delta: part.text });
284
+ if (streaming && partIndex === lastContentIndex) {
285
+ (kind === "text" ? this.activeText : this.activeReasoning).add(contentIndex);
286
+ } else chunks.push({ type: `${kind}-end`, id });
287
+ } else if (part.type === "dynamic-tool") {
288
+ if (part.state === "input-streaming") {
289
+ chunks.push({ type: "tool-input-start", toolCallId: part.toolCallId, toolName: part.toolName, dynamic: true });
290
+ this.toolCallIdByIndex.set(contentIndex, part.toolCallId);
291
+ this.streamingToolById.set(part.toolCallId, { toolName: part.toolName, started: true, deltas: [] });
292
+ } else {
293
+ chunks.push(...this.toolInput(part.toolCallId, part.toolName, part.input));
294
+ if (part.state === "approval-requested") chunks.push(...this.approvalRequested(part.toolCallId, part.approval.id));
295
+ else if (part.state === "output-available") chunks.push({ type: "tool-output-available", toolCallId: part.toolCallId, output: part.output, preliminary: part.preliminary, dynamic: true });
296
+ else if (part.state === "output-error") chunks.push({ type: "tool-output-error", toolCallId: part.toolCallId, errorText: part.errorText, dynamic: true });
297
+ else if (part.state === "output-denied") chunks.push({ type: "tool-output-denied", toolCallId: part.toolCallId });
298
+ }
299
+ }
300
+ contentIndex += 1;
301
+ }
302
+ if (!hasStep) chunks.push({ type: "start-step" });
303
+ else if (!streaming) chunks.push({ type: "finish-step" });
304
+ return chunks;
305
+ }
306
+
223
307
  toolInput(
224
308
  toolCallId: string,
225
309
  toolName: string,
226
310
  input: unknown,
227
311
  ): UIMessageChunk[] {
228
- const safe = safeInput(input);
312
+ const safe = safeToolInput(toolName, input, this.prepareToolInput);
229
313
  this.toolInputs.set(toolCallId, { toolName, input: safe });
230
314
  return [{
231
315
  type: "tool-input-available",
@@ -262,7 +346,10 @@ export class PiChunkEncoder {
262
346
  ];
263
347
  }
264
348
 
265
- encode(event: AgentEvent): UIMessageChunk[] {
349
+ encode(
350
+ event: AgentEvent,
351
+ options: { readonly terminal?: boolean } = {},
352
+ ): UIMessageChunk[] {
266
353
  if (this.finished) return [];
267
354
  switch (event.type) {
268
355
  case "agent_start":
@@ -318,7 +405,7 @@ export class PiChunkEncoder {
318
405
  case "message_end":
319
406
  this.firstTurnStarted = false;
320
407
  return event.message.role === "assistant"
321
- ? this.endAssistant(event.message)
408
+ ? this.endAssistant(event.message, options.terminal !== false)
322
409
  : [];
323
410
  case "agent_end":
324
411
  case "turn_end":
@@ -450,33 +537,36 @@ export class PiChunkEncoder {
450
537
  streaming.deltas.length = 0;
451
538
  }
452
539
  if (update.type === "toolcall_end") {
453
- const input = safeInput(update.toolCall.arguments);
454
- this.toolInputs.set(toolCallId, {
455
- toolName: update.toolCall.name,
456
- input,
457
- });
458
- chunks.push({
459
- type: "tool-input-available",
540
+ chunks.push(...this.toolInput(
460
541
  toolCallId,
461
- toolName: update.toolCall.name,
462
- input,
463
- dynamic: true,
464
- });
542
+ update.toolCall.name,
543
+ update.toolCall.arguments,
544
+ ));
465
545
  this.pendingToolByIndex.delete(index);
466
546
  this.toolCallIdByIndex.delete(index);
467
547
  }
468
548
  return chunks;
469
549
  }
470
550
 
471
- private endAssistant(message: AssistantMessage): UIMessageChunk[] {
551
+ private endAssistant(message: AssistantMessage, terminal = true): UIMessageChunk[] {
472
552
  this.pendingToolByIndex.clear();
473
553
  this.toolCallIdByIndex.clear();
474
554
  const chunks = this.closeOpenParts();
475
- if (message.stopReason === "toolUse") {
555
+ if (message.stopReason === "toolUse" || !terminal) {
476
556
  chunks.push({ type: "finish-step" });
477
557
  return chunks;
478
558
  }
559
+ const outcome = classifyPiTerminalStopReason(message.stopReason);
560
+ if (outcome.outcome === "succeeded") chunks.push({ type: "finish-step" });
561
+ chunks.push(...this.finish(message));
562
+ return chunks;
563
+ }
564
+
565
+ /** Finalize the UI operation after its last model step has already closed. */
566
+ finish(message: Pick<AssistantMessage, "stopReason" | "timestamp" | "errorMessage">): UIMessageChunk[] {
567
+ if (this.finished) return [];
479
568
  this.finished = true;
569
+ const chunks: UIMessageChunk[] = [];
480
570
  const publicError = publicAssistantError(message.errorMessage);
481
571
  const terminal = classifyPiTerminalStopReason(message.stopReason);
482
572
  if (this.startedAt !== undefined) {
@@ -507,7 +597,6 @@ export class PiChunkEncoder {
507
597
  });
508
598
  return chunks;
509
599
  }
510
- chunks.push({ type: "finish-step" });
511
600
  chunks.push({
512
601
  type: "finish",
513
602
  finishReason: terminal.finishReason ?? "other",
@@ -549,15 +638,7 @@ function projectedTool(
549
638
  call: ToolCall,
550
639
  approval?: PiToolApprovalView,
551
640
  ): DynamicToolUIPart {
552
- const input = safeInput(call.arguments);
553
- if (call.name === "ask_user" && typeof input.questions === "string") {
554
- try {
555
- const questions: unknown = JSON.parse(input.questions);
556
- if (Array.isArray(questions)) input.questions = questions;
557
- } catch {
558
- // Preserve malformed legacy input for the normal unavailable state.
559
- }
560
- }
641
+ const input = safeToolInput(call.name, call.arguments);
561
642
  const base = {
562
643
  type: "dynamic-tool" as const,
563
644
  toolCallId: call.id,
@@ -601,15 +682,16 @@ export function piAssistantToUIMessage(
601
682
  readonly id: string;
602
683
  readonly metadata?: unknown;
603
684
  readonly approvals?: Readonly<Record<string, PiToolApprovalView | undefined>>;
685
+ readonly state?: "streaming" | "done";
604
686
  },
605
687
  ): UIMessage {
606
688
  const parts = message.content.flatMap(
607
689
  (content): Array<UIMessage["parts"][number]> => {
608
690
  if (content.type === "text") {
609
- return [{ type: "text", text: content.text, state: "done" }];
691
+ return [{ type: "text", text: content.text, state: options.state ?? "done" }];
610
692
  }
611
693
  if (content.type === "thinking") {
612
- return [{ type: "reasoning", text: content.thinking, state: "done" }];
694
+ return [{ type: "reasoning", text: content.thinking, state: options.state ?? "done" }];
613
695
  }
614
696
  if (content.type === "toolCall") {
615
697
  return [projectedTool(content, options.approvals?.[content.id])];
@@ -629,6 +711,7 @@ export function piAssistantToUIMessage(
629
711
  export function applyPiToolResult(
630
712
  message: UIMessage,
631
713
  result: ToolResultMessage,
714
+ options: { readonly preliminary?: boolean } = {},
632
715
  ): UIMessage {
633
716
  return {
634
717
  ...message,
@@ -658,7 +741,7 @@ export function applyPiToolResult(
658
741
  state: "output-available",
659
742
  input: part.input,
660
743
  output: toolOutput({ content: result.content, details: result.details }),
661
- preliminary: false,
744
+ preliminary: options.preliminary ?? false,
662
745
  ...(approval ? { approval } : {}),
663
746
  };
664
747
  }),
@@ -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,196 @@
1
+ import type { ApprovalReceipt } from "../submission/contracts";
2
+ import type { StoredSubmission } from "../submission/repository";
3
+ import type { PendingApprovalSummary } from "../tools/runtime/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
+ * 本 Session 当前是否拒绝普通用户输入。
73
+ *
74
+ * 定时 Run 未结束时它独占输入:宿主据此禁用输入框并显示原因。
75
+ * 服务端准入用同一事实拒绝消息,所以客户端状态与后端判定不会漂移。
76
+ */
77
+ export interface RuntimeInputLock {
78
+ /** 稳定原因码,由宿主/UI 决定文案。 */
79
+ reason: string;
80
+ }
81
+
82
+ /**
83
+ * 保存 Kernel 在当前会话里自己维护的审批状态。
84
+ *
85
+ * @remarks
86
+ * `RuntimeAgent` 把它作为 Agent state 的形状,并在审批记录变化时读写。
87
+ *
88
+ * 它故意不保存用户、Agent 或 Session 等业务身份,避免 Runtime 越过 Host 的归属边界。
89
+ *
90
+ * `Runtime`、`Kernel` 与 `Host` 等核心术语见 `../index.ts`。
91
+ */
92
+ export interface RuntimeState {
93
+ /** 当前实例的 Runtime 装载状态;旧状态记录没有该字段时等同 `idle`。 */
94
+ runtimeLoad?: RuntimeLoadState;
95
+ /** 供 Host 列表持久化的粗粒度活动状态;Submission 与 Approval 仍是执行真相。 */
96
+ activity?: RuntimeActivityProjection;
97
+ approvals?: ApprovalReceipt[];
98
+ turn?: RuntimeTurnState;
99
+ /** 有值时普通输入被锁定,直到锁定的 Submission 进入终态。 */
100
+ inputLock?: RuntimeInputLock;
101
+ }
102
+
103
+ type SubmissionProjection = Pick<
104
+ StoredSubmission,
105
+ "submissionId" | "requestId" | "assistantMessageId" | "userMessageId" |
106
+ "queuedInputJson" | "createdAt"
107
+ >;
108
+
109
+ /** Rebuild client-visible activity from current facts without retaining lifecycle state. */
110
+ export function projectRuntimeState(input: {
111
+ readonly approvals: readonly PendingApprovalSummary[];
112
+ readonly temporaryApprovals: readonly ApprovalReceipt[];
113
+ readonly running: SubmissionProjection | null | undefined;
114
+ readonly pending: readonly SubmissionProjection[];
115
+ readonly hasPendingApproval: (submissionId: string) => boolean;
116
+ readonly hasPendingInteraction: (submissionId: string) => boolean;
117
+ readonly hasPendingSteer: (submissionId: string) => boolean;
118
+ readonly hasRunningAgentTools: boolean;
119
+ /** A non-terminal scheduled Submission owns ordinary input until it ends. */
120
+ readonly hasUnfinishedScheduled?: boolean;
121
+ readonly previousState: RuntimeState;
122
+ }): { state: RuntimeState; changed: boolean } {
123
+ const approvals: ApprovalReceipt[] = [
124
+ ...input.approvals.map((approval) => ({
125
+ executionId: approval.executionId,
126
+ source: approval.source as ApprovalReceipt["source"],
127
+ action: approval.toolName,
128
+ summary: approval.summary,
129
+ executionLevel: approval.executionLevel,
130
+ requiredExecutionLevel: approval.requiredExecutionLevel,
131
+ inputJson: approval.inputJson,
132
+ requestId: approval.requestId,
133
+ })),
134
+ ...input.temporaryApprovals,
135
+ ];
136
+ const current = input.running ?? input.pending[0];
137
+ const queued = (input.running ? input.pending : input.pending.slice(1)).map(
138
+ (submission, index): RuntimeQueuedSubmission => {
139
+ let preview = "Queued message";
140
+ if (submission.queuedInputJson) {
141
+ try {
142
+ const message = JSON.parse(submission.queuedInputJson) as PiCanonicalUserInput;
143
+ preview = (typeof message.content === "string" ? message.content :
144
+ message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join(" "))
145
+ .trim().slice(0, 160) || preview;
146
+ } catch {
147
+ // The queue pump reports corrupt payloads when it activates the Submission.
148
+ }
149
+ }
150
+ return {
151
+ submissionId: submission.submissionId,
152
+ messageId: submission.userMessageId ?? submission.submissionId,
153
+ preview,
154
+ position: index + 1,
155
+ createdAt: submission.createdAt,
156
+ };
157
+ },
158
+ );
159
+ const turn: RuntimeTurnState = {
160
+ ...(current ? {
161
+ activeSubmissionId: current.submissionId,
162
+ activeMessageId: current.assistantMessageId,
163
+ ...(current.requestId ? { activeRequestId: current.requestId } : {}),
164
+ } : {}),
165
+ steerable: Boolean(current && !input.hasPendingApproval(current.submissionId)),
166
+ hasPendingSteer: Boolean(current && input.hasPendingSteer(current.submissionId)),
167
+ queued,
168
+ };
169
+ const activity: RuntimeActivity = approvals.length > 0 ||
170
+ (current && input.hasPendingInteraction(current.submissionId))
171
+ ? "needs-input"
172
+ : current || queued.length > 0 ? "working" : "idle";
173
+ const previousActivity = input.previousState.activity;
174
+ const nextActivity: RuntimeActivityProjection = previousActivity?.activity === activity &&
175
+ previousActivity.backgroundWork === input.hasRunningAgentTools
176
+ ? previousActivity
177
+ : {
178
+ activity,
179
+ backgroundWork: input.hasRunningAgentTools,
180
+ revision: (previousActivity?.revision ?? 0) + 1,
181
+ };
182
+ const inputLock: RuntimeInputLock | undefined = input.hasUnfinishedScheduled
183
+ ? { reason: "scheduled-run" }
184
+ : undefined;
185
+ const changed = input.previousState.approvals === undefined ||
186
+ JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
187
+ JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
188
+ JSON.stringify(input.previousState.inputLock) !== JSON.stringify(inputLock) ||
189
+ nextActivity !== previousActivity;
190
+ return {
191
+ state: changed
192
+ ? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
193
+ : input.previousState,
194
+ changed,
195
+ };
196
+ }
@@ -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";