pi-agent-replicate 0.0.0-stage → 1.4.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.
Files changed (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,51 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ const ALLOWED_FIELDS = [
5
+ "childId",
6
+ "deliveryId",
7
+ "status",
8
+ "resultPath",
9
+ "phase",
10
+ "error",
11
+ "waitedSince",
12
+ "waitedMs",
13
+ "attempt",
14
+ "cause",
15
+ "reason",
16
+ "surface",
17
+ ] as const;
18
+
19
+ function normalizeError(value: unknown): string | undefined {
20
+ if (value == null) return undefined;
21
+ return value instanceof Error ? value.message : String(value);
22
+ }
23
+
24
+ /** Create a contained append-only JSON-lines delivery recorder. */
25
+ export function createDeliveryLog(options: {
26
+ logPath: string;
27
+ now?: () => number;
28
+ onFailure?: (error: unknown) => void;
29
+ }): { record(event: string, fields?: Record<string, unknown>): void } {
30
+ const now = options.now ?? Date.now;
31
+ return {
32
+ record(event, fields = {}) {
33
+ try {
34
+ mkdirSync(dirname(options.logPath), { recursive: true });
35
+ const record: Record<string, unknown> = {
36
+ version: 1,
37
+ time: new Date(now()).toISOString(),
38
+ event,
39
+ };
40
+ for (const key of ALLOWED_FIELDS) {
41
+ if (!(key in fields)) continue;
42
+ const value = key === "error" ? normalizeError(fields[key]) : fields[key];
43
+ if (value !== undefined) record[key] = value;
44
+ }
45
+ appendFileSync(options.logPath, `${JSON.stringify(record)}\n`, "utf8");
46
+ } catch (error) {
47
+ try { options.onFailure?.(error); } catch { /* containment sink is best effort */ }
48
+ }
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,55 @@
1
+ /** Fixed statuses used for durable subagent result delivery. */
2
+ export const DELIVERY_STATUSES = [
3
+ "completed",
4
+ "settled",
5
+ "help-request",
6
+ "closed",
7
+ "crashed",
8
+ "recovery-failed",
9
+ "cancelled",
10
+ "error",
11
+ "empty",
12
+ ] as const;
13
+
14
+ /** Status stored in result files, inbox entries, and model-visible payloads. */
15
+ export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
16
+
17
+ export interface DeliveryPayloadInput {
18
+ subject?: "Replica";
19
+ status: DeliveryStatus;
20
+ agentName: string;
21
+ childSessionId: string;
22
+ childSessionFile?: string;
23
+ answer?: string;
24
+ resultPath?: string;
25
+ deliveryId?: string;
26
+ elapsed?: number;
27
+ exitCode?: number;
28
+ }
29
+
30
+ /** Build one complete direct-child payload without truncating the answer or references. */
31
+ export function frameDeliveryPayload(input: DeliveryPayloadInput): string {
32
+ if (!(DELIVERY_STATUSES as readonly string[]).includes(input.status)) {
33
+ throw new Error(`Unsupported delivery status: ${String(input.status)}`);
34
+ }
35
+
36
+ const lines = [
37
+ `${input.subject ?? "Subagent"} ${input.agentName} ${input.status}.`,
38
+ "",
39
+ input.answer ?? "No answer was captured.",
40
+ ];
41
+ // Replica results disclose only the work outcome; host references stay in metadata.
42
+ if (input.subject === "Replica") return lines.join("\n");
43
+ if (input.resultPath) lines.push("", `Result file: ${input.resultPath}`);
44
+ if (input.childSessionFile) {
45
+ lines.push(
46
+ "",
47
+ `Session: ${input.childSessionFile}`,
48
+ `Resume: pi --session ${input.childSessionFile}`,
49
+ );
50
+ } else {
51
+ lines.push("", `Child session: ${input.childSessionId}`);
52
+ }
53
+ if (input.deliveryId) lines.push(`Delivery: ${input.deliveryId}`);
54
+ return lines.join("\n");
55
+ }
@@ -0,0 +1,219 @@
1
+ import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { interpretExitSidecar, readExitSidecar } from "./completion.ts";
4
+ import { materializeResultFile, type ResultFileHandle, type ResultFileInput } from "./delivery-files.ts";
5
+ import { frameDeliveryPayload, type DeliveryStatus } from "./delivery-payload.ts";
6
+ import {
7
+ claimLineageInboxMaterialization,
8
+ completeLineageInboxMaterialization,
9
+ discoverLineageRoots,
10
+ pendingLineageInboxes,
11
+ releaseLineageInboxMaterialization,
12
+ } from "./lineage.ts";
13
+
14
+ export type DeliveryState = "none" | "delivery-pending" | "delivery-failed" | "delivered";
15
+ export interface DeliveryProjection { state: DeliveryState; error?: string; resultPath?: string }
16
+
17
+ /** Project model-visible delivery truth from durable inbox and acknowledgement facts. */
18
+ export function projectDeliveryState(facts: {
19
+ hasInbox?: boolean;
20
+ materialized?: boolean;
21
+ failure?: string;
22
+ resultPath?: string;
23
+ }): DeliveryProjection {
24
+ if (facts.failure) return { state: "delivery-failed", error: facts.failure, ...(facts.resultPath ? { resultPath: facts.resultPath } : {}) };
25
+ if (facts.materialized) return { state: "delivered", ...(facts.resultPath ? { resultPath: facts.resultPath } : {}) };
26
+ if (facts.hasInbox) return { state: "delivery-pending", ...(facts.resultPath ? { resultPath: facts.resultPath } : {}) };
27
+ return { state: "none" };
28
+ }
29
+
30
+ interface ParentDestination { sessionId: string; sessionFile: string; active: boolean }
31
+ interface DeliverySinks {
32
+ materializeResultFile?: (input: ResultFileInput) => ResultFileHandle;
33
+ publishInbox?: (payload: Record<string, unknown>) => boolean;
34
+ acknowledgeSidecar?: (path: string) => void;
35
+ claimMaterialization?: (parent: Omit<ParentDestination, "active">) => { status: "acquired" | "materialized" | "busy"; token?: string };
36
+ completeMaterialization?: (token: string) => boolean;
37
+ releaseMaterialization?: (token: string) => void;
38
+ sendSteer?: (parent: Omit<ParentDestination, "active">, payload: string) => void | Promise<void>;
39
+ log?: (event: string, fields?: Record<string, unknown>) => void;
40
+ projectWidget?: (projection: DeliveryProjection) => void;
41
+ onFailure?: (error: unknown, fields?: Record<string, unknown>) => void;
42
+ }
43
+
44
+ export interface DeliveryTransactionInput {
45
+ sessionsDir: string;
46
+ childSessionId: string;
47
+ childSessionFile?: string;
48
+ deliveryId: string;
49
+ status: DeliveryStatus;
50
+ agentName: string;
51
+ answer?: string;
52
+ parent: ParentDestination;
53
+ sidecarPath?: string;
54
+ now?: () => number;
55
+ sinks?: DeliverySinks;
56
+ }
57
+
58
+ function errorText(error: unknown): string {
59
+ return error instanceof Error ? error.message : String(error);
60
+ }
61
+
62
+ function containedFailure(sinks: DeliverySinks, error: unknown, fields: Record<string, unknown>): void {
63
+ const projection = projectDeliveryState({ hasInbox: true, failure: errorText(error), resultPath: typeof fields.resultPath === "string" ? fields.resultPath : undefined });
64
+ try { sinks.log?.("delivery-failed", { ...fields, error: errorText(error) }); }
65
+ catch (logError) { try { sinks.onFailure?.(logError, { ...fields, phase: "log" }); } catch { /* contained */ } }
66
+ try { sinks.projectWidget?.(projection); }
67
+ catch (widgetError) { try { sinks.onFailure?.(widgetError, { ...fields, phase: "widget" }); } catch { /* contained */ } }
68
+ }
69
+
70
+ /** Execute the one ordered durable result transaction. Failures stay retryable. */
71
+ export async function runDeliveryTransaction(input: DeliveryTransactionInput): Promise<{ projection: DeliveryProjection; resultPath?: string }> {
72
+ const sinks = input.sinks ?? {};
73
+ const destination = { sessionId: input.parent.sessionId, sessionFile: input.parent.sessionFile };
74
+ if (input.sidecarPath && !existsSync(input.sidecarPath)) {
75
+ if (!sinks.claimMaterialization) return { projection: { state: "delivered" } };
76
+ const existing = sinks.claimMaterialization(destination);
77
+ if (existing.status === "materialized") return { projection: { state: "delivered" } };
78
+ if (existing.status === "busy") return { projection: { state: "delivery-pending" } };
79
+ if (existing.token) sinks.releaseMaterialization?.(existing.token);
80
+ }
81
+ let handle: ResultFileHandle | undefined;
82
+ let claimToken: string | undefined;
83
+ let phase = "materialize-result-file";
84
+ try {
85
+ handle = (sinks.materializeResultFile ?? materializeResultFile)({
86
+ sessionsDir: input.sessionsDir,
87
+ childSessionId: input.childSessionId,
88
+ deliveryId: input.deliveryId,
89
+ status: input.status,
90
+ agentName: input.agentName,
91
+ answer: input.answer,
92
+ now: (input.now ?? Date.now)(),
93
+ });
94
+ const payload = frameDeliveryPayload({
95
+ status: input.status,
96
+ agentName: input.agentName,
97
+ childSessionId: input.childSessionId,
98
+ childSessionFile: input.childSessionFile,
99
+ answer: input.answer,
100
+ resultPath: handle.path,
101
+ deliveryId: input.deliveryId,
102
+ });
103
+ phase = "publish-inbox";
104
+ if (!sinks.publishInbox || sinks.publishInbox({
105
+ kind: input.status === "settled" ? "settled" : "terminal",
106
+ status: input.status,
107
+ resultContent: payload,
108
+ answer: input.answer,
109
+ resultPath: handle.path,
110
+ childSessionFile: input.childSessionFile,
111
+ deliveryId: input.deliveryId,
112
+ }) !== true) throw new Error("Inbox publication failed");
113
+
114
+ phase = "acknowledge-sidecar";
115
+ if (input.sidecarPath) (sinks.acknowledgeSidecar ?? ((path: string) => rmSync(path, { force: true })))(input.sidecarPath);
116
+ if (!input.parent.active) {
117
+ const projection = projectDeliveryState({ hasInbox: true, resultPath: handle.path });
118
+ try { sinks.projectWidget?.(projection); } catch (error) { try { sinks.onFailure?.(error, { deliveryId: input.deliveryId, phase: "widget" }); } catch { /* contained */ } }
119
+ return { projection, resultPath: handle.path };
120
+ }
121
+
122
+ phase = "claim-materialization";
123
+ if (!sinks.claimMaterialization) throw new Error("Materialization claim sink is unavailable");
124
+ const claim = sinks.claimMaterialization(destination);
125
+ if (claim.status === "materialized") return { projection: projectDeliveryState({ hasInbox: true, materialized: true, resultPath: handle.path }), resultPath: handle.path };
126
+ if (claim.status !== "acquired" || !claim.token) throw new Error("Materialization claim is busy");
127
+ claimToken = claim.token;
128
+
129
+ phase = "send-steer";
130
+ if (!sinks.sendSteer) throw new Error("Exact-parent steer sink is unavailable");
131
+ await sinks.sendSteer(destination, payload);
132
+ phase = "complete-materialization";
133
+ if (!sinks.completeMaterialization?.(claimToken)) throw new Error("Unable to publish materialization acknowledgement");
134
+ claimToken = undefined;
135
+ const projection = projectDeliveryState({ hasInbox: true, materialized: true, resultPath: handle.path });
136
+ try { sinks.log?.("delivery-sent", { childId: input.childSessionId, deliveryId: input.deliveryId, status: input.status, resultPath: handle.path, phase }); } catch (error) { try { sinks.onFailure?.(error, { deliveryId: input.deliveryId, phase: "log" }); } catch { /* contained */ } }
137
+ try { sinks.projectWidget?.(projection); } catch (error) { try { sinks.onFailure?.(error, { deliveryId: input.deliveryId, phase: "widget" }); } catch { /* contained */ } }
138
+ return { projection, resultPath: handle.path };
139
+ } catch (error) {
140
+ if (claimToken) {
141
+ try { sinks.releaseMaterialization?.(claimToken); } catch { /* later recovery reclaims stale ownership */ }
142
+ }
143
+ const fields = { childId: input.childSessionId, deliveryId: input.deliveryId, status: input.status, resultPath: handle?.path, phase };
144
+ containedFailure(sinks, error, fields);
145
+ return { projection: projectDeliveryState({ hasInbox: Boolean(handle), failure: errorText(error), resultPath: handle?.path }), ...(handle ? { resultPath: handle.path } : {}) };
146
+ }
147
+ }
148
+
149
+ /** Read a retained completion sidecar and route it through the durable transaction. */
150
+ export async function processCompletionSidecarDelivery(input: {
151
+ sessionFile: string;
152
+ delivery: Omit<DeliveryTransactionInput, "answer" | "sidecarPath"> & { answer?: string };
153
+ }): Promise<{ projection: DeliveryProjection; resultPath?: string }> {
154
+ const sidecarPath = `${input.sessionFile}.exit`;
155
+ const sidecar = readExitSidecar(input.sessionFile);
156
+ if (!sidecar) return { projection: { state: "delivered" } };
157
+ const result = interpretExitSidecar(sidecar);
158
+ const answer = input.delivery.answer ?? result.summary ?? result.errorMessage ?? result.ping?.message;
159
+ return runDeliveryTransaction({ ...input.delivery, answer, sidecarPath });
160
+ }
161
+
162
+ function findLineageRoots(root: string): string[] {
163
+ const found = new Set(discoverLineageRoots(root));
164
+ const visit = (dir: string, depth: number) => {
165
+ if (depth > 4) return;
166
+ let entries: ReturnType<typeof readdirSync>;
167
+ try { entries = readdirSync(dir, { withFileTypes: true }) as any; } catch { return; }
168
+ const names = new Set((entries as any[]).map((entry) => entry.name));
169
+ if (names.has("events") && names.has("materialization-claims")) found.add(dir);
170
+ for (const entry of entries as any[]) if (entry.isDirectory()) visit(join(dir, entry.name), depth + 1);
171
+ };
172
+ visit(root, 0);
173
+ return [...found].sort();
174
+ }
175
+
176
+ /** Recover pending inboxes for one exact parent session, preserving retry ordering. */
177
+ export async function recoverPendingInboxDeliveries(input: {
178
+ sessionDir: string;
179
+ parentSessionId: string;
180
+ parentSessionFile: string;
181
+ now?: () => number;
182
+ deliver: (record: { deliveryId: string; nodeId: string; content: string; resultPath?: string }) => void | Promise<void>;
183
+ log?: (event: string, fields?: Record<string, unknown>) => void;
184
+ }): Promise<{ materialized: string[]; skipped: string[]; failed: string[] }> {
185
+ const materialized: string[] = [];
186
+ const skipped: string[] = [];
187
+ const failed: string[] = [];
188
+ for (const rootDir of findLineageRoots(input.sessionDir)) {
189
+ for (const record of pendingLineageInboxes(rootDir, input.parentSessionId, input.parentSessionFile)) {
190
+ const claim = claimLineageInboxMaterialization({
191
+ rootDir,
192
+ deliveryId: record.deliveryId,
193
+ nodeId: record.nodeId,
194
+ hasExactSessionEvidence: () => {
195
+ try { return existsSync(input.parentSessionFile) && readFileSync(input.parentSessionFile, "utf8").includes(record.deliveryId); }
196
+ catch { return false; }
197
+ },
198
+ });
199
+ if (claim.status !== "acquired") { skipped.push(record.deliveryId); continue; }
200
+ try {
201
+ const content = typeof record.payload.resultContent === "string" ? record.payload.resultContent : "";
202
+ await input.deliver({ deliveryId: record.deliveryId, nodeId: record.nodeId, content, resultPath: typeof record.payload.resultPath === "string" ? record.payload.resultPath : undefined });
203
+ if (!completeLineageInboxMaterialization(rootDir, record.deliveryId, record.nodeId, claim.token)) throw new Error("Unable to publish materialization acknowledgement");
204
+ materialized.push(record.deliveryId);
205
+ } catch (error) {
206
+ releaseLineageInboxMaterialization({
207
+ rootDir,
208
+ deliveryId: record.deliveryId,
209
+ nodeId: record.nodeId,
210
+ token: claim.token,
211
+ hasExactSessionEvidence: () => false,
212
+ });
213
+ failed.push(record.deliveryId);
214
+ try { input.log?.("delivery-recovery-failed", { deliveryId: record.deliveryId, childId: record.nodeId, phase: "deliver", error: errorText(error) }); } catch { /* contained */ }
215
+ }
216
+ }
217
+ }
218
+ return { materialized, skipped, failed };
219
+ }
@@ -0,0 +1,30 @@
1
+ /** Validate persisted ownership without manufacturing a root for legacy records. */
2
+ export function readExecutionOwner(value) {
3
+ if (!value || typeof value !== "object")
4
+ return undefined;
5
+ const owner = value;
6
+ return typeof owner.toolCallId === "string" && !!owner.toolCallId &&
7
+ typeof owner.parentToolCallId === "string" && !!owner.parentToolCallId
8
+ ? { toolCallId: owner.toolCallId, parentToolCallId: owner.parentToolCallId }
9
+ : undefined;
10
+ }
11
+ /** Capture only Pi's execution edges; callers snapshot ownership before awaiting launch. */
12
+ export function trackExecutionOwners(pi) {
13
+ const active = new Map();
14
+ pi.on("tool_execution_start", (event) => {
15
+ const parent = event.parentToolCallId;
16
+ const root = parent
17
+ ? active.get(parent)?.parentToolCallId
18
+ : event.toolCallId;
19
+ if (root)
20
+ active.set(event.toolCallId, { toolCallId: event.toolCallId, parentToolCallId: root });
21
+ });
22
+ pi.on("tool_execution_end", (event) => { active.delete(event.toolCallId); });
23
+ pi.on("session_start", () => { active.clear(); });
24
+ pi.on("session_shutdown", () => { active.clear(); });
25
+ return (id) => active.get(id);
26
+ }
27
+ /** Stereo consumes the visible root; retain the leaf independently for provenance. */
28
+ export function linkedOutputDetails(owner) {
29
+ return owner ? { originatingToolCallId: owner.toolCallId, parentToolCallId: owner.parentToolCallId } : {};
30
+ }
@@ -0,0 +1,38 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Actual attempt execution and its visible outer tool call, never inferred from IDs. */
4
+ export interface ExecutionOwner {
5
+ toolCallId: string;
6
+ parentToolCallId: string;
7
+ }
8
+
9
+ /** Validate persisted ownership without manufacturing a root for legacy records. */
10
+ export function readExecutionOwner(value: unknown): ExecutionOwner | undefined {
11
+ if (!value || typeof value !== "object") return undefined;
12
+ const owner = value as Partial<ExecutionOwner>;
13
+ return typeof owner.toolCallId === "string" && !!owner.toolCallId &&
14
+ typeof owner.parentToolCallId === "string" && !!owner.parentToolCallId
15
+ ? { toolCallId: owner.toolCallId, parentToolCallId: owner.parentToolCallId }
16
+ : undefined;
17
+ }
18
+
19
+ /** Capture only Pi's execution edges; callers snapshot ownership before awaiting launch. */
20
+ export function trackExecutionOwners(pi: ExtensionAPI): (id: string) => ExecutionOwner | undefined {
21
+ const active = new Map<string, ExecutionOwner>();
22
+ pi.on("tool_execution_start", (event) => {
23
+ const parent = (event as typeof event & { parentToolCallId?: string }).parentToolCallId;
24
+ const root = parent
25
+ ? active.get(parent)?.parentToolCallId
26
+ : event.toolCallId;
27
+ if (root) active.set(event.toolCallId, { toolCallId: event.toolCallId, parentToolCallId: root });
28
+ });
29
+ pi.on("tool_execution_end", (event) => { active.delete(event.toolCallId); });
30
+ pi.on("session_start", () => { active.clear(); });
31
+ pi.on("session_shutdown", () => { active.clear(); });
32
+ return (id) => active.get(id);
33
+ }
34
+
35
+ /** Stereo consumes the visible root; retain the leaf independently for provenance. */
36
+ export function linkedOutputDetails(owner: ExecutionOwner | undefined): Record<string, string> {
37
+ return owner ? { originatingToolCallId: owner.toolCallId, parentToolCallId: owner.parentToolCallId } : {};
38
+ }