@llblab/pi-actors 0.52.0 → 0.53.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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -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/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/skills/actors/SKILL.md +17 -7
- package/dist/skills/music-player/SKILL.md +3 -3
- package/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/dist/skills/swarm/SKILL.md +2 -6
- package/dist/skills/swarm/references/development-swarm.md +2 -31
- package/docs/async-runs.md +1 -1
- package/docs/coordinator-delivery.md +18 -23
- package/docs/recipe-library.md +1 -1
- 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/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 +1 -1
- package/skills/actors/SKILL.md +17 -7
- package/skills/music-player/SKILL.md +3 -3
- package/skills/music-player/genapps/music-player.mjs +6 -4
- package/skills/music-player/scripts/playback.mjs +85 -18
- package/skills/swarm/SKILL.md +2 -6
- package/skills/swarm/references/development-swarm.md +2 -31
- package/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/skills/music-player/scripts/playback-client.mjs +0 -143
package/dist/lib/run-delivery.js
CHANGED
|
@@ -11,6 +11,10 @@ import * as Limits from "./limits.js";
|
|
|
11
11
|
const RUN_DELIVERY_SCHEMA = "run-delivery-v1";
|
|
12
12
|
const MEMBER_FIELDS = new Set([
|
|
13
13
|
"artifacts",
|
|
14
|
+
"parent_run",
|
|
15
|
+
"parent_run_instance_id",
|
|
16
|
+
"parent_state_dir",
|
|
17
|
+
"output",
|
|
14
18
|
"run",
|
|
15
19
|
"run_instance_id",
|
|
16
20
|
"state_dir",
|
|
@@ -107,8 +111,32 @@ function parseMember(value) {
|
|
|
107
111
|
const record = asRecord(value, "Completion batch member");
|
|
108
112
|
assertExactFields(record, MEMBER_FIELDS, "Completion batch member");
|
|
109
113
|
const parsedArtifacts = artifacts(record.artifacts);
|
|
114
|
+
const parentRun = record.parent_run === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: boundedString(record.parent_run, "Completion batch member parent_run", 120);
|
|
117
|
+
const parentRunInstanceId = record.parent_run_instance_id === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: boundedString(record.parent_run_instance_id, "Completion batch member parent_run_instance_id", 256);
|
|
120
|
+
const parentStateDir = record.parent_state_dir === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: boundedString(record.parent_state_dir, "Completion batch member parent_state_dir", 4_096);
|
|
123
|
+
if (Boolean(parentRun) !== Boolean(parentRunInstanceId) ||
|
|
124
|
+
Boolean(parentRun) !== Boolean(parentStateDir)) {
|
|
125
|
+
throw new Error("Completion batch member parent lineage is incomplete");
|
|
126
|
+
}
|
|
127
|
+
const output = record.output === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: boundedString(record.output, "Completion batch member output", 4_000);
|
|
110
130
|
return {
|
|
111
131
|
...(parsedArtifacts ? { artifacts: parsedArtifacts } : {}),
|
|
132
|
+
...(output ? { output } : {}),
|
|
133
|
+
...(parentRun
|
|
134
|
+
? {
|
|
135
|
+
parent_run: parentRun,
|
|
136
|
+
parent_run_instance_id: parentRunInstanceId,
|
|
137
|
+
parent_state_dir: parentStateDir,
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
112
140
|
run: boundedString(record.run, "Completion batch member run", 120),
|
|
113
141
|
run_instance_id: boundedString(record.run_instance_id, "Completion batch member run_instance_id", 256),
|
|
114
142
|
state_dir: boundedString(record.state_dir, "Completion batch member state_dir", 4_096),
|
|
@@ -566,13 +594,79 @@ function compactModelText(value, limit) {
|
|
|
566
594
|
const compact = value.replaceAll(/\s+/g, " ").replaceAll("`", "'").trim();
|
|
567
595
|
return compact.length > limit ? `${compact.slice(0, limit - 1)}…` : compact;
|
|
568
596
|
}
|
|
569
|
-
function
|
|
597
|
+
function memberIdentity(member) {
|
|
598
|
+
return `${member.state_dir}\0${member.run_instance_id}`;
|
|
599
|
+
}
|
|
600
|
+
function completionDepth(member, byIdentity) {
|
|
601
|
+
const visited = new Set();
|
|
602
|
+
let current = member;
|
|
603
|
+
let depth = 0;
|
|
604
|
+
while (current.parent_state_dir && current.parent_run_instance_id) {
|
|
605
|
+
const identity = `${current.parent_state_dir}\0${current.parent_run_instance_id}`;
|
|
606
|
+
if (visited.has(identity))
|
|
607
|
+
break;
|
|
608
|
+
visited.add(identity);
|
|
609
|
+
const parent = byIdentity.get(identity);
|
|
610
|
+
if (!parent)
|
|
611
|
+
break;
|
|
612
|
+
depth += 1;
|
|
613
|
+
current = parent;
|
|
614
|
+
}
|
|
615
|
+
return depth;
|
|
616
|
+
}
|
|
617
|
+
function orderCompletionForest(members) {
|
|
618
|
+
const byIdentity = new Map(members.map((member) => [memberIdentity(member), member]));
|
|
619
|
+
const children = new Map();
|
|
620
|
+
const roots = [];
|
|
621
|
+
for (const member of members) {
|
|
622
|
+
const parentIdentity = member.parent_state_dir && member.parent_run_instance_id
|
|
623
|
+
? `${member.parent_state_dir}\0${member.parent_run_instance_id}`
|
|
624
|
+
: undefined;
|
|
625
|
+
if (!parentIdentity || !byIdentity.has(parentIdentity)) {
|
|
626
|
+
roots.push(member);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const siblings = children.get(parentIdentity) ?? [];
|
|
630
|
+
siblings.push(member);
|
|
631
|
+
children.set(parentIdentity, siblings);
|
|
632
|
+
}
|
|
633
|
+
const ordered = [];
|
|
634
|
+
const visited = new Set();
|
|
635
|
+
const visit = (member) => {
|
|
636
|
+
const identity = memberIdentity(member);
|
|
637
|
+
if (visited.has(identity))
|
|
638
|
+
return;
|
|
639
|
+
visited.add(identity);
|
|
640
|
+
ordered.push(member);
|
|
641
|
+
for (const child of children.get(identity) ?? [])
|
|
642
|
+
visit(child);
|
|
643
|
+
};
|
|
644
|
+
for (const root of roots)
|
|
645
|
+
visit(root);
|
|
646
|
+
for (const member of members)
|
|
647
|
+
visit(member);
|
|
648
|
+
return ordered;
|
|
649
|
+
}
|
|
650
|
+
function formatModelOutput(value, indent) {
|
|
651
|
+
return value
|
|
652
|
+
.replaceAll("`", "'")
|
|
653
|
+
.split(/\r?\n/u)
|
|
654
|
+
.map((line) => line.trimEnd())
|
|
655
|
+
.filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
|
|
656
|
+
.map((line) => `${indent} ${line}`)
|
|
657
|
+
.join("\n");
|
|
658
|
+
}
|
|
659
|
+
function formatCompletionMember(member, byIdentity) {
|
|
570
660
|
const summary = compactModelText(member.summary, 200);
|
|
571
661
|
const artifactEntries = Object.entries(member.artifacts ?? {}).slice(0, 4);
|
|
572
662
|
const artifactText = artifactEntries.length === 0
|
|
573
663
|
? ""
|
|
574
664
|
: `; artifacts: ${artifactEntries.map(([name, path]) => `${compactModelText(name, 120)}=\`${compactModelText(path, 320)}\``).join(", ")}`;
|
|
575
|
-
|
|
665
|
+
const indent = " ".repeat(Math.min(completionDepth(member, byIdentity), 8));
|
|
666
|
+
const output = member.output
|
|
667
|
+
? `\n${indent} Output:\n${formatModelOutput(member.output, indent)}`
|
|
668
|
+
: "";
|
|
669
|
+
return `${indent}- \`${compactModelText(member.run, 120)}\` — \`${member.status}\`: ${summary}${artifactText}${output}`;
|
|
576
670
|
}
|
|
577
671
|
function formatStatusCounts(members) {
|
|
578
672
|
const counts = new Map();
|
|
@@ -594,9 +688,13 @@ export function formatRunCompletionBatchMessage(batch) {
|
|
|
594
688
|
`Window: \`${terminalTimes[0]}\` → \`${terminalTimes.at(-1)}\``,
|
|
595
689
|
`Statuses: \`${formatStatusCounts(safe.members)}\``,
|
|
596
690
|
];
|
|
597
|
-
const
|
|
691
|
+
const byIdentity = new Map(safe.members.map((member) => [
|
|
692
|
+
memberIdentity(member),
|
|
693
|
+
member,
|
|
694
|
+
]));
|
|
695
|
+
const candidateRows = orderCompletionForest(safe.members)
|
|
598
696
|
.slice(0, Limits.RUN_DELIVERY_MODEL_MAX_MEMBERS)
|
|
599
|
-
.map(formatCompletionMember);
|
|
697
|
+
.map((member) => formatCompletionMember(member, byIdentity));
|
|
600
698
|
const rows = [];
|
|
601
699
|
for (const row of candidateRows) {
|
|
602
700
|
const omitted = safe.members.length - rows.length - 1;
|
|
@@ -9,18 +9,18 @@ import * as Observability from "./observability.js";
|
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
10
10
|
import * as Pi from "./pi.js";
|
|
11
11
|
import * as RunDelivery from "./run-delivery.js";
|
|
12
|
+
import * as RunDeliveryLineage from "./run-delivery-lineage.js";
|
|
12
13
|
export function createRunUiRuntime(deps) {
|
|
13
14
|
let activeContext;
|
|
14
15
|
let activeOwnerId;
|
|
15
16
|
let animationInterval;
|
|
16
17
|
let deliveryTimeout;
|
|
17
18
|
let notifyTimeout;
|
|
18
|
-
let recoverQueuedBatch = false;
|
|
19
19
|
let recoverQueuedSteers = false;
|
|
20
20
|
let running = false;
|
|
21
21
|
let lastWatcherDiagnosticId = 0;
|
|
22
22
|
const observation = Observability.createRunUiObservationState();
|
|
23
|
-
const
|
|
23
|
+
const deliveryObservation = Observability.createRunUiObservationState();
|
|
24
24
|
const retirementAttempts = new Set();
|
|
25
25
|
const steerDiagnostics = new Set();
|
|
26
26
|
const close = () => {
|
|
@@ -45,9 +45,7 @@ export function createRunUiRuntime(deps) {
|
|
|
45
45
|
if (deliveryTimeout)
|
|
46
46
|
clearTimeout(deliveryTimeout);
|
|
47
47
|
deliveryTimeout = undefined;
|
|
48
|
-
recoverQueuedBatch = false;
|
|
49
48
|
recoverQueuedSteers = false;
|
|
50
|
-
deliveryRecoveryDiagnostics.clear();
|
|
51
49
|
steerDiagnostics.clear();
|
|
52
50
|
if (animationInterval)
|
|
53
51
|
clearInterval(animationInterval);
|
|
@@ -173,12 +171,11 @@ export function createRunUiRuntime(deps) {
|
|
|
173
171
|
}
|
|
174
172
|
}
|
|
175
173
|
};
|
|
176
|
-
const admitCompletionTransitions = (ownerId,
|
|
174
|
+
const admitCompletionTransitions = (ownerId, snapshot) => {
|
|
177
175
|
const existing = journal(ownerId).completion_batch;
|
|
178
176
|
if (existing)
|
|
179
177
|
return true;
|
|
180
|
-
const members = Observability.collectRunCompletionBatchMembers(transitions)
|
|
181
|
-
.slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
|
|
178
|
+
const members = Observability.collectRunCompletionBatchMembers(snapshot.transitions, snapshot.summary.runs).slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
|
|
182
179
|
if (members.length === 0)
|
|
183
180
|
return false;
|
|
184
181
|
RunDelivery.admitRunCompletionBatch({
|
|
@@ -189,6 +186,10 @@ export function createRunUiRuntime(deps) {
|
|
|
189
186
|
return true;
|
|
190
187
|
};
|
|
191
188
|
const isIdle = (ctx) => typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
189
|
+
const isDeliveryRoot = (ownerId) => {
|
|
190
|
+
const inheritedOwner = RunDeliveryLineage.getProcessDeliveryOwnerId();
|
|
191
|
+
return inheritedOwner === undefined || inheritedOwner === ownerId;
|
|
192
|
+
};
|
|
192
193
|
let flushCompletionBatch = (_ctx) => false;
|
|
193
194
|
const flushSteers = (ctx, ownerId) => {
|
|
194
195
|
const recovering = recoverQueuedSteers;
|
|
@@ -273,10 +274,19 @@ export function createRunUiRuntime(deps) {
|
|
|
273
274
|
return false;
|
|
274
275
|
const sink = Pi.createNotificationSink(deps.pi, ctx);
|
|
275
276
|
retireCandidateRuns(ctx, snapshot.summary);
|
|
276
|
-
const
|
|
277
|
-
|
|
277
|
+
const deliverySnapshot = isDeliveryRoot(ownerId)
|
|
278
|
+
? Observability.readRunDeliverySnapshot(deliveryObservation, ownerId)
|
|
279
|
+
: undefined;
|
|
280
|
+
const hasCompletionCandidates = deliverySnapshot
|
|
281
|
+
? Observability.collectRunCompletionBatchMembers(deliverySnapshot.transitions, deliverySnapshot.summary.runs).length > 0
|
|
282
|
+
: false;
|
|
283
|
+
const hasCompletionBatch = isDeliveryRoot(ownerId) &&
|
|
284
|
+
Boolean(journal(ownerId).completion_batch);
|
|
278
285
|
admitSteerEvents(ctx, ownerId, snapshot.attentionEvents);
|
|
279
286
|
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
287
|
+
if (deliverySnapshot) {
|
|
288
|
+
Observability.pruneRunUiObservationState(deliveryObservation, deliverySnapshot);
|
|
289
|
+
}
|
|
280
290
|
if (!terminalOnly) {
|
|
281
291
|
Observability.deliverRunAttentionNotifications(snapshot.attentionEvents.filter((event) => !Observability.isRunSteerAttentionEvent(event)), sink);
|
|
282
292
|
}
|
|
@@ -333,27 +343,34 @@ export function createRunUiRuntime(deps) {
|
|
|
333
343
|
},
|
|
334
344
|
});
|
|
335
345
|
flushCompletionBatch = (ctx) => {
|
|
336
|
-
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx)
|
|
337
|
-
return false;
|
|
338
346
|
const ownerId = activeOwnerId;
|
|
339
|
-
if (!ownerId)
|
|
347
|
+
if (!running || !ownerId)
|
|
348
|
+
return false;
|
|
349
|
+
try {
|
|
350
|
+
if (Pi.getSessionId(ctx) !== ownerId)
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
340
354
|
return false;
|
|
355
|
+
}
|
|
341
356
|
if (flushSteers(ctx, ownerId))
|
|
342
357
|
return true;
|
|
358
|
+
if (!isDeliveryRoot(ownerId))
|
|
359
|
+
return false;
|
|
343
360
|
let batch = journal(ownerId).completion_batch;
|
|
344
361
|
if (!batch) {
|
|
345
|
-
const snapshot = Observability.
|
|
346
|
-
admitCompletionTransitions(ownerId, snapshot
|
|
347
|
-
Observability.pruneRunUiObservationState(
|
|
362
|
+
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
363
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
364
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
348
365
|
batch = journal(ownerId).completion_batch;
|
|
349
366
|
}
|
|
350
367
|
if (!batch)
|
|
351
368
|
return false;
|
|
352
369
|
if (batch.phase === "presented") {
|
|
353
370
|
finishPresentedBatch(ownerId, batch);
|
|
354
|
-
const snapshot = Observability.
|
|
355
|
-
admitCompletionTransitions(ownerId, snapshot
|
|
356
|
-
Observability.pruneRunUiObservationState(
|
|
371
|
+
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
372
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
373
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
357
374
|
batch = journal(ownerId).completion_batch;
|
|
358
375
|
if (!batch)
|
|
359
376
|
return false;
|
|
@@ -362,27 +379,16 @@ export function createRunUiRuntime(deps) {
|
|
|
362
379
|
return true;
|
|
363
380
|
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
364
381
|
if (batch.phase === "queued") {
|
|
365
|
-
if (!
|
|
366
|
-
return true;
|
|
367
|
-
recoverQueuedBatch = false;
|
|
368
|
-
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, batch.batch_id, content);
|
|
369
|
-
if (evidence.status === "present")
|
|
370
|
-
return true;
|
|
371
|
-
if (evidence.status !== "absent") {
|
|
372
|
-
const diagnosticKey = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
373
|
-
if (!deliveryRecoveryDiagnostics.has(diagnosticKey)) {
|
|
374
|
-
deliveryRecoveryDiagnostics.add(diagnosticKey);
|
|
375
|
-
ctx.ui.notify(`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains queued.`, "warning");
|
|
376
|
-
}
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
if (!RunDelivery.resetRunCompletionBatchPending({
|
|
382
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
380
383
|
batchId: batch.batch_id,
|
|
381
384
|
ownerId,
|
|
382
385
|
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
383
386
|
}))
|
|
384
387
|
return true;
|
|
385
|
-
|
|
388
|
+
const accepted = journal(ownerId).completion_batch;
|
|
389
|
+
if (accepted)
|
|
390
|
+
finishPresentedBatch(ownerId, accepted);
|
|
391
|
+
return flushCompletionBatch(ctx);
|
|
386
392
|
}
|
|
387
393
|
if (!isIdle(ctx))
|
|
388
394
|
return true;
|
|
@@ -405,19 +411,33 @@ export function createRunUiRuntime(deps) {
|
|
|
405
411
|
})) {
|
|
406
412
|
throw new Error("Completion batch changed before queue acknowledgment");
|
|
407
413
|
}
|
|
408
|
-
|
|
414
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
415
|
+
batchId: batch.batch_id,
|
|
416
|
+
ownerId,
|
|
417
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
418
|
+
})) {
|
|
419
|
+
throw new Error("Completion batch changed before durable acceptance");
|
|
420
|
+
}
|
|
421
|
+
const accepted = journal(ownerId).completion_batch;
|
|
422
|
+
if (accepted)
|
|
423
|
+
finishPresentedBatch(ownerId, accepted);
|
|
424
|
+
ctx.ui.notify(`Actor completions ready: ${batch.members.length}`, "info");
|
|
409
425
|
return true;
|
|
410
426
|
};
|
|
411
427
|
return {
|
|
412
428
|
close,
|
|
413
429
|
flushCompletionBatch,
|
|
414
430
|
projectContext(messages, ctx) {
|
|
415
|
-
|
|
431
|
+
const ownerId = activeOwnerId;
|
|
432
|
+
if (!running || !ownerId)
|
|
416
433
|
return messages;
|
|
434
|
+
try {
|
|
435
|
+
if (Pi.getSessionId(ctx) !== ownerId)
|
|
436
|
+
return messages;
|
|
417
437
|
}
|
|
418
|
-
|
|
419
|
-
if (!ownerId)
|
|
438
|
+
catch {
|
|
420
439
|
return messages;
|
|
440
|
+
}
|
|
421
441
|
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
422
442
|
let contextMessages = steerContext.messages;
|
|
423
443
|
for (const steer of journal(ownerId).steers) {
|
|
@@ -486,7 +506,6 @@ export function createRunUiRuntime(deps) {
|
|
|
486
506
|
close();
|
|
487
507
|
activeContext = ctx;
|
|
488
508
|
activeOwnerId = ownerId;
|
|
489
|
-
recoverQueuedBatch = true;
|
|
490
509
|
recoverQueuedSteers = true;
|
|
491
510
|
running = true;
|
|
492
511
|
try {
|
package/dist/lib/runtime.js
CHANGED
|
@@ -223,6 +223,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
223
223
|
let rootWatcher;
|
|
224
224
|
let parentWatcher;
|
|
225
225
|
let failureNotified = false;
|
|
226
|
+
let notifyAfterReload = false;
|
|
226
227
|
const setWatchStatus = (watchStatus) => runtime.setWatchStatus?.(watchStatus);
|
|
227
228
|
const close = () => {
|
|
228
229
|
const closingRoot = rootWatcher;
|
|
@@ -234,6 +235,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
234
235
|
if (reloadTimeout)
|
|
235
236
|
clearTimeout(reloadTimeout);
|
|
236
237
|
reloadTimeout = undefined;
|
|
238
|
+
notifyAfterReload = false;
|
|
237
239
|
setWatchStatus("closed");
|
|
238
240
|
};
|
|
239
241
|
const reportCallbackError = (error) => {
|
|
@@ -256,15 +258,19 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
256
258
|
reportCallbackError(error);
|
|
257
259
|
}
|
|
258
260
|
};
|
|
259
|
-
const scheduleReload = (ctx) => {
|
|
261
|
+
const scheduleReload = (ctx, notifyActiveChange = false) => {
|
|
260
262
|
failureNotified = false;
|
|
263
|
+
notifyAfterReload ||= notifyActiveChange;
|
|
261
264
|
if (reloadTimeout)
|
|
262
265
|
clearTimeout(reloadTimeout);
|
|
263
266
|
reloadTimeout = setTimeout(() => {
|
|
264
267
|
reloadTimeout = undefined;
|
|
265
268
|
try {
|
|
266
269
|
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
267
|
-
|
|
270
|
+
if (notifyAfterReload) {
|
|
271
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
272
|
+
}
|
|
273
|
+
notifyAfterReload = false;
|
|
268
274
|
}
|
|
269
275
|
catch (error) {
|
|
270
276
|
notifyFailure(ctx);
|
|
@@ -295,7 +301,7 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
295
301
|
parentWatcher = undefined;
|
|
296
302
|
watcher.close();
|
|
297
303
|
watchRoot(ctx, recipeRoot);
|
|
298
|
-
scheduleReload(ctx);
|
|
304
|
+
scheduleReload(ctx, true);
|
|
299
305
|
});
|
|
300
306
|
parentWatcher = watcher;
|
|
301
307
|
setWatchStatus("watching_parent");
|
|
@@ -319,17 +325,19 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
319
325
|
return;
|
|
320
326
|
}
|
|
321
327
|
try {
|
|
322
|
-
const watcher = watchPath(recipeRoot, () => {
|
|
328
|
+
const watcher = watchPath(recipeRoot, (_event, changedFile) => {
|
|
323
329
|
if (rootWatcher !== watcher)
|
|
324
330
|
return;
|
|
325
331
|
if (!pathExists(recipeRoot)) {
|
|
326
332
|
rootWatcher = undefined;
|
|
327
333
|
watcher.close();
|
|
328
|
-
scheduleReload(ctx);
|
|
334
|
+
scheduleReload(ctx, true);
|
|
329
335
|
watchParent(ctx, recipeRoot);
|
|
330
336
|
return;
|
|
331
337
|
}
|
|
332
|
-
|
|
338
|
+
const relativeChange = changedFile ? String(changedFile) : "";
|
|
339
|
+
const firstSegment = relativeChange.split(/[\\/]/u)[0];
|
|
340
|
+
scheduleReload(ctx, Boolean(relativeChange) && firstSegment !== "drafts");
|
|
333
341
|
});
|
|
334
342
|
rootWatcher = watcher;
|
|
335
343
|
setWatchStatus("watching_root");
|
|
@@ -76,16 +76,26 @@ Then:
|
|
|
76
76
|
|
|
77
77
|
Use direct delegation for the same maintained capability under a persistent name or narrower defaults. Use named imports only when one Recipe graph contains reusable child nodes. See [persistent tools](./references/persistent-tools.md) and [Recipes](./references/recipes.md).
|
|
78
78
|
|
|
79
|
-
##
|
|
79
|
+
## Capability map
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Recipes remain with the Skill that owns their behavior. Load that Skill for selection and caller inputs; this map is routing, not a copied Recipe contract.
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
| Owner | Recipe families |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `actors` | `command-validate`, `recipe-validate`, `jsonl-tail`, `run-summary`, `run-state-files`, `run-ops-snapshot`, `resource-locker`, `resource-locker-snapshot` |
|
|
86
|
+
| `swarm` | Lens and quorum reviews, research synthesis, architecture, development tasking, readiness, and supporting `subagent-*` components |
|
|
87
|
+
| `artifacts` | Report, write, manifest, bundle, and supporting file-write |
|
|
88
|
+
| `project-work` | Repository health, docs maintenance, release readiness/summary, Run reports, and supporting deterministic snapshots |
|
|
89
|
+
| `music-player` | The controlled `playback` singleton |
|
|
90
|
+
| `recipe-memory` | Internal automatic reviewers; use its Skill only for diagnosis/recovery |
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
Use the `actors/*` helpers only for their narrow requested result or intentional composition. Prefer public `inspect` for ordinary runtime diagnosis and the capability's primary workflow for a complete outcome.
|
|
87
93
|
|
|
88
|
-
Delegation
|
|
94
|
+
## Delegation boundary
|
|
95
|
+
|
|
96
|
+
The current Pi instance owns user authority and the final result; pi-actors creates explicit local Runs, while companion transports provide presence rather than hidden instance creation. Keep short single-boundary work inline when delegation adds no value. For several participants, reasoning allocation, quorum, or integration methodology, read `swarm`.
|
|
97
|
+
|
|
98
|
+
Treat `attention: "steer"` as an actor-authored urgent semantic checkpoint at Pi's next safe boundary, never as a status-derived completion signal; the later root terminal still arrives through its ordinary completion batch.
|
|
89
99
|
|
|
90
100
|
## Run workflow
|
|
91
101
|
|
|
@@ -97,7 +107,7 @@ Run = Recipe + Trace + Control
|
|
|
97
107
|
```
|
|
98
108
|
|
|
99
109
|
1. Spawn with the exact logical Recipe identity and caller-owned values.
|
|
100
|
-
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.
|
|
101
111
|
3. Inspect `view=trace` when retained observations or attention matter.
|
|
102
112
|
4. Inspect `view=control` before diagnosing service readiness, stale work, or saturation.
|
|
103
113
|
5. Send `message` only for an action declared and consumed by that controlled Recipe.
|
|
@@ -13,7 +13,7 @@ When a Telegram-originated turn or explicit Telegram-control question makes repe
|
|
|
13
13
|
|
|
14
14
|
## Playback
|
|
15
15
|
|
|
16
|
-
`music-player/playback` is a singleton async controlled service with the canonical address `run:music-player`. The Recipe is the sole lifecycle owner: it starts the service, supervises it, and stops playback when the Run closes. The service owns queue, backend, checkpoint, and playback state. `playback
|
|
16
|
+
`music-player/playback` is a singleton async controlled service with the canonical address `run:music-player`. The Recipe is the sole lifecycle owner: it starts the service, supervises it, and stops playback when the Run closes. The service owns queue, backend, checkpoint, and playback state. `scripts/playback.mjs` owns both foreground playback and the bounded control CLI. Its `control <state-dir> <action> [percent]` entrypoint observes or controls an existing owner without starting, adopting, or supervising it. Explicit foreground `serve` supports a caller-owned standalone host without importing the Actor runtime; Actor and standalone ownership of one state directory are mutually exclusive.
|
|
17
17
|
|
|
18
18
|
```text
|
|
19
19
|
spawn recipe=music-player/playback values={"source":"~/Music","player":"auto"}
|
|
@@ -35,14 +35,14 @@ Use only declared actions: `play`, `pause`, `resume`, `toggle`, `next`, `previou
|
|
|
35
35
|
- `status` is read-only and exposes bounded machine-readable player state, including the current absolute volume and a duration-derived progress percentage projected at read time.
|
|
36
36
|
- `stop` ends the live process without silently deleting the saved queue.
|
|
37
37
|
|
|
38
|
-
External local views use
|
|
38
|
+
External local views use `playback.mjs control <state-dir> <action> [percent]`. For Actor-owned playback, the script validates Run availability, queues canonical Control, and waits for that exact record to become handled or failed. For standalone playback, it sends a bounded generation-fenced command to the service endpoint without creating Run, Control, or Trace state. Views themselves never read or edit Run files, signal processes, construct Control records, or import pi-actors internals. `status` is read-only and reports `actor_available` separately from playback state.
|
|
39
39
|
|
|
40
40
|
## Maintained Telegram View
|
|
41
41
|
|
|
42
42
|
> [!NOTE]
|
|
43
43
|
> This Skill includes a ready Music Player Generative App at `genapps/music-player.mjs`. When `telegram_bind` is available and Telegram interaction is relevant, use it to copy and install the app as `music-player`; hot replacement keeps the same app name with `replace: true`.
|
|
44
44
|
|
|
45
|
-
Bind with absolute `control
|
|
45
|
+
Bind with absolute `control` (the `scripts/playback.mjs` path), `stateDir`, and `node` arguments. This maintained adapter targets Actor-owned playback and uses the unified `control` entrypoint above for status and mutation; it displays controls only when `actor_available` is true. Exact terminal Control evidence remains visible in the Run inspector and failures reach Telegram. The adapter neither imports extension internals nor starts playback. Its stopped-state Start button returns to Pi so the composition root can spawn `music-player/playback`, while active controls remain deterministic Generative App actions that bypass the model. `pi-telegram` owns only the generic Generative App runtime.
|
|
46
46
|
|
|
47
47
|
## Sources And Backends
|
|
48
48
|
|
|
@@ -70,7 +70,7 @@ function normalizePlayback(status) {
|
|
|
70
70
|
async function readPlayback(adapter, run) {
|
|
71
71
|
const result = await run({
|
|
72
72
|
command: adapter.node,
|
|
73
|
-
args: [adapter.control, "
|
|
73
|
+
args: [adapter.control, "control", adapter.stateDir, "status"],
|
|
74
74
|
cwd: dirname(adapter.control),
|
|
75
75
|
timeoutMs: 5_000,
|
|
76
76
|
});
|
|
@@ -188,8 +188,9 @@ async function applyVolume(percent, context) {
|
|
|
188
188
|
command: context.state.adapter.node,
|
|
189
189
|
args: [
|
|
190
190
|
context.state.adapter.control,
|
|
191
|
-
"
|
|
191
|
+
"control",
|
|
192
192
|
context.state.adapter.stateDir,
|
|
193
|
+
"volume",
|
|
193
194
|
String(percent),
|
|
194
195
|
],
|
|
195
196
|
cwd: dirname(context.state.adapter.control),
|
|
@@ -224,8 +225,9 @@ async function applySeek(percent, context) {
|
|
|
224
225
|
command: context.state.adapter.node,
|
|
225
226
|
args: [
|
|
226
227
|
context.state.adapter.control,
|
|
227
|
-
"
|
|
228
|
+
"control",
|
|
228
229
|
context.state.adapter.stateDir,
|
|
230
|
+
"seek",
|
|
229
231
|
String(percent),
|
|
230
232
|
],
|
|
231
233
|
cwd: dirname(context.state.adapter.control),
|
|
@@ -253,7 +255,7 @@ async function apply(action, context) {
|
|
|
253
255
|
const before = await readPlayback(context.state.adapter, context.run);
|
|
254
256
|
const result = await context.run({
|
|
255
257
|
command: context.state.adapter.node,
|
|
256
|
-
args: [context.state.adapter.control,
|
|
258
|
+
args: [context.state.adapter.control, "control", context.state.adapter.stateDir, action],
|
|
257
259
|
cwd: dirname(context.state.adapter.control),
|
|
258
260
|
timeoutMs: 5_000,
|
|
259
261
|
});
|