@llblab/pi-actors 0.51.0 → 0.52.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 (58) hide show
  1. package/AGENTS.md +6 -3
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +3 -3
  5. package/dist/index.js +3 -0
  6. package/dist/lib/async-runs.d.ts +2 -2
  7. package/dist/lib/async-runs.js +42 -19
  8. package/dist/lib/extension-runtime.d.ts +1 -0
  9. package/dist/lib/extension-runtime.js +6 -1
  10. package/dist/lib/limits.d.ts +9 -0
  11. package/dist/lib/limits.js +9 -0
  12. package/dist/lib/observability.d.ts +10 -11
  13. package/dist/lib/observability.js +79 -56
  14. package/dist/lib/pi.d.ts +31 -0
  15. package/dist/lib/pi.js +180 -0
  16. package/dist/lib/run-delivery.d.ts +115 -0
  17. package/dist/lib/run-delivery.js +623 -0
  18. package/dist/lib/run-ui-runtime.d.ts +3 -0
  19. package/dist/lib/run-ui-runtime.js +341 -13
  20. package/dist/lib/runs-trace.d.ts +1 -1
  21. package/dist/lib/runs-trace.js +5 -3
  22. package/dist/lib/session-evidence.d.ts +16 -0
  23. package/dist/lib/session-evidence.js +143 -0
  24. package/dist/lib/temp.js +1 -1
  25. package/dist/lib/tools-inspect.js +3 -1
  26. package/dist/skills/actors/SKILL.md +17 -7
  27. package/dist/skills/actors/references/runs.md +1 -1
  28. package/dist/skills/music-player/SKILL.md +3 -3
  29. package/dist/skills/music-player/genapps/music-player.mjs +6 -4
  30. package/dist/skills/music-player/scripts/playback.mjs +85 -18
  31. package/dist/skills/swarm/SKILL.md +2 -6
  32. package/dist/skills/swarm/references/development-swarm.md +2 -31
  33. package/docs/README.md +1 -0
  34. package/docs/async-runs.md +4 -3
  35. package/docs/coordinator-delivery.md +207 -0
  36. package/docs/recipe-library.md +1 -1
  37. package/index.ts +3 -0
  38. package/lib/async-runs.ts +42 -21
  39. package/lib/extension-runtime.ts +6 -1
  40. package/lib/limits.ts +9 -0
  41. package/lib/observability.ts +96 -78
  42. package/lib/pi.ts +210 -0
  43. package/lib/run-delivery.ts +800 -0
  44. package/lib/run-ui-runtime.ts +370 -18
  45. package/lib/runs-trace.ts +6 -4
  46. package/lib/session-evidence.ts +153 -0
  47. package/lib/temp.ts +1 -1
  48. package/lib/tools-inspect.ts +4 -1
  49. package/package.json +1 -1
  50. package/skills/actors/SKILL.md +17 -7
  51. package/skills/actors/references/runs.md +1 -1
  52. package/skills/music-player/SKILL.md +3 -3
  53. package/skills/music-player/genapps/music-player.mjs +6 -4
  54. package/skills/music-player/scripts/playback.mjs +85 -18
  55. package/skills/swarm/SKILL.md +2 -6
  56. package/skills/swarm/references/development-swarm.md +2 -31
  57. package/dist/skills/music-player/scripts/playback-client.mjs +0 -143
  58. package/skills/music-player/scripts/playback-client.mjs +0 -143
package/index.ts CHANGED
@@ -15,6 +15,9 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
15
15
  );
16
16
  pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
17
17
  pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
18
+ pi.on("context", async (event, ctx) => ({
19
+ messages: runtime.onContext(event.messages, ctx) as typeof event.messages,
20
+ }));
18
21
  pi.on("session_shutdown", async (event, ctx) =>
19
22
  runtime.onSessionShutdown(event.reason, ctx),
20
23
  );
package/lib/async-runs.ts CHANGED
@@ -1136,30 +1136,51 @@ function stopRun(
1136
1136
  export function markRunTerminalNotificationHandled(
1137
1137
  stateDir: string,
1138
1138
  status: string,
1139
- ): void {
1140
- markTerminalHandled(stateDir, {
1141
- event: "run.notification",
1142
- status,
1143
- });
1139
+ expectedRunInstanceId: string,
1140
+ ): boolean {
1141
+ const releaseLock = RunsStart.acquireStateStartLock(stateDir);
1142
+ try {
1143
+ if (!existsSync(join(stateDir, "run.json"))) return false;
1144
+ const current = getRunStatus(stateDir);
1145
+ if (
1146
+ current.run_instance_id !== expectedRunInstanceId ||
1147
+ current.status !== status
1148
+ ) return false;
1149
+ markTerminalHandled(stateDir, {
1150
+ event: "run.notification",
1151
+ run_instance_id: expectedRunInstanceId,
1152
+ status,
1153
+ });
1154
+ rmSync(join(stateDir, "terminal-delivery-failure.json"), { force: true });
1155
+ return true;
1156
+ } finally {
1157
+ releaseLock();
1158
+ }
1144
1159
  }
1145
1160
 
1146
- export function recordRunTerminalDeliveryFailure(
1161
+ export function markRunSteerPresentationHandled(
1147
1162
  stateDir: string,
1148
- status: string,
1149
- error: unknown,
1150
- ): void {
1151
- const path = join(stateDir, "terminal-delivery-failure.json");
1152
- const previous = readJson(path);
1153
- const message = (error instanceof Error ? error.message : String(error))
1154
- .replaceAll(/\s+/g, " ")
1155
- .trim()
1156
- .slice(0, 500);
1157
- writeJsonAtomic(path, {
1158
- attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
1159
- error: message || "unknown delivery failure",
1160
- status,
1161
- ts: new Date().toISOString(),
1162
- });
1163
+ expectedRunInstanceId: string,
1164
+ eventId: string,
1165
+ steerId: string,
1166
+ ): boolean {
1167
+ const releaseLock = RunsStart.acquireStateStartLock(stateDir);
1168
+ try {
1169
+ if (!existsSync(join(stateDir, "run.json"))) return false;
1170
+ const current = getRunStatus(stateDir);
1171
+ if (current.run_instance_id !== expectedRunInstanceId) return false;
1172
+ appendRunTraceEvent(stateDir, {
1173
+ data: {
1174
+ event_id: eventId,
1175
+ run_instance_id: expectedRunInstanceId,
1176
+ steer_id: steerId,
1177
+ },
1178
+ kind: "delivery.steer_presented",
1179
+ });
1180
+ return true;
1181
+ } finally {
1182
+ releaseLock();
1183
+ }
1163
1184
  }
1164
1185
 
1165
1186
  export function cancelRun(
@@ -26,6 +26,7 @@ export interface ActorExtensionRuntime {
26
26
  discoverResources(metaUrl: string): { skillPaths: string[] } | undefined;
27
27
  getRunOwnerId(ctx: Pi.ExtensionContext): string;
28
28
  onAgentSettled(ctx: Pi.ExtensionContext): void;
29
+ onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
29
30
  onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
30
31
  onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
31
32
  registerCoreTools(): void;
@@ -142,7 +143,11 @@ export function createActorExtensionRuntime(
142
143
  },
143
144
  getRunOwnerId,
144
145
  onAgentSettled(ctx) {
145
- if (activeRunContext === ctx) automaticReview.schedule();
146
+ if (activeRunContext !== ctx) return;
147
+ if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
148
+ },
149
+ onContext(messages, ctx) {
150
+ return runUiRuntime.projectContext(messages, ctx);
146
151
  },
147
152
  onSessionShutdown(reason, ctx) {
148
153
  const ownerId = runOwnerIdsByContext.get(ctx);
package/lib/limits.ts CHANGED
@@ -28,3 +28,12 @@ export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
28
28
  export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
29
29
  export const RUN_RETENTION_MAX_RECORDS = 256;
30
30
  export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
31
+ export const RUN_DELIVERY_BATCH_MAX_MEMBERS = 256;
32
+ export const RUN_DELIVERY_MODEL_MAX_MEMBERS = 64;
33
+ export const RUN_DELIVERY_MODEL_MAX_BYTES = 16 * 1024;
34
+ export const RUN_DELIVERY_RECEIPT_LIMIT = 128;
35
+ export const RUN_DELIVERY_SESSION_MAX_ENTRIES = 256;
36
+ export const RUN_DELIVERY_SESSION_MAX_BYTES = 1024 * 1024;
37
+ export const RUN_DELIVERY_STEER_MAX_ENVELOPES = 64;
38
+ export const RUN_DELIVERY_STEER_MAX_BYTES = 16 * 1024;
39
+ export const RUN_DELIVERY_JOURNAL_MAX_BYTES = 1024 * 1024;
@@ -26,6 +26,7 @@ import {
26
26
  import * as AsyncRuns from "./async-runs.ts";
27
27
  import * as Paths from "./paths.ts";
28
28
  import * as RunsTrace from "./runs-trace.ts";
29
+ import type { RunCompletionBatchMember } from "./run-delivery.ts";
29
30
  import { readJsonlFileResilient } from "./state-readers.ts";
30
31
 
31
32
  export type RunObservedStatus =
@@ -35,7 +36,7 @@ export type RunObservedStatus =
35
36
  | "exited"
36
37
  | "cancelled"
37
38
  | "killed";
38
- export type RunTraceAttention = "log" | "notify" | "followup";
39
+ export type RunTraceAttention = "log" | "notify" | "followup" | "steer";
39
40
  export type RunTraceLevel = "info" | "warning" | "error";
40
41
 
41
42
  export interface RunObservation {
@@ -53,6 +54,7 @@ export interface RunObservation {
53
54
  terminalHandled?: boolean;
54
55
  retireWhen?: string;
55
56
  run: string;
57
+ runInstanceId?: string;
56
58
  semanticResult?: RunTerminalSemanticResult;
57
59
  tool?: string;
58
60
  stateDir?: string;
@@ -149,70 +151,6 @@ export function pruneRunUiObservationState(
149
151
  );
150
152
  }
151
153
 
152
- export function deliverRunTransitionNotifications(
153
- transitions: RunTransition[],
154
- sink: RunUiNotificationSink,
155
- inFlight: Set<string> = new Set(),
156
- ): void {
157
- for (const transition of transitions) {
158
- if (!shouldNotifyRunTransition(transition)) continue;
159
- const key = transition.stateDir ?? transition.run;
160
- if (inFlight.has(key)) continue;
161
- inFlight.add(key);
162
- try {
163
- const text = formatRunTransitionMessage(transition);
164
- sink.notify(text, getRunTransitionNotificationType(transition));
165
- if (!shouldSendRunTransitionFollowUp(transition)) continue;
166
- sink.sendFollowUp({
167
- customType: "pi-actors-run",
168
- content: text,
169
- display: false,
170
- details: transition,
171
- });
172
- if (transition.stateDir) {
173
- AsyncRuns.markRunTerminalNotificationHandled(
174
- transition.stateDir,
175
- transition.to,
176
- );
177
- }
178
- } catch (error) {
179
- if (transition.stateDir) {
180
- AsyncRuns.recordRunTerminalDeliveryFailure(
181
- transition.stateDir,
182
- transition.to,
183
- error,
184
- );
185
- }
186
- const message = error instanceof Error ? error.message : String(error);
187
- sink.notify(
188
- `Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
189
- "error",
190
- );
191
- } finally {
192
- inFlight.delete(key);
193
- }
194
- }
195
- }
196
-
197
- export function reconcileRunTerminalNotifications(input: {
198
- inFlight?: Set<string>;
199
- ownerId: string;
200
- sink: RunUiNotificationSink;
201
- state: RunUiObservationState;
202
- stateRoot?: string;
203
- includeAttention?: boolean;
204
- }): RunUiSnapshot {
205
- const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
206
- includeAttention: input.includeAttention,
207
- stateRoot: input.stateRoot,
208
- });
209
- deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
210
- if (input.includeAttention)
211
- deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
212
- pruneRunUiObservationState(input.state, snapshot);
213
- return snapshot;
214
- }
215
-
216
154
  export function deliverRunAttentionNotifications(
217
155
  events: RunAttentionEvent[],
218
156
  sink: RunUiNotificationSink,
@@ -447,7 +385,9 @@ export interface RunRetirementExecutorOptions {
447
385
  export interface RunTransition {
448
386
  from: RunObservedStatus;
449
387
  run: string;
388
+ runInstanceId?: string;
450
389
  stateDir?: string;
390
+ terminalAt?: string;
451
391
  artifacts?: Record<string, string>;
452
392
  launchCorrelation?: Record<string, string>;
453
393
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
@@ -477,6 +417,7 @@ export interface RunAttentionEvent {
477
417
  level: RunTraceLevel;
478
418
  metadata?: Record<string, unknown>;
479
419
  run: string;
420
+ runInstanceId?: string;
480
421
  stateDir: string;
481
422
  summary: string;
482
423
  ts: string;
@@ -513,11 +454,18 @@ function getProgress(status: Record<string, unknown>): Record<string, unknown> {
513
454
 
514
455
  function getUpdatedAt(status: Record<string, unknown>): string | undefined {
515
456
  const progress = getProgress(status);
516
- return typeof progress.updatedAt === "string"
517
- ? progress.updatedAt
518
- : typeof status.createdAt === "string"
519
- ? status.createdAt
520
- : undefined;
457
+ const result = status.result &&
458
+ typeof status.result === "object" &&
459
+ !Array.isArray(status.result)
460
+ ? status.result as Record<string, unknown>
461
+ : {};
462
+ return typeof result.completed_at === "string"
463
+ ? result.completed_at
464
+ : typeof progress.updatedAt === "string"
465
+ ? progress.updatedAt
466
+ : typeof status.createdAt === "string"
467
+ ? status.createdAt
468
+ : undefined;
521
469
  }
522
470
 
523
471
  function scanRunStateDirs(
@@ -658,6 +606,9 @@ function observeRun(stateDir: string): RunObservation | undefined {
658
606
  ...(typeof status.recipe_file === "string"
659
607
  ? { recipeFile: status.recipe_file }
660
608
  : {}),
609
+ ...(typeof status.run_instance_id === "string"
610
+ ? { runInstanceId: status.run_instance_id }
611
+ : {}),
661
612
  ...(status.terminal_handled ? { terminalHandled: true } : {}),
662
613
  ...(typeof status.retire_when === "string"
663
614
  ? { retireWhen: status.retire_when }
@@ -1060,8 +1011,10 @@ export function detectRunTransitions(
1060
1011
  ...(run.launchSource ? { launchSource: run.launchSource } : {}),
1061
1012
  ...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
1062
1013
  ...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
1014
+ ...(run.runInstanceId ? { runInstanceId: run.runInstanceId } : {}),
1063
1015
  ...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
1064
1016
  ...(run.terminalHandled ? { terminalHandled: true } : {}),
1017
+ ...(run.updatedAt ? { terminalAt: run.updatedAt } : {}),
1065
1018
  to: run.status,
1066
1019
  ...(run.tool ? { tool: run.tool } : {}),
1067
1020
  });
@@ -1107,7 +1060,9 @@ function parseAttentionRecord(
1107
1060
  ...(raw.body !== undefined ? { body: raw.body } : {}),
1108
1061
  ...(raw.data !== undefined ? { data: raw.data } : {}),
1109
1062
  attention:
1110
- raw.attention === "notify" || raw.attention === "followup"
1063
+ raw.attention === "notify" ||
1064
+ raw.attention === "followup" ||
1065
+ raw.attention === "steer"
1111
1066
  ? raw.attention
1112
1067
  : normalizeTraceAttention(raw.delivery),
1113
1068
  id,
@@ -1119,6 +1074,7 @@ function parseAttentionRecord(
1119
1074
  ? { metadata: raw.metadata as Record<string, unknown> }
1120
1075
  : {}),
1121
1076
  run: run.run,
1077
+ ...(run.runInstanceId ? { runInstanceId: run.runInstanceId } : {}),
1122
1078
  stateDir: run.stateDir,
1123
1079
  summary,
1124
1080
  ts,
@@ -1167,6 +1123,16 @@ export function detectRunAttentionEvents(
1167
1123
  const read = readTraceAttentionRecords(run);
1168
1124
  const retained = new Set<string>();
1169
1125
  const seen = seenEventIds.get(key) ?? new Set<string>();
1126
+ const presentedSteerIds = new Set(read.records.flatMap((record) => {
1127
+ if (
1128
+ record.kind !== "delivery.steer_presented" ||
1129
+ !record.data ||
1130
+ typeof record.data !== "object" ||
1131
+ Array.isArray(record.data)
1132
+ ) return [];
1133
+ const eventId = (record.data as Record<string, unknown>).event_id;
1134
+ return typeof eventId === "string" && eventId ? [eventId] : [];
1135
+ }));
1170
1136
  const start = read.canonical ? 0
1171
1137
  : Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
1172
1138
  for (const [index, record] of read.records.entries()) {
@@ -1174,8 +1140,14 @@ export function detectRunAttentionEvents(
1174
1140
  if (!event || !shouldNotifyRunAttentionEvent(event) ||
1175
1141
  event.kind === "runtime.trace_compacted") continue;
1176
1142
  retained.add(event.id);
1177
- if (prime || run.notificationPolicy === "silent") seen.add(event.id);
1178
- else if (index >= start && !seen.has(event.id)) {
1143
+ if (isRunSteerAttentionEvent(event) && presentedSteerIds.has(event.id)) {
1144
+ seen.add(event.id);
1145
+ continue;
1146
+ }
1147
+ if (run.notificationPolicy === "silent") seen.add(event.id);
1148
+ else if (prime) {
1149
+ if (!isRunSteerAttentionEvent(event)) seen.add(event.id);
1150
+ } else if (index >= start && !seen.has(event.id)) {
1179
1151
  events.push(event); seen.add(event.id);
1180
1152
  }
1181
1153
  }
@@ -1194,7 +1166,20 @@ export function getRunAttentionNotificationType(
1194
1166
 
1195
1167
  export function shouldNotifyRunAttentionEvent(event: RunAttentionEvent): boolean {
1196
1168
  if (event.kind === "command.done") return false;
1197
- return event.attention === "notify" || event.attention === "followup";
1169
+ return event.attention === "notify" ||
1170
+ event.attention === "followup" ||
1171
+ event.attention === "steer";
1172
+ }
1173
+
1174
+ export function isRunSteerAttentionEvent(event: RunAttentionEvent): boolean {
1175
+ return event.kind !== "command.done" && event.attention === "steer";
1176
+ }
1177
+
1178
+ export function retryRunAttentionEvent(
1179
+ state: RunUiObservationState,
1180
+ event: Pick<RunAttentionEvent, "id" | "stateDir">,
1181
+ ): void {
1182
+ state.attentionEventIds.get(event.stateDir)?.delete(event.id);
1198
1183
  }
1199
1184
 
1200
1185
  export function shouldSendRunAttentionFollowUp(event: RunAttentionEvent): boolean {
@@ -1297,10 +1282,43 @@ export function shouldNotifyRunTransition(transition: RunTransition): boolean {
1297
1282
  );
1298
1283
  }
1299
1284
 
1300
- export function shouldSendRunTransitionFollowUp(
1301
- transition: RunTransition,
1302
- ): boolean {
1303
- return shouldNotifyRunTransition(transition);
1285
+ /** Build exact immutable generation members for owner-journal admission. */
1286
+ export function collectRunCompletionBatchMembers(
1287
+ transitions: RunTransition[],
1288
+ ): RunCompletionBatchMember[] {
1289
+ return transitions.flatMap((transition) => {
1290
+ if (
1291
+ !shouldNotifyRunTransition(transition) ||
1292
+ !transition.stateDir ||
1293
+ !transition.runInstanceId ||
1294
+ !transition.terminalAt ||
1295
+ Number.isNaN(Date.parse(transition.terminalAt))
1296
+ ) return [];
1297
+ const artifactEntries = Object.entries(transition.artifacts ?? {})
1298
+ .filter((entry): entry is [string, string] =>
1299
+ typeof entry[1] === "string" && Boolean(entry[1]))
1300
+ .slice(0, 4);
1301
+ const rawSummary = transition.semanticResult?.summary.trim() ||
1302
+ `Run ${transition.to}.`;
1303
+ return [{
1304
+ ...(artifactEntries.length > 0
1305
+ ? { artifacts: Object.fromEntries(artifactEntries) }
1306
+ : {}),
1307
+ run: transition.run,
1308
+ run_instance_id: transition.runInstanceId,
1309
+ state_dir: transition.stateDir,
1310
+ status: transition.to as RunCompletionBatchMember["status"],
1311
+ summary: rawSummary.length > 1_000
1312
+ ? `${rawSummary.slice(0, 999)}…`
1313
+ : rawSummary,
1314
+ terminal_at: transition.terminalAt,
1315
+ }];
1316
+ }).sort((left, right) =>
1317
+ left.terminal_at.localeCompare(right.terminal_at) ||
1318
+ left.run.localeCompare(right.run) ||
1319
+ left.run_instance_id.localeCompare(right.run_instance_id) ||
1320
+ left.state_dir.localeCompare(right.state_dir)
1321
+ );
1304
1322
  }
1305
1323
 
1306
1324
  const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
package/lib/pi.ts CHANGED
@@ -9,8 +9,13 @@ import type {
9
9
  ExtensionContext,
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
 
12
+ import * as SessionEvidence from "./session-evidence.ts";
13
+
12
14
  export type { ExtensionAPI, ExtensionContext };
13
15
 
16
+ export const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
17
+ export const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
18
+
14
19
  export interface PiNotificationSink {
15
20
  notify(message: string, level: "info" | "warning" | "error"): void;
16
21
  sendFollowUp(message: {
@@ -39,6 +44,211 @@ export function createNotificationSink(
39
44
  };
40
45
  }
41
46
 
47
+ export function sendRunCompletionBatch(
48
+ pi: ExtensionAPI,
49
+ batchId: string,
50
+ content: string,
51
+ ): void {
52
+ pi.sendMessage({
53
+ customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
54
+ content,
55
+ display: false,
56
+ details: {
57
+ pi_actors_delivery: {
58
+ batch_id: batchId,
59
+ kind: "completion_batch",
60
+ },
61
+ },
62
+ }, {
63
+ deliverAs: "followUp",
64
+ triggerTurn: true,
65
+ });
66
+ }
67
+
68
+ export function sendRunSteer(
69
+ pi: ExtensionAPI,
70
+ input: { content: string; eventId: string; steerId: string },
71
+ ): void {
72
+ pi.sendMessage({
73
+ customType: RUN_STEER_CUSTOM_TYPE,
74
+ content: input.content,
75
+ display: false,
76
+ details: {
77
+ pi_actors_delivery: {
78
+ event_id: input.eventId,
79
+ kind: "urgent_steer",
80
+ steer_id: input.steerId,
81
+ },
82
+ },
83
+ }, {
84
+ deliverAs: "steer",
85
+ triggerTurn: true,
86
+ });
87
+ }
88
+
89
+ function completionBatchMessage(
90
+ message: unknown,
91
+ ): { batchId: string; content: string } | undefined {
92
+ if (!message || typeof message !== "object" || Array.isArray(message)) return undefined;
93
+ const record = message as Record<string, unknown>;
94
+ if (
95
+ record.customType !== RUN_COMPLETION_BATCH_CUSTOM_TYPE ||
96
+ typeof record.content !== "string"
97
+ ) return undefined;
98
+ const details = record.details;
99
+ if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
100
+ const delivery = (details as Record<string, unknown>).pi_actors_delivery;
101
+ if (!delivery || typeof delivery !== "object" || Array.isArray(delivery)) return undefined;
102
+ const envelope = delivery as Record<string, unknown>;
103
+ if (
104
+ envelope.kind !== "completion_batch" ||
105
+ typeof envelope.batch_id !== "string" ||
106
+ !envelope.batch_id ||
107
+ envelope.batch_id.length > 128
108
+ ) return undefined;
109
+ return { batchId: envelope.batch_id, content: record.content };
110
+ }
111
+
112
+ /** Collapse exact retry duplicates and remove conflicting delivery envelopes. */
113
+ export function dedupeRunCompletionBatchContext(messages: unknown[]): {
114
+ batches: Map<string, string>;
115
+ conflicts: Set<string>;
116
+ messages: unknown[];
117
+ } {
118
+ const batches = new Map<string, string>();
119
+ const conflicts = new Set<string>();
120
+ for (const message of messages) {
121
+ const batch = completionBatchMessage(message);
122
+ if (!batch) continue;
123
+ const existing = batches.get(batch.batchId);
124
+ if (existing !== undefined && existing !== batch.content) {
125
+ conflicts.add(batch.batchId);
126
+ } else if (existing === undefined) {
127
+ batches.set(batch.batchId, batch.content);
128
+ }
129
+ }
130
+ const retained = new Set<string>();
131
+ const filtered = messages.filter((message) => {
132
+ const batch = completionBatchMessage(message);
133
+ if (!batch) return true;
134
+ if (conflicts.has(batch.batchId) || retained.has(batch.batchId)) return false;
135
+ retained.add(batch.batchId);
136
+ return true;
137
+ });
138
+ for (const batchId of conflicts) batches.delete(batchId);
139
+ return { batches, conflicts, messages: filtered };
140
+ }
141
+
142
+ export function removeRunCompletionBatchFromContext(
143
+ messages: unknown[],
144
+ batchId: string,
145
+ ): unknown[] {
146
+ return messages.filter((message) =>
147
+ completionBatchMessage(message)?.batchId !== batchId);
148
+ }
149
+
150
+ function steerMessage(
151
+ message: unknown,
152
+ ): { content: string; eventId: string; steerId: string } | undefined {
153
+ if (!message || typeof message !== "object" || Array.isArray(message)) return undefined;
154
+ const record = message as Record<string, unknown>;
155
+ if (record.customType !== RUN_STEER_CUSTOM_TYPE || typeof record.content !== "string") {
156
+ return undefined;
157
+ }
158
+ const details = record.details;
159
+ if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
160
+ const delivery = (details as Record<string, unknown>).pi_actors_delivery;
161
+ if (!delivery || typeof delivery !== "object" || Array.isArray(delivery)) return undefined;
162
+ const envelope = delivery as Record<string, unknown>;
163
+ if (
164
+ envelope.kind !== "urgent_steer" ||
165
+ typeof envelope.steer_id !== "string" ||
166
+ !envelope.steer_id ||
167
+ envelope.steer_id.length > 128 ||
168
+ typeof envelope.event_id !== "string" ||
169
+ !envelope.event_id ||
170
+ envelope.event_id.length > 256
171
+ ) return undefined;
172
+ return {
173
+ content: record.content,
174
+ eventId: envelope.event_id,
175
+ steerId: envelope.steer_id,
176
+ };
177
+ }
178
+
179
+ export function dedupeRunSteerContext(messages: unknown[]): {
180
+ conflicts: Set<string>;
181
+ messages: unknown[];
182
+ steers: Map<string, { content: string; eventId: string }>;
183
+ } {
184
+ const conflicts = new Set<string>();
185
+ const steers = new Map<string, { content: string; eventId: string }>();
186
+ for (const message of messages) {
187
+ const steer = steerMessage(message);
188
+ if (!steer) continue;
189
+ const existing = steers.get(steer.steerId);
190
+ if (existing &&
191
+ (existing.content !== steer.content || existing.eventId !== steer.eventId)) {
192
+ conflicts.add(steer.steerId);
193
+ } else if (!existing) {
194
+ steers.set(steer.steerId, {
195
+ content: steer.content,
196
+ eventId: steer.eventId,
197
+ });
198
+ }
199
+ }
200
+ const retained = new Set<string>();
201
+ const filtered = messages.filter((message) => {
202
+ const steer = steerMessage(message);
203
+ if (!steer) return true;
204
+ if (conflicts.has(steer.steerId) || retained.has(steer.steerId)) return false;
205
+ retained.add(steer.steerId);
206
+ return true;
207
+ });
208
+ for (const steerId of conflicts) steers.delete(steerId);
209
+ return { conflicts, messages: filtered, steers };
210
+ }
211
+
212
+ export function removeRunSteerFromContext(
213
+ messages: unknown[],
214
+ steerId: string,
215
+ ): unknown[] {
216
+ return messages.filter((message) => steerMessage(message)?.steerId !== steerId);
217
+ }
218
+
219
+ export function inspectRunSteerSessionEvidence(
220
+ ctx: ExtensionContext,
221
+ input: { content: string; eventId: string; steerId: string },
222
+ ): SessionEvidence.ActiveSessionEntryEvidence {
223
+ return SessionEvidence.inspectBoundedActiveSessionEntries({
224
+ getEntry: (id) => ctx.sessionManager.getEntry(id),
225
+ leaf: ctx.sessionManager.getLeafEntry(),
226
+ match: (entry) => {
227
+ const steer = steerMessage(entry);
228
+ if (!steer || steer.steerId !== input.steerId) return undefined;
229
+ return steer.content === input.content && steer.eventId === input.eventId
230
+ ? "present"
231
+ : "conflict";
232
+ },
233
+ });
234
+ }
235
+
236
+ export function inspectRunCompletionBatchSessionEvidence(
237
+ ctx: ExtensionContext,
238
+ batchId: string,
239
+ content: string,
240
+ ): SessionEvidence.ActiveSessionEntryEvidence {
241
+ return SessionEvidence.inspectBoundedActiveSessionEntries({
242
+ getEntry: (id) => ctx.sessionManager.getEntry(id),
243
+ leaf: ctx.sessionManager.getLeafEntry(),
244
+ match: (entry) => {
245
+ const batch = completionBatchMessage(entry);
246
+ if (!batch || batch.batchId !== batchId) return undefined;
247
+ return batch.content === content ? "present" : "conflict";
248
+ },
249
+ });
250
+ }
251
+
42
252
  export function registerToolDefinitions(
43
253
  pi: ExtensionAPI,
44
254
  definitions: Iterable<unknown>,