@llblab/pi-actors 0.52.1 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/dist/index.js +4 -1
- package/dist/lib/async-runs.d.ts +3 -0
- package/dist/lib/async-runs.js +14 -1
- package/dist/lib/command-templates.js +45 -3
- package/dist/lib/extension-runtime.js +1 -1
- package/dist/lib/observability.d.ts +16 -3
- package/dist/lib/observability.js +92 -7
- package/dist/lib/paths.d.ts +6 -0
- package/dist/lib/paths.js +19 -1
- package/dist/lib/pi.d.ts +0 -1
- package/dist/lib/pi.js +15 -24
- package/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/dist/lib/run-delivery-lineage.js +44 -0
- package/dist/lib/run-delivery.d.ts +4 -0
- package/dist/lib/run-delivery.js +102 -4
- package/dist/lib/run-ui-runtime.js +58 -39
- package/dist/lib/runtime.js +14 -6
- package/dist/scripts/build-dist.mjs +94 -30
- package/dist/skills/actors/SKILL.md +1 -1
- package/docs/async-runs.md +1 -1
- package/docs/coordinator-delivery.md +18 -23
- package/index.ts +6 -3
- package/lib/async-runs.ts +18 -1
- package/lib/command-templates.ts +41 -3
- package/lib/extension-runtime.ts +1 -1
- package/lib/observability.ts +119 -5
- package/lib/paths.ts +27 -1
- package/lib/pi.ts +15 -28
- package/lib/run-delivery-lineage.ts +68 -0
- package/lib/run-delivery.ts +120 -4
- package/lib/run-ui-runtime.ts +69 -44
- package/lib/runtime.ts +17 -6
- package/package.json +3 -8
- package/scripts/build-dist.mjs +94 -30
- package/skills/actors/SKILL.md +1 -1
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Coordinator Delivery Scheduler
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: Implemented coordinator-delivery contract.
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
7
7
|
Separate durable Run completion truth from the scheduling of model turns:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
one Run generation -> one
|
|
11
|
-
one
|
|
10
|
+
one Run generation -> one terminal record with inherited tree lineage
|
|
11
|
+
one ready completion forest per bounded epoch -> one root-coordinator turn
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Ordinary
|
|
14
|
+
Ordinary terminals accumulate across nested actor sessions while Pi is active. Descendants wait for their containing top-level Run, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch after Pi settles. Only an explicitly actor-authored urgent semantic checkpoint may steer an active agent loop.
|
|
15
15
|
|
|
16
16
|
## Non-Goals
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ Ordinary root terminals accumulate while Pi is active and reach the coordinator
|
|
|
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
|
-
-
|
|
30
|
+
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees instead of sending one follow-up 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
|
|
|
@@ -81,11 +81,11 @@ Phases are monotonic:
|
|
|
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
|
-
|
|
84
|
+
Completion and steer acknowledgments intentionally differ. Successful `sendMessage()` acceptance finalizes an ordinary completion batch immediately and marks its members through the existing terminal-handled authority; this prevents a missed context callback from blocking every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after queueing, 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
|
|
|
88
|
-
Reconciliation admits unhandled
|
|
88
|
+
Reconciliation admits unhandled terminal generations with status `done`, `failed`, `killed`, or `exited` that belong to the active root delivery owner. A descendant becomes eligible only when its exact parent chain reaches a terminal top-level Run.
|
|
89
89
|
|
|
90
90
|
It excludes:
|
|
91
91
|
|
|
@@ -97,7 +97,7 @@ 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 `ctx.isIdle()` is false, candidates remain durable in their Run state and no terminal follow-up is sent. A flush snapshots eligible candidates into one immutable batch. While
|
|
100
|
+
While `ctx.isIdle()` is false, candidates remain durable in their Run state and no terminal follow-up is sent. A flush snapshots eligible candidates into one immutable batch. While transport acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
|
|
101
101
|
|
|
102
102
|
## Batch Flush
|
|
103
103
|
|
|
@@ -107,25 +107,20 @@ Flush one batch when:
|
|
|
107
107
|
2. terminals arrive while Pi is already idle and survive one short debounce window;
|
|
108
108
|
3. session restoration discovers unhandled terminal generations or recoverable queued delivery state.
|
|
109
109
|
|
|
110
|
-
The
|
|
110
|
+
The custom message uses `customType: "pi-actors-run-batch"`, `display: true`, `deliverAs: "followUp"`, and `triggerTurn: true`. It restores the visible gray completion card for the operator while supplying the same single 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
111
|
|
|
112
112
|
- batch ID and completion window;
|
|
113
113
|
- counts by terminal status;
|
|
114
|
-
- stable Run
|
|
114
|
+
- stable parent-child Run rows with status, compact semantic output, and bounded artifacts;
|
|
115
115
|
- explicit overflow evidence and the canonical runtime Inspect route.
|
|
116
116
|
|
|
117
117
|
The journal may retain at most 256 members and 1 MiB. Model-facing content lists at most 64 exact rows within the centralized model-output bound. Additional members remain represented by exact status counts and supported Inspect guidance. More than 256 unhandled generations form a later batch rather than being discarded.
|
|
118
118
|
|
|
119
119
|
Completion member details remain redacted through existing terminal projection rules: no raw model policy, secrets, private Recipe paths, or machine-local source paths enter the message.
|
|
120
120
|
|
|
121
|
-
## Presentation
|
|
121
|
+
## Acceptance, Presentation, And Recovery
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
1. moves the envelope to `presented`;
|
|
126
|
-
2. marks every still-present member generation terminal-handled;
|
|
127
|
-
3. records a non-attention `delivery.steer_presented` marker in the exact Run generation for a presented steer;
|
|
128
|
-
4. retains a bounded owner receipt sufficient for near-term deduplication and diagnostics.
|
|
123
|
+
Ordinary completion delivery atomically advances through queued acceptance to finalization immediately after `sendMessage()` returns, marks every still-present member generation terminal-handled, and retains a bounded owner receipt. A `context` lifecycle adapter remains for exact urgent-steer presentation and harmless completion deduplication; presented steer writes `delivery.steer_presented` in the exact Run generation.
|
|
129
124
|
|
|
130
125
|
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.
|
|
131
126
|
|
|
@@ -133,9 +128,9 @@ Recovery rules:
|
|
|
133
128
|
|
|
134
129
|
- Send failure: keep `pending`, record failure evidence, and retry.
|
|
135
130
|
- Crash after queueing: inspect existing owned Pi session evidence for the exact custom message ID.
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
- Presented
|
|
131
|
+
- Completion send accepted: finalize immediately and release the next epoch.
|
|
132
|
+
- Crash leaves a queued completion: queued itself proves `sendMessage()` acceptance, so finalize without resend or content reformatting.
|
|
133
|
+
- Presented urgent steer: never resend.
|
|
139
134
|
- Session or context replacement: close timers and callbacks; never deliver through stale context.
|
|
140
135
|
- Owner mismatch: do not inspect, acknowledge, or deliver the envelope.
|
|
141
136
|
|
|
@@ -188,8 +183,8 @@ Implementation is complete only when source and packed-extension tests prove:
|
|
|
188
183
|
2. Idle completions inside the debounce window form one batch.
|
|
189
184
|
3. Completion/settled races project every generation exactly once.
|
|
190
185
|
4. Send failure and restart before queueing retry without a handled marker.
|
|
191
|
-
5. Restart
|
|
192
|
-
6.
|
|
186
|
+
5. Restart in the narrow queue-acceptance/finalization boundary neither loses nor duplicates the batch.
|
|
187
|
+
6. Successful completion acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
|
|
193
188
|
7. Replacement generations sharing a Run id remain distinct.
|
|
194
189
|
8. Silent, stopped, cancelled, handled, and foreign-owner Runs remain excluded.
|
|
195
190
|
9. Legacy or malformed `command.done` attention, including `steer`, remains non-projectable.
|
|
@@ -202,6 +197,6 @@ Focused observability and delivery tests precede TypeScript/build/import checks.
|
|
|
202
197
|
|
|
203
198
|
## Rollout
|
|
204
199
|
|
|
205
|
-
This is one minor release because durable batching,
|
|
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.
|
|
206
201
|
|
|
207
202
|
Update README and Run documentation only when implementation establishes the new public behavior. Move the accepted outcome from BACKLOG to CHANGELOG only after complete validation.
|
package/index.ts
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
import * as ExtensionRuntime from "./lib/extension-runtime.ts";
|
|
8
8
|
import * as InspectorCommand from "./lib/inspector-command.ts";
|
|
9
|
+
import * as Paths from "./lib/paths.ts";
|
|
9
10
|
import * as Pi from "./lib/pi.ts";
|
|
10
11
|
|
|
11
12
|
export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
12
13
|
const runtime = ExtensionRuntime.createActorExtensionRuntime(pi);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if (Paths.isRawExtensionCheckout(import.meta.url)) {
|
|
15
|
+
pi.on("resources_discover", async () =>
|
|
16
|
+
runtime.discoverResources(import.meta.url),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
16
19
|
pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
|
|
17
20
|
pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
|
|
18
21
|
pi.on("context", async (event, ctx) => ({
|
package/lib/async-runs.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CurrentPolicyProvenance,
|
|
33
33
|
} from "./model-context.ts";
|
|
34
34
|
import * as Paths from "./paths.ts";
|
|
35
|
+
import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
35
36
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
36
37
|
import * as RecipesUsage from "./recipes-usage.ts";
|
|
37
38
|
import * as Schema from "./schema.ts";
|
|
@@ -186,6 +187,8 @@ export interface AsyncRunMeta {
|
|
|
186
187
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
187
188
|
control?: string[];
|
|
188
189
|
control_endpoint?: AsyncRunControlEndpoint;
|
|
190
|
+
delivery_owner_id?: string;
|
|
191
|
+
delivery_parent?: RunDeliveryLineage.RunDeliveryParent;
|
|
189
192
|
model_policy?: CurrentPolicyProvenance;
|
|
190
193
|
notification_policy?: "normal" | "silent";
|
|
191
194
|
process_identity?: RunProcessIdentity;
|
|
@@ -666,6 +669,10 @@ export function startRun(
|
|
|
666
669
|
startParams.transport_context,
|
|
667
670
|
);
|
|
668
671
|
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
672
|
+
const runInstanceId = randomUUID();
|
|
673
|
+
const deliveryLineage = RunDeliveryLineage.inheritedRunDeliveryLineage(
|
|
674
|
+
startParams.ownerId,
|
|
675
|
+
);
|
|
669
676
|
const meta: AsyncRunMeta = {
|
|
670
677
|
argv: [process.execPath, ...argv],
|
|
671
678
|
createdAt: new Date().toISOString(),
|
|
@@ -683,7 +690,7 @@ export function startRun(
|
|
|
683
690
|
...(recipe ? { recipe } : {}),
|
|
684
691
|
...(recipeFile ? { recipe_file: recipeFile } : {}),
|
|
685
692
|
run,
|
|
686
|
-
run_instance_id:
|
|
693
|
+
run_instance_id: runInstanceId,
|
|
687
694
|
state_dir: stateDir,
|
|
688
695
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
689
696
|
status: "running",
|
|
@@ -696,6 +703,7 @@ export function startRun(
|
|
|
696
703
|
...(startParams.control_endpoint
|
|
697
704
|
? { control_endpoint: startParams.control_endpoint }
|
|
698
705
|
: {}),
|
|
706
|
+
...(deliveryLineage ?? {}),
|
|
699
707
|
...(startParams.notification_policy === "silent"
|
|
700
708
|
? { notification_policy: "silent" as const }
|
|
701
709
|
: {}),
|
|
@@ -726,6 +734,15 @@ export function startRun(
|
|
|
726
734
|
const child = spawn(process.execPath, argv, {
|
|
727
735
|
cwd,
|
|
728
736
|
detached: true,
|
|
737
|
+
...(deliveryLineage
|
|
738
|
+
? {
|
|
739
|
+
env: RunDeliveryLineage.runDeliveryChildEnv(deliveryLineage, {
|
|
740
|
+
run,
|
|
741
|
+
run_instance_id: runInstanceId,
|
|
742
|
+
state_dir: stateDir,
|
|
743
|
+
}),
|
|
744
|
+
}
|
|
745
|
+
: {}),
|
|
729
746
|
stdio: ["ignore", outFd, errFd],
|
|
730
747
|
});
|
|
731
748
|
closeSync(outFd);
|
package/lib/command-templates.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { accessSync, appendFileSync, constants, mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { basename, delimiter, extname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
@@ -1010,12 +1010,50 @@ function execCommandTemplateOnce(
|
|
|
1010
1010
|
let settled = false;
|
|
1011
1011
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
1012
1012
|
let killTimeoutId: NodeJS.Timeout | undefined;
|
|
1013
|
+
const signalProcessTree = (signal: NodeJS.Signals): void => {
|
|
1014
|
+
if (proc.pid === undefined) return;
|
|
1015
|
+
if (process.platform === "win32") {
|
|
1016
|
+
spawnSync("taskkill", [
|
|
1017
|
+
"/PID",
|
|
1018
|
+
String(proc.pid),
|
|
1019
|
+
"/T",
|
|
1020
|
+
...(signal === "SIGKILL" ? ["/F"] : []),
|
|
1021
|
+
]);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const snapshot = spawnSync("ps", ["-eo", "pid=,ppid="], { encoding: "utf8" });
|
|
1025
|
+
const children = new Map<number, number[]>();
|
|
1026
|
+
for (const line of snapshot.stdout?.split(/\r?\n/u) ?? []) {
|
|
1027
|
+
const [pidText, parentText] = line.trim().split(/\s+/u);
|
|
1028
|
+
const pid = Number(pidText);
|
|
1029
|
+
const parent = Number(parentText);
|
|
1030
|
+
if (!Number.isSafeInteger(pid) || !Number.isSafeInteger(parent)) continue;
|
|
1031
|
+
const siblings = children.get(parent) ?? [];
|
|
1032
|
+
siblings.push(pid);
|
|
1033
|
+
children.set(parent, siblings);
|
|
1034
|
+
}
|
|
1035
|
+
const descendants: number[] = [];
|
|
1036
|
+
const collect = (parent: number): void => {
|
|
1037
|
+
for (const child of children.get(parent) ?? []) {
|
|
1038
|
+
collect(child);
|
|
1039
|
+
descendants.push(child);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
collect(proc.pid);
|
|
1043
|
+
for (const pid of [...descendants, proc.pid]) {
|
|
1044
|
+
try {
|
|
1045
|
+
process.kill(pid, signal);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1013
1051
|
const killProcess = (): void => {
|
|
1014
1052
|
if (killed) return;
|
|
1015
1053
|
killed = true;
|
|
1016
|
-
|
|
1054
|
+
signalProcessTree("SIGTERM");
|
|
1017
1055
|
killTimeoutId = setTimeout(() => {
|
|
1018
|
-
if (!settled)
|
|
1056
|
+
if (!settled) signalProcessTree("SIGKILL");
|
|
1019
1057
|
}, options.killGrace ?? 5000);
|
|
1020
1058
|
};
|
|
1021
1059
|
const settle = (code: number): void => {
|
package/lib/extension-runtime.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function createActorExtensionRuntime(
|
|
|
143
143
|
},
|
|
144
144
|
getRunOwnerId,
|
|
145
145
|
onAgentSettled(ctx) {
|
|
146
|
-
if (
|
|
146
|
+
if (!activeRunOwnerId || getRunOwnerId(ctx) !== activeRunOwnerId) return;
|
|
147
147
|
if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
|
|
148
148
|
},
|
|
149
149
|
onContext(messages, ctx) {
|
package/lib/observability.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface RunObservation {
|
|
|
43
43
|
activeSubagents?: number;
|
|
44
44
|
completed?: number;
|
|
45
45
|
descendantSubagents?: number;
|
|
46
|
+
deliveryOwnerId?: string;
|
|
47
|
+
deliveryParent?: {
|
|
48
|
+
run: string;
|
|
49
|
+
run_instance_id: string;
|
|
50
|
+
state_dir: string;
|
|
51
|
+
};
|
|
46
52
|
failures?: number;
|
|
47
53
|
ownerId?: string;
|
|
48
54
|
artifacts?: Record<string, string>;
|
|
@@ -136,6 +142,21 @@ export function readRunUiSnapshot(
|
|
|
136
142
|
};
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/** Observe every Run generation routed to one root coordinator. */
|
|
146
|
+
export function readRunDeliverySnapshot(
|
|
147
|
+
state: RunUiObservationState,
|
|
148
|
+
deliveryOwnerId: string,
|
|
149
|
+
stateRoot?: string,
|
|
150
|
+
): RunUiSnapshot {
|
|
151
|
+
const summary = summarizeRuns(stateRoot, undefined, deliveryOwnerId);
|
|
152
|
+
return {
|
|
153
|
+
attentionEvents: [],
|
|
154
|
+
status: undefined,
|
|
155
|
+
summary,
|
|
156
|
+
transitions: detectRunTransitions(state.observed, summary),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
export function pruneRunUiObservationState(
|
|
140
161
|
state: RunUiObservationState,
|
|
141
162
|
snapshot: Pick<RunUiSnapshot, "summary" | "transitions">,
|
|
@@ -383,6 +404,11 @@ export interface RunRetirementExecutorOptions {
|
|
|
383
404
|
}
|
|
384
405
|
|
|
385
406
|
export interface RunTransition {
|
|
407
|
+
deliveryParent?: {
|
|
408
|
+
run: string;
|
|
409
|
+
run_instance_id: string;
|
|
410
|
+
state_dir: string;
|
|
411
|
+
};
|
|
386
412
|
from: RunObservedStatus;
|
|
387
413
|
run: string;
|
|
388
414
|
runInstanceId?: string;
|
|
@@ -584,6 +610,23 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
584
610
|
failures: Array.isArray(progress.failures)
|
|
585
611
|
? progress.failures.length
|
|
586
612
|
: undefined,
|
|
613
|
+
...(typeof status.delivery_owner_id === "string"
|
|
614
|
+
? { deliveryOwnerId: status.delivery_owner_id }
|
|
615
|
+
: {}),
|
|
616
|
+
...(status.delivery_parent &&
|
|
617
|
+
typeof status.delivery_parent === "object" &&
|
|
618
|
+
!Array.isArray(status.delivery_parent) &&
|
|
619
|
+
typeof (status.delivery_parent as Record<string, unknown>).run === "string" &&
|
|
620
|
+
typeof (status.delivery_parent as Record<string, unknown>).run_instance_id === "string" &&
|
|
621
|
+
typeof (status.delivery_parent as Record<string, unknown>).state_dir === "string"
|
|
622
|
+
? {
|
|
623
|
+
deliveryParent: status.delivery_parent as {
|
|
624
|
+
run: string;
|
|
625
|
+
run_instance_id: string;
|
|
626
|
+
state_dir: string;
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
587
630
|
...(typeof status.ownerId === "string"
|
|
588
631
|
? { ownerId: status.ownerId }
|
|
589
632
|
: {}),
|
|
@@ -630,6 +673,7 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
630
673
|
export function summarizeRuns(
|
|
631
674
|
stateRoot = Paths.getRunStateRoot(),
|
|
632
675
|
ownerId?: string,
|
|
676
|
+
deliveryOwnerId?: string,
|
|
633
677
|
): RunSummary {
|
|
634
678
|
if (!existsSync(stateRoot)) {
|
|
635
679
|
return {
|
|
@@ -650,7 +694,10 @@ export function summarizeRuns(
|
|
|
650
694
|
)
|
|
651
695
|
.map((stateDir) => observeRun(stateDir))
|
|
652
696
|
.filter((run): run is RunObservation => Boolean(run))
|
|
653
|
-
.filter((run) =>
|
|
697
|
+
.filter((run) => deliveryOwnerId !== undefined
|
|
698
|
+
? run.deliveryOwnerId === deliveryOwnerId ||
|
|
699
|
+
(run.deliveryOwnerId === undefined && run.ownerId === deliveryOwnerId)
|
|
700
|
+
: ownerId === undefined || run.ownerId === ownerId)
|
|
654
701
|
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
655
702
|
const processSubagentsByRun = countRunningSubagentsByRun(stateRoot, ownerId);
|
|
656
703
|
const runsWithDescendants = runs.map((run) => {
|
|
@@ -1005,6 +1052,7 @@ export function detectRunTransitions(
|
|
|
1005
1052
|
transitions.push({
|
|
1006
1053
|
from: old ?? "running",
|
|
1007
1054
|
run: run.run,
|
|
1055
|
+
...(run.deliveryParent ? { deliveryParent: run.deliveryParent } : {}),
|
|
1008
1056
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
1009
1057
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
1010
1058
|
...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
|
|
@@ -1282,11 +1330,57 @@ export function shouldNotifyRunTransition(transition: RunTransition): boolean {
|
|
|
1282
1330
|
);
|
|
1283
1331
|
}
|
|
1284
1332
|
|
|
1285
|
-
|
|
1333
|
+
function deliveryGenerationIdentity(input: {
|
|
1334
|
+
runInstanceId?: string;
|
|
1335
|
+
stateDir?: string;
|
|
1336
|
+
}): string | undefined {
|
|
1337
|
+
return input.stateDir && input.runInstanceId
|
|
1338
|
+
? `${input.stateDir}\0${input.runInstanceId}`
|
|
1339
|
+
: undefined;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function deliveryTreePosition(
|
|
1343
|
+
input: {
|
|
1344
|
+
deliveryParent?: RunObservation["deliveryParent"];
|
|
1345
|
+
runInstanceId?: string;
|
|
1346
|
+
stateDir?: string;
|
|
1347
|
+
},
|
|
1348
|
+
runsByIdentity: Map<string, RunObservation>,
|
|
1349
|
+
): { depth: number; rootIdentity: string } | undefined {
|
|
1350
|
+
const ownIdentity = deliveryGenerationIdentity(input);
|
|
1351
|
+
if (!ownIdentity) return undefined;
|
|
1352
|
+
let parent = input.deliveryParent;
|
|
1353
|
+
let depth = 0;
|
|
1354
|
+
let rootIdentity = ownIdentity;
|
|
1355
|
+
const visited = new Set([ownIdentity]);
|
|
1356
|
+
while (parent) {
|
|
1357
|
+
const identity = `${parent.state_dir}\0${parent.run_instance_id}`;
|
|
1358
|
+
if (visited.has(identity)) return undefined;
|
|
1359
|
+
visited.add(identity);
|
|
1360
|
+
const ancestor = runsByIdentity.get(identity);
|
|
1361
|
+
if (!ancestor || ancestor.notificationPolicy === "silent") return undefined;
|
|
1362
|
+
depth += 1;
|
|
1363
|
+
rootIdentity = identity;
|
|
1364
|
+
parent = ancestor.deliveryParent;
|
|
1365
|
+
}
|
|
1366
|
+
return { depth, rootIdentity };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** Build exact immutable members only after each containing root Run is terminal. */
|
|
1286
1370
|
export function collectRunCompletionBatchMembers(
|
|
1287
1371
|
transitions: RunTransition[],
|
|
1372
|
+
deliveryRuns: RunObservation[] = [],
|
|
1288
1373
|
): RunCompletionBatchMember[] {
|
|
1289
|
-
|
|
1374
|
+
const runsByIdentity = new Map(deliveryRuns.flatMap((run) => {
|
|
1375
|
+
const identity = deliveryGenerationIdentity(run);
|
|
1376
|
+
return identity ? [[identity, run] as const] : [];
|
|
1377
|
+
}));
|
|
1378
|
+
const incompleteRoots = new Set(deliveryRuns.flatMap((run) => {
|
|
1379
|
+
if (TERMINAL.has(run.status)) return [];
|
|
1380
|
+
const position = deliveryTreePosition(run, runsByIdentity);
|
|
1381
|
+
return position ? [position.rootIdentity] : [];
|
|
1382
|
+
}));
|
|
1383
|
+
const admitted = transitions.flatMap((transition) => {
|
|
1290
1384
|
if (
|
|
1291
1385
|
!shouldNotifyRunTransition(transition) ||
|
|
1292
1386
|
!transition.stateDir ||
|
|
@@ -1294,6 +1388,9 @@ export function collectRunCompletionBatchMembers(
|
|
|
1294
1388
|
!transition.terminalAt ||
|
|
1295
1389
|
Number.isNaN(Date.parse(transition.terminalAt))
|
|
1296
1390
|
) return [];
|
|
1391
|
+
const position = deliveryTreePosition(transition, runsByIdentity);
|
|
1392
|
+
if (!position || incompleteRoots.has(position.rootIdentity)) return [];
|
|
1393
|
+
const { depth } = position;
|
|
1297
1394
|
const artifactEntries = Object.entries(transition.artifacts ?? {})
|
|
1298
1395
|
.filter((entry): entry is [string, string] =>
|
|
1299
1396
|
typeof entry[1] === "string" && Boolean(entry[1]))
|
|
@@ -1304,6 +1401,20 @@ export function collectRunCompletionBatchMembers(
|
|
|
1304
1401
|
...(artifactEntries.length > 0
|
|
1305
1402
|
? { artifacts: Object.fromEntries(artifactEntries) }
|
|
1306
1403
|
: {}),
|
|
1404
|
+
...(transition.semanticResult?.body?.trim()
|
|
1405
|
+
? {
|
|
1406
|
+
output: transition.semanticResult.body.trim().length > 4_000
|
|
1407
|
+
? `${transition.semanticResult.body.trim().slice(0, 3_999)}…`
|
|
1408
|
+
: transition.semanticResult.body.trim(),
|
|
1409
|
+
}
|
|
1410
|
+
: {}),
|
|
1411
|
+
...(transition.deliveryParent
|
|
1412
|
+
? {
|
|
1413
|
+
parent_run: transition.deliveryParent.run,
|
|
1414
|
+
parent_run_instance_id: transition.deliveryParent.run_instance_id,
|
|
1415
|
+
parent_state_dir: transition.deliveryParent.state_dir,
|
|
1416
|
+
}
|
|
1417
|
+
: {}),
|
|
1307
1418
|
run: transition.run,
|
|
1308
1419
|
run_instance_id: transition.runInstanceId,
|
|
1309
1420
|
state_dir: transition.stateDir,
|
|
@@ -1312,13 +1423,16 @@ export function collectRunCompletionBatchMembers(
|
|
|
1312
1423
|
? `${rawSummary.slice(0, 999)}…`
|
|
1313
1424
|
: rawSummary,
|
|
1314
1425
|
terminal_at: transition.terminalAt,
|
|
1426
|
+
tree_depth: depth,
|
|
1315
1427
|
}];
|
|
1316
|
-
})
|
|
1428
|
+
});
|
|
1429
|
+
return admitted.sort((left, right) =>
|
|
1430
|
+
left.tree_depth - right.tree_depth ||
|
|
1317
1431
|
left.terminal_at.localeCompare(right.terminal_at) ||
|
|
1318
1432
|
left.run.localeCompare(right.run) ||
|
|
1319
1433
|
left.run_instance_id.localeCompare(right.run_instance_id) ||
|
|
1320
1434
|
left.state_dir.localeCompare(right.state_dir)
|
|
1321
|
-
);
|
|
1435
|
+
).map(({ tree_depth: _depth, ...member }) => member);
|
|
1322
1436
|
}
|
|
1323
1437
|
|
|
1324
1438
|
const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
|
package/lib/paths.ts
CHANGED
|
@@ -93,8 +93,34 @@ export function getExtensionRuntimePaths(
|
|
|
93
93
|
|
|
94
94
|
export const EXTENSION_RUNTIME_PATHS = getExtensionRuntimePaths();
|
|
95
95
|
|
|
96
|
+
export function getExtensionPackageRoot(extensionUrl: string): string {
|
|
97
|
+
let current = dirname(fileURLToPath(extensionUrl));
|
|
98
|
+
while (true) {
|
|
99
|
+
if (existsSync(join(current, "package.json"))) return current;
|
|
100
|
+
const parent = dirname(current);
|
|
101
|
+
if (parent === current) return dirname(fileURLToPath(extensionUrl));
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RawExtensionCheckoutOptions {
|
|
107
|
+
agentDir?: string;
|
|
108
|
+
cwd?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isRawExtensionCheckout(
|
|
112
|
+
extensionUrl: string,
|
|
113
|
+
options: RawExtensionCheckoutOptions = {},
|
|
114
|
+
): boolean {
|
|
115
|
+
const packageRoot = resolve(getExtensionPackageRoot(extensionUrl));
|
|
116
|
+
const agentDir = resolve(options.agentDir ?? getAgentDir());
|
|
117
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
118
|
+
return dirname(packageRoot) === join(agentDir, "extensions") ||
|
|
119
|
+
dirname(packageRoot) === join(cwd, ".pi", "extensions");
|
|
120
|
+
}
|
|
121
|
+
|
|
96
122
|
export function getExtensionSkillsDir(extensionUrl: string): string {
|
|
97
|
-
return join(
|
|
123
|
+
return join(getExtensionPackageRoot(extensionUrl), "skills");
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
export function getExistingExtensionSkillPaths(extensionUrl: string): string[] {
|
package/lib/pi.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function sendRunCompletionBatch(
|
|
|
52
52
|
pi.sendMessage({
|
|
53
53
|
customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
|
|
54
54
|
content,
|
|
55
|
-
display:
|
|
55
|
+
display: true,
|
|
56
56
|
details: {
|
|
57
57
|
pi_actors_delivery: {
|
|
58
58
|
batch_id: batchId,
|
|
@@ -96,17 +96,20 @@ function completionBatchMessage(
|
|
|
96
96
|
typeof record.content !== "string"
|
|
97
97
|
) return undefined;
|
|
98
98
|
const details = record.details;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const envelope = delivery
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
envelope.batch_id
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
const delivery = details && typeof details === "object" && !Array.isArray(details)
|
|
100
|
+
? (details as Record<string, unknown>).pi_actors_delivery
|
|
101
|
+
: undefined;
|
|
102
|
+
const envelope = delivery && typeof delivery === "object" && !Array.isArray(delivery)
|
|
103
|
+
? delivery as Record<string, unknown>
|
|
104
|
+
: undefined;
|
|
105
|
+
const detailedId = envelope?.kind === "completion_batch" &&
|
|
106
|
+
typeof envelope.batch_id === "string"
|
|
107
|
+
? envelope.batch_id
|
|
108
|
+
: undefined;
|
|
109
|
+
const contentId = /^Batch: `([^`]{1,128})`$/mu.exec(record.content)?.[1];
|
|
110
|
+
const batchId = detailedId ?? contentId;
|
|
111
|
+
if (!batchId || batchId.length > 128) return undefined;
|
|
112
|
+
return { batchId, content: record.content };
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
/** Collapse exact retry duplicates and remove conflicting delivery envelopes. */
|
|
@@ -233,22 +236,6 @@ export function inspectRunSteerSessionEvidence(
|
|
|
233
236
|
});
|
|
234
237
|
}
|
|
235
238
|
|
|
236
|
-
export function inspectRunCompletionBatchSessionEvidence(
|
|
237
|
-
ctx: ExtensionContext,
|
|
238
|
-
batchId: string,
|
|
239
|
-
content: string,
|
|
240
|
-
): SessionEvidence.ActiveSessionEntryEvidence {
|
|
241
|
-
return SessionEvidence.inspectBoundedActiveSessionEntries({
|
|
242
|
-
getEntry: (id) => ctx.sessionManager.getEntry(id),
|
|
243
|
-
leaf: ctx.sessionManager.getLeafEntry(),
|
|
244
|
-
match: (entry) => {
|
|
245
|
-
const batch = completionBatchMessage(entry);
|
|
246
|
-
if (!batch || batch.batchId !== batchId) return undefined;
|
|
247
|
-
return batch.content === content ? "present" : "conflict";
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
239
|
export function registerToolDefinitions(
|
|
253
240
|
pi: ExtensionAPI,
|
|
254
241
|
definitions: Iterable<unknown>,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run delivery lineage.
|
|
3
|
+
* Zones: inherited coordinator root, parent Run generation, delivery-tree filtering
|
|
4
|
+
* Owns durable completion ancestry; excludes delivery persistence and Pi scheduling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RunDeliveryParent {
|
|
8
|
+
run: string;
|
|
9
|
+
run_instance_id: string;
|
|
10
|
+
state_dir: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RunDeliveryLineage {
|
|
14
|
+
delivery_owner_id: string;
|
|
15
|
+
delivery_parent?: RunDeliveryParent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DELIVERY_OWNER_ENV = "PI_ACTORS_DELIVERY_OWNER_ID";
|
|
19
|
+
const PARENT_RUN_ENV = "PI_ACTORS_DELIVERY_PARENT_RUN";
|
|
20
|
+
const PARENT_INSTANCE_ENV = "PI_ACTORS_DELIVERY_PARENT_INSTANCE";
|
|
21
|
+
const PARENT_STATE_DIR_ENV = "PI_ACTORS_DELIVERY_PARENT_STATE_DIR";
|
|
22
|
+
|
|
23
|
+
function nonEmpty(value: string | undefined): string | undefined {
|
|
24
|
+
const text = value?.trim();
|
|
25
|
+
return text || undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function inheritedRunDeliveryLineage(
|
|
29
|
+
ownerId: string | undefined,
|
|
30
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
31
|
+
): RunDeliveryLineage | undefined {
|
|
32
|
+
const localOwner = nonEmpty(ownerId);
|
|
33
|
+
if (!localOwner) return undefined;
|
|
34
|
+
const deliveryOwner = nonEmpty(env[DELIVERY_OWNER_ENV]) ?? localOwner;
|
|
35
|
+
const run = nonEmpty(env[PARENT_RUN_ENV]);
|
|
36
|
+
const runInstanceId = nonEmpty(env[PARENT_INSTANCE_ENV]);
|
|
37
|
+
const stateDir = nonEmpty(env[PARENT_STATE_DIR_ENV]);
|
|
38
|
+
const completeParent = run && runInstanceId && stateDir
|
|
39
|
+
? { run, run_instance_id: runInstanceId, state_dir: stateDir }
|
|
40
|
+
: undefined;
|
|
41
|
+
if ((run || runInstanceId || stateDir) && !completeParent) {
|
|
42
|
+
throw new Error("Inherited Run delivery parent lineage is incomplete");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
delivery_owner_id: deliveryOwner,
|
|
46
|
+
...(completeParent ? { delivery_parent: completeParent } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runDeliveryChildEnv(
|
|
51
|
+
lineage: RunDeliveryLineage,
|
|
52
|
+
parent: RunDeliveryParent,
|
|
53
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
54
|
+
): NodeJS.ProcessEnv {
|
|
55
|
+
return {
|
|
56
|
+
...env,
|
|
57
|
+
[DELIVERY_OWNER_ENV]: lineage.delivery_owner_id,
|
|
58
|
+
[PARENT_RUN_ENV]: parent.run,
|
|
59
|
+
[PARENT_INSTANCE_ENV]: parent.run_instance_id,
|
|
60
|
+
[PARENT_STATE_DIR_ENV]: parent.state_dir,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getProcessDeliveryOwnerId(
|
|
65
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
66
|
+
): string | undefined {
|
|
67
|
+
return nonEmpty(env[DELIVERY_OWNER_ENV]);
|
|
68
|
+
}
|