@llblab/pi-actors 0.53.2 → 0.55.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 +5 -1
- package/CHANGELOG.md +8 -1
- package/LICENSE +21 -0
- package/README.md +3 -3
- package/dist/index.js +1 -0
- package/dist/lib/extension-runtime.d.ts +1 -0
- package/dist/lib/extension-runtime.js +3 -0
- package/dist/lib/pi.d.ts +7 -2
- package/dist/lib/pi.js +20 -6
- package/dist/lib/run-ui-runtime.d.ts +1 -0
- package/dist/lib/run-ui-runtime.js +74 -74
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/actors/references/runs.md +1 -1
- package/docs/README.md +1 -1
- package/docs/async-runs.md +3 -1
- package/docs/coordinator-delivery.md +23 -21
- package/index.ts +1 -0
- package/lib/extension-runtime.ts +4 -0
- package/lib/pi.ts +33 -9
- package/lib/run-ui-runtime.ts +72 -69
- package/package.json +4 -3
- package/skills/actors/SKILL.md +1 -1
- package/skills/actors/references/runs.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -26,6 +26,8 @@ Public Run verbs remain `spawn`, `message`, and `inspect`.
|
|
|
26
26
|
|
|
27
27
|
## Core Structure
|
|
28
28
|
|
|
29
|
+
Require Pi ≥1.0.0 for all declared Pi peers. Keep source and packed lifecycle tests aligned with that baseline; do not restore an older host floor independently of those checks. Validation fixtures must use temporary agent directories, never write into the operator's live Recipe registry.
|
|
30
|
+
|
|
29
31
|
```text
|
|
30
32
|
Pi host
|
|
31
33
|
-> index.ts composition root
|
|
@@ -138,7 +140,9 @@ Tool result/error text contributes exactly one leading line break. Keep model-fa
|
|
|
138
140
|
|
|
139
141
|
File watchers accelerate reconciliation; a bounded interval recovers missed terminal and retained-attention events. Canonical attention observation uses stable retained ids and bounds seen memory to the current suffix across compaction; only allowlisted legacy outbox fallback uses line offsets. Ordinary `notify` and `followup` hints prime at startup without replay and require durable state or artifacts for recovery. Explicit `steer` instead re-enters through the owner journal until exact presentation records its newer generation-fenced Trace marker.
|
|
140
142
|
|
|
141
|
-
Ordinary terminals retain inherited delivery lineage from each nested Actor process to the top-level coordinator. Only that root coordinator
|
|
143
|
+
Ordinary terminals retain inherited delivery lineage from each nested Actor process to the top-level coordinator. Only that root coordinator schedules completion delivery; descendant sessions stay quiet and wait for their complete containing top-level tree. A completed active `turn_end` snapshots the ready forest into one immutable batch, preserves earlier boundary entries, and returns one visible custom-message entry plus one continuation, never a native steering/follow-up enqueue. Aborted/error boundaries and pending native input defer projection. Already-idle Pi retains debounced follow-up delivery. The same batch renders as one operator card and one model prompt, fenced by owner and generation and bounded at 256 durable members, 64 model-facing rows, 16 KiB of context, and a 1 MiB journal.
|
|
144
|
+
|
|
145
|
+
Returning a boundary draft is not acceptance: keep it pending until exact model context or bounded active-session parent-chain evidence proves the committed entry. Successful idle `sendMessage()` acceptance finalizes immediately; durable queued phase also proves acceptance. Only proven absence permits retry; incomplete or conflicting pending evidence stays pending with diagnostics. Exact acceptance authorizes generation-fenced terminal handled markers and releases later epochs without depending on one context callback. Retry duplicates collapse before presentation, malformed envelopes fail closed, and automatic review waits behind an active completion batch.
|
|
142
146
|
|
|
143
147
|
When deferred Run results gate the next step, wait for their completion batch. Inspect early only for operator request, meaningful attention, or diagnosis of an overdue Run.
|
|
144
148
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.55.0: Active Completion Overlays
|
|
6
|
+
|
|
7
|
+
- `Active Completion Overlays`: Batches ready Actor trees at the next completed model-step boundary, after the full tool batch, so the running coordinator receives fresh results before idle without native steering. Preserves prior boundary entries, exact owner/generation fences, and debounced idle wakeups. Proposed entries remain pending until exact context or bounded session evidence proves acceptance; dropped proposals retry without premature handled markers or duplicate accepted reports.
|
|
8
|
+
|
|
9
|
+
## 0.54.0: Pi 1.0 Baseline and Package Licensing
|
|
10
|
+
|
|
11
|
+
- `Licensing`: Includes the MIT LICENSE in source checkouts and npm packages, preserving existing author attribution.
|
|
12
|
+
- `Pi Baseline`: Requires Pi 1.0.0 or newer for both coding-agent and TUI peers. Source and installed-package lifecycle checks use the same baseline; runtime ownership, Recipe/Run semantics and persistent registries are unchanged. Stored-tool normalization tests now use a temporary agent registry rather than writing fixtures into the operator's home.
|
|
6
13
|
|
|
7
14
|
## 0.53.2: Stable Inspector Trace Sequences
|
|
8
15
|
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 llblab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ An **actor** is any runnable local capability: a script, tool, service, pipeline
|
|
|
15
15
|
|
|
16
16
|
Multi-instance systems commonly put instance creation and routing in an external gateway. pi-actors supports a different topology: the current Pi instance remains the coordinator, companion extensions such as Telegram provide presence, and explicit Runs perform bounded delegated work. The coordinator receives high-level outcomes, decomposes them, stays available for decisions, and owns integration plus final validation instead of becoming another undifferentiated worker.
|
|
17
17
|
|
|
18
|
-
This topology does not require every task to become a subagent. Short work with one natural validation boundary stays inline; delegation pays when clean context, asynchronous execution, independent judgement, parallel ownership, or coordinator availability exceeds its coordination cost. Bounded implementation can run with reasoning off while the coordinator selectively launches clean-context reasoning-enabled reviewers; several independent reviews can provide broader evidence than one author self-review.
|
|
18
|
+
This topology does not require every task to become a subagent. Short work with one natural validation boundary stays inline; delegation pays when clean context, asynchronous execution, independent judgement, parallel ownership, or coordinator availability exceeds its coordination cost. Bounded implementation can run with reasoning off while the coordinator selectively launches clean-context reasoning-enabled reviewers; several independent reviews can provide broader evidence than one author self-review. Boundary-delivered completion batches, durable Trace, and declared artifacts replace tight polling loops.
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
|
-
Requires Node.js 22.19.0 or newer and Pi 0.
|
|
22
|
+
Requires Node.js 22.19.0 or newer and Pi 1.0.0 or newer.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
pi install npm:@llblab/pi-actors
|
|
@@ -153,7 +153,7 @@ Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `lev
|
|
|
153
153
|
|
|
154
154
|
Generic command lifecycle is Trace-only: runner-owned `command.done` records preserve completion and execution evidence but never request or project attention, including malformed legacy records carrying `steer`. There is no Recipe-level command-completion delivery switch. Semantic attention is explicit: `notify` shows visible status without a model turn, `followup` supplies ordinary checkpoint context, and `steer` requests urgent delivery at Pi's next safe assistant/tool boundary. `notify` and `followup` remain retained wake hints that may compact away, so persist recovery state or an artifact first. `steer` is admitted to the bounded owner delivery journal before `deliverAs: "steer"`, acknowledged only in exact model-bound context, and never suppresses the eventual root-terminal batch. Store large evidence in artifacts or bounded execution captures.
|
|
155
155
|
|
|
156
|
-
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Nested actors inherit durable parent-generation lineage: their own Pi sessions never emit completion turns, their results wait
|
|
156
|
+
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Nested actors inherit durable parent-generation lineage: their own Pi sessions never emit completion turns, and their results wait for the complete containing top-level tree. While the root Pi is active, the next completed `turn_end` boundary snapshots all ready trees into one immutable bounded batch, appends one visible custom message after the full tool batch, and requests one next model step. This overlay uses boundary entries, not Pi's native steering or follow-up queue, so the model can integrate fresh results before its current run settles. Already-idle Pi retains a short debounce and one automatic follow-up wake. The operator and model receive the same parent-child tree with bounded actor outputs. Proposed boundary entries stay pending until exact model context or bounded active-session evidence proves acceptance; an idle send finalizes on successful Pi acceptance without requiring a later context callback. Sequence, parallel, repeat, and imported branches remain internal, while separately launched Runs retain exact independent generations. Silent Runs and synchronous stop acknowledgments remain non-projecting.
|
|
157
157
|
|
|
158
158
|
## Control
|
|
159
159
|
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export default function toolRegistryExtension(pi) {
|
|
|
12
12
|
pi.on("resources_discover", async () => runtime.discoverResources(import.meta.url));
|
|
13
13
|
}
|
|
14
14
|
pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
|
|
15
|
+
pi.on("turn_end", (event, ctx) => runtime.onTurnEnd(event, ctx));
|
|
15
16
|
pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
|
|
16
17
|
pi.on("context", async (event, ctx) => ({
|
|
17
18
|
messages: runtime.onContext(event.messages, ctx),
|
|
@@ -14,6 +14,7 @@ export interface ActorExtensionRuntime {
|
|
|
14
14
|
} | undefined;
|
|
15
15
|
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
16
16
|
onAgentSettled(ctx: Pi.ExtensionContext): void;
|
|
17
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
17
18
|
onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
18
19
|
onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
|
|
19
20
|
onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
|
|
@@ -118,6 +118,9 @@ export function createActorExtensionRuntime(pi) {
|
|
|
118
118
|
if (!runUiRuntime.flushCompletionBatch(ctx))
|
|
119
119
|
automaticReview.schedule();
|
|
120
120
|
},
|
|
121
|
+
onTurnEnd(event, ctx) {
|
|
122
|
+
return runUiRuntime.onTurnEnd(event, ctx);
|
|
123
|
+
},
|
|
121
124
|
onContext(messages, ctx) {
|
|
122
125
|
return runUiRuntime.projectContext(messages, ctx);
|
|
123
126
|
},
|
package/dist/lib/pi.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Zones: pi agent sdk boundary, extension host adapters
|
|
4
4
|
* Owns direct pi SDK imports and exposes narrow pi-actors-facing helpers/types for the composition root.
|
|
5
5
|
*/
|
|
6
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext, CustomMessageEntryDraft, TurnEndEvent, TurnEndEventResult } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import * as SessionEvidence from "./session-evidence.ts";
|
|
8
|
-
export type { ExtensionAPI, ExtensionContext };
|
|
8
|
+
export type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult };
|
|
9
9
|
export declare const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
|
|
10
10
|
export declare const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
|
|
11
11
|
export interface PiNotificationSink {
|
|
@@ -19,6 +19,7 @@ export interface PiNotificationSink {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function getSessionId(ctx: ExtensionContext): string;
|
|
21
21
|
export declare function createNotificationSink(pi: ExtensionAPI, ctx: ExtensionContext): PiNotificationSink;
|
|
22
|
+
export declare function createRunCompletionBatchEntry(batchId: string, content: string): CustomMessageEntryDraft;
|
|
22
23
|
export declare function sendRunCompletionBatch(pi: ExtensionAPI, batchId: string, content: string): void;
|
|
23
24
|
export declare function sendRunSteer(pi: ExtensionAPI, input: {
|
|
24
25
|
content: string;
|
|
@@ -41,6 +42,10 @@ export declare function dedupeRunSteerContext(messages: unknown[]): {
|
|
|
41
42
|
}>;
|
|
42
43
|
};
|
|
43
44
|
export declare function removeRunSteerFromContext(messages: unknown[], steerId: string): unknown[];
|
|
45
|
+
export declare function inspectRunCompletionBatchSessionEvidence(ctx: ExtensionContext, input: {
|
|
46
|
+
batchId: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}): SessionEvidence.ActiveSessionEntryEvidence;
|
|
44
49
|
export declare function inspectRunSteerSessionEvidence(ctx: ExtensionContext, input: {
|
|
45
50
|
content: string;
|
|
46
51
|
eventId: string;
|
package/dist/lib/pi.js
CHANGED
|
@@ -18,8 +18,9 @@ export function createNotificationSink(pi, ctx) {
|
|
|
18
18
|
}),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
export function
|
|
22
|
-
|
|
21
|
+
export function createRunCompletionBatchEntry(batchId, content) {
|
|
22
|
+
return {
|
|
23
|
+
type: "custom_message",
|
|
23
24
|
customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
|
|
24
25
|
content,
|
|
25
26
|
display: true,
|
|
@@ -29,10 +30,11 @@ export function sendRunCompletionBatch(pi, batchId, content) {
|
|
|
29
30
|
kind: "completion_batch",
|
|
30
31
|
},
|
|
31
32
|
},
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function sendRunCompletionBatch(pi, batchId, content) {
|
|
36
|
+
const { type: _type, ...message } = createRunCompletionBatchEntry(batchId, content);
|
|
37
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
36
38
|
}
|
|
37
39
|
export function sendRunSteer(pi, input) {
|
|
38
40
|
pi.sendMessage({
|
|
@@ -172,6 +174,18 @@ export function dedupeRunSteerContext(messages) {
|
|
|
172
174
|
export function removeRunSteerFromContext(messages, steerId) {
|
|
173
175
|
return messages.filter((message) => steerMessage(message)?.steerId !== steerId);
|
|
174
176
|
}
|
|
177
|
+
export function inspectRunCompletionBatchSessionEvidence(ctx, input) {
|
|
178
|
+
return SessionEvidence.inspectBoundedActiveSessionEntries({
|
|
179
|
+
getEntry: (id) => ctx.sessionManager.getEntry(id),
|
|
180
|
+
leaf: ctx.sessionManager.getLeafEntry(),
|
|
181
|
+
match: (entry) => {
|
|
182
|
+
const batch = completionBatchMessage(entry);
|
|
183
|
+
if (!batch || batch.batchId !== input.batchId)
|
|
184
|
+
return undefined;
|
|
185
|
+
return batch.content === input.content ? "present" : "conflict";
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
175
189
|
export function inspectRunSteerSessionEvidence(ctx, input) {
|
|
176
190
|
return SessionEvidence.inspectBoundedActiveSessionEntries({
|
|
177
191
|
getEntry: (id) => ctx.sessionManager.getEntry(id),
|
|
@@ -9,6 +9,7 @@ import * as Pi from "./pi.ts";
|
|
|
9
9
|
export interface RunUiRuntime {
|
|
10
10
|
close(): void;
|
|
11
11
|
flushCompletionBatch(ctx: Pi.ExtensionContext): boolean;
|
|
12
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
12
13
|
projectContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
13
14
|
shutdown(eventReason: string, ownerId: string | undefined, ctx?: Pi.ExtensionContext): void;
|
|
14
15
|
start(ctx: Pi.ExtensionContext, ownerId: string): void;
|
|
@@ -23,6 +23,7 @@ export function createRunUiRuntime(deps) {
|
|
|
23
23
|
const deliveryObservation = Observability.createRunUiObservationState();
|
|
24
24
|
const retirementAttempts = new Set();
|
|
25
25
|
const steerDiagnostics = new Set();
|
|
26
|
+
const completionDiagnostics = new Set();
|
|
26
27
|
const close = () => {
|
|
27
28
|
running = false;
|
|
28
29
|
activeContext = undefined;
|
|
@@ -47,6 +48,7 @@ export function createRunUiRuntime(deps) {
|
|
|
47
48
|
deliveryTimeout = undefined;
|
|
48
49
|
recoverQueuedSteers = false;
|
|
49
50
|
steerDiagnostics.clear();
|
|
51
|
+
completionDiagnostics.clear();
|
|
50
52
|
if (animationInterval)
|
|
51
53
|
clearInterval(animationInterval);
|
|
52
54
|
animationInterval = undefined;
|
|
@@ -185,6 +187,57 @@ export function createRunUiRuntime(deps) {
|
|
|
185
187
|
});
|
|
186
188
|
return true;
|
|
187
189
|
};
|
|
190
|
+
const acceptCompletionBatch = (ownerId, batchId) => {
|
|
191
|
+
const input = { batchId, ownerId, tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir };
|
|
192
|
+
if (!RunDelivery.markRunCompletionBatchQueued(input))
|
|
193
|
+
return false;
|
|
194
|
+
if (!RunDelivery.markRunCompletionBatchPresented(input))
|
|
195
|
+
return false;
|
|
196
|
+
const accepted = journal(ownerId).completion_batch;
|
|
197
|
+
return accepted ? finishPresentedBatch(ownerId, accepted) : false;
|
|
198
|
+
};
|
|
199
|
+
const reconcileCompletionBatch = (ctx, ownerId) => {
|
|
200
|
+
const batch = journal(ownerId).completion_batch;
|
|
201
|
+
if (!batch)
|
|
202
|
+
return false;
|
|
203
|
+
if (batch.phase === "pending") {
|
|
204
|
+
// Returning boundary drafts is not acceptance: prove the exact entry before acknowledging it.
|
|
205
|
+
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, {
|
|
206
|
+
batchId: batch.batch_id,
|
|
207
|
+
content: RunDelivery.formatRunCompletionBatchMessage(batch),
|
|
208
|
+
});
|
|
209
|
+
if (evidence.status === "absent")
|
|
210
|
+
return false;
|
|
211
|
+
if (evidence.status !== "present") {
|
|
212
|
+
const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
213
|
+
if (!completionDiagnostics.has(key)) {
|
|
214
|
+
completionDiagnostics.add(key);
|
|
215
|
+
ctx.ui.notify(`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains pending.`, "warning");
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return !acceptCompletionBatch(ownerId, batch.batch_id);
|
|
221
|
+
};
|
|
222
|
+
const pendingCompletionBatch = (ownerId) => {
|
|
223
|
+
const existing = journal(ownerId).completion_batch;
|
|
224
|
+
if (existing)
|
|
225
|
+
return existing;
|
|
226
|
+
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
227
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
228
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
229
|
+
return journal(ownerId).completion_batch;
|
|
230
|
+
};
|
|
231
|
+
const isCurrentOwner = (ctx) => {
|
|
232
|
+
if (!running || !activeOwnerId)
|
|
233
|
+
return false;
|
|
234
|
+
try {
|
|
235
|
+
return Pi.getSessionId(ctx) === activeOwnerId;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
188
241
|
const isIdle = (ctx) => typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
189
242
|
const isDeliveryRoot = (ownerId) => {
|
|
190
243
|
const inheritedOwner = RunDeliveryLineage.getProcessDeliveryOwnerId();
|
|
@@ -344,52 +397,20 @@ export function createRunUiRuntime(deps) {
|
|
|
344
397
|
});
|
|
345
398
|
flushCompletionBatch = (ctx) => {
|
|
346
399
|
const ownerId = activeOwnerId;
|
|
347
|
-
if (!
|
|
400
|
+
if (!ownerId || !isCurrentOwner(ctx))
|
|
348
401
|
return false;
|
|
349
|
-
try {
|
|
350
|
-
if (Pi.getSessionId(ctx) !== ownerId)
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
402
|
if (flushSteers(ctx, ownerId))
|
|
357
403
|
return true;
|
|
358
404
|
if (!isDeliveryRoot(ownerId))
|
|
359
405
|
return false;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
364
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
365
|
-
batch = journal(ownerId).completion_batch;
|
|
366
|
-
}
|
|
406
|
+
if (reconcileCompletionBatch(ctx, ownerId))
|
|
407
|
+
return true;
|
|
408
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
367
409
|
if (!batch)
|
|
368
410
|
return false;
|
|
369
|
-
if (batch.phase === "presented") {
|
|
370
|
-
finishPresentedBatch(ownerId, batch);
|
|
371
|
-
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
372
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
373
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
374
|
-
batch = journal(ownerId).completion_batch;
|
|
375
|
-
if (!batch)
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
411
|
if (!isIdle(ctx))
|
|
379
412
|
return true;
|
|
380
413
|
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
381
|
-
if (batch.phase === "queued") {
|
|
382
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
383
|
-
batchId: batch.batch_id,
|
|
384
|
-
ownerId,
|
|
385
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
386
|
-
}))
|
|
387
|
-
return true;
|
|
388
|
-
const accepted = journal(ownerId).completion_batch;
|
|
389
|
-
if (accepted)
|
|
390
|
-
finishPresentedBatch(ownerId, accepted);
|
|
391
|
-
return flushCompletionBatch(ctx);
|
|
392
|
-
}
|
|
393
414
|
if (!isIdle(ctx))
|
|
394
415
|
return true;
|
|
395
416
|
try {
|
|
@@ -404,40 +425,33 @@ export function createRunUiRuntime(deps) {
|
|
|
404
425
|
});
|
|
405
426
|
throw error;
|
|
406
427
|
}
|
|
407
|
-
if (!
|
|
408
|
-
batchId: batch.batch_id,
|
|
409
|
-
ownerId,
|
|
410
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
411
|
-
})) {
|
|
412
|
-
throw new Error("Completion batch changed before queue acknowledgment");
|
|
413
|
-
}
|
|
414
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
415
|
-
batchId: batch.batch_id,
|
|
416
|
-
ownerId,
|
|
417
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
418
|
-
})) {
|
|
428
|
+
if (!acceptCompletionBatch(ownerId, batch.batch_id)) {
|
|
419
429
|
throw new Error("Completion batch changed before durable acceptance");
|
|
420
430
|
}
|
|
421
|
-
const accepted = journal(ownerId).completion_batch;
|
|
422
|
-
if (accepted)
|
|
423
|
-
finishPresentedBatch(ownerId, accepted);
|
|
424
431
|
ctx.ui.notify(`Actor completions ready: ${batch.members.length}`, "info");
|
|
425
432
|
return true;
|
|
426
433
|
};
|
|
427
434
|
return {
|
|
428
435
|
close,
|
|
429
436
|
flushCompletionBatch,
|
|
437
|
+
onTurnEnd(event, ctx) {
|
|
438
|
+
const ownerId = activeOwnerId;
|
|
439
|
+
if (!ownerId || !isCurrentOwner(ctx) || !isDeliveryRoot(ownerId) ||
|
|
440
|
+
event.outcome !== "completed" || isIdle(ctx) || ctx.signal?.aborted ||
|
|
441
|
+
ctx.hasPendingMessages?.() || event.context.pendingMessages.length > 0)
|
|
442
|
+
return undefined;
|
|
443
|
+
if (flushSteers(ctx, ownerId) || reconcileCompletionBatch(ctx, ownerId))
|
|
444
|
+
return undefined;
|
|
445
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
446
|
+
if (!batch)
|
|
447
|
+
return undefined;
|
|
448
|
+
const entry = Pi.createRunCompletionBatchEntry(batch.batch_id, RunDelivery.formatRunCompletionBatchMessage(batch));
|
|
449
|
+
return { entries: [...event.entries, entry], continue: true };
|
|
450
|
+
},
|
|
430
451
|
projectContext(messages, ctx) {
|
|
431
452
|
const ownerId = activeOwnerId;
|
|
432
|
-
if (!
|
|
433
|
-
return messages;
|
|
434
|
-
try {
|
|
435
|
-
if (Pi.getSessionId(ctx) !== ownerId)
|
|
436
|
-
return messages;
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
453
|
+
if (!ownerId || !isCurrentOwner(ctx))
|
|
439
454
|
return messages;
|
|
440
|
-
}
|
|
441
455
|
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
442
456
|
let contextMessages = steerContext.messages;
|
|
443
457
|
for (const steer of journal(ownerId).steers) {
|
|
@@ -472,21 +486,7 @@ export function createRunUiRuntime(deps) {
|
|
|
472
486
|
if (content !== RunDelivery.formatRunCompletionBatchMessage(batch)) {
|
|
473
487
|
return Pi.removeRunCompletionBatchFromContext(projected.messages, batch.batch_id);
|
|
474
488
|
}
|
|
475
|
-
|
|
476
|
-
batchId: batch.batch_id,
|
|
477
|
-
ownerId,
|
|
478
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
479
|
-
}))
|
|
480
|
-
return projected.messages;
|
|
481
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
482
|
-
batchId: batch.batch_id,
|
|
483
|
-
ownerId,
|
|
484
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
485
|
-
}))
|
|
486
|
-
return projected.messages;
|
|
487
|
-
const presented = journal(ownerId).completion_batch;
|
|
488
|
-
if (presented)
|
|
489
|
-
finishPresentedBatch(ownerId, presented);
|
|
489
|
+
acceptCompletionBatch(ownerId, batch.batch_id);
|
|
490
490
|
return projected.messages;
|
|
491
491
|
},
|
|
492
492
|
shutdown(eventReason, ownerId, ctx) {
|
|
@@ -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 the root coordinator's
|
|
110
|
+
2. Retain the returned `run:<id>` and normally wait for the root coordinator's completion batch instead of polling. While Pi is active, ready results arrive together at the next safe model-step boundary after the full tool batch; already-idle Pi wakes through a debounced follow-up. Nested actor completions wait for their complete containing top-level tree and arrive in that one 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.
|
|
@@ -14,7 +14,7 @@ A rare Skill Recipe may declare `singleton: true`. Do not pass `as`: the runtime
|
|
|
14
14
|
|
|
15
15
|
## Observe
|
|
16
16
|
|
|
17
|
-
Normally wait for the
|
|
17
|
+
Normally wait for the completion batch: ready trees reach an active root coordinator at the next safe model-step boundary after the full tool batch, or wake an already-idle coordinator through a debounced follow-up. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
inspect target=run:<id> view=recipe
|
package/docs/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Living index of all documentation in the `/docs` directory.
|
|
|
7
7
|
- [command-templates.md](./command-templates.md) — Portable synchronous command execution standard
|
|
8
8
|
- [template-recipes.md](./template-recipes.md) — Saved JSON/Markdown recipe standard, imports, and reusable command-template graph composition
|
|
9
9
|
- [async-runs.md](./async-runs.md) — Run lifecycle, state, Control, Trace, cancellation, and terminal reconciliation
|
|
10
|
-
- [coordinator-delivery.md](./coordinator-delivery.md) —
|
|
10
|
+
- [coordinator-delivery.md](./coordinator-delivery.md) — Active model-step completion overlays, idle batching, durable acceptance, and explicit urgent Pi steering
|
|
11
11
|
- [actor-inspector.md](./actor-inspector.md) — Owner-filtered actor-instance navigation through Recipe, Trace, and Control
|
|
12
12
|
- [inspection.md](./inspection.md) — Complete `inspect` target/view matrix, authorization boundaries, and diagnostic routes
|
|
13
13
|
- [tool-registry.md](./tool-registry.md) — Local `pi-actors` registry storage and `register_tool` adaptation
|
package/docs/async-runs.md
CHANGED
|
@@ -110,7 +110,9 @@ Statuses include `running`, `done`, `failed`, `exited`, `cancelled`, and `killed
|
|
|
110
110
|
|
|
111
111
|
Ambient observation detects root terminal transitions and explicit retained Trace attention. Terminal transitions reconcile before semantic attention. Canonical attention is an in-memory wake hint, not a durable queue: observers prime retained ids at startup, deliver each later retained unseen id once, and bound memory to the current retained set across compaction. Persist durable recovery state or an artifact before emitting attention; compaction may discard older hints and its marker makes that history loss explicit. Terminal follow-up delivery persists handled/failure evidence so reloads retry unhandled transitions without duplicating completed notifications.
|
|
112
112
|
|
|
113
|
-
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its containing top-level
|
|
113
|
+
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its complete containing top-level tree is terminal; descendant Pi sessions never schedule completion delivery. At an active completed `turn_end`, the root scheduler snapshots all ready trees into one owner-fenced immutable batch of at most 256 exact generations, preserves preceding boundary entries, and returns one visible `custom_message` plus `continue: true`. The full tool batch finishes before insertion, and the next model request receives fresh results within the same agent run without using native steering or follow-up queues. Aborted/error boundaries and pending native input defer insertion. Already-idle Pi still wakes through `agent_settled`, session recovery, or a short idle debounce. One batch renders one gray operator card and exposes at most 64 bounded parent-child rows with semantic outputs.
|
|
114
|
+
|
|
115
|
+
Boundary proposals remain pending until exact model context or bounded active-session evidence proves the entry was committed; returning a draft never marks member terminals handled. An idle send finalizes immediately on Pi acceptance, and durable queued phase also proves acceptance. Recovery retries a pending batch only after proving absence from the bounded active session chain; incomplete or conflicting evidence stays pending with diagnostics. Exact acceptance marks the member generations handled and releases the next epoch without depending on one later context callback. Pending send failures retain bounded retry evidence. Duplicate exact context envelopes collapse before presentation. See [Coordinator Delivery](./coordinator-delivery.md) for scheduling and recovery details.
|
|
114
116
|
|
|
115
117
|
Sequence, parallel, repeat, and imported branches are internal execution topology and never own branch-level turns. Each separately launched Run owns its own generation and terminal lifecycle; compatible singleton reuse is not a new launch. Explicit semantic attention may intentionally add a checkpoint turn; Runs marked silent and synchronously acknowledged stop outcomes suppress automatic projection. Large semantic results stay outside compact completion rows and remain available in structured details, execution captures, or artifacts.
|
|
116
118
|
|
|
@@ -8,10 +8,10 @@ Separate durable Run completion truth from the scheduling of model turns:
|
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
one Run generation -> one terminal record with inherited tree lineage
|
|
11
|
-
one ready completion forest per bounded epoch -> one root-coordinator
|
|
11
|
+
one ready completion forest per bounded epoch -> one root-coordinator model step
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Ordinary terminals accumulate across nested actor sessions
|
|
14
|
+
Ordinary terminals accumulate across nested actor sessions. Descendants wait for their complete containing top-level tree, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch. During active work, a public `turn_end` overlay supplies fresh results to the next model request after the full tool batch, without waiting for idle or using native steering. Already-idle Pi retains debounced follow-up wakeups. Explicit actor-authored urgent semantic checkpoints remain a separate native-steer delivery class.
|
|
15
15
|
|
|
16
16
|
## Non-Goals
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
|
|
|
27
27
|
- `attention: "notify"` remains visible UI status without a model turn.
|
|
28
28
|
- `attention: "followup"` retains its existing explicit semantic follow-up behavior.
|
|
29
29
|
- New `attention: "steer"` requests urgent semantic delivery at Pi's next safe assistant/tool boundary.
|
|
30
|
-
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees
|
|
30
|
+
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees into a model-step overlay or an already-idle follow-up, never one turn per Run or descendant session.
|
|
31
31
|
|
|
32
32
|
`command.done` remains non-projectable even if malformed or legacy Trace attaches any attention value.
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
|
|
|
37
37
|
- `runs-trace.ts` owns admission of the new `steer` attention value.
|
|
38
38
|
- `observability.ts` discovers terminal candidates and explicit semantic attention without deciding Pi delivery timing.
|
|
39
39
|
- A new `run-delivery.ts` domain owns the owner-scoped delivery journal, batch construction, bounds, phases, formatting, recovery, and acknowledgments.
|
|
40
|
-
- `run-ui-runtime.ts` owns idle detection, debounce, reconciliation, flushing, and stale-context containment.
|
|
40
|
+
- `run-ui-runtime.ts` owns active model-step boundaries, idle detection, debounce, reconciliation, flushing, and stale-context containment.
|
|
41
41
|
- `extension-runtime.ts` orders completion flushes before automatic Recipe review.
|
|
42
42
|
- `index.ts` remains a thin registration root and adds only the required Pi lifecycle adapter.
|
|
43
43
|
- `pi.ts` exposes narrow ports for batched follow-up and urgent steer delivery.
|
|
@@ -75,13 +75,13 @@ Completion batch shape:
|
|
|
75
75
|
|
|
76
76
|
Phases are monotonic:
|
|
77
77
|
|
|
78
|
-
1. `pending`: The exact member snapshot is durable but
|
|
78
|
+
1. `pending`: The exact member snapshot is durable but Pi acceptance is unproven. Returning a boundary draft does not advance this phase.
|
|
79
79
|
2. `queued`: Pi accepted a custom message carrying the exact batch or steer ID.
|
|
80
|
-
3. `presented`:
|
|
80
|
+
3. `presented`: Exact completion acceptance is proven, or a Pi `context` event observed the urgent-steer ID in messages being supplied to an LLM call.
|
|
81
81
|
|
|
82
82
|
Every journal mutation uses the canonical token-owned lock, expected-phase fencing, owner and generation validation, and atomic replacement. Repeated transitions are idempotent. Corrupt, oversized, foreign-owner, or stale-generation state fails closed with bounded diagnostics.
|
|
83
83
|
|
|
84
|
-
Completion and steer acknowledgments intentionally differ. Successful `sendMessage()` acceptance finalizes an ordinary completion batch immediately
|
|
84
|
+
Completion and steer acknowledgments intentionally differ. An active completion overlay remains pending until exact model context or bounded active-session evidence proves its committed entry. Successful idle `sendMessage()` acceptance finalizes an ordinary completion batch immediately; durable queued phase also proves acceptance. Both paths mark members through the existing terminal-handled authority, so a missed context callback cannot block every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after acceptance, the bounded delivery snapshot remains sufficient and the missing state write becomes a diagnostic rather than invalidating acceptance.
|
|
85
85
|
|
|
86
86
|
## Completion Collection
|
|
87
87
|
|
|
@@ -97,17 +97,20 @@ It excludes:
|
|
|
97
97
|
|
|
98
98
|
Candidates sort by terminal timestamp, then stable Run identity, then `run_instance_id`. Replacement generations with the same logical Run id remain distinct internal members.
|
|
99
99
|
|
|
100
|
-
While
|
|
100
|
+
While Pi is active, candidates remain durable until the next completed `turn_end`; watchers do not enqueue ordinary terminal follow-ups into the native message queue. The boundary itself reads current Run state, so a missed watcher cannot make the next step stale. A flush snapshots eligible candidates into one immutable batch. While acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
|
|
101
101
|
|
|
102
102
|
## Batch Flush
|
|
103
103
|
|
|
104
104
|
Flush one batch when:
|
|
105
105
|
|
|
106
|
-
1. `
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
106
|
+
1. A completed active `turn_end` reaches the exact root owner without an aborted signal or native pending input;
|
|
107
|
+
2. `agent_settled` fires for the still-active owner and `ctx.isIdle()` remains true;
|
|
108
|
+
3. Terminals arrive while Pi is already idle and survive the 100 ms default debounce window;
|
|
109
|
+
4. Session restoration discovers unhandled terminal generations or recoverable delivery state.
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
Active delivery returns `{ entries: [...event.entries, entry], continue: true }`, where `entry` is one `custom_message` with `customType: "pi-actors-run-batch"` and `display: true`. It preserves previous extensions' boundary entries and requests one next model step in the same agent run after the entire tool batch. Aborted/error boundaries, queued native input, and outstanding urgent steer defer the overlay. It never uses `sendMessage(..., { deliverAs: "steer" })` or a native follow-up queue for active completion delivery.
|
|
112
|
+
|
|
113
|
+
Already-idle delivery uses the same custom message with `deliverAs: "followUp"` and `triggerTurn: true` to wake Pi. Both paths render one gray operator card and supply the same tree-compressed prompt to the model. The bounded content retains the exact batch identity for session evidence and deduplication even though Pi omits private message details from model context. It includes:
|
|
111
114
|
|
|
112
115
|
- batch ID and completion window;
|
|
113
116
|
- counts by terminal status;
|
|
@@ -120,14 +123,14 @@ Completion member details remain redacted through existing terminal projection r
|
|
|
120
123
|
|
|
121
124
|
## Acceptance, Presentation, And Recovery
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
Returning boundary drafts is a proposal, not an SDK receipt. An active completion batch advances through queued acceptance to finalization only when exact model-bound `context` or a bounded active-session parent-chain read proves the committed entry. If another extension replaces or invalidates the proposed draft chain, the absent batch remains pending and can retry with the same identity. Idle delivery advances immediately after `sendMessage()` returns. Acceptance marks every still-present member generation terminal-handled and retains a bounded owner receipt. The `context` lifecycle adapter also deduplicates completion envelopes and handles exact urgent-steer presentation; presented steer writes `delivery.steer_presented` in the exact Run generation.
|
|
124
127
|
|
|
125
128
|
The generation-fenced Trace marker prevents a retained historical steer from replaying after bounded owner receipts rotate: suffix compaction cannot retain the older steer while discarding its newer presentation marker. Missing, archived, pruned, or replaced Run state needs no marker because it can no longer replay that original generation.
|
|
126
129
|
|
|
127
130
|
Recovery rules:
|
|
128
131
|
|
|
129
132
|
- Send failure: keep `pending`, record failure evidence, and retry.
|
|
130
|
-
-
|
|
133
|
+
- Pending batch after a boundary attempt or restart: inspect the bounded active Pi session chain for the exact batch ID and content. Proven presence finalizes; proven absence permits retry; incomplete or conflicting evidence stays pending with a diagnostic.
|
|
131
134
|
- Completion send accepted: finalize immediately and release the next epoch.
|
|
132
135
|
- Crash leaves a queued completion: queued itself proves `sendMessage()` acceptance, so finalize without resend or content reformatting.
|
|
133
136
|
- Presented urgent steer: never resend.
|
|
@@ -179,24 +182,23 @@ Another extension may start work during `agent_settled`; recheck `ctx.isIdle()`
|
|
|
179
182
|
|
|
180
183
|
Implementation is complete only when source and packed-extension tests prove:
|
|
181
184
|
|
|
182
|
-
1. Multiple Runs finishing during one
|
|
185
|
+
1. Multiple Runs finishing during active work produce one boundary entry and one next model request before idle, after all tools in the current batch finish; no native steering/follow-up enqueue occurs.
|
|
183
186
|
2. Idle completions inside the debounce window form one batch.
|
|
184
187
|
3. Completion/settled races project every generation exactly once.
|
|
185
188
|
4. Send failure and restart before queueing retry without a handled marker.
|
|
186
189
|
5. Restart in the narrow queue-acceptance/finalization boundary neither loses nor duplicates the batch.
|
|
187
|
-
6.
|
|
190
|
+
6. Boundary proposals do not acknowledge terminals early. Exact committed-entry evidence or idle send acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
|
|
188
191
|
7. Replacement generations sharing a Run id remain distinct.
|
|
189
192
|
8. Silent, stopped, cancelled, handled, and foreign-owner Runs remain excluded.
|
|
190
193
|
9. Legacy or malformed `command.done` attention, including `steer`, remains non-projectable.
|
|
191
194
|
10. Explicit steer reaches the next safe Pi boundary once and root terminal still batches later.
|
|
192
195
|
11. Overflow, corruption, journal backpressure, archive/prune races, and stale contexts fail safely.
|
|
193
196
|
12. Completion flushing precedes automatic Recipe review.
|
|
194
|
-
13. Pi 0.
|
|
197
|
+
13. Pi 1.0.0 is the minimum source and packed lifecycle baseline; coding-agent and TUI peers share that floor.
|
|
198
|
+
14. The packed extension runs through the public SDK with a synthetic provider, proving one batch reaches the next request before settlement for both a full tool batch and a tool-free final answer, while preserving preceding boundary entries.
|
|
195
199
|
|
|
196
200
|
Focused observability and delivery tests precede TypeScript/build/import checks. The acceptance checkpoint then runs full product validation, dependency audit, package dry-run, and ABCd context validation.
|
|
197
201
|
|
|
198
|
-
##
|
|
199
|
-
|
|
200
|
-
This is one minor release because durable batching, acceptance acknowledgment, lifecycle ordering, and explicit steer share one model-delivery invariant. Completion acceptance must release later epochs without depending on a context callback; steer must not ship before its durable presentation-deduplication path exists.
|
|
202
|
+
## Product Boundaries
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
Active overlays and idle follow-ups share the same batch identity, bounds, lineage, terminal authority, and acceptance journal. They differ only in the Pi scheduling port and its acceptance evidence. Urgent semantic steer remains independent; completion status never implies urgency. The extension has no dependency on Telegram and does not modify Telegram reply or preview ownership.
|
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
|
|
20
|
+
pi.on("turn_end", (event, ctx) => runtime.onTurnEnd(event, ctx));
|
|
20
21
|
pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
|
|
21
22
|
pi.on("context", async (event, ctx) => ({
|
|
22
23
|
messages: runtime.onContext(event.messages, ctx) as typeof event.messages,
|
package/lib/extension-runtime.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface ActorExtensionRuntime {
|
|
|
26
26
|
discoverResources(metaUrl: string): { skillPaths: string[] } | undefined;
|
|
27
27
|
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
28
28
|
onAgentSettled(ctx: Pi.ExtensionContext): void;
|
|
29
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
29
30
|
onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
30
31
|
onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
|
|
31
32
|
onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
|
|
@@ -146,6 +147,9 @@ export function createActorExtensionRuntime(
|
|
|
146
147
|
if (!activeRunOwnerId || getRunOwnerId(ctx) !== activeRunOwnerId) return;
|
|
147
148
|
if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
|
|
148
149
|
},
|
|
150
|
+
onTurnEnd(event, ctx) {
|
|
151
|
+
return runUiRuntime.onTurnEnd(event, ctx);
|
|
152
|
+
},
|
|
149
153
|
onContext(messages, ctx) {
|
|
150
154
|
return runUiRuntime.projectContext(messages, ctx);
|
|
151
155
|
},
|
package/lib/pi.ts
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
import type {
|
|
8
8
|
ExtensionAPI,
|
|
9
9
|
ExtensionContext,
|
|
10
|
+
CustomMessageEntryDraft,
|
|
11
|
+
TurnEndEvent,
|
|
12
|
+
TurnEndEventResult,
|
|
10
13
|
} from "@earendil-works/pi-coding-agent";
|
|
11
14
|
|
|
12
15
|
import * as SessionEvidence from "./session-evidence.ts";
|
|
13
16
|
|
|
14
|
-
export type { ExtensionAPI, ExtensionContext };
|
|
17
|
+
export type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult };
|
|
15
18
|
|
|
16
19
|
export const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
|
|
17
20
|
export const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
|
|
@@ -44,12 +47,12 @@ export function createNotificationSink(
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function
|
|
48
|
-
pi: ExtensionAPI,
|
|
50
|
+
export function createRunCompletionBatchEntry(
|
|
49
51
|
batchId: string,
|
|
50
52
|
content: string,
|
|
51
|
-
):
|
|
52
|
-
|
|
53
|
+
): CustomMessageEntryDraft {
|
|
54
|
+
return {
|
|
55
|
+
type: "custom_message",
|
|
53
56
|
customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
|
|
54
57
|
content,
|
|
55
58
|
display: true,
|
|
@@ -59,10 +62,16 @@ export function sendRunCompletionBatch(
|
|
|
59
62
|
kind: "completion_batch",
|
|
60
63
|
},
|
|
61
64
|
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sendRunCompletionBatch(
|
|
69
|
+
pi: ExtensionAPI,
|
|
70
|
+
batchId: string,
|
|
71
|
+
content: string,
|
|
72
|
+
): void {
|
|
73
|
+
const { type: _type, ...message } = createRunCompletionBatchEntry(batchId, content);
|
|
74
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export function sendRunSteer(
|
|
@@ -219,6 +228,21 @@ export function removeRunSteerFromContext(
|
|
|
219
228
|
return messages.filter((message) => steerMessage(message)?.steerId !== steerId);
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
export function inspectRunCompletionBatchSessionEvidence(
|
|
232
|
+
ctx: ExtensionContext,
|
|
233
|
+
input: { batchId: string; content: string },
|
|
234
|
+
): SessionEvidence.ActiveSessionEntryEvidence {
|
|
235
|
+
return SessionEvidence.inspectBoundedActiveSessionEntries({
|
|
236
|
+
getEntry: (id) => ctx.sessionManager.getEntry(id),
|
|
237
|
+
leaf: ctx.sessionManager.getLeafEntry(),
|
|
238
|
+
match: (entry) => {
|
|
239
|
+
const batch = completionBatchMessage(entry);
|
|
240
|
+
if (!batch || batch.batchId !== input.batchId) return undefined;
|
|
241
|
+
return batch.content === input.content ? "present" : "conflict";
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
222
246
|
export function inspectRunSteerSessionEvidence(
|
|
223
247
|
ctx: ExtensionContext,
|
|
224
248
|
input: { content: string; eventId: string; steerId: string },
|
package/lib/run-ui-runtime.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
|
15
15
|
export interface RunUiRuntime {
|
|
16
16
|
close(): void;
|
|
17
17
|
flushCompletionBatch(ctx: Pi.ExtensionContext): boolean;
|
|
18
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
18
19
|
projectContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
19
20
|
shutdown(
|
|
20
21
|
eventReason: string,
|
|
@@ -51,6 +52,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
51
52
|
const deliveryObservation = Observability.createRunUiObservationState();
|
|
52
53
|
const retirementAttempts = new Set<string>();
|
|
53
54
|
const steerDiagnostics = new Set<string>();
|
|
55
|
+
const completionDiagnostics = new Set<string>();
|
|
54
56
|
|
|
55
57
|
const close = (): void => {
|
|
56
58
|
running = false;
|
|
@@ -72,6 +74,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
72
74
|
deliveryTimeout = undefined;
|
|
73
75
|
recoverQueuedSteers = false;
|
|
74
76
|
steerDiagnostics.clear();
|
|
77
|
+
completionDiagnostics.clear();
|
|
75
78
|
if (animationInterval) clearInterval(animationInterval);
|
|
76
79
|
animationInterval = undefined;
|
|
77
80
|
};
|
|
@@ -242,6 +245,53 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
242
245
|
});
|
|
243
246
|
return true;
|
|
244
247
|
};
|
|
248
|
+
const acceptCompletionBatch = (ownerId: string, batchId: string): boolean => {
|
|
249
|
+
const input = { batchId, ownerId, tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir };
|
|
250
|
+
if (!RunDelivery.markRunCompletionBatchQueued(input)) return false;
|
|
251
|
+
if (!RunDelivery.markRunCompletionBatchPresented(input)) return false;
|
|
252
|
+
const accepted = journal(ownerId).completion_batch;
|
|
253
|
+
return accepted ? finishPresentedBatch(ownerId, accepted) : false;
|
|
254
|
+
};
|
|
255
|
+
const reconcileCompletionBatch = (ctx: Pi.ExtensionContext, ownerId: string): boolean => {
|
|
256
|
+
const batch = journal(ownerId).completion_batch;
|
|
257
|
+
if (!batch) return false;
|
|
258
|
+
if (batch.phase === "pending") {
|
|
259
|
+
// Returning boundary drafts is not acceptance: prove the exact entry before acknowledging it.
|
|
260
|
+
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, {
|
|
261
|
+
batchId: batch.batch_id,
|
|
262
|
+
content: RunDelivery.formatRunCompletionBatchMessage(batch),
|
|
263
|
+
});
|
|
264
|
+
if (evidence.status === "absent") return false;
|
|
265
|
+
if (evidence.status !== "present") {
|
|
266
|
+
const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
267
|
+
if (!completionDiagnostics.has(key)) {
|
|
268
|
+
completionDiagnostics.add(key);
|
|
269
|
+
ctx.ui.notify(
|
|
270
|
+
`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains pending.`,
|
|
271
|
+
"warning",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return !acceptCompletionBatch(ownerId, batch.batch_id);
|
|
278
|
+
};
|
|
279
|
+
const pendingCompletionBatch = (ownerId: string): RunDelivery.RunCompletionBatch | undefined => {
|
|
280
|
+
const existing = journal(ownerId).completion_batch;
|
|
281
|
+
if (existing) return existing;
|
|
282
|
+
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
283
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
284
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
285
|
+
return journal(ownerId).completion_batch;
|
|
286
|
+
};
|
|
287
|
+
const isCurrentOwner = (ctx: Pi.ExtensionContext): boolean => {
|
|
288
|
+
if (!running || !activeOwnerId) return false;
|
|
289
|
+
try {
|
|
290
|
+
return Pi.getSessionId(ctx) === activeOwnerId;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
245
295
|
const isIdle = (ctx: Pi.ExtensionContext): boolean =>
|
|
246
296
|
typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
247
297
|
const isDeliveryRoot = (ownerId: string): boolean => {
|
|
@@ -422,48 +472,14 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
422
472
|
|
|
423
473
|
flushCompletionBatch = (ctx: Pi.ExtensionContext): boolean => {
|
|
424
474
|
const ownerId = activeOwnerId;
|
|
425
|
-
if (!
|
|
426
|
-
try {
|
|
427
|
-
if (Pi.getSessionId(ctx) !== ownerId) return false;
|
|
428
|
-
} catch {
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
475
|
+
if (!ownerId || !isCurrentOwner(ctx)) return false;
|
|
431
476
|
if (flushSteers(ctx, ownerId)) return true;
|
|
432
477
|
if (!isDeliveryRoot(ownerId)) return false;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const snapshot = Observability.readRunDeliverySnapshot(
|
|
436
|
-
deliveryObservation,
|
|
437
|
-
ownerId,
|
|
438
|
-
);
|
|
439
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
440
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
441
|
-
batch = journal(ownerId).completion_batch;
|
|
442
|
-
}
|
|
478
|
+
if (reconcileCompletionBatch(ctx, ownerId)) return true;
|
|
479
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
443
480
|
if (!batch) return false;
|
|
444
|
-
if (batch.phase === "presented") {
|
|
445
|
-
finishPresentedBatch(ownerId, batch);
|
|
446
|
-
const snapshot = Observability.readRunDeliverySnapshot(
|
|
447
|
-
deliveryObservation,
|
|
448
|
-
ownerId,
|
|
449
|
-
);
|
|
450
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
451
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
452
|
-
batch = journal(ownerId).completion_batch;
|
|
453
|
-
if (!batch) return false;
|
|
454
|
-
}
|
|
455
481
|
if (!isIdle(ctx)) return true;
|
|
456
482
|
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
457
|
-
if (batch.phase === "queued") {
|
|
458
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
459
|
-
batchId: batch.batch_id,
|
|
460
|
-
ownerId,
|
|
461
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
462
|
-
})) return true;
|
|
463
|
-
const accepted = journal(ownerId).completion_batch;
|
|
464
|
-
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
465
|
-
return flushCompletionBatch(ctx);
|
|
466
|
-
}
|
|
467
483
|
if (!isIdle(ctx)) return true;
|
|
468
484
|
try {
|
|
469
485
|
Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
|
|
@@ -476,22 +492,9 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
476
492
|
});
|
|
477
493
|
throw error;
|
|
478
494
|
}
|
|
479
|
-
if (!
|
|
480
|
-
batchId: batch.batch_id,
|
|
481
|
-
ownerId,
|
|
482
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
483
|
-
})) {
|
|
484
|
-
throw new Error("Completion batch changed before queue acknowledgment");
|
|
485
|
-
}
|
|
486
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
487
|
-
batchId: batch.batch_id,
|
|
488
|
-
ownerId,
|
|
489
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
490
|
-
})) {
|
|
495
|
+
if (!acceptCompletionBatch(ownerId, batch.batch_id)) {
|
|
491
496
|
throw new Error("Completion batch changed before durable acceptance");
|
|
492
497
|
}
|
|
493
|
-
const accepted = journal(ownerId).completion_batch;
|
|
494
|
-
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
495
498
|
ctx.ui.notify(
|
|
496
499
|
`Actor completions ready: ${batch.members.length}`,
|
|
497
500
|
"info",
|
|
@@ -502,14 +505,25 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
502
505
|
return {
|
|
503
506
|
close,
|
|
504
507
|
flushCompletionBatch,
|
|
508
|
+
onTurnEnd(event, ctx) {
|
|
509
|
+
const ownerId = activeOwnerId;
|
|
510
|
+
if (
|
|
511
|
+
!ownerId || !isCurrentOwner(ctx) || !isDeliveryRoot(ownerId) ||
|
|
512
|
+
event.outcome !== "completed" || isIdle(ctx) || ctx.signal?.aborted ||
|
|
513
|
+
ctx.hasPendingMessages?.() || event.context.pendingMessages.length > 0
|
|
514
|
+
) return undefined;
|
|
515
|
+
if (flushSteers(ctx, ownerId) || reconcileCompletionBatch(ctx, ownerId)) return undefined;
|
|
516
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
517
|
+
if (!batch) return undefined;
|
|
518
|
+
const entry = Pi.createRunCompletionBatchEntry(
|
|
519
|
+
batch.batch_id,
|
|
520
|
+
RunDelivery.formatRunCompletionBatchMessage(batch),
|
|
521
|
+
);
|
|
522
|
+
return { entries: [...event.entries, entry], continue: true };
|
|
523
|
+
},
|
|
505
524
|
projectContext(messages, ctx) {
|
|
506
525
|
const ownerId = activeOwnerId;
|
|
507
|
-
if (!
|
|
508
|
-
try {
|
|
509
|
-
if (Pi.getSessionId(ctx) !== ownerId) return messages;
|
|
510
|
-
} catch {
|
|
511
|
-
return messages;
|
|
512
|
-
}
|
|
526
|
+
if (!ownerId || !isCurrentOwner(ctx)) return messages;
|
|
513
527
|
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
514
528
|
let contextMessages = steerContext.messages;
|
|
515
529
|
for (const steer of journal(ownerId).steers) {
|
|
@@ -549,18 +563,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
549
563
|
batch.batch_id,
|
|
550
564
|
);
|
|
551
565
|
}
|
|
552
|
-
|
|
553
|
-
batchId: batch.batch_id,
|
|
554
|
-
ownerId,
|
|
555
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
556
|
-
})) return projected.messages;
|
|
557
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
558
|
-
batchId: batch.batch_id,
|
|
559
|
-
ownerId,
|
|
560
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
561
|
-
})) return projected.messages;
|
|
562
|
-
const presented = journal(ownerId).completion_batch;
|
|
563
|
-
if (presented) finishPresentedBatch(ownerId, presented);
|
|
566
|
+
acceptCompletionBatch(ownerId, batch.batch_id);
|
|
564
567
|
return projected.messages;
|
|
565
568
|
},
|
|
566
569
|
shutdown(eventReason, ownerId, ctx) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-actors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local Actor Kernel for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"prepack": "npm run build"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
|
+
"LICENSE",
|
|
41
42
|
"index.ts",
|
|
42
43
|
"dist",
|
|
43
44
|
"lib",
|
|
@@ -61,8 +62,8 @@
|
|
|
61
62
|
"image": "https://raw.githubusercontent.com/llblab/pi-actors/main/banner.jpg"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
65
|
-
"@earendil-works/pi-tui": ">=0.
|
|
65
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
66
|
+
"@earendil-works/pi-tui": ">=1.0.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@types/node": "latest",
|
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 the root coordinator's
|
|
110
|
+
2. Retain the returned `run:<id>` and normally wait for the root coordinator's completion batch instead of polling. While Pi is active, ready results arrive together at the next safe model-step boundary after the full tool batch; already-idle Pi wakes through a debounced follow-up. Nested actor completions wait for their complete containing top-level tree and arrive in that one 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.
|
|
@@ -14,7 +14,7 @@ A rare Skill Recipe may declare `singleton: true`. Do not pass `as`: the runtime
|
|
|
14
14
|
|
|
15
15
|
## Observe
|
|
16
16
|
|
|
17
|
-
Normally wait for the
|
|
17
|
+
Normally wait for the completion batch: ready trees reach an active root coordinator at the next safe model-step boundary after the full tool batch, or wake an already-idle coordinator through a debounced follow-up. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
inspect target=run:<id> view=recipe
|