@llblab/pi-actors 0.43.0 → 0.44.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 (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Pure bounded Run-evidence policy.
3
+ * Zones: Trace retention, compaction accounting, Control capacity and admission
4
+ * Owns deterministic limits policy; journal I/O, locks, lifecycle authorization, and projection stay in adapters.
5
+ * Trace keeps the newest physical suffix because semantic priority would silently
6
+ * redefine causal history. Admitted Controls never expire: only a real terminal
7
+ * transition or generation replacement proves that durable work is finished.
8
+ */
9
+
10
+ import * as Limits from "./limits.ts";
11
+
12
+ export const TRACE_COMPACTION_KIND = "runtime.trace_compacted";
13
+ export const TRACE_COMPACTION_VERSION = 1;
14
+ export type TraceJournalUsage = { bytes: number; events: number };
15
+ export type TraceRetentionCandidate<T> = { encodedBytes: number; value: T };
16
+ export type TraceRetentionSelection<T> = {
17
+ droppedEvents: number; retained: TraceRetentionCandidate<T>[];
18
+ retainedBytes: number; retainedEvents: number;
19
+ };
20
+ export interface TraceCompactionData {
21
+ version: typeof TRACE_COMPACTION_VERSION;
22
+ compactions_total: number;
23
+ dropped_valid_events_total: number;
24
+ dropped_malformed_lines_total: number;
25
+ dropped_bytes_total: number;
26
+ dropped_event_count_exact: boolean;
27
+ retained_events: number;
28
+ retained_bytes: number;
29
+ history_complete: false;
30
+ }
31
+ export interface TraceCompactionDelta {
32
+ dropped_valid_events: number;
33
+ dropped_malformed_lines: number;
34
+ dropped_bytes: number;
35
+ dropped_event_count_exact: boolean;
36
+ retained_events: number;
37
+ retained_bytes: number;
38
+ }
39
+ export type RunControlStatus = "queued" | "delivered" | "claimed" | "handled" | "failed";
40
+ export type RunControlCapacity = {
41
+ available: number; backpressured: boolean; limit: number;
42
+ oldest_pending_at?: string; pending: number;
43
+ };
44
+ export type RunControlAdmissionIntegrity =
45
+ | "valid" | "unreadable" | "malformed" | "noncanonical"
46
+ | "invalid_record" | "invalid_status_timestamp" | "generation_mismatch"
47
+ | "journal_bytes";
48
+ export interface RunControlAdmissionErrorDetails {
49
+ reason: "control_backpressure" | "control_journal_integrity";
50
+ pending: number;
51
+ limit: number;
52
+ journal_bytes: number;
53
+ journal_limit: number;
54
+ oldest_pending_at?: string;
55
+ run_instance_id: string;
56
+ next_actions: string[];
57
+ integrity_reason?: string;
58
+ }
59
+ export type RunControlAdmissionDecision =
60
+ { admitted: true; capacity: RunControlCapacity } |
61
+ { admitted: false; capacity: RunControlCapacity; error: RunControlAdmissionErrorDetails };
62
+ export type RunControlAdmissionInput = {
63
+ integrity: RunControlAdmissionIntegrity; journalBytes: number;
64
+ newRecordBytes: number; records: readonly unknown[];
65
+ retainedJournalBytes: number; runInstanceId: string;
66
+ capacity?: RunControlCapacity;
67
+ };
68
+
69
+ const PENDING_CONTROL_STATUSES = new Set<RunControlStatus>(["queued", "delivered", "claimed"]);
70
+ const TERMINAL_CONTROL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
71
+ const CONTROL_ADMISSION_NEXT_ACTIONS = [
72
+ "inspect target=run:<id> view=control",
73
+ "inspect target=runtime view=triage",
74
+ ];
75
+
76
+ function isCount(value: number): boolean {
77
+ return Number.isSafeInteger(value) && value >= 0;
78
+ }
79
+ function assertCount(value: number, name: string): void {
80
+ if (!isCount(value)) throw new RangeError(`${name} must be a non-negative safe integer`);
81
+ }
82
+ function boundedAdd(left: number, right: number): number {
83
+ return Math.min(Number.MAX_SAFE_INTEGER, left + right);
84
+ }
85
+ export function traceAppendFits(usage: TraceJournalUsage, encodedEventBytes: number): boolean {
86
+ if (!isCount(usage.events) || !isCount(usage.bytes) || !isCount(encodedEventBytes)) return false;
87
+ return usage.events + 1 <= Limits.TRACE_JOURNAL_MAX_EVENTS &&
88
+ usage.bytes + encodedEventBytes <= Limits.TRACE_JOURNAL_MAX_BYTES;
89
+ }
90
+
91
+ export function selectNewestTraceSuffix<T>(
92
+ candidates: readonly TraceRetentionCandidate<T>[],
93
+ reserve: { bytes?: number; events?: number } = {},
94
+ ): TraceRetentionSelection<T> {
95
+ const reservedBytes = reserve.bytes ?? 0;
96
+ const reservedEvents = reserve.events ?? 0;
97
+ assertCount(reservedBytes, "reserved Trace bytes");
98
+ assertCount(reservedEvents, "reserved Trace events");
99
+ const byteBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_BYTES - reservedBytes);
100
+ const eventBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_EVENTS - reservedEvents);
101
+ let retainedBytes = 0;
102
+ let start = candidates.length;
103
+ while (start > 0 && candidates.length - start < eventBudget) {
104
+ const candidate = candidates[start - 1]!;
105
+ assertCount(candidate.encodedBytes, "encoded Trace bytes");
106
+ if (retainedBytes + candidate.encodedBytes > byteBudget) break;
107
+ retainedBytes += candidate.encodedBytes;
108
+ start -= 1;
109
+ }
110
+ const retained = candidates.slice(start);
111
+ return {
112
+ droppedEvents: candidates.length - retained.length,
113
+ retained,
114
+ retainedBytes,
115
+ retainedEvents: retained.length,
116
+ };
117
+ }
118
+
119
+ export function accumulateTraceCompactionStatistics(
120
+ previous: TraceCompactionData | undefined,
121
+ delta: TraceCompactionDelta,
122
+ ): TraceCompactionData {
123
+ for (const [name, value] of Object.entries(delta)) {
124
+ if (name !== "dropped_event_count_exact") assertCount(value as number, name);
125
+ }
126
+ const prior = previous ?? {
127
+ compactions_total: 0,
128
+ dropped_bytes_total: 0,
129
+ dropped_event_count_exact: true,
130
+ dropped_malformed_lines_total: 0,
131
+ dropped_valid_events_total: 0,
132
+ };
133
+ const eventCountOverflow = prior.dropped_valid_events_total +
134
+ delta.dropped_valid_events > Number.MAX_SAFE_INTEGER;
135
+ return {
136
+ version: TRACE_COMPACTION_VERSION,
137
+ compactions_total: boundedAdd(prior.compactions_total, 1),
138
+ dropped_valid_events_total: boundedAdd(prior.dropped_valid_events_total, delta.dropped_valid_events),
139
+ dropped_malformed_lines_total: boundedAdd(prior.dropped_malformed_lines_total, delta.dropped_malformed_lines),
140
+ dropped_bytes_total: boundedAdd(prior.dropped_bytes_total, delta.dropped_bytes),
141
+ dropped_event_count_exact: prior.dropped_event_count_exact &&
142
+ delta.dropped_event_count_exact && !eventCountOverflow,
143
+ retained_events: delta.retained_events,
144
+ retained_bytes: delta.retained_bytes,
145
+ history_complete: false,
146
+ };
147
+ }
148
+
149
+ export function traceCompactionMarkerInput(data: TraceCompactionData): {
150
+ data: TraceCompactionData; kind: typeof TRACE_COMPACTION_KIND; level: "warning";
151
+ } {
152
+ return { data, kind: TRACE_COMPACTION_KIND, level: "warning" };
153
+ }
154
+
155
+ export function classifyRunControlRecord(value: unknown): "pending" | "terminal" | undefined {
156
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
157
+ const status = (value as { status?: unknown }).status as RunControlStatus;
158
+ if (PENDING_CONTROL_STATUSES.has(status)) return "pending";
159
+ if (TERMINAL_CONTROL_STATUSES.has(status)) return "terminal";
160
+ return undefined;
161
+ }
162
+
163
+ export function runControlStatusTimestamp(
164
+ record: Record<string, unknown>, status: RunControlStatus,
165
+ ): unknown {
166
+ if (status === "failed") return record.failed_at;
167
+ if (status === "handled") return record.handled_at;
168
+ if (status === "claimed") return record.claimed_at;
169
+ if (status === "delivered") return record.delivered_at;
170
+ return record.queued_at;
171
+ }
172
+
173
+ export function computeRunControlCapacity(records: readonly unknown[]): RunControlCapacity {
174
+ let oldest: { timestamp: string; timestampMs: number } | undefined;
175
+ let pending = 0;
176
+ for (const value of records) {
177
+ if (classifyRunControlRecord(value) !== "pending") continue;
178
+ pending += 1;
179
+ const record = value as Record<string, unknown>;
180
+ const timestamp = runControlStatusTimestamp(record, record.status as RunControlStatus);
181
+ const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
182
+ if (typeof timestamp === "string" && Number.isFinite(timestampMs) &&
183
+ (!oldest || timestampMs < oldest.timestampMs)) {
184
+ oldest = { timestamp, timestampMs };
185
+ }
186
+ }
187
+ return {
188
+ available: Math.max(0, Limits.RUN_CONTROL_PENDING_LIMIT - pending),
189
+ backpressured: pending >= Limits.RUN_CONTROL_PENDING_LIMIT,
190
+ limit: Limits.RUN_CONTROL_PENDING_LIMIT,
191
+ ...(oldest ? { oldest_pending_at: oldest.timestamp } : {}),
192
+ pending,
193
+ };
194
+ }
195
+
196
+ export function decideRunControlAdmission(
197
+ input: RunControlAdmissionInput,
198
+ ): RunControlAdmissionDecision {
199
+ const capacity = input.capacity ?? computeRunControlCapacity(input.records);
200
+ const reject = (
201
+ reason: RunControlAdmissionErrorDetails["reason"],
202
+ integrityReason?: string,
203
+ ): RunControlAdmissionDecision => ({
204
+ admitted: false,
205
+ capacity,
206
+ error: {
207
+ reason,
208
+ pending: capacity.pending,
209
+ limit: capacity.limit,
210
+ journal_bytes: input.journalBytes,
211
+ journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
212
+ ...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
213
+ run_instance_id: input.runInstanceId,
214
+ next_actions: [...CONTROL_ADMISSION_NEXT_ACTIONS],
215
+ ...(integrityReason ? { integrity_reason: integrityReason } : {}),
216
+ },
217
+ });
218
+ if (input.integrity !== "valid") return reject("control_journal_integrity", input.integrity);
219
+ if (!isCount(input.journalBytes) || !isCount(input.retainedJournalBytes) ||
220
+ !isCount(input.newRecordBytes) ||
221
+ input.journalBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
222
+ return reject("control_journal_integrity", "journal_bytes");
223
+ }
224
+ for (const value of input.records) {
225
+ const classification = classifyRunControlRecord(value);
226
+ if (!classification) return reject("control_journal_integrity", "invalid_record");
227
+ if (classification !== "pending") continue;
228
+ const record = value as Record<string, unknown>;
229
+ const timestamp = runControlStatusTimestamp(record, record.status as RunControlStatus);
230
+ if (typeof timestamp !== "string" || !Number.isFinite(Date.parse(timestamp))) {
231
+ return reject("control_journal_integrity", "invalid_status_timestamp");
232
+ }
233
+ if (record.run_instance_id !== input.runInstanceId) {
234
+ return reject("control_journal_integrity", "generation_mismatch");
235
+ }
236
+ }
237
+ if (capacity.backpressured ||
238
+ input.retainedJournalBytes + input.newRecordBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
239
+ return reject("control_backpressure");
240
+ }
241
+ return { admitted: true, capacity };
242
+ }
@@ -106,6 +106,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
106
106
  ownerId: deps.getRunOwnerId(ctx),
107
107
  sink: Pi.createNotificationSink(deps.pi, ctx),
108
108
  state: observation,
109
+ includeAttention: true,
109
110
  });
110
111
  reportDiagnostics(ctx);
111
112
  },
@@ -141,6 +142,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
141
142
  },
142
143
  start(ctx) {
143
144
  close();
145
+ Observability.primeRunAttentionState(observation, deps.getRunOwnerId(ctx));
144
146
  update(ctx, true, true);
145
147
  watcher.refresh();
146
148
  reconciliation.start();
@@ -15,6 +15,8 @@ import {
15
15
  import { createConnection } from "node:net";
16
16
  import { join } from "node:path";
17
17
 
18
+ import * as Control from "./control.ts";
19
+ import * as Limits from "./limits.ts";
18
20
  import {
19
21
  appendRunControlInStateDir,
20
22
  updateRunControlStatusInStateDir,
@@ -37,7 +39,26 @@ export interface DeliverRunControlRequest {
37
39
  run_instance_id: string;
38
40
  }
39
41
 
40
- export const FIFO_ATOMIC_CONTROL_MAX_BYTES = 512;
42
+ const PREFLIGHT_CONTROL_ID = "00000000-0000-4000-8000-000000000000";
43
+
44
+ export function encodeRunControlWire(wire: {
45
+ action: string;
46
+ id: string;
47
+ input?: unknown;
48
+ }): { bytes: number; payload: string } {
49
+ const payload = `${JSON.stringify({
50
+ id: wire.id,
51
+ action: wire.action,
52
+ ...(wire.input !== undefined ? { input: wire.input } : {}),
53
+ })}\n`;
54
+ const bytes = Buffer.byteLength(payload);
55
+ if (bytes > Limits.CONTROL_WIRE_MAX_BYTES) {
56
+ throw new Error(
57
+ `Run Control wire record exceeds the ${Limits.CONTROL_WIRE_MAX_BYTES}-byte portable bound`,
58
+ );
59
+ }
60
+ return { bytes, payload };
61
+ }
41
62
 
42
63
  export function readRunControlEndpoint(
43
64
  stateDir: string,
@@ -107,7 +128,21 @@ export async function deliverRunControl(
107
128
  request: DeliverRunControlRequest,
108
129
  options: DeliverRunControlOptions = {},
109
130
  ): Promise<Record<string, unknown>> {
110
- const control = appendRunControlInStateDir(stateDir, request);
131
+ const action = Control.normalizeControlAction(request.action);
132
+ const input = request.input === undefined
133
+ ? undefined
134
+ : Control.normalizeControlInput(request.input);
135
+ const normalizedRequest = {
136
+ action,
137
+ ...(input !== undefined ? { input } : {}),
138
+ run_instance_id: request.run_instance_id,
139
+ };
140
+ encodeRunControlWire({
141
+ id: PREFLIGHT_CONTROL_ID,
142
+ action,
143
+ ...(input !== undefined ? { input } : {}),
144
+ });
145
+ const control = appendRunControlInStateDir(stateDir, normalizedRequest);
111
146
  const endpoint = readRunControlEndpoint(stateDir, request.run_instance_id);
112
147
  if (!endpoint) {
113
148
  updateRunControlStatusInStateDir(
@@ -118,31 +153,24 @@ export async function deliverRunControl(
118
153
  ["queued", "delivered"],
119
154
  );
120
155
  throw Object.assign(new Error("Run Control endpoint is not ready."), {
121
- action: request.action,
156
+ action,
122
157
  control_id: control.id,
123
158
  reason: "endpoint_not_ready",
124
159
  run,
125
160
  run_instance_id: request.run_instance_id,
126
161
  });
127
162
  }
128
- const wire = {
129
- id: control.id,
130
- action: request.action,
131
- ...(request.input !== undefined ? { input: request.input } : {}),
132
- };
133
- const payload = `${JSON.stringify(wire)}\n`;
134
- const bytes = Buffer.byteLength(payload);
135
163
  try {
164
+ const { bytes, payload } = encodeRunControlWire({
165
+ id: control.id,
166
+ action,
167
+ ...(input !== undefined ? { input } : {}),
168
+ });
136
169
  let written: number;
137
170
  if (endpoint.type === "fifo") {
138
171
  if ((options.platform ?? process.platform) === "win32") {
139
172
  throw new Error("FIFO Control delivery is unsupported on native Windows");
140
173
  }
141
- if (bytes > FIFO_ATOMIC_CONTROL_MAX_BYTES) {
142
- throw new Error(
143
- `FIFO Control payload exceeds the ${FIFO_ATOMIC_CONTROL_MAX_BYTES}-byte portable atomic-write bound`,
144
- );
145
- }
146
174
  written = sendToFifo(endpoint, payload);
147
175
  } else {
148
176
  written = await sendToNamedPipe(endpoint, payload, options.namedPipeSend);
@@ -152,7 +180,7 @@ export async function deliverRunControl(
152
180
  }
153
181
  updateRunControlStatusInStateDir(stateDir, control.id, "delivered");
154
182
  return {
155
- action: request.action,
183
+ action,
156
184
  bytes,
157
185
  control_id: control.id,
158
186
  delivery: "delivered",
@@ -170,7 +198,7 @@ export async function deliverRunControl(
170
198
  ["queued", "delivered"],
171
199
  );
172
200
  throw Object.assign(new Error(`Run Control delivery failed: ${reason}`), {
173
- action: request.action,
201
+ action,
174
202
  control_id: control.id,
175
203
  endpoint_type: endpoint.type,
176
204
  reason: "delivery_failed",