@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
@@ -0,0 +1,167 @@
1
+ import type { CancelQueuedResult, LaneSnapshot } from "@earendil-works/pi-agent-core";
2
+ import type { UIMessage } from "ai";
3
+ import type { ConversationHistory } from "../conversation/history";
4
+ import type { SubmitMessageOptions } from "./admission";
5
+ import type { SubmissionHandle } from "./lifecycle";
6
+ import { submissionAbortReason, type StoredSubmission, type SubmissionRepository } from "./repository";
7
+ import type { PiCanonicalUserInput } from "../conversation/message/contract";
8
+ import type { MessageUiRepository } from "../conversation/repository";
9
+ import type { PendingSteer, SteerRepository } from "./steer-repository";
10
+
11
+ interface PendingSteerDeliveryOptions {
12
+ steers: SteerRepository;
13
+ messageUi: MessageUiRepository;
14
+ transaction<T>(run: () => T): T;
15
+ enqueue(message: PiCanonicalUserInput): Promise<string>;
16
+ }
17
+
18
+ /** Reconciles Host steer receipts with Pi queue identities, without owning execution. */
19
+ export class PendingSteerDelivery {
20
+ constructor(private readonly options: PendingSteerDeliveryOptions) {}
21
+
22
+ async queue(steer: PendingSteer): Promise<void> {
23
+ const entryId = await this.options.enqueue(
24
+ JSON.parse(steer.canonicalJson) as PiCanonicalUserInput,
25
+ );
26
+ this.link(steer, entryId);
27
+ }
28
+
29
+ /** Called after Pi commits the original queued user entry. */
30
+ settle(entryId: string): boolean {
31
+ const steer = this.options.steers.findByHarnessEntryId(entryId);
32
+ if (!steer) return false;
33
+ this.options.transaction(() => {
34
+ this.persistSidecar(steer, entryId);
35
+ this.options.steers.deleteByMessageId(steer.messageId);
36
+ });
37
+ return true;
38
+ }
39
+
40
+ async reconcile(
41
+ submissionId: string,
42
+ snapshot: Pick<LaneSnapshot, "queues" | "transcript">,
43
+ ): Promise<void> {
44
+ const pending = this.options.steers.listForSubmission(submissionId);
45
+ const queuedIds = new Set(snapshot.queues.map(({ entryId }) => entryId));
46
+ const transcriptIds = new Set(snapshot.transcript.map(({ id }) => id));
47
+ const linkedIds = new Set(pending.flatMap(({ harnessEntryId }) =>
48
+ harnessEntryId ? [harnessEntryId] : []
49
+ ));
50
+ const unlinked = snapshot.queues.filter((entry) =>
51
+ entry.kind === "steer" && entry.type === "message" &&
52
+ !linkedIds.has(entry.entryId)
53
+ );
54
+ // Both durable queues preserve acceptance order; identical text is not identity.
55
+ for (const steer of pending) {
56
+ if (steer.harnessEntryId && transcriptIds.has(steer.harnessEntryId)) {
57
+ this.settle(steer.harnessEntryId);
58
+ } else if (steer.harnessEntryId && queuedIds.has(steer.harnessEntryId)) {
59
+ this.options.transaction(() =>
60
+ this.persistSidecar(steer, steer.harnessEntryId!)
61
+ );
62
+ } else {
63
+ const queued = !steer.harnessEntryId ? unlinked.shift() : undefined;
64
+ if (queued) this.link(steer, queued.entryId);
65
+ else await this.queue(steer);
66
+ }
67
+ }
68
+ }
69
+
70
+ private link(steer: PendingSteer, entryId: string): void {
71
+ this.options.transaction(() => {
72
+ if (steer.harnessEntryId && steer.harnessEntryId !== entryId) {
73
+ this.options.messageUi.delete(steer.harnessEntryId);
74
+ }
75
+ this.options.steers.linkHarnessEntry(steer.messageId, entryId);
76
+ this.persistSidecar(steer, entryId);
77
+ });
78
+ }
79
+
80
+ private persistSidecar(steer: PendingSteer, entryId: string): void {
81
+ if (steer.uiMessageJson) {
82
+ this.options.messageUi.upsert(entryId, steer.uiMessageJson);
83
+ }
84
+ }
85
+ }
86
+
87
+
88
+ /** Keep accepted steers visible on stop, or transfer unconsumed messages to new Submissions. */
89
+ export async function finishPendingSteers(options: {
90
+ submissionId: string;
91
+ aborted: boolean;
92
+ submissions: SubmissionRepository;
93
+ cancelHandoff(submissionId: string, reason: string): StoredSubmission;
94
+ steers: SteerRepository;
95
+ messageUi: MessageUiRepository;
96
+ transaction<T>(run: () => T): T;
97
+ conversation: Pick<ConversationHistory, "append">;
98
+ cancelQueued(entryId: string): Promise<CancelQueuedResult | undefined>;
99
+ submit(
100
+ message: PiCanonicalUserInput,
101
+ options: SubmitMessageOptions,
102
+ ): Promise<SubmissionHandle<StoredSubmission>>;
103
+ waitUntil(promise: Promise<unknown>): void;
104
+ }): Promise<void> {
105
+ for (const steer of options.steers.listForSubmission(options.submissionId)) {
106
+ const message = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
107
+ const userMessage = steer.uiMessageJson
108
+ ? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
109
+ : undefined;
110
+ const requestId = `${options.submissionId}:steer:${steer.steerId}`;
111
+ if (options.aborted) {
112
+ options.transaction(() => {
113
+ let handoff = options.submissions.findByRequestId(requestId);
114
+ if (handoff?.status === "pending") {
115
+ const reason = submissionAbortReason(options.submissions.find(options.submissionId)) ?? "Cancelled";
116
+ options.submissions.updateAbortReason(handoff.submissionId, reason);
117
+ handoff = options.cancelHandoff(handoff.submissionId, reason);
118
+ }
119
+ if (steer.harnessEntryId) options.messageUi.delete(steer.harnessEntryId);
120
+ options.conversation.append(steer.messageId, message, {
121
+ submissionId: handoff?.status === "aborted" || handoff?.status === "skipped"
122
+ ? handoff.submissionId
123
+ : options.submissionId,
124
+ ...(userMessage ? { userMessage } : {}),
125
+ });
126
+ options.steers.deleteByMessageId(steer.messageId);
127
+ });
128
+ continue;
129
+ }
130
+ if (steer.harnessEntryId) {
131
+ const cancelled = await options.cancelQueued(steer.harnessEntryId);
132
+ if (cancelled) {
133
+ if (!cancelled.ok) throw cancelled.error;
134
+ if (cancelled.value.kind === "already_consumed") {
135
+ options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
136
+ continue;
137
+ }
138
+ options.transaction(() => options.messageUi.delete(steer.harnessEntryId!));
139
+ }
140
+ }
141
+ // A queued Submission moved to this Turn already owns messageId and is aborted.
142
+ // Handoff needs its own stable identity while preserving the user's message identity.
143
+ const submitted = await options.submit(message, {
144
+ requestId,
145
+ idempotencyKey: requestId,
146
+ userMessageId: steer.messageId,
147
+ steerHandoff: { submissionId: options.submissionId, steerId: steer.steerId },
148
+ ...(userMessage ? { userMessage } : {}),
149
+ });
150
+ options.waitUntil(submitted.completion);
151
+ options.transaction(() => {
152
+ let handoff = options.submissions.find(submitted.receipt.submissionId) ?? submitted.receipt;
153
+ const abortReason = submissionAbortReason(options.submissions.find(options.submissionId));
154
+ if (abortReason && handoff.status === "pending" && handoff.requestId === requestId) {
155
+ options.submissions.updateAbortReason(handoff.submissionId, abortReason);
156
+ handoff = options.cancelHandoff(handoff.submissionId, abortReason);
157
+ }
158
+ if (handoff.status === "skipped" || handoff.status === "aborted") {
159
+ options.conversation.append(steer.messageId, message, {
160
+ submissionId: handoff.submissionId,
161
+ ...(userMessage ? { userMessage } : {}),
162
+ });
163
+ }
164
+ options.steers.deleteByMessageId(steer.messageId);
165
+ });
166
+ }
167
+ }
@@ -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
@@ -0,0 +1,40 @@
1
+ /** Host-confirmed authority pinned to one scheduled task Requirement version.
2
+ * Credentials never belong in scope; only trusted Host ports consume it.
3
+ */
4
+ export interface RuntimeUnattendedExecutionGrant {
5
+ readonly taskId: string;
6
+ readonly requirementVersion: string;
7
+ readonly authorizationVersion: string;
8
+ readonly scope: Readonly<Record<string, unknown>>;
9
+ }
10
+
11
+ export function pinnedUnattendedExecutionGrant(
12
+ value: RuntimeUnattendedExecutionGrant,
13
+ ): RuntimeUnattendedExecutionGrant {
14
+ for (const [name, version] of [
15
+ ["task ID", value.taskId],
16
+ ["Requirement", value.requirementVersion],
17
+ ["authorization", value.authorizationVersion],
18
+ ] as const) {
19
+ if (typeof version !== "string" || !version.trim()) {
20
+ throw new Error(`Unattended execution ${name} is required`);
21
+ }
22
+ }
23
+ if (!value.scope || typeof value.scope !== "object" || Array.isArray(value.scope)) {
24
+ throw new Error("Unattended execution scope must be an object");
25
+ }
26
+ try {
27
+ const scope = JSON.parse(JSON.stringify(value.scope)) as unknown;
28
+ if (!scope || typeof scope !== "object" || Array.isArray(scope)) {
29
+ throw new Error("scope changed shape during serialization");
30
+ }
31
+ return Object.freeze({
32
+ taskId: value.taskId,
33
+ requirementVersion: value.requirementVersion,
34
+ authorizationVersion: value.authorizationVersion,
35
+ scope: scope as Readonly<Record<string, unknown>>,
36
+ });
37
+ } catch (error) {
38
+ throw new Error("Unattended execution scope must be JSON-serializable", { cause: error });
39
+ }
40
+ }
@@ -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/subagent/temporary/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(