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

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 +1539 -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} +61 -118
  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
@@ -4,6 +4,7 @@ export interface PendingSteer {
4
4
  steerId: string;
5
5
  submissionId: string;
6
6
  messageId: string;
7
+ harnessEntryId: string | null;
7
8
  canonicalJson: string;
8
9
  uiMessageJson: string | null;
9
10
  createdAt: number;
@@ -13,6 +14,7 @@ type PendingSteerRow = {
13
14
  steer_id: string;
14
15
  submission_id: string;
15
16
  message_id: string;
17
+ harness_entry_id: string | null;
16
18
  canonical_json: string;
17
19
  ui_message_json: string | null;
18
20
  created_at: number;
@@ -23,6 +25,7 @@ function mapRow(row: PendingSteerRow): PendingSteer {
23
25
  steerId: row.steer_id,
24
26
  submissionId: row.submission_id,
25
27
  messageId: row.message_id,
28
+ harnessEntryId: row.harness_entry_id,
26
29
  canonicalJson: row.canonical_json,
27
30
  uiMessageJson: row.ui_message_json,
28
31
  createdAt: row.created_at,
@@ -35,28 +38,29 @@ export class SteerRepository {
35
38
  insert(steer: PendingSteer): void {
36
39
  this.sql`
37
40
  INSERT INTO pi_pending_steers (
38
- steer_id, submission_id, message_id, canonical_json,
41
+ steer_id, submission_id, message_id, harness_entry_id, canonical_json,
39
42
  ui_message_json, created_at
40
43
  ) VALUES (
41
44
  ${steer.steerId}, ${steer.submissionId}, ${steer.messageId},
42
- ${steer.canonicalJson}, ${steer.uiMessageJson}, ${steer.createdAt}
45
+ ${steer.harnessEntryId}, ${steer.canonicalJson},
46
+ ${steer.uiMessageJson}, ${steer.createdAt}
43
47
  )
44
48
  `;
45
49
  }
46
50
 
47
51
  listForSubmission(submissionId: string): PendingSteer[] {
48
52
  return this.sql<PendingSteerRow>`
49
- SELECT steer_id, submission_id, message_id, canonical_json,
53
+ SELECT steer_id, submission_id, message_id, harness_entry_id, canonical_json,
50
54
  ui_message_json, created_at
51
55
  FROM pi_pending_steers
52
56
  WHERE submission_id = ${submissionId}
53
- ORDER BY created_at ASC, rowid ASC
57
+ ORDER BY rowid ASC
54
58
  `.map(mapRow);
55
59
  }
56
60
 
57
61
  findByMessageId(messageId: string): PendingSteer | null {
58
62
  const row = this.sql<PendingSteerRow>`
59
- SELECT steer_id, submission_id, message_id, canonical_json,
63
+ SELECT steer_id, submission_id, message_id, harness_entry_id, canonical_json,
60
64
  ui_message_json, created_at
61
65
  FROM pi_pending_steers
62
66
  WHERE message_id = ${messageId}
@@ -64,6 +68,24 @@ export class SteerRepository {
64
68
  return row ? mapRow(row) : null;
65
69
  }
66
70
 
71
+ findByHarnessEntryId(entryId: string): PendingSteer | null {
72
+ const row = this.sql<PendingSteerRow>`
73
+ SELECT steer_id, submission_id, message_id, harness_entry_id, canonical_json,
74
+ ui_message_json, created_at
75
+ FROM pi_pending_steers
76
+ WHERE harness_entry_id = ${entryId}
77
+ `[0];
78
+ return row ? mapRow(row) : null;
79
+ }
80
+
81
+ linkHarnessEntry(messageId: string, entryId: string): void {
82
+ this.sql`
83
+ UPDATE pi_pending_steers
84
+ SET harness_entry_id = ${entryId}
85
+ WHERE message_id = ${messageId}
86
+ `;
87
+ }
88
+
67
89
  deleteByMessageId(messageId: string): void {
68
90
  this.sql`
69
91
  DELETE FROM pi_pending_steers
@@ -88,6 +88,12 @@ export interface TelemetryPayloadByType {
88
88
  readonly generationId: string;
89
89
  readonly outcome: "completed" | "failed" | "cancelled";
90
90
  readonly durationMs?: number;
91
+ readonly responseHeadersMs?: number;
92
+ readonly firstRawEventMs?: number;
93
+ readonly firstMeaningfulEventMs?: number;
94
+ readonly firstTextEventMs?: number;
95
+ readonly sourceWaitMs?: number;
96
+ readonly forwardingOverheadMs?: number;
91
97
  readonly inputTokens?: number;
92
98
  readonly outputTokens?: number;
93
99
  readonly totalTokens?: number;
@@ -288,6 +294,12 @@ function assertPayload(
288
294
  }
289
295
  for (const field of [
290
296
  "durationMs",
297
+ "responseHeadersMs",
298
+ "firstRawEventMs",
299
+ "firstMeaningfulEventMs",
300
+ "firstTextEventMs",
301
+ "sourceWaitMs",
302
+ "forwardingOverheadMs",
291
303
  "inputTokens",
292
304
  "outputTokens",
293
305
  "totalTokens",
@@ -4,6 +4,8 @@ import type { TelemetryOutboxWriter } from "./recorder";
4
4
  import { TelemetryRecorder } from "./recorder";
5
5
  import { RuntimeTelemetry } from "./runtime-telemetry";
6
6
  import type { AgentTelemetryBinding } from "./contract";
7
+ import type { PiGenerationLifecycleEvent } from "../models";
8
+ import type { TemporaryAgentLifecycleEvent } from "../tools/temporary-agent/coordinator";
7
9
 
8
10
  const TELEMETRY_COLD_BUFFER_LIMIT = 1_000;
9
11
 
@@ -93,6 +95,69 @@ export class RuntimeTelemetryCoordinator {
93
95
  }
94
96
  }
95
97
 
98
+ generation(submissionId: string, event: PiGenerationLifecycleEvent): void {
99
+ if (event.type === "started") {
100
+ this.capture("generationStarted", {
101
+ submissionId,
102
+ generationId: event.generationId,
103
+ provider: event.provider,
104
+ model: event.model,
105
+ input: event.input,
106
+ occurredAt: event.timestamp,
107
+ });
108
+ return;
109
+ }
110
+ this.capture("generationFinished", {
111
+ submissionId,
112
+ generationId: event.generationId,
113
+ outcome: event.outcome === "success" ? "completed"
114
+ : event.outcome === "cancelled" ? "cancelled" : "failed",
115
+ durationMs: event.durationMs,
116
+ responseHeadersMs: event.responseHeadersMs,
117
+ firstRawEventMs: event.firstRawEventMs,
118
+ firstMeaningfulEventMs: event.firstMeaningfulEventMs,
119
+ firstTextEventMs: event.firstTextEventMs,
120
+ sourceWaitMs: event.sourceWaitMs,
121
+ forwardingOverheadMs: event.forwardingOverheadMs,
122
+ ...(event.usage ? {
123
+ inputTokens: event.usage.input,
124
+ outputTokens: event.usage.output,
125
+ totalTokens: event.usage.totalTokens,
126
+ cost: event.usage.cost.total,
127
+ } : {}),
128
+ ...(event.errorName ? { error: event.errorName } : {}),
129
+ ...(event.output ? { output: event.output } : {}),
130
+ occurredAt: event.timestamp,
131
+ });
132
+ }
133
+
134
+ temporaryAgent(
135
+ submissionId: string,
136
+ toolCallId: string,
137
+ event: TemporaryAgentLifecycleEvent,
138
+ ): void {
139
+ const identity = { submissionId, toolCallId, subagentId: `temporary:${toolCallId}` };
140
+ if (event.type === "started") {
141
+ this.capture("subagentStarted", {
142
+ ...identity,
143
+ agentType: event.request.subagentName,
144
+ prompt: { instructions: event.request.instructions, task: event.request.task },
145
+ occurredAt: event.occurredAt,
146
+ });
147
+ return;
148
+ }
149
+ this.capture("subagentFinished", {
150
+ ...identity,
151
+ outcome: event.outcome,
152
+ durationMs: event.durationMs,
153
+ occurredAt: event.occurredAt,
154
+ ...(event.output === undefined ? {} : { output: event.output }),
155
+ ...(event.error === undefined ? {} : {
156
+ error: event.error instanceof Error ? event.error.message : String(event.error),
157
+ }),
158
+ });
159
+ }
160
+
96
161
  recover(ensureReady: () => Promise<void>): void {
97
162
  if (!this.options.outbox()?.hasPending()) return;
98
163
  this.options.waitUntil(
@@ -1,9 +1,9 @@
1
1
  import type {
2
2
  StoredTelemetryOutboxEvent,
3
- } from "../db/telemetry-outbox.repo";
3
+ } from "./repository";
4
4
  import {
5
5
  TELEMETRY_OUTBOX_BATCH_SIZE,
6
- } from "../db/telemetry-outbox.repo";
6
+ } from "./repository";
7
7
  import {
8
8
  assertUniversalTelemetryEvent,
9
9
  type AgentTelemetryPort,
@@ -5,3 +5,8 @@ export * from "./ids";
5
5
  export * from "./recorder";
6
6
  export * from "./runtime-telemetry";
7
7
  export * from "./sanitize";
8
+ export * from "./model-usage";
9
+ export * from "./repository";
10
+ export * from "./runtime-event-repository";
11
+ export * from "./subagent-lifecycle";
12
+ export * from "./turn-events";
@@ -1,5 +1,5 @@
1
1
  import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
2
- import type { RuntimeModelUsageEvent } from "./kernel/bindings";
2
+ import type { RuntimeModelUsageEvent } from "../config/ports";
3
3
 
4
4
  type ModelUsage = Pick<RuntimeModelUsageEvent,
5
5
  "api" | "provider" | "model" | "responseModel" | "stopReason" | "usage"
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  NewTelemetryOutboxEvent,
3
3
  TelemetryOutboxInsertResult,
4
- } from "../db/telemetry-outbox.repo";
4
+ } from "./repository";
5
5
  import {
6
6
  assertUniversalTelemetryEvent,
7
7
  type TelemetryPrivacyMode,
@@ -188,6 +188,12 @@ export class RuntimeTelemetry {
188
188
  generationId: string;
189
189
  outcome: "completed" | "failed" | "cancelled";
190
190
  durationMs?: number;
191
+ responseHeadersMs?: number;
192
+ firstRawEventMs?: number;
193
+ firstMeaningfulEventMs?: number;
194
+ firstTextEventMs?: number;
195
+ sourceWaitMs?: number;
196
+ forwardingOverheadMs?: number;
191
197
  inputTokens?: number;
192
198
  outputTokens?: number;
193
199
  totalTokens?: number;
@@ -204,6 +210,12 @@ export class RuntimeTelemetry {
204
210
  generationId: input.generationId,
205
211
  outcome: input.outcome,
206
212
  ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
213
+ responseHeadersMs: input.responseHeadersMs,
214
+ firstRawEventMs: input.firstRawEventMs,
215
+ firstMeaningfulEventMs: input.firstMeaningfulEventMs,
216
+ firstTextEventMs: input.firstTextEventMs,
217
+ sourceWaitMs: input.sourceWaitMs,
218
+ forwardingOverheadMs: input.forwardingOverheadMs,
207
219
  ...(input.inputTokens === undefined ? {} : { inputTokens: input.inputTokens }),
208
220
  ...(input.outputTokens === undefined ? {} : { outputTokens: input.outputTokens }),
209
221
  ...(input.totalTokens === undefined ? {} : { totalTokens: input.totalTokens }),
@@ -2,7 +2,7 @@ import type {
2
2
  RuntimeEventConfirmation,
3
3
  RuntimeLifecycleFact,
4
4
  RuntimeSubagentUsageEvent,
5
- } from "./submission-authority";
5
+ } from "../tools/subagent-authority";
6
6
 
7
7
  export interface RuntimeLifecycleSink {
8
8
  save(
@@ -0,0 +1,279 @@
1
+ import type { AgentHarness, Entry } from "@earendil-works/pi-agent-core";
2
+ import type {
3
+ RuntimeLifecycleFact,
4
+ RuntimeModelUsageEvent,
5
+ RuntimeSubagentUsageEvent,
6
+ RuntimeToolSettlementEvent,
7
+ RuntimeTurnEventsPort,
8
+ } from "../config/ports";
9
+ import type { StoredSubmission } from "../submission/repository";
10
+ import { decodeSubagentCompletion, isZeroSubagentUsage, subagentTerminalStatus, type RuntimeSubagentRunMetadata } from "../tools/subagent-runtime";
11
+ import type { RuntimeTelemetryCoordinator } from "./coordinator";
12
+ import { summarizeModelUsage } from "./model-usage";
13
+ import type { RuntimeEventOutboxRepository } from "./runtime-event-repository";
14
+ import { RuntimeSubagentLifecycleRecorder } from "./subagent-lifecycle";
15
+
16
+ const RETRY_SECONDS = 10;
17
+ const MAX_ATTEMPTS = 5;
18
+
19
+ type Payload =
20
+ | { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
21
+ | { readonly type: "tool-settlement"; readonly event: RuntimeToolSettlementEvent }
22
+ | { readonly type: "subagent-usage"; readonly event: RuntimeSubagentUsageEvent }
23
+ | { readonly type: "lifecycle"; readonly event: RuntimeLifecycleFact };
24
+
25
+ type RequestedModel = Parameters<typeof summarizeModelUsage>[0];
26
+ type SubagentRun = {
27
+ readonly runId: string;
28
+ readonly agentType: string;
29
+ readonly parentToolCallId?: string;
30
+ readonly startedAt: number;
31
+ readonly completedAt?: number;
32
+ };
33
+
34
+ /** Canonical Pi facts become durable Host events; delivery never drives Pi. */
35
+ export class RuntimeTurnEvents {
36
+ private drainTail?: Promise<void>;
37
+
38
+ constructor(private readonly options: {
39
+ readonly outbox: RuntimeEventOutboxRepository;
40
+ readonly transaction: <T>(run: () => T) => T;
41
+ readonly port: () => RuntimeTurnEventsPort | undefined;
42
+ readonly telemetry: Pick<RuntimeTelemetryCoordinator, "capture">;
43
+ readonly waitUntil: (promise: Promise<void>) => void;
44
+ readonly scheduleRetry: (delaySeconds: number, idempotent: boolean) => Promise<void>;
45
+ }) {}
46
+
47
+ private save(payload: Payload, createdAt: number): void {
48
+ this.options.transaction(() => this.options.outbox.insert({
49
+ eventId: payload.event.eventId,
50
+ body: JSON.stringify(payload),
51
+ createdAt,
52
+ }));
53
+ }
54
+
55
+ submissionAdmitted(
56
+ submission: Pick<StoredSubmission, "submissionId" | "requestId" | "createdAt"> & { idempotencyKey?: string | null },
57
+ hostAccepted: boolean,
58
+ ): void {
59
+ if (hostAccepted) this.save({ type: "lifecycle", event: {
60
+ kind: "submission-admitted",
61
+ eventId: `${submission.submissionId}:admitted`,
62
+ submissionId: submission.submissionId,
63
+ runId: submission.submissionId,
64
+ } }, submission.createdAt);
65
+ this.options.telemetry.capture("turnAdmitted", {
66
+ submissionId: submission.submissionId,
67
+ requestId: submission.requestId,
68
+ ...(submission.idempotencyKey ? { idempotencyKey: submission.idempotencyKey } : {}),
69
+ occurredAt: submission.createdAt,
70
+ });
71
+ }
72
+
73
+ submissionTerminal(
74
+ submission: Pick<StoredSubmission, "submissionId" | "runId" | "status"> & { completedAt?: number | null },
75
+ ): void {
76
+ if (submission.status === "pending" || submission.status === "running") {
77
+ throw new Error("Submission terminal fact requires a terminal status");
78
+ }
79
+ this.save({ type: "lifecycle", event: {
80
+ kind: "submission-terminal",
81
+ eventId: `${submission.submissionId}:terminal`,
82
+ submissionId: submission.submissionId,
83
+ runId: submission.runId ?? submission.submissionId,
84
+ status: submission.status,
85
+ childRegistrationClosed: true,
86
+ zeroUsage: !this.options.outbox.hasNonZeroModelUsage(submission.submissionId),
87
+ } }, submission.completedAt ?? Date.now());
88
+ }
89
+
90
+ private recordEntry(submissionId: string, entry: Entry, model: RequestedModel): void {
91
+ if (entry.type === "message" && entry.message.role === "assistant") {
92
+ this.save({ type: "model-usage", event: {
93
+ eventId: `${submissionId}:model:${entry.id}`,
94
+ submissionId,
95
+ runId: submissionId,
96
+ kind: "assistant",
97
+ ...summarizeModelUsage(model, [entry.message]),
98
+ } }, entry.message.timestamp);
99
+ } else if (entry.type === "message" && entry.message.role === "toolResult") {
100
+ this.save({ type: "tool-settlement", event: {
101
+ eventId: `${submissionId}:tool:${entry.message.toolCallId}`,
102
+ submissionId,
103
+ toolCallId: entry.message.toolCallId,
104
+ toolName: entry.message.toolName,
105
+ status: entry.message.isError ? "error" : "success",
106
+ } }, entry.message.timestamp);
107
+ } else if (entry.type === "compaction" || entry.type === "branch_summary") {
108
+ if (entry.usage) {
109
+ this.save({ type: "model-usage", event: {
110
+ eventId: `${submissionId}:compaction:${entry.id}`,
111
+ submissionId,
112
+ runId: submissionId,
113
+ kind: "compaction",
114
+ api: model.api,
115
+ provider: model.provider,
116
+ model: model.id,
117
+ usage: entry.usage,
118
+ } }, entry.timestamp);
119
+ }
120
+ this.options.telemetry.capture("contextCompacted", {
121
+ submissionId,
122
+ compactionId: entry.id,
123
+ ...(entry.usage ? {
124
+ inputTokens: entry.usage.input,
125
+ outputTokens: entry.usage.output,
126
+ } : {}),
127
+ summary: entry.summary,
128
+ occurredAt: entry.timestamp,
129
+ });
130
+ }
131
+ }
132
+
133
+ /** Live observation and recovery use exactly the same committed-entry projection. */
134
+ observe(harness: Pick<AgentHarness, "events">, submissionId: string, model: RequestedModel): void {
135
+ harness.events.on("entry_added", ({ entry }) => {
136
+ this.recordEntry(submissionId, entry, model);
137
+ this.options.waitUntil(this.drain());
138
+ });
139
+ harness.events.on("tool_start", (event) => {
140
+ this.options.telemetry.capture("toolStarted", {
141
+ submissionId,
142
+ toolCallId: event.toolCallId,
143
+ toolName: event.toolName,
144
+ input: event.args,
145
+ });
146
+ });
147
+ harness.events.on("tool_end", (event) => {
148
+ this.options.telemetry.capture("toolFinished", {
149
+ submissionId,
150
+ toolCallId: event.toolCallId,
151
+ toolName: event.toolName,
152
+ outcome: event.isError ? "failed" : "completed",
153
+ output: event.result,
154
+ outputBytes: JSON.stringify(event.result).length,
155
+ });
156
+ });
157
+ }
158
+
159
+ backfill(
160
+ submissionId: string,
161
+ model: RequestedModel,
162
+ entries: readonly { readonly entry: Entry; readonly submissionId: string | null }[],
163
+ ): void {
164
+ for (const stored of entries) {
165
+ if (stored.submissionId === submissionId) this.recordEntry(submissionId, stored.entry, model);
166
+ }
167
+ }
168
+
169
+ private subagentLifecycle(input: { submissionId: string; parentRunId: string }): RuntimeSubagentLifecycleRecorder {
170
+ return new RuntimeSubagentLifecycleRecorder(input, {
171
+ save: async (event) => {
172
+ this.save(event.kind === "subagent"
173
+ ? { type: "subagent-usage", event }
174
+ : { type: "lifecycle", event }, Date.now());
175
+ await this.drain();
176
+ },
177
+ has: (eventId) => this.options.outbox.has(eventId),
178
+ hasSubagentUsage: (submissionId, subagentRunId) =>
179
+ this.options.outbox.hasSubagentUsage(submissionId, subagentRunId),
180
+ });
181
+ }
182
+
183
+ async subagentStarted(metadata: RuntimeSubagentRunMetadata, run: SubagentRun): Promise<void> {
184
+ await this.subagentLifecycle(metadata).register(run.runId);
185
+ this.options.telemetry.capture("subagentStarted", {
186
+ submissionId: metadata.submissionId,
187
+ ...(run.parentToolCallId ? { toolCallId: run.parentToolCallId } : {}),
188
+ subagentId: run.runId,
189
+ agentType: run.agentType,
190
+ occurredAt: run.startedAt,
191
+ });
192
+ }
193
+
194
+ async subagentFinished(metadata: RuntimeSubagentRunMetadata, run: SubagentRun, result: {
195
+ readonly status: string;
196
+ readonly summary?: string;
197
+ readonly error?: string;
198
+ readonly childStillRunning?: boolean;
199
+ }): Promise<void> {
200
+ const lifecycle = this.subagentLifecycle(metadata);
201
+ await lifecycle.register(run.runId);
202
+ if (result.status === "interrupted" && result.childStillRunning) return;
203
+ const completion = decodeSubagentCompletion(result.summary);
204
+ const hasUsage = completion && !isZeroSubagentUsage(completion.usage);
205
+ const status = subagentTerminalStatus(result.status);
206
+ if (completion && hasUsage) await lifecycle.usage({
207
+ eventId: `${run.runId}:usage`,
208
+ submissionId: metadata.submissionId,
209
+ runId: run.runId,
210
+ subagentRunId: run.runId,
211
+ parentRunId: metadata.parentRunId,
212
+ kind: "subagent",
213
+ status,
214
+ usage: completion.usage,
215
+ });
216
+ await lifecycle.terminal({ subagentRunId: run.runId, status, zeroUsage: !hasUsage });
217
+ const occurredAt = run.completedAt ?? Date.now();
218
+ this.options.telemetry.capture("subagentFinished", {
219
+ submissionId: metadata.submissionId,
220
+ ...(run.parentToolCallId ? { toolCallId: run.parentToolCallId } : {}),
221
+ subagentId: run.runId,
222
+ outcome: status === "completed" ? "completed"
223
+ : status === "aborted" || status === "skipped" ? "cancelled" : "failed",
224
+ durationMs: Math.max(0, occurredAt - run.startedAt),
225
+ ...(completion ? { output: completion.output } : {}),
226
+ ...(result.error ? { error: result.error } : {}),
227
+ occurredAt,
228
+ });
229
+ }
230
+
231
+ drain(idempotentRetry = true): Promise<void> {
232
+ const drain = (this.drainTail ?? Promise.resolve())
233
+ .catch(() => undefined)
234
+ .then(() => this.deliver(idempotentRetry));
235
+ this.drainTail = drain;
236
+ return drain.finally(() => {
237
+ if (this.drainTail === drain) this.drainTail = undefined;
238
+ });
239
+ }
240
+
241
+ private async deliver(idempotentRetry: boolean): Promise<void> {
242
+ const port = this.options.port();
243
+ if (!port) return;
244
+ for (const row of this.options.outbox.listPending()) {
245
+ try {
246
+ const payload = JSON.parse(row.body) as Payload;
247
+ if (payload.type === "model-usage") {
248
+ await port.onModelUsage?.(payload.event);
249
+ } else if (payload.type === "tool-settlement") {
250
+ await port.onToolSettled?.(payload.event);
251
+ } else if (payload.type === "subagent-usage") {
252
+ if (port.onSubagentUsage && (await port.onSubagentUsage(payload.event))?.confirmed !== true) {
253
+ throw new Error("Runtime event was not confirmed");
254
+ }
255
+ } else if (port.onLifecycleFact && (await port.onLifecycleFact(payload.event))?.confirmed !== true) {
256
+ throw new Error("Runtime event was not confirmed");
257
+ }
258
+ this.options.transaction(() => this.options.outbox.markDelivered(row.eventId, Date.now()));
259
+ } catch (error) {
260
+ const failure = error instanceof Error ? error.message : String(error);
261
+ const attemptCount = this.options.transaction(() => {
262
+ const count = this.options.outbox.markFailed(row.eventId, failure);
263
+ if (count >= MAX_ATTEMPTS) this.options.outbox.quarantine(row.eventId, Date.now(), failure);
264
+ return count;
265
+ });
266
+ console.warn(attemptCount >= MAX_ATTEMPTS
267
+ ? "[runtime-turn-event:quarantined]"
268
+ : "[runtime-turn-event:degraded]", JSON.stringify({ eventId: row.eventId, attemptCount, error: failure }));
269
+ if (attemptCount >= MAX_ATTEMPTS) continue;
270
+ try {
271
+ await this.options.scheduleRetry(RETRY_SECONDS, idempotentRetry);
272
+ } catch (error) {
273
+ console.warn("[runtime-turn-event-retry:degraded]", String(error));
274
+ }
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  AgentToolResult,
8
8
  } from "@earendil-works/pi-agent-core";
9
9
  import { asSchema, type Tool as AiTool } from "ai";
10
- import { serializeOutput } from "../../lib/artifacts";
10
+ import { serializeOutput } from "../lib/artifacts";
11
11
 
12
12
  /** 控制一个 ai-sdk Tool 如何投影到 Pi AgentTool。 */
13
13
  export interface AiToolAdapterOptions {
@@ -87,7 +87,7 @@ function modelContent(
87
87
  /**
88
88
  * 把一个可执行的 ai-sdk Tool 转成 Pi AgentTool。
89
89
  *
90
- * Workspace、Skill 和其他 ai-sdk 工具工厂在把候选项交给 `compilePiTools()` 前调用。
90
+ * Workspace、Skill 和其他 ai-sdk 工具工厂在把候选项交给 `compileHarnessTools()` 前调用。
91
91
  *
92
92
  * 实现必须在一处同时对齐 schema、execute 参数和结果形状,否则 Pi 的校验、取消信号和 artifact 处理会绕过统一工具边界。
93
93
  *
@@ -0,0 +1 @@
1
+ export * from "./repository";
@@ -1,5 +1,5 @@
1
1
  import type { SqlTaggedTemplate } from "agents/chat";
2
- import type { ExecutionLevel } from "../lib/execution-level";
2
+ import type { ExecutionLevel } from "../../lib/execution-level";
3
3
 
4
4
  // Approval 的统一定义见 ./index.ts。
5
5
  // #region 状态与行类型
@@ -55,11 +55,6 @@ export interface ApprovalView {
55
55
  status: ApprovalStatus;
56
56
  }
57
57
 
58
- export interface DecidedApprovalRef {
59
- executionId: string;
60
- submissionId: string;
61
- }
62
-
63
58
  type ApprovalRow = {
64
59
  execution_id: string;
65
60
  submission_id: string;
@@ -105,12 +100,12 @@ function mapRow(row: ApprovalRow): StoredApproval {
105
100
  // #region Repository
106
101
  export class ApprovalRepository {
107
102
  // 保存当前 Agent SQLite 的查询入口。
108
- // RuntimeDatabase 构造时调用,ApprovalLifecycle 和 Transcript 随后共享这个 Repository。
103
+ // RuntimeDatabase 构造时调用,HarnessDecisionGate 和 Transcript 随后共享这个 Repository。
109
104
  // Approval 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
110
105
  constructor(private readonly sql: SqlTaggedTemplate) {}
111
106
 
112
107
  // 按 executionId 读取一条完整 Approval,不存在时返回 null。
113
- // ApprovalLifecycle 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
108
+ // HarnessDecisionGate 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
114
109
  // execution_id 是主键,LIMIT 1 明确只消费一行;不能改成按非唯一字段随意取首行。
115
110
  find(executionId: string): StoredApproval | null {
116
111
  const row = this.sql<ApprovalRow>`
@@ -126,7 +121,7 @@ export class ApprovalRepository {
126
121
  }
127
122
 
128
123
  // 持久化一条新的 Approval。
129
- // ApprovalLifecycle.ensurePending 在准入事务内确认没有同一记录后调用。
124
+ // HarnessDecisionGate 在 `before_tool` 事务内确认没有同一记录后调用。
130
125
  // 主键和 submission_id/tool_call_id 唯一约束是持久化去重防线,不能用覆盖式 INSERT 掩盖冲突审批。
131
126
  insert(approval: NewApproval): void {
132
127
  this.sql`
@@ -153,7 +148,7 @@ export class ApprovalRepository {
153
148
  }
154
149
 
155
150
  // 只把 pending Approval 写成指定决策,并记录时间和原因。
156
- // applyPiRecoveryMutations 在处理用户决策或恢复命令时调用,迟到的重复决策不应覆盖首次结果。
151
+ // Runtime 处理用户决策时调用,迟到的重复决策不应覆盖首次结果。
157
152
  // WHERE status='pending' 实现首次决策胜出,不能去掉这个条件;调用方在事务后重读来确认是否真的生效。
158
153
  decide(
159
154
  executionId: string,
@@ -172,7 +167,7 @@ export class ApprovalRepository {
172
167
  }
173
168
 
174
169
  // 按创建顺序列出某次 Submission 仍待决策的 executionId。
175
- // ApprovalLifecycle.rejectPending 在终止 Turn 时调用,随后为每条待决策记录追加拒绝恢复事实。
170
+ // Runtime 终止 Turn 时调用,保证没有待决策记录泊留在终态 Submission 上。
176
171
  // ORDER BY created_at 保留决策的原始顺序,不能依赖 SQLite 未声明的默认行顺序。
177
172
  listPendingForSubmission(submissionId: string): string[] {
178
173
  return this.sql<{ execution_id: string }>`
@@ -185,7 +180,7 @@ export class ApprovalRepository {
185
180
  }
186
181
 
187
182
  // 列出所属 Submission 仍活跃的全部待决策 Approval 摘要。
188
- // ApprovalLifecycle.list 和广播流程在生成用户可见审批列表时调用。
183
+ // Runtime 广播流程在生成用户可见审批列表时调用。
189
184
  // JOIN Submission 是为了隐藏已终止 Turn 的孤立 pending 行,不能只按 Approval 自身状态暴露给用户。
190
185
  listPendingActive(): PendingApprovalSummary[] {
191
186
  return this.sql<{
@@ -222,23 +217,8 @@ export class ApprovalRepository {
222
217
  }));
223
218
  }
224
219
 
225
- // 按决策时间列出所有已批准或已拒绝的 Approval 引用。
226
- // ApprovalLifecycle.dispatchPendingContinuations 在 Runtime 启动时调用,用恢复决策判断哪些记录仍需要继续。
227
- // 这里故意不预先猜测“已分派”,幂等性由 Recovery Milestone 决定;不能用内存队列替代跨重启扫描。
228
- listDecided(): DecidedApprovalRef[] {
229
- return this.sql<{ execution_id: string; submission_id: string }>`
230
- SELECT execution_id, submission_id
231
- FROM pi_approvals
232
- WHERE status IN ('approved', 'rejected')
233
- ORDER BY decided_at ASC
234
- `.map((row) => ({
235
- executionId: row.execution_id,
236
- submissionId: row.submission_id,
237
- }));
238
- }
239
-
240
220
  // 计算所属 Submission 仍活跃的 pending Approval 数量。
241
- // ApprovalLifecycle.countPending 在报告交互阻塞和 Runtime 稳定状态时调用。
221
+ // Runtime 在报告交互阻塞和稳定状态时调用。
242
222
  // 与 listPendingActive 使用同样的 JOIN 和状态条件才能保证计数和用户可见列表一致,修改其一时必须核对另一个。
243
223
  countPendingActive(): number {
244
224
  return (
@@ -254,7 +234,7 @@ export class ApprovalRepository {
254
234
  }
255
235
 
256
236
  // 只在某个 toolCallId 唯一匹配一条活跃 pending Approval 时返回 executionId。
257
- // Runtime 处理工具审批决策时通过 ApprovalLifecycle 调用,返回 null 表示没有或存在歧义。
237
+ // Runtime 处理工具审批决策时通过 HarnessDecisionGate 调用,返回 null 表示没有或存在歧义。
258
238
  // toolCallId 只在单个 Submission 内唯一,LIMIT 2 用最小查询识别跨 Submission 重名;不能用 LIMIT 1 静默选中任意 Approval。
259
239
  findPendingByToolCallId(toolCallId: string): string | null {
260
240
  const matches = this.sql<{ execution_id: string }>`
@@ -274,7 +254,7 @@ export class ApprovalRepository {
274
254
 
275
255
  // 列出某次 Submission 的审批 ID、工具调用 ID 和状态。
276
256
  // Transcript 投影浏览器消息时调用,用这些精简字段补齐工具调用视图。
277
- // 这里不返回风险、输入等内容,避免 Transcript 和 ApprovalLifecycle 共享过宽的数据契约。
257
+ // 这里不返回风险、输入等内容,避免 Transcript 和 HarnessDecisionGate 共享过宽的数据契约。
278
258
  listApprovalViews(submissionId: string): ApprovalView[] {
279
259
  return this.sql<{
280
260
  execution_id: string;