@llblab/pi-actors 0.52.1 → 0.53.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.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/dist/index.js +4 -1
- package/dist/lib/async-runs.d.ts +3 -0
- package/dist/lib/async-runs.js +14 -1
- package/dist/lib/command-templates.js +45 -3
- package/dist/lib/extension-runtime.js +1 -1
- package/dist/lib/observability.d.ts +16 -3
- package/dist/lib/observability.js +92 -7
- package/dist/lib/paths.d.ts +6 -0
- package/dist/lib/paths.js +19 -1
- package/dist/lib/pi.d.ts +0 -1
- package/dist/lib/pi.js +15 -24
- package/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/dist/lib/run-delivery-lineage.js +44 -0
- package/dist/lib/run-delivery.d.ts +4 -0
- package/dist/lib/run-delivery.js +102 -4
- package/dist/lib/run-ui-runtime.js +58 -39
- package/dist/lib/runtime.js +14 -6
- package/dist/scripts/build-dist.mjs +94 -30
- package/dist/skills/actors/SKILL.md +1 -1
- package/docs/async-runs.md +1 -1
- package/docs/coordinator-delivery.md +18 -23
- package/index.ts +6 -3
- package/lib/async-runs.ts +18 -1
- package/lib/command-templates.ts +41 -3
- package/lib/extension-runtime.ts +1 -1
- package/lib/observability.ts +119 -5
- package/lib/paths.ts +27 -1
- package/lib/pi.ts +15 -28
- package/lib/run-delivery-lineage.ts +68 -0
- package/lib/run-delivery.ts +120 -4
- package/lib/run-ui-runtime.ts +69 -44
- package/lib/runtime.ts +17 -6
- package/package.json +3 -8
- package/scripts/build-dist.mjs +94 -30
- package/skills/actors/SKILL.md +1 -1
package/lib/run-delivery.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type RunDeliveryPhase = "pending" | "queued" | "presented";
|
|
|
16
16
|
|
|
17
17
|
export interface RunCompletionBatchMember {
|
|
18
18
|
artifacts?: Record<string, string>;
|
|
19
|
+
parent_run?: string;
|
|
20
|
+
parent_run_instance_id?: string;
|
|
21
|
+
parent_state_dir?: string;
|
|
22
|
+
output?: string;
|
|
19
23
|
run: string;
|
|
20
24
|
run_instance_id: string;
|
|
21
25
|
state_dir: string;
|
|
@@ -109,6 +113,10 @@ export interface RunSteerTransitionInput {
|
|
|
109
113
|
const RUN_DELIVERY_SCHEMA = "run-delivery-v1";
|
|
110
114
|
const MEMBER_FIELDS = new Set([
|
|
111
115
|
"artifacts",
|
|
116
|
+
"parent_run",
|
|
117
|
+
"parent_run_instance_id",
|
|
118
|
+
"parent_state_dir",
|
|
119
|
+
"output",
|
|
112
120
|
"run",
|
|
113
121
|
"run_instance_id",
|
|
114
122
|
"state_dir",
|
|
@@ -216,8 +224,40 @@ function parseMember(value: unknown): RunCompletionBatchMember {
|
|
|
216
224
|
const record = asRecord(value, "Completion batch member");
|
|
217
225
|
assertExactFields(record, MEMBER_FIELDS, "Completion batch member");
|
|
218
226
|
const parsedArtifacts = artifacts(record.artifacts);
|
|
227
|
+
const parentRun = record.parent_run === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: boundedString(record.parent_run, "Completion batch member parent_run", 120);
|
|
230
|
+
const parentRunInstanceId = record.parent_run_instance_id === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: boundedString(
|
|
233
|
+
record.parent_run_instance_id,
|
|
234
|
+
"Completion batch member parent_run_instance_id",
|
|
235
|
+
256,
|
|
236
|
+
);
|
|
237
|
+
const parentStateDir = record.parent_state_dir === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: boundedString(
|
|
240
|
+
record.parent_state_dir,
|
|
241
|
+
"Completion batch member parent_state_dir",
|
|
242
|
+
4_096,
|
|
243
|
+
);
|
|
244
|
+
if (Boolean(parentRun) !== Boolean(parentRunInstanceId) ||
|
|
245
|
+
Boolean(parentRun) !== Boolean(parentStateDir)) {
|
|
246
|
+
throw new Error("Completion batch member parent lineage is incomplete");
|
|
247
|
+
}
|
|
248
|
+
const output = record.output === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: boundedString(record.output, "Completion batch member output", 4_000);
|
|
219
251
|
return {
|
|
220
252
|
...(parsedArtifacts ? { artifacts: parsedArtifacts } : {}),
|
|
253
|
+
...(output ? { output } : {}),
|
|
254
|
+
...(parentRun
|
|
255
|
+
? {
|
|
256
|
+
parent_run: parentRun,
|
|
257
|
+
parent_run_instance_id: parentRunInstanceId!,
|
|
258
|
+
parent_state_dir: parentStateDir!,
|
|
259
|
+
}
|
|
260
|
+
: {}),
|
|
221
261
|
run: boundedString(record.run, "Completion batch member run", 120),
|
|
222
262
|
run_instance_id: boundedString(
|
|
223
263
|
record.run_instance_id,
|
|
@@ -740,7 +780,75 @@ function compactModelText(value: string, limit: number): string {
|
|
|
740
780
|
return compact.length > limit ? `${compact.slice(0, limit - 1)}…` : compact;
|
|
741
781
|
}
|
|
742
782
|
|
|
743
|
-
function
|
|
783
|
+
function memberIdentity(member: RunCompletionBatchMember): string {
|
|
784
|
+
return `${member.state_dir}\0${member.run_instance_id}`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function completionDepth(
|
|
788
|
+
member: RunCompletionBatchMember,
|
|
789
|
+
byIdentity: Map<string, RunCompletionBatchMember>,
|
|
790
|
+
): number {
|
|
791
|
+
const visited = new Set<string>();
|
|
792
|
+
let current = member;
|
|
793
|
+
let depth = 0;
|
|
794
|
+
while (current.parent_state_dir && current.parent_run_instance_id) {
|
|
795
|
+
const identity = `${current.parent_state_dir}\0${current.parent_run_instance_id}`;
|
|
796
|
+
if (visited.has(identity)) break;
|
|
797
|
+
visited.add(identity);
|
|
798
|
+
const parent = byIdentity.get(identity);
|
|
799
|
+
if (!parent) break;
|
|
800
|
+
depth += 1;
|
|
801
|
+
current = parent;
|
|
802
|
+
}
|
|
803
|
+
return depth;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function orderCompletionForest(
|
|
807
|
+
members: RunCompletionBatchMember[],
|
|
808
|
+
): RunCompletionBatchMember[] {
|
|
809
|
+
const byIdentity = new Map(members.map((member) => [memberIdentity(member), member]));
|
|
810
|
+
const children = new Map<string, RunCompletionBatchMember[]>();
|
|
811
|
+
const roots: RunCompletionBatchMember[] = [];
|
|
812
|
+
for (const member of members) {
|
|
813
|
+
const parentIdentity = member.parent_state_dir && member.parent_run_instance_id
|
|
814
|
+
? `${member.parent_state_dir}\0${member.parent_run_instance_id}`
|
|
815
|
+
: undefined;
|
|
816
|
+
if (!parentIdentity || !byIdentity.has(parentIdentity)) {
|
|
817
|
+
roots.push(member);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const siblings = children.get(parentIdentity) ?? [];
|
|
821
|
+
siblings.push(member);
|
|
822
|
+
children.set(parentIdentity, siblings);
|
|
823
|
+
}
|
|
824
|
+
const ordered: RunCompletionBatchMember[] = [];
|
|
825
|
+
const visited = new Set<string>();
|
|
826
|
+
const visit = (member: RunCompletionBatchMember): void => {
|
|
827
|
+
const identity = memberIdentity(member);
|
|
828
|
+
if (visited.has(identity)) return;
|
|
829
|
+
visited.add(identity);
|
|
830
|
+
ordered.push(member);
|
|
831
|
+
for (const child of children.get(identity) ?? []) visit(child);
|
|
832
|
+
};
|
|
833
|
+
for (const root of roots) visit(root);
|
|
834
|
+
for (const member of members) visit(member);
|
|
835
|
+
return ordered;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function formatModelOutput(value: string, indent: string): string {
|
|
839
|
+
return value
|
|
840
|
+
.replaceAll("`", "'")
|
|
841
|
+
.split(/\r?\n/u)
|
|
842
|
+
.map((line) => line.trimEnd())
|
|
843
|
+
.filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
|
|
844
|
+
.map((line) => `${indent} ${line}`)
|
|
845
|
+
.join("\n");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function formatCompletionMember(
|
|
849
|
+
member: RunCompletionBatchMember,
|
|
850
|
+
byIdentity: Map<string, RunCompletionBatchMember>,
|
|
851
|
+
): string {
|
|
744
852
|
const summary = compactModelText(member.summary, 200);
|
|
745
853
|
const artifactEntries = Object.entries(member.artifacts ?? {}).slice(0, 4);
|
|
746
854
|
const artifactText = artifactEntries.length === 0
|
|
@@ -748,7 +856,11 @@ function formatCompletionMember(member: RunCompletionBatchMember): string {
|
|
|
748
856
|
: `; artifacts: ${artifactEntries.map(([name, path]) =>
|
|
749
857
|
`${compactModelText(name, 120)}=\`${compactModelText(path, 320)}\``
|
|
750
858
|
).join(", ")}`;
|
|
751
|
-
|
|
859
|
+
const indent = " ".repeat(Math.min(completionDepth(member, byIdentity), 8));
|
|
860
|
+
const output = member.output
|
|
861
|
+
? `\n${indent} Output:\n${formatModelOutput(member.output, indent)}`
|
|
862
|
+
: "";
|
|
863
|
+
return `${indent}- \`${compactModelText(member.run, 120)}\` — \`${member.status}\`: ${summary}${artifactText}${output}`;
|
|
752
864
|
}
|
|
753
865
|
|
|
754
866
|
function formatStatusCounts(members: RunCompletionBatchMember[]): string {
|
|
@@ -772,9 +884,13 @@ export function formatRunCompletionBatchMessage(batch: RunCompletionBatch): stri
|
|
|
772
884
|
`Window: \`${terminalTimes[0]}\` → \`${terminalTimes.at(-1)}\``,
|
|
773
885
|
`Statuses: \`${formatStatusCounts(safe.members)}\``,
|
|
774
886
|
];
|
|
775
|
-
const
|
|
887
|
+
const byIdentity = new Map(safe.members.map((member) => [
|
|
888
|
+
memberIdentity(member),
|
|
889
|
+
member,
|
|
890
|
+
]));
|
|
891
|
+
const candidateRows = orderCompletionForest(safe.members)
|
|
776
892
|
.slice(0, Limits.RUN_DELIVERY_MODEL_MAX_MEMBERS)
|
|
777
|
-
.map(formatCompletionMember);
|
|
893
|
+
.map((member) => formatCompletionMember(member, byIdentity));
|
|
778
894
|
const rows: string[] = [];
|
|
779
895
|
for (const row of candidateRows) {
|
|
780
896
|
const omitted = safe.members.length - rows.length - 1;
|
package/lib/run-ui-runtime.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as Observability from "./observability.ts";
|
|
|
10
10
|
import * as Paths from "./paths.ts";
|
|
11
11
|
import * as Pi from "./pi.ts";
|
|
12
12
|
import * as RunDelivery from "./run-delivery.ts";
|
|
13
|
+
import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
13
14
|
|
|
14
15
|
export interface RunUiRuntime {
|
|
15
16
|
close(): void;
|
|
@@ -43,12 +44,11 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
43
44
|
let animationInterval: NodeJS.Timeout | undefined;
|
|
44
45
|
let deliveryTimeout: NodeJS.Timeout | undefined;
|
|
45
46
|
let notifyTimeout: NodeJS.Timeout | undefined;
|
|
46
|
-
let recoverQueuedBatch = false;
|
|
47
47
|
let recoverQueuedSteers = false;
|
|
48
48
|
let running = false;
|
|
49
49
|
let lastWatcherDiagnosticId = 0;
|
|
50
50
|
const observation = Observability.createRunUiObservationState();
|
|
51
|
-
const
|
|
51
|
+
const deliveryObservation = Observability.createRunUiObservationState();
|
|
52
52
|
const retirementAttempts = new Set<string>();
|
|
53
53
|
const steerDiagnostics = new Set<string>();
|
|
54
54
|
|
|
@@ -70,9 +70,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
70
70
|
notifyTimeout = undefined;
|
|
71
71
|
if (deliveryTimeout) clearTimeout(deliveryTimeout);
|
|
72
72
|
deliveryTimeout = undefined;
|
|
73
|
-
recoverQueuedBatch = false;
|
|
74
73
|
recoverQueuedSteers = false;
|
|
75
|
-
deliveryRecoveryDiagnostics.clear();
|
|
76
74
|
steerDiagnostics.clear();
|
|
77
75
|
if (animationInterval) clearInterval(animationInterval);
|
|
78
76
|
animationInterval = undefined;
|
|
@@ -228,12 +226,14 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
228
226
|
};
|
|
229
227
|
const admitCompletionTransitions = (
|
|
230
228
|
ownerId: string,
|
|
231
|
-
|
|
229
|
+
snapshot: Observability.RunUiSnapshot,
|
|
232
230
|
): boolean => {
|
|
233
231
|
const existing = journal(ownerId).completion_batch;
|
|
234
232
|
if (existing) return true;
|
|
235
|
-
const members = Observability.collectRunCompletionBatchMembers(
|
|
236
|
-
.
|
|
233
|
+
const members = Observability.collectRunCompletionBatchMembers(
|
|
234
|
+
snapshot.transitions,
|
|
235
|
+
snapshot.summary.runs,
|
|
236
|
+
).slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
|
|
237
237
|
if (members.length === 0) return false;
|
|
238
238
|
RunDelivery.admitRunCompletionBatch({
|
|
239
239
|
members,
|
|
@@ -244,6 +244,10 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
244
244
|
};
|
|
245
245
|
const isIdle = (ctx: Pi.ExtensionContext): boolean =>
|
|
246
246
|
typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
247
|
+
const isDeliveryRoot = (ownerId: string): boolean => {
|
|
248
|
+
const inheritedOwner = RunDeliveryLineage.getProcessDeliveryOwnerId();
|
|
249
|
+
return inheritedOwner === undefined || inheritedOwner === ownerId;
|
|
250
|
+
};
|
|
247
251
|
let flushCompletionBatch = (_ctx: Pi.ExtensionContext): boolean => false;
|
|
248
252
|
const flushSteers = (ctx: Pi.ExtensionContext, ownerId: string): boolean => {
|
|
249
253
|
const recovering = recoverQueuedSteers;
|
|
@@ -334,11 +338,25 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
334
338
|
if (!notify) return false;
|
|
335
339
|
const sink = Pi.createNotificationSink(deps.pi, ctx);
|
|
336
340
|
retireCandidateRuns(ctx, snapshot.summary);
|
|
337
|
-
const
|
|
338
|
-
Observability.
|
|
339
|
-
|
|
341
|
+
const deliverySnapshot = isDeliveryRoot(ownerId)
|
|
342
|
+
? Observability.readRunDeliverySnapshot(deliveryObservation, ownerId)
|
|
343
|
+
: undefined;
|
|
344
|
+
const hasCompletionCandidates = deliverySnapshot
|
|
345
|
+
? Observability.collectRunCompletionBatchMembers(
|
|
346
|
+
deliverySnapshot.transitions,
|
|
347
|
+
deliverySnapshot.summary.runs,
|
|
348
|
+
).length > 0
|
|
349
|
+
: false;
|
|
350
|
+
const hasCompletionBatch = isDeliveryRoot(ownerId) &&
|
|
351
|
+
Boolean(journal(ownerId).completion_batch);
|
|
340
352
|
admitSteerEvents(ctx, ownerId, snapshot.attentionEvents);
|
|
341
353
|
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
354
|
+
if (deliverySnapshot) {
|
|
355
|
+
Observability.pruneRunUiObservationState(
|
|
356
|
+
deliveryObservation,
|
|
357
|
+
deliverySnapshot,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
342
360
|
if (!terminalOnly) {
|
|
343
361
|
Observability.deliverRunAttentionNotifications(
|
|
344
362
|
snapshot.attentionEvents.filter((event) =>
|
|
@@ -403,54 +421,48 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
403
421
|
});
|
|
404
422
|
|
|
405
423
|
flushCompletionBatch = (ctx: Pi.ExtensionContext): boolean => {
|
|
406
|
-
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx) return false;
|
|
407
424
|
const ownerId = activeOwnerId;
|
|
408
|
-
if (!ownerId) return false;
|
|
425
|
+
if (!running || !ownerId) return false;
|
|
426
|
+
try {
|
|
427
|
+
if (Pi.getSessionId(ctx) !== ownerId) return false;
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
409
431
|
if (flushSteers(ctx, ownerId)) return true;
|
|
432
|
+
if (!isDeliveryRoot(ownerId)) return false;
|
|
410
433
|
let batch = journal(ownerId).completion_batch;
|
|
411
434
|
if (!batch) {
|
|
412
|
-
const snapshot = Observability.
|
|
413
|
-
|
|
414
|
-
|
|
435
|
+
const snapshot = Observability.readRunDeliverySnapshot(
|
|
436
|
+
deliveryObservation,
|
|
437
|
+
ownerId,
|
|
438
|
+
);
|
|
439
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
440
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
415
441
|
batch = journal(ownerId).completion_batch;
|
|
416
442
|
}
|
|
417
443
|
if (!batch) return false;
|
|
418
444
|
if (batch.phase === "presented") {
|
|
419
445
|
finishPresentedBatch(ownerId, batch);
|
|
420
|
-
const snapshot = Observability.
|
|
421
|
-
|
|
422
|
-
|
|
446
|
+
const snapshot = Observability.readRunDeliverySnapshot(
|
|
447
|
+
deliveryObservation,
|
|
448
|
+
ownerId,
|
|
449
|
+
);
|
|
450
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
451
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
423
452
|
batch = journal(ownerId).completion_batch;
|
|
424
453
|
if (!batch) return false;
|
|
425
454
|
}
|
|
426
455
|
if (!isIdle(ctx)) return true;
|
|
427
456
|
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
428
457
|
if (batch.phase === "queued") {
|
|
429
|
-
if (!
|
|
430
|
-
recoverQueuedBatch = false;
|
|
431
|
-
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(
|
|
432
|
-
ctx,
|
|
433
|
-
batch.batch_id,
|
|
434
|
-
content,
|
|
435
|
-
);
|
|
436
|
-
if (evidence.status === "present") return true;
|
|
437
|
-
if (evidence.status !== "absent") {
|
|
438
|
-
const diagnosticKey = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
439
|
-
if (!deliveryRecoveryDiagnostics.has(diagnosticKey)) {
|
|
440
|
-
deliveryRecoveryDiagnostics.add(diagnosticKey);
|
|
441
|
-
ctx.ui.notify(
|
|
442
|
-
`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains queued.`,
|
|
443
|
-
"warning",
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
if (!RunDelivery.resetRunCompletionBatchPending({
|
|
458
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
449
459
|
batchId: batch.batch_id,
|
|
450
460
|
ownerId,
|
|
451
461
|
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
452
462
|
})) return true;
|
|
453
|
-
|
|
463
|
+
const accepted = journal(ownerId).completion_batch;
|
|
464
|
+
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
465
|
+
return flushCompletionBatch(ctx);
|
|
454
466
|
}
|
|
455
467
|
if (!isIdle(ctx)) return true;
|
|
456
468
|
try {
|
|
@@ -471,7 +483,19 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
471
483
|
})) {
|
|
472
484
|
throw new Error("Completion batch changed before queue acknowledgment");
|
|
473
485
|
}
|
|
474
|
-
|
|
486
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
487
|
+
batchId: batch.batch_id,
|
|
488
|
+
ownerId,
|
|
489
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
490
|
+
})) {
|
|
491
|
+
throw new Error("Completion batch changed before durable acceptance");
|
|
492
|
+
}
|
|
493
|
+
const accepted = journal(ownerId).completion_batch;
|
|
494
|
+
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
495
|
+
ctx.ui.notify(
|
|
496
|
+
`Actor completions ready: ${batch.members.length}`,
|
|
497
|
+
"info",
|
|
498
|
+
);
|
|
475
499
|
return true;
|
|
476
500
|
};
|
|
477
501
|
|
|
@@ -479,11 +503,13 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
479
503
|
close,
|
|
480
504
|
flushCompletionBatch,
|
|
481
505
|
projectContext(messages, ctx) {
|
|
482
|
-
|
|
506
|
+
const ownerId = activeOwnerId;
|
|
507
|
+
if (!running || !ownerId) return messages;
|
|
508
|
+
try {
|
|
509
|
+
if (Pi.getSessionId(ctx) !== ownerId) return messages;
|
|
510
|
+
} catch {
|
|
483
511
|
return messages;
|
|
484
512
|
}
|
|
485
|
-
const ownerId = activeOwnerId;
|
|
486
|
-
if (!ownerId) return messages;
|
|
487
513
|
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
488
514
|
let contextMessages = steerContext.messages;
|
|
489
515
|
for (const steer of journal(ownerId).steers) {
|
|
@@ -560,7 +586,6 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
560
586
|
close();
|
|
561
587
|
activeContext = ctx;
|
|
562
588
|
activeOwnerId = ownerId;
|
|
563
|
-
recoverQueuedBatch = true;
|
|
564
589
|
recoverQueuedSteers = true;
|
|
565
590
|
running = true;
|
|
566
591
|
try {
|
package/lib/runtime.ts
CHANGED
|
@@ -325,6 +325,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
325
325
|
let rootWatcher: FSWatcher | undefined;
|
|
326
326
|
let parentWatcher: FSWatcher | undefined;
|
|
327
327
|
let failureNotified = false;
|
|
328
|
+
let notifyAfterReload = false;
|
|
328
329
|
const setWatchStatus = (watchStatus: RecipeRegistryStatus["watch_status"]): void =>
|
|
329
330
|
runtime.setWatchStatus?.(watchStatus);
|
|
330
331
|
const close = (): void => {
|
|
@@ -336,6 +337,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
336
337
|
closingParent?.close();
|
|
337
338
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
338
339
|
reloadTimeout = undefined;
|
|
340
|
+
notifyAfterReload = false;
|
|
339
341
|
setWatchStatus("closed");
|
|
340
342
|
};
|
|
341
343
|
const reportCallbackError = (error: unknown): void => {
|
|
@@ -358,14 +360,21 @@ export function createRecipeToolReloadWatcher(
|
|
|
358
360
|
reportCallbackError(error);
|
|
359
361
|
}
|
|
360
362
|
};
|
|
361
|
-
const scheduleReload = (
|
|
363
|
+
const scheduleReload = (
|
|
364
|
+
ctx: RuntimeContext,
|
|
365
|
+
notifyActiveChange = false,
|
|
366
|
+
): void => {
|
|
362
367
|
failureNotified = false;
|
|
368
|
+
notifyAfterReload ||= notifyActiveChange;
|
|
363
369
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
364
370
|
reloadTimeout = setTimeout(() => {
|
|
365
371
|
reloadTimeout = undefined;
|
|
366
372
|
try {
|
|
367
373
|
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
368
|
-
|
|
374
|
+
if (notifyAfterReload) {
|
|
375
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
376
|
+
}
|
|
377
|
+
notifyAfterReload = false;
|
|
369
378
|
} catch (error) {
|
|
370
379
|
notifyFailure(ctx);
|
|
371
380
|
reportCallbackError(error);
|
|
@@ -394,7 +403,7 @@ export function createRecipeToolReloadWatcher(
|
|
|
394
403
|
parentWatcher = undefined;
|
|
395
404
|
watcher.close();
|
|
396
405
|
watchRoot(ctx, recipeRoot);
|
|
397
|
-
scheduleReload(ctx);
|
|
406
|
+
scheduleReload(ctx, true);
|
|
398
407
|
});
|
|
399
408
|
parentWatcher = watcher;
|
|
400
409
|
setWatchStatus("watching_parent");
|
|
@@ -415,16 +424,18 @@ export function createRecipeToolReloadWatcher(
|
|
|
415
424
|
return;
|
|
416
425
|
}
|
|
417
426
|
try {
|
|
418
|
-
const watcher = watchPath(recipeRoot, () => {
|
|
427
|
+
const watcher = watchPath(recipeRoot, (_event, changedFile) => {
|
|
419
428
|
if (rootWatcher !== watcher) return;
|
|
420
429
|
if (!pathExists(recipeRoot)) {
|
|
421
430
|
rootWatcher = undefined;
|
|
422
431
|
watcher.close();
|
|
423
|
-
scheduleReload(ctx);
|
|
432
|
+
scheduleReload(ctx, true);
|
|
424
433
|
watchParent(ctx, recipeRoot);
|
|
425
434
|
return;
|
|
426
435
|
}
|
|
427
|
-
|
|
436
|
+
const relativeChange = changedFile ? String(changedFile) : "";
|
|
437
|
+
const firstSegment = relativeChange.split(/[\\/]/u)[0];
|
|
438
|
+
scheduleReload(ctx, Boolean(relativeChange) && firstSegment !== "drafts");
|
|
428
439
|
});
|
|
429
440
|
rootWatcher = watcher;
|
|
430
441
|
setWatchStatus("watching_root");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-actors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local Actor Kernel for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
"test:preservation": "node --experimental-strip-types --test --test-concurrency=1 tests/async-runs.test.ts tests/observability.test.ts tests/session-evidence.test.ts tests/inspector-security.test.ts tests/inspector-overlay.test.ts tests/review-control.test.ts",
|
|
32
32
|
"pack:dry": "npm pack --dry-run",
|
|
33
33
|
"recipes:qa": "node skills/actors/scripts/validate-recipe.mjs skills --skills --qa --summary",
|
|
34
|
-
"validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run conformance && npm run pack:dry",
|
|
34
|
+
"validate": "npx tsc --noEmit && npm run recipes:qa && npm run build:check && npm run check && npm test && npm run conformance && npm run pack:dry",
|
|
35
35
|
"audit:dependencies": "npm audit --audit-level=high --omit=peer",
|
|
36
36
|
"build": "node scripts/build-dist.mjs",
|
|
37
|
+
"build:check": "node scripts/build-dist.mjs --check",
|
|
37
38
|
"prepack": "npm run build"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
@@ -54,15 +55,9 @@
|
|
|
54
55
|
"extensions": [
|
|
55
56
|
"./dist/pi-actors/index.js"
|
|
56
57
|
],
|
|
57
|
-
"sourceExtensions": [
|
|
58
|
-
"./index.ts"
|
|
59
|
-
],
|
|
60
58
|
"skills": [
|
|
61
59
|
"./dist/skills"
|
|
62
60
|
],
|
|
63
|
-
"sourceSkills": [
|
|
64
|
-
"./skills"
|
|
65
|
-
],
|
|
66
61
|
"image": "https://raw.githubusercontent.com/llblab/pi-actors/main/banner.jpg"
|
|
67
62
|
},
|
|
68
63
|
"peerDependencies": {
|
package/scripts/build-dist.mjs
CHANGED
|
@@ -1,47 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* This is intentionally standalone: it is package/build glue, not reusable
|
|
7
|
-
* actor-domain behavior. It cleans dist, compiles TypeScript, mirrors runtime
|
|
8
|
-
* assets, and syntax-checks packaged scripts.
|
|
4
|
+
* Builds or verifies the JavaScript-only distributive tree without exposing a
|
|
5
|
+
* partial tree to Pi or destroying the previous distributive on failure.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import { spawnSync } from "node:child_process";
|
|
12
9
|
import {
|
|
13
10
|
cpSync,
|
|
11
|
+
existsSync,
|
|
14
12
|
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
15
|
readdirSync,
|
|
16
|
+
renameSync,
|
|
16
17
|
rmSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
+
import { join, relative } from "node:path";
|
|
21
|
+
|
|
22
|
+
const DIST_DIR = "dist";
|
|
23
|
+
const checkOnly = process.argv.includes("--check");
|
|
20
24
|
|
|
21
25
|
function run(command, args) {
|
|
22
26
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
23
27
|
if (result.error) throw result.error;
|
|
24
|
-
if (result.status !== 0)
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
throw new Error(`${command} exited with status ${result.status ?? "unknown"}.`);
|
|
30
|
+
}
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"-p",
|
|
33
|
-
"tsconfig.build.json",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
mkdirSync(join("dist", "pi-actors"), { recursive: true });
|
|
37
|
-
writeFileSync(
|
|
38
|
-
join("dist", "pi-actors", "index.js"),
|
|
39
|
-
'export { default } from "../index.js";\n',
|
|
40
|
-
"utf8",
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
for (const dir of ["scripts", "fixtures", "skills"]) {
|
|
44
|
-
cpSync(dir, join("dist", dir), { recursive: true });
|
|
33
|
+
function listFiles(root, current = root) {
|
|
34
|
+
return readdirSync(current, { withFileTypes: true }).flatMap((entry) => {
|
|
35
|
+
const path = join(current, entry.name);
|
|
36
|
+
return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)];
|
|
37
|
+
}).sort();
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
function listModuleScripts(root) {
|
|
@@ -52,9 +45,80 @@ function listModuleScripts(root) {
|
|
|
52
45
|
});
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
function normalizeTextFiles(root) {
|
|
49
|
+
const textSuffixes = [".js", ".ts", ".mjs", ".json", ".md"];
|
|
50
|
+
for (const path of listFiles(root)) {
|
|
51
|
+
if (!textSuffixes.some((suffix) => path.endsWith(suffix))) continue;
|
|
52
|
+
const absolutePath = join(root, path);
|
|
53
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
54
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
55
|
+
if (normalized !== source) writeFileSync(absolutePath, normalized, "utf8");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertTreesEqual(expectedRoot, actualRoot) {
|
|
60
|
+
if (!existsSync(expectedRoot)) {
|
|
61
|
+
throw new Error(`${expectedRoot} is missing; run npm run build.`);
|
|
62
|
+
}
|
|
63
|
+
const expectedFiles = listFiles(expectedRoot);
|
|
64
|
+
const actualFiles = listFiles(actualRoot);
|
|
65
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles)) {
|
|
66
|
+
throw new Error("dist file inventory is stale; run npm run build.");
|
|
67
|
+
}
|
|
68
|
+
for (const path of expectedFiles) {
|
|
69
|
+
if (!readFileSync(join(expectedRoot, path)).equals(readFileSync(join(actualRoot, path)))) {
|
|
70
|
+
throw new Error(`dist/${path} is stale; run npm run build.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function replaceDist(candidate) {
|
|
76
|
+
const backup = `.dist-backup-${process.pid}-${Date.now()}`;
|
|
77
|
+
const hadDist = existsSync(DIST_DIR);
|
|
78
|
+
if (hadDist) renameSync(DIST_DIR, backup);
|
|
79
|
+
try {
|
|
80
|
+
renameSync(candidate, DIST_DIR);
|
|
81
|
+
if (hadDist) rmSync(backup, { recursive: true, force: true });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (hadDist && existsSync(backup) && !existsSync(DIST_DIR)) {
|
|
84
|
+
renameSync(backup, DIST_DIR);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
59
89
|
|
|
60
|
-
|
|
90
|
+
const candidate = mkdtempSync(join(process.cwd(), ".dist-build-"));
|
|
91
|
+
try {
|
|
92
|
+
run(process.execPath, [
|
|
93
|
+
join("node_modules", "typescript", "bin", "tsc"),
|
|
94
|
+
"-p",
|
|
95
|
+
"tsconfig.build.json",
|
|
96
|
+
"--outDir",
|
|
97
|
+
candidate,
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
mkdirSync(join(candidate, "pi-actors"), { recursive: true });
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(candidate, "pi-actors", "index.js"),
|
|
103
|
+
'export { default } from "../index.js";\n',
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
normalizeTextFiles(candidate);
|
|
107
|
+
for (const dir of ["scripts", "fixtures", "skills"]) {
|
|
108
|
+
cpSync(dir, join(candidate, dir), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
const builtScripts = [
|
|
111
|
+
...listModuleScripts(join(candidate, "scripts")),
|
|
112
|
+
...listModuleScripts(join(candidate, "skills")),
|
|
113
|
+
];
|
|
114
|
+
run(process.execPath, ["--check", ...builtScripts]);
|
|
115
|
+
|
|
116
|
+
if (checkOnly) {
|
|
117
|
+
assertTreesEqual(DIST_DIR, candidate);
|
|
118
|
+
console.log("pi-actors: dist is current");
|
|
119
|
+
} else {
|
|
120
|
+
replaceDist(candidate);
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
124
|
+
}
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -107,7 +107,7 @@ Run = Recipe + Trace + Control
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
1. Spawn with the exact logical Recipe identity and caller-owned values.
|
|
110
|
-
2. Retain the returned `run:<id>` and normally wait for
|
|
110
|
+
2. Retain the returned `run:<id>` and normally wait for the root coordinator's settled completion batch instead of polling. Nested actor completions accumulate under their containing top-level Run and arrive in that one tree-compressed batch; descendant sessions do not need notification options.
|
|
111
111
|
3. Inspect `view=trace` when retained observations or attention matter.
|
|
112
112
|
4. Inspect `view=control` before diagnosing service readiness, stale work, or saturation.
|
|
113
113
|
5. Send `message` only for an action declared and consumed by that controlled Recipe.
|