@llblab/pi-kit 0.1.13 → 0.3.0

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 (86) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
  5. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
  7. package/node_modules/@llblab/pi-actors/README.md +5 -3
  8. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  9. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
  10. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
  11. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
  12. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
  13. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
  16. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
  17. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
  23. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
  24. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
  25. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
  27. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
  28. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
  29. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
  30. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
  31. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  32. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
  33. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  34. package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
  35. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
  36. package/node_modules/@llblab/pi-actors/index.ts +4 -1
  37. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
  38. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
  39. package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
  40. package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
  41. package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
  42. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
  43. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
  44. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
  45. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
  46. package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
  47. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
  48. package/node_modules/@llblab/pi-actors/package.json +3 -3
  49. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
  50. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
  51. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  52. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
  53. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  54. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +25 -0
  55. package/node_modules/@llblab/pi-telegram/README.md +4 -4
  56. package/node_modules/@llblab/pi-telegram/docs/activity.md +4 -4
  57. package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
  58. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +16 -14
  59. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  60. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -22
  61. package/node_modules/@llblab/pi-telegram/docs/public-api.md +6 -7
  62. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
  63. package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
  64. package/node_modules/@llblab/pi-telegram/index.ts +0 -1
  65. package/node_modules/@llblab/pi-telegram/lib/activity.ts +10 -5
  66. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +16 -17
  67. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
  68. package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
  69. package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
  70. package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
  71. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
  72. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
  73. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +212 -82
  74. package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
  75. package/node_modules/@llblab/pi-telegram/lib/status.ts +23 -4
  76. package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
  77. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
  78. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  79. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -205
  80. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
  81. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
  82. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +70 -110
  83. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
  84. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  85. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
  86. package/package.json +3 -3
@@ -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
- if (input.includeAttention)
210
- deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
211
- deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
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
  }
@@ -1193,7 +1165,21 @@ export function getRunAttentionNotificationType(
1193
1165
  }
1194
1166
 
1195
1167
  export function shouldNotifyRunAttentionEvent(event: RunAttentionEvent): boolean {
1196
- return event.attention === "notify" || event.attention === "followup";
1168
+ if (event.kind === "command.done") return false;
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);
1197
1183
  }
1198
1184
 
1199
1185
  export function shouldSendRunAttentionFollowUp(event: RunAttentionEvent): boolean {
@@ -1296,10 +1282,43 @@ export function shouldNotifyRunTransition(transition: RunTransition): boolean {
1296
1282
  );
1297
1283
  }
1298
1284
 
1299
- export function shouldSendRunTransitionFollowUp(
1300
- transition: RunTransition,
1301
- ): boolean {
1302
- 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
+ );
1303
1322
  }
1304
1323
 
1305
1324
  const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
@@ -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>,