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.
- package/AGENTS.md +27 -0
- package/CHANGELOG.md +152 -0
- package/CONTEXT.md +63 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/RELEASING.md +132 -0
- package/assets/banner.png +0 -0
- package/config.json.example +6 -0
- package/docs/README.md +23 -0
- package/docs/adr/0003-installable-role-packs.md +29 -0
- package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
- package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
- package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
- package/docs/configuration.md +14 -0
- package/docs/development.md +27 -0
- package/docs/public-subagent-tree-api.md +58 -0
- package/docs/retained-roles.md +71 -0
- package/docs/settled-lifecycle-contract.md +71 -0
- package/docs/worktree-subagents.md +58 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/index.d.ts +116 -0
- package/index.js +1064 -0
- package/index.ts +155 -0
- package/package.json +70 -4
- package/pi-extension/subagents/activity.js +575 -0
- package/pi-extension/subagents/activity.ts +980 -0
- package/pi-extension/subagents/completion.js +184 -0
- package/pi-extension/subagents/completion.ts +229 -0
- package/pi-extension/subagents/delivery-drain.js +15 -0
- package/pi-extension/subagents/delivery-drain.ts +20 -0
- package/pi-extension/subagents/delivery-files.js +86 -0
- package/pi-extension/subagents/delivery-files.ts +105 -0
- package/pi-extension/subagents/delivery-log.js +51 -0
- package/pi-extension/subagents/delivery-log.ts +51 -0
- package/pi-extension/subagents/delivery-payload.ts +55 -0
- package/pi-extension/subagents/delivery.ts +219 -0
- package/pi-extension/subagents/execution-owner.js +30 -0
- package/pi-extension/subagents/execution-owner.ts +38 -0
- package/pi-extension/subagents/herdr.js +645 -0
- package/pi-extension/subagents/herdr.ts +927 -0
- package/pi-extension/subagents/index.ts +3400 -0
- package/pi-extension/subagents/launch.js +774 -0
- package/pi-extension/subagents/launch.ts +1124 -0
- package/pi-extension/subagents/lifecycle.js +477 -0
- package/pi-extension/subagents/lifecycle.ts +582 -0
- package/pi-extension/subagents/lineage.js +806 -0
- package/pi-extension/subagents/lineage.ts +841 -0
- package/pi-extension/subagents/replica-background.js +32 -0
- package/pi-extension/subagents/replica-background.ts +38 -0
- package/pi-extension/subagents/replica-branch.ts +45 -0
- package/pi-extension/subagents/replica-directory-settings.ts +51 -0
- package/pi-extension/subagents/replica-history.js +22 -0
- package/pi-extension/subagents/replica-history.ts +21 -0
- package/pi-extension/subagents/replica-work.js +326 -0
- package/pi-extension/subagents/replica-work.ts +336 -0
- package/pi-extension/subagents/replica-worker.js +74 -0
- package/pi-extension/subagents/replica-worker.ts +71 -0
- package/pi-extension/subagents/replication.ts +99 -0
- package/pi-extension/subagents/result-delivery-queue.ts +33 -0
- package/pi-extension/subagents/role-catalog.js +123 -0
- package/pi-extension/subagents/role-catalog.ts +24 -0
- package/pi-extension/subagents/runtime-routing.js +154 -0
- package/pi-extension/subagents/runtime-routing.ts +239 -0
- package/pi-extension/subagents/session-binding.js +346 -0
- package/pi-extension/subagents/session-binding.ts +339 -0
- package/pi-extension/subagents/session-work.js +219 -0
- package/pi-extension/subagents/session-work.ts +167 -0
- package/pi-extension/subagents/session.js +318 -0
- package/pi-extension/subagents/session.ts +448 -0
- package/pi-extension/subagents/settled-contract.js +47 -0
- package/pi-extension/subagents/settled-contract.ts +175 -0
- package/pi-extension/subagents/source-state.js +180 -0
- package/pi-extension/subagents/source-state.ts +184 -0
- package/pi-extension/subagents/status.ts +493 -0
- package/pi-extension/subagents/subagent-done.js +805 -0
- package/pi-extension/subagents/subagent-done.ts +878 -0
- package/pi-extension/subagents/terminal.js +111 -0
- package/pi-extension/subagents/terminal.ts +208 -0
- package/pi-extension/subagents/watchdog.js +122 -0
- package/pi-extension/subagents/watchdog.ts +171 -0
package/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import type { PiRunningChild } from "./pi-extension/subagents/launch.ts";
|
|
6
|
+
import { discoverAgentCatalog, effectiveRoleTools, resolveAgent, type AgentDefinition } from "./pi-extension/subagents/role-catalog.ts";
|
|
7
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
|
+
import { resolveRuntimePlan, wrapPiModelRegistry, type ResolvedRuntimePlan } from "./pi-extension/subagents/runtime-routing.ts";
|
|
9
|
+
|
|
10
|
+
const TREE_REGISTRY = Symbol.for("pi-lpt-herdr-subagents/public-tree/v2");
|
|
11
|
+
const PROCESS_ID = Symbol.for("pi-lpt-herdr-subagents/public-tree/process");
|
|
12
|
+
const MAX_METADATA = 64 * 1024;
|
|
13
|
+
const MAX_ERROR = 4000;
|
|
14
|
+
const MAX_SESSION = 10000;
|
|
15
|
+
const CANCEL_TIMEOUT_MS = 10000;
|
|
16
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
17
|
+
export interface SubagentTreeMetadata {
|
|
18
|
+
readonly value: unknown;
|
|
19
|
+
/** Immutable package-owned identity; value is the consumer-owned opaque metadata. */
|
|
20
|
+
readonly callerId?: string;
|
|
21
|
+
readonly treeId?: string;
|
|
22
|
+
readonly ownerId?: string;
|
|
23
|
+
readonly nodeId?: string;
|
|
24
|
+
readonly parentId?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SubagentError { readonly code: string; readonly message: string; }
|
|
27
|
+
export interface SubagentWorktree { readonly path?: string; readonly branch?: string; readonly baseRef?: string; readonly baseSha?: string; readonly workspaceId?: string; readonly paneId?: string; readonly headSha?: string | null; readonly commitsAhead?: number | null; readonly clean?: boolean | null; readonly conflicted?: boolean | null; readonly changedFiles?: string[] | null; readonly untrackedFiles?: string[] | null; readonly manifestFile?: string; }
|
|
28
|
+
export type SubagentChildOutcome = "clean" | "empty" | "error" | "intentional-abort" | "unexpected-abort";
|
|
29
|
+
export interface SubagentChildResult { readonly resultId: string; readonly assistantEntryId: string; readonly answer: string | null; readonly outcome: SubagentChildOutcome; readonly sessionReference?: string; readonly error?: SubagentError; readonly worktree?: SubagentWorktree; readonly metadata?: SubagentTreeMetadata; }
|
|
30
|
+
export interface SubagentNodeResult { readonly nodeId: string; readonly parentId: string; readonly outcome: SubagentChildOutcome; readonly open: boolean; readonly sessionReference?: string; readonly error?: SubagentError; }
|
|
31
|
+
export interface SubagentTreeResult { readonly terminalId: string; readonly state: "completed" | "failed" | "cancelled"; readonly rootResult: SubagentChildResult; readonly callerMetadata: SubagentTreeMetadata; readonly nodes: readonly SubagentNodeResult[]; readonly error?: SubagentError; }
|
|
32
|
+
export interface SubagentChildHandle { readonly nodeId: string; readonly parentId: string; readonly result: Promise<SubagentChildResult>; }
|
|
33
|
+
export interface LaunchChildOptions { readonly parentId: string; readonly name: string; readonly task: string; readonly agent?: string; readonly skills?: string; readonly tools: readonly string[]; readonly cwd?: string; readonly worktree?: { readonly branch: string; readonly base?: string }; readonly metadata?: unknown; }
|
|
34
|
+
export interface SubagentTreeHandle { readonly callerId: string; readonly treeId: string; readonly ownershipToken: string; readonly ownerId: string; readonly children: ReadonlyMap<string, SubagentChildHandle>; readonly result: Promise<SubagentTreeResult>; launchChild(options: LaunchChildOptions): Promise<SubagentChildHandle>; bindFinalCallback(callback: (result: SubagentTreeResult) => void | Promise<void>): void; cancel(): Promise<SubagentTreeResult>; }
|
|
35
|
+
export interface CreateSubagentTreeOptions { readonly pi: ExtensionAPI; readonly ctx: ExtensionContext; readonly metadata?: unknown; }
|
|
36
|
+
export interface ForkLineage { readonly previousSessionFile: string; }
|
|
37
|
+
export interface AttachSubagentTreeOptions { readonly pi: ExtensionAPI; readonly ctx: ExtensionContext; readonly callerId?: string; readonly treeId?: string; readonly ownershipToken?: string; readonly forkLineage?: ForkLineage; }
|
|
38
|
+
export class SubagentTreeError extends Error { constructor(message: string) { super(message); this.name = "SubagentTreeError"; } }
|
|
39
|
+
export class SubagentTreeValidationError extends SubagentTreeError { constructor(message: string) { super(message); this.name = "SubagentTreeValidationError"; } }
|
|
40
|
+
export class SubagentTreeOwnershipError extends SubagentTreeError { constructor(message: string) { super(message); this.name = "SubagentTreeOwnershipError"; } }
|
|
41
|
+
interface NodeRecord { nodeId:string; parentId:string; ownerId:string; name:string; surface?:string; sessionFile?:string; status:"queued"|"running"|"settled"; open?:boolean; resultPath:string; metadata?:SubagentTreeMetadata; }
|
|
42
|
+
interface TreeMeta { version:2; treeId:string; callerId:string; ownershipToken:string; artifactDir:string; treeDir:string; callerMetadata:SubagentTreeMetadata; boundSessionFile:string; acceptedSessionFiles?:Record<string,string[]>; }
|
|
43
|
+
interface Deferred<T> { promise:Promise<T>; resolve:(value:T)=>void; }
|
|
44
|
+
interface State { meta:TreeMeta; ownerId:string; pi:ExtensionAPI; ctx:ExtensionContext; boundSessionFile:string; children:Map<string,SubagentChildHandle>; pending:Map<string,Deferred<SubagentChildResult>>; launchTransactions:Set<Promise<void>>; launchControllers:Set<AbortController>; callback?: (r:SubagentTreeResult)=>void|Promise<void>; callbackVersion:number; deliverySerial:Promise<void>; terminal?:SubagentTreeResult; resolveTree:(r:SubagentTreeResult)=>void; treePromise:Promise<SubagentTreeResult>; cancellation?:Promise<SubagentTreeResult>; branchCancellation?:Promise<void>; cancellationRequested:boolean; }
|
|
45
|
+
function processIdentity():string { const g=globalThis as typeof globalThis & {[PROCESS_ID]?:string}; return g[PROCESS_ID]??= `${process.pid}-${randomUUID()}`; }
|
|
46
|
+
function registry():Map<string,State>{const g=globalThis as typeof globalThis & {[TREE_REGISTRY]?:Map<string,State>};return g[TREE_REGISTRY]??=new Map();}
|
|
47
|
+
const stateKey=(tree:string,owner:string)=>`${tree}:${owner}`;
|
|
48
|
+
const readJson=<T>(path:string):T|undefined=>{try{return JSON.parse(readFileSync(path,"utf8")) as T;}catch{return undefined;}};
|
|
49
|
+
function atomic(path:string,value:unknown):void{mkdirSync(dirname(path),{recursive:true});const tmp=`${path}.${process.pid}.${randomUUID()}.tmp`;writeFileSync(tmp,`${JSON.stringify(value)}\n`);renameSync(tmp,path);}
|
|
50
|
+
function exclusiveAtomic(path:string,value:unknown):boolean{mkdirSync(dirname(path),{recursive:true});const tmp=`${path}.${process.pid}.${randomUUID()}.tmp`;try{writeFileSync(tmp,`${JSON.stringify(value)}\n`);linkSync(tmp,path);return true;}catch(e){if((e as NodeJS.ErrnoException).code==="EEXIST")return false;throw e;}finally{try{unlinkSync(tmp);}catch{}}}
|
|
51
|
+
const bounded=(s:string,n:number)=>s.length<=n?s:s.slice(0,n);
|
|
52
|
+
const message=(e:unknown)=>e instanceof Error?e.message:String(e);
|
|
53
|
+
function metadata(value:unknown):SubagentTreeMetadata{let encoded:string|undefined;try{encoded=JSON.stringify(value);}catch(e){throw new SubagentTreeValidationError(`metadata must be JSON-compatible: ${message(e)}`);}if(encoded===undefined)throw new SubagentTreeValidationError("metadata must be JSON-compatible");if(Buffer.byteLength(encoded,"utf8")>MAX_METADATA)throw new SubagentTreeValidationError("metadata exceeds the 64 KiB limit");return deepFreeze({value:JSON.parse(encoded) as JsonValue});}
|
|
54
|
+
function identityMetadata(base:SubagentTreeMetadata,meta:TreeMeta,ownerId:string,nodeId?:string,parentId?:string):SubagentTreeMetadata{return deepFreeze({...base,callerId:meta.callerId,treeId:meta.treeId,ownerId,...(nodeId?{nodeId}:{}),...(parentId?{parentId}:{})});}
|
|
55
|
+
function context(ctx:ExtensionContext){const m=ctx.sessionManager;const file=m.getSessionFile();if(!file)throw new SubagentTreeValidationError("No session file");return {sessionFile:file,sessionId:m.getSessionId(),sessionDir:m.getSessionDir(),cwd:ctx.cwd??process.cwd(),agentDir:process.env.PI_CODING_AGENT_DIR};}
|
|
56
|
+
function ownerSession(meta:TreeMeta,owner:string){if(owner===meta.callerId)return meta.boundSessionFile;return readJson<NodeRecord>(join(meta.treeDir,"nodes",`${owner}.json`))?.sessionFile;}
|
|
57
|
+
function header(path:string){try{const x=JSON.parse(readFileSync(path,"utf8").split("\n",1)[0]) as {parentSession?:unknown};return typeof x.parentSession==="string"?x.parentSession:undefined;}catch{return undefined;}}
|
|
58
|
+
function findTreeMeta(sessionDir:string,treeId:string):TreeMeta|undefined{const root=join(sessionDir,"artifacts");if(!existsSync(root))return;for(const sid of readdirSync(root)){const p=join(root,sid,"subagent-trees",treeId,"tree.json"),m=readJson<TreeMeta>(p);if(m)return m;}return;}
|
|
59
|
+
function validateLineage(meta:TreeMeta,owner:string,ctx:ExtensionContext,lineage?:ForkLineage,bound?:string){const fresh=context(ctx).sessionFile;if(lineage===undefined)return fresh;if(typeof lineage.previousSessionFile!=="string"||!lineage.previousSessionFile)throw new SubagentTreeOwnershipError("fork lineage previousSessionFile is required");const prior=bound??ownerSession(meta,owner);if(!prior||prior!==lineage.previousSessionFile)throw new SubagentTreeOwnershipError("fork lineage does not match the currently bound session");if(header(fresh)!==lineage.previousSessionFile)throw new SubagentTreeOwnershipError("fork session header does not match the previous session");return fresh;}
|
|
60
|
+
function processAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code !== "ESRCH"; } }
|
|
61
|
+
/** Reclaim a dead claim without unlinking a newly-created owner claim. */
|
|
62
|
+
function reclaimDeadClaim(path: string, owner: Record<string, unknown>): boolean {
|
|
63
|
+
for (;;) {
|
|
64
|
+
if (exclusiveAtomic(path, owner)) return true;
|
|
65
|
+
const old = readJson<{ process?: string; pid?: number }>(path);
|
|
66
|
+
if (old?.process === owner.process) return false;
|
|
67
|
+
if (typeof old?.pid === "number" && processAlive(old.pid)) return false;
|
|
68
|
+
let before: { dev: number; ino: number };
|
|
69
|
+
try { const st = statSync(path); before = { dev: st.dev, ino: st.ino }; } catch { continue; }
|
|
70
|
+
const moved = `${path}.${processIdentity()}.${randomUUID()}.stale`;
|
|
71
|
+
try { renameSync(path, moved); } catch { continue; }
|
|
72
|
+
let same = false;
|
|
73
|
+
try { const st = statSync(moved); same = st.dev === before.dev && st.ino === before.ino; } catch {}
|
|
74
|
+
if (!same) { try { if (!existsSync(path)) renameSync(moved, path); } catch {} continue; }
|
|
75
|
+
try { unlinkSync(moved); } catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function claimBranch(meta:TreeMeta,owner:string){const p=join(meta.treeDir,"branches",`${owner}.claim`), identity={process:processIdentity(),pid:process.pid,owner,token:randomUUID()};if(reclaimDeadClaim(p,identity))return;throw new SubagentTreeOwnershipError("subagent branch is owned by another process");}
|
|
79
|
+
function claimRoot(s:State):void{const p=join(s.meta.treeDir,"root.claim"),identity={process:processIdentity(),pid:process.pid,token:randomUUID()};if(exclusiveAtomic(p,identity))return;const old=readJson<{process?:string;pid?:number}>(p);if(old?.process===identity.process)throw new SubagentTreeOwnershipError("subagent tree already has a direct root");if(readdirSync(join(s.meta.treeDir,"nodes")).some(x=>x.endsWith(".json")))throw new SubagentTreeOwnershipError("subagent tree already has a direct root");if(reclaimDeadClaim(p,identity))return;throw new SubagentTreeOwnershipError("subagent tree already has a direct root");}
|
|
80
|
+
function deferred<T>():Deferred<T>{let resolve!:(value:T)=>void;const promise=new Promise<T>(r=>{resolve=r;});return {promise,resolve};}
|
|
81
|
+
function nodePath(s:State,id:string){return join(s.meta.treeDir,"nodes",`${id}.json`);}
|
|
82
|
+
function inflightPath(s:State,id:string){return join(s.meta.treeDir,"branches",`${s.ownerId}.${id}.inflight.json`);}
|
|
83
|
+
function clearInflight(s:State,id:string,token:string){const p=inflightPath(s,id),record=readJson<{token?:string}>(p);if(record?.token===token)try{unlinkSync(p);}catch{}}
|
|
84
|
+
type InflightRecord = {path:string;pid?:number;ownerId?:string;nodeId?:string};
|
|
85
|
+
function inflightRecords(s:State):InflightRecord[] {const dir=join(s.meta.treeDir,"branches");if(!existsSync(dir))return [];return readdirSync(dir).filter(x=>x.endsWith(".inflight.json")).flatMap(x=>{const path=join(dir,x),r=readJson<Omit<InflightRecord,"path">>(path);return r?[{path,...r}]:[{path}];});}
|
|
86
|
+
function writeNode(s:State,node:NodeRecord){atomic(nodePath(s,node.nodeId),node);}
|
|
87
|
+
function terminalIntentPath(s:State){return join(s.meta.treeDir,"terminal-intent.json");}
|
|
88
|
+
function claimTerminalIntent(s:State,kind:"completed"|"cancelled"):boolean{return exclusiveAtomic(terminalIntentPath(s),{kind,ownerId:s.ownerId,process:processIdentity(),terminalId:randomUUID(),at:Date.now()});}
|
|
89
|
+
function currentTerminalIntent(s:State):"completed"|"cancelled"|undefined{return readJson<{kind?:unknown}>(terminalIntentPath(s))?.kind as "completed"|"cancelled"|undefined;}
|
|
90
|
+
function branchNodes(s:State):NodeRecord[]{const d=join(s.meta.treeDir,"nodes");if(!existsSync(d))return [];return readdirSync(d).filter(x=>x.endsWith(".json")).flatMap(f=>{const n=readJson<NodeRecord>(join(d,f));return n?.ownerId===s.ownerId?[n]:[];});}
|
|
91
|
+
function allNodes(s:State):NodeRecord[]{const d=join(s.meta.treeDir,"nodes");if(!existsSync(d))return [];return readdirSync(d).filter(x=>x.endsWith(".json")).flatMap(f=>{const n=readJson<NodeRecord>(join(d,f));return n?[n]:[];});}
|
|
92
|
+
function pendingInflightRecords(s:State,ownerId?:string):InflightRecord[]{const nodes=allNodes(s);return inflightRecords(s).filter(r=>{if(ownerId!==undefined&&r.ownerId!==ownerId)return false;const node=r.nodeId?nodes.find(n=>n.nodeId===r.nodeId):undefined;return !node||node.ownerId!==r.ownerId||!node.surface;});}
|
|
93
|
+
function pendingBranchEvidence(s:State,ownerId:string){const nodes=allNodes(s),inflight=pendingInflightRecords(s,ownerId).length,queued=nodes.filter(n=>n.ownerId===ownerId&&n.status==="queued"&&!existsSync(n.resultPath)).length;return {inflight,queued};}
|
|
94
|
+
async function waitBranchTransactions(s:State):Promise<boolean>{const deadline=Date.now()+CANCEL_TIMEOUT_MS;while(s.launchTransactions.size&&Date.now()<deadline)await new Promise(r=>setTimeout(r,25));return s.launchTransactions.size===0;}
|
|
95
|
+
async function cancelBranch(s:State):Promise<void>{
|
|
96
|
+
if(s.branchCancellation)return s.branchCancellation;
|
|
97
|
+
s.cancellationRequested=true;
|
|
98
|
+
s.branchCancellation=(async()=>{
|
|
99
|
+
const mux=await import("./pi-extension/subagents/terminal.ts");
|
|
100
|
+
for(const controller of s.launchControllers)controller.abort();
|
|
101
|
+
const transactionsSettled=await waitBranchTransactions(s),pids=new Set<number>(),nodePids=new Map<string,number[]>();
|
|
102
|
+
for(const n of branchNodes(s)){
|
|
103
|
+
if(n.open===false)continue;
|
|
104
|
+
if(!n.surface){
|
|
105
|
+
if(n.status!=="settled"){const result=writeResult(s,n,failedResult("tree_cancelled","queued child cancelled",n.metadata));s.pending.get(n.nodeId)?.resolve(result);}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if(n.status!=="settled")try{const info=mux.getPaneProcessInfo(n.surface),captured=info.pids.filter(pid=>Number.isInteger(pid)&&pid>0);for(const pid of captured)pids.add(pid);if(captured.length)nodePids.set(n.nodeId,captured);}catch{}
|
|
109
|
+
try{mux.closePane(n.surface);}catch{}
|
|
110
|
+
try{n.open=!(await mux.waitForPaneAbsence(n.surface,{timeoutMs:5000}));}catch(e){n.open=!/pane_not_found|pane not found/i.test(message(e));}
|
|
111
|
+
if(n.status!=="settled"){const result=writeResult(s,n,failedResult("tree_cancelled","running child cancelled",n.metadata));s.pending.get(n.nodeId)?.resolve(result);}else writeNode(s,n);
|
|
112
|
+
}
|
|
113
|
+
let remaining:number[]=[];
|
|
114
|
+
let processCheckConfirmed=true;
|
|
115
|
+
try{remaining=await mux.waitForProcessesExit([...pids],{timeoutMs:5000});}catch{processCheckConfirmed=false;}
|
|
116
|
+
const inflight=pendingInflightRecords(s,s.ownerId),ack={ownerId:s.ownerId,process:processIdentity(),pid:process.pid,at:Date.now(),transactionsSettled,inflight:inflight.length,remainingPids:remaining,nodes:branchNodes(s).map(n=>({nodeId:n.nodeId,open:n.open??false}))};
|
|
117
|
+
if(!existsSync(join(s.meta.treeDir,"branches",`${s.ownerId}.cancelled.json`)))atomic(join(s.meta.treeDir,"branches",`${s.ownerId}.cancelled.json`),ack);
|
|
118
|
+
if(s.ownerId===s.meta.callerId&&processCheckConfirmed&&!remaining.length)for(const n of branchNodes(s)){
|
|
119
|
+
// The root may close a child branch before it can publish its own acknowledgement.
|
|
120
|
+
// Captured process exit plus pane absence is equivalent termination evidence.
|
|
121
|
+
const captured=nodePids.get(n.nodeId);
|
|
122
|
+
if(!captured?.length||n.open!==false||captured.some(pid=>remaining.includes(pid)))continue;
|
|
123
|
+
const childAckPath=join(s.meta.treeDir,"branches",`${n.nodeId}.cancelled.json`),pending=pendingBranchEvidence(s,n.nodeId);
|
|
124
|
+
if(!existsSync(childAckPath))atomic(childAckPath,{ownerId:n.nodeId,process:processIdentity(),pid:process.pid,at:Date.now(),synthesized:true,transactionsSettled:pending.inflight===0&&pending.queued===0,inflight:pending.inflight,unresolvedQueued:pending.queued,remainingPids:[],nodes:[{nodeId:n.nodeId,open:false}]});
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
return s.branchCancellation;
|
|
128
|
+
}
|
|
129
|
+
function writeResult(s:State,node:NodeRecord,result:SubagentChildResult){const frozen=deepFreeze(result);if(!exclusiveAtomic(node.resultPath,frozen)){const winner=readJson<SubagentChildResult>(node.resultPath);if(winner){atomic(nodePath(s,node.nodeId),{...node,status:"settled"});return deepFreeze(winner);}return frozen;}atomic(nodePath(s,node.nodeId),{...node,status:"settled"});return frozen;}
|
|
130
|
+
function isCancelled(s:State){return s.cancellationRequested||existsSync(join(s.meta.treeDir,"cancel.request"));}
|
|
131
|
+
function failedResult(code:string,msg:string,meta?:SubagentTreeMetadata):SubagentChildResult{return {resultId:randomUUID(),assistantEntryId:randomUUID(),answer:null,outcome:"unexpected-abort",error:{code,message:bounded(msg,MAX_ERROR)},...(meta?{metadata:meta}:{})};}
|
|
132
|
+
function rows(s:State):SubagentNodeResult[]{const d=join(s.meta.treeDir,"nodes");if(!existsSync(d))return [];return readdirSync(d).filter(x=>x.endsWith(".json")).sort().flatMap(f=>{const n=readJson<NodeRecord>(join(d,f));if(!n)return [];const r=readJson<SubagentChildResult>(n.resultPath);return [{nodeId:n.nodeId,parentId:n.parentId,outcome:r?.outcome??"unexpected-abort",open:n.open??(r?.outcome!=="clean"&&r?.outcome!=="empty"),...(n.sessionFile?{sessionReference:bounded(n.sessionFile,MAX_SESSION)}:{}),...(r?.error?{error:r.error}:{}),}];});}
|
|
133
|
+
function rootResult(s:State){const d=join(s.meta.treeDir,"nodes");if(!existsSync(d))return undefined;const roots=readdirSync(d).filter(x=>x.endsWith(".json")).map(f=>readJson<NodeRecord>(join(d,f))).filter((n):n is NodeRecord=>Boolean(n&&n.parentId===s.meta.callerId));if(roots.length!==1)return undefined;return readJson<SubagentChildResult>(roots[0].resultPath);}
|
|
134
|
+
async function authoritative(s:State,candidate:SubagentTreeResult):Promise<SubagentTreeResult>{const terminalPath=join(s.meta.treeDir,"terminal.json");if(exclusiveAtomic(terminalPath,candidate))return candidate;for(;;){const record=readJson<SubagentTreeResult>(terminalPath);if(record)return record;await new Promise(r=>setTimeout(r,10));}}
|
|
135
|
+
function adopt(s:State,r:SubagentTreeResult){if(s.terminal)return;s.terminal=deepFreeze(r);s.resolveTree(s.terminal);void deliver(s,s.terminal);}
|
|
136
|
+
async function finish(s:State,r:SubagentTreeResult){adopt(s,await authoritative(s,r));}
|
|
137
|
+
async function closeCleanPanes(s:State):Promise<void>{const terminal=await import("./pi-extension/subagents/terminal.ts"),dir=join(s.meta.treeDir,"nodes");for(const f of readdirSync(dir).filter(x=>x.endsWith(".json"))){const n=readJson<NodeRecord>(join(dir,f)),r=n?readJson<SubagentChildResult>(n.resultPath):undefined;if(!n||!r||!n.surface||r.worktree||(r.outcome!=="clean"&&r.outcome!=="empty")||n.open===false)continue;try{terminal.closePane(n.surface);n.open=!(await terminal.waitForPaneAbsence(n.surface,{timeoutMs:5000}));writeNode(s,n);}catch{n.open=true;writeNode(s,n);}}}
|
|
138
|
+
async function maybeFinish(s:State){if(s.terminal||s.ownerId!==s.meta.callerId)return;const existing=readJson<SubagentTreeResult>(join(s.meta.treeDir,"terminal.json"));if(existing){adopt(s,existing);return;}if(isCancelled(s)||currentTerminalIntent(s)==="cancelled")return;const root=rootResult(s);if(!root)return;let all=rows(s);if(all.some(r=>!readJson(join(s.meta.treeDir,"results",`${r.nodeId}.json`))))return;if(!claimTerminalIntent(s,"completed")){if(currentTerminalIntent(s)!=="completed")return;return;}await closeCleanPanes(s);all=rows(s);const state=root.outcome==="clean"||root.outcome==="empty"?"completed":"failed";await finish(s,{terminalId:randomUUID(),state,rootResult:root,callerMetadata:s.meta.callerMetadata,nodes:all});}
|
|
139
|
+
async function deliverAttempt(s:State,r:SubagentTreeResult){if(!s.callback||existsSync(join(s.meta.treeDir,"delivery.done")))return;const claimPath=join(s.meta.treeDir,"delivery.claim"),claim={process:processIdentity(),pid:process.pid,token:randomUUID(),terminalId:r.terminalId,version:s.callbackVersion};if(!reclaimDeadClaim(claimPath,claim))return;const callback=s.callback;try{if(!callback)return;await callback(r);if(!existsSync(join(s.meta.treeDir,"delivery.done")))writeFileSync(join(s.meta.treeDir,"delivery.done"),`${r.terminalId}\n`,{flag:"wx"});}catch{}finally{const held=readJson<{token?:string}>(claimPath);if(!existsSync(join(s.meta.treeDir,"delivery.done"))&&held?.token===claim.token)try{unlinkSync(claimPath);}catch{}}}
|
|
140
|
+
async function deliver(s:State,r:SubagentTreeResult){const attempt=s.deliverySerial.then(()=>deliverAttempt(s,r));s.deliverySerial=attempt.catch(()=>{});return attempt;}
|
|
141
|
+
async function watch(s:State,node:NodeRecord,running:PiRunningChild,pending:Deferred<SubagentChildResult>){const [{waitForCompletion},terminal,session,activity,launchModule]=await Promise.all([import("./pi-extension/subagents/completion.ts"),import("./pi-extension/subagents/terminal.ts"),import("./pi-extension/subagents/session.ts"),import("./pi-extension/subagents/activity.ts"),import("./pi-extension/subagents/launch.ts")]);const abort=new AbortController();try{const completion=await Promise.race([waitForCompletion(abort.signal,{intervalMs:100,sessionFile:running.sessionFile,readTerminalTail:()=>terminal.readPaneAsync(running.surface,5),inspectPane:()=>terminal.inspectPane(running.surface)}),(async()=>{for(;;){if(isCancelled(s))throw new Error("tree cancellation requested");const e=activity.readSubagentSettledEventsFile(running.settledEventsFile,running.id).at(-1);if(e?.outcome==="error")return {reason:"error",exitCode:1,errorMessage:e.errorMessage} as const;if(e?.outcome==="clean"||e?.outcome==="empty")return {reason:"done",exitCode:0} as const;await new Promise(r=>setTimeout(r,100));}})()]);abort.abort();const a=session.inspectNewestAppendedAssistant(running.sessionFile,running.sessionBaseline)??{id:randomUUID(),text:null,empty:true,stopReason:"error",errorMessage:"Subagent produced no settled assistant response"};const bad=completion.reason==="error"||a.stopReason==="error"||completion.exitCode!==0;if(isCancelled(s))return;let open=true;try{const inspection=await terminal.inspectPane(running.surface);const processInfo=terminal.getPaneProcessInfo(running.surface);open=inspection.kind!=="missing"||processInfo.pids.length>0;node.open=open;}catch{node.open=true;}const outcome=a.stopReason==="aborted"?"unexpected-abort":bad?"error":a.empty?"empty":"clean";const result=writeResult(s,node,{resultId:randomUUID(),assistantEntryId:a.id,answer:a.text,outcome,sessionReference:bounded(running.sessionFile,MAX_SESSION),...(outcome==="error"||outcome==="unexpected-abort"?{error:{code:a.stopReason==="error"?"provider_error":"runtime_error",message:bounded(a.errorMessage??completion.errorMessage??"Subagent failed",MAX_ERROR)}}:{}),...(node.metadata?{metadata:node.metadata}:{}),...(running.worktree?{worktree:launchModule.captureWorktreeHandoff(running.worktree)}:{})});pending.resolve(result);if(!isCancelled(s))void maybeFinish(s);}catch(e){if(!readJson(node.resultPath)&&!isCancelled(s)){const result=writeResult(s,node,failedResult("runtime_error",message(e),node.metadata));pending.resolve(result);void maybeFinish(s);}}}
|
|
142
|
+
async function launch(s:State,o:LaunchChildOptions):Promise<SubagentChildHandle>{if(s.terminal||readJson(join(s.meta.treeDir,"terminal.json"))||isCancelled(s))throw new SubagentTreeError("subagent tree is terminal");if(o.parentId!==s.ownerId)throw new SubagentTreeOwnershipError("parent is not owned by this branch");if(!o.name||!o.task)throw new SubagentTreeValidationError("child name and task are required");if(!Array.isArray(o.tools)||!o.tools.length)throw new SubagentTreeValidationError("child tools are required");const consumerMetadata=o.metadata===undefined?metadata(null):metadata(o.metadata);const role=o.agent?resolveAgent(o.agent,s.pi):undefined;if(o.agent&&!role){const d=discoverAgentCatalog(s.pi).diagnostics.find(x=>x.agentName===o.agent);throw new SubagentTreeValidationError(d?.message??`Agent ${JSON.stringify(o.agent)} was not found`);}let tools:string[];try{tools=effectiveRoleTools(o.tools,role);}catch(e){throw new SubagentTreeValidationError(message(e));}if(isCancelled(s))throw new SubagentTreeError("subagent tree is terminal");const parent=context(s.ctx);if(!s.ctx.model)throw new SubagentTreeValidationError("No parent model is selected");const thinking=s.pi.getThinkingLevel();if(typeof thinking!=="string")throw new SubagentTreeValidationError(`Unsupported thinking level: ${thinking}`);const registryAdapter=wrapPiModelRegistry((s.ctx as ExtensionContext & {modelRegistry:Parameters<typeof wrapPiModelRegistry>[0]}).modelRegistry);const plan:ResolvedRuntimePlan=resolveRuntimePlan({model:role?.model,thinking:role?.thinking},{provider:s.ctx.model.provider,modelId:s.ctx.model.id,thinking:thinking as ThinkingLevel},registryAdapter);if(o.parentId===s.meta.callerId)claimRoot(s);const id=randomUUID(),childMetadata=identityMetadata(consumerMetadata,s.meta,s.ownerId,id,o.parentId),node:NodeRecord={nodeId:id,parentId:o.parentId,ownerId:s.ownerId,name:o.name,resultPath:join(s.meta.treeDir,"results",`${id}.json`),status:"queued",metadata:childMetadata},pending=deferred<SubagentChildResult>();s.pending.set(id,pending);s.children.set(id,Object.freeze({nodeId:id,parentId:o.parentId,result:pending.promise}));writeNode(s,node);const transaction=deferred<void>(),controller=new AbortController(),inflightToken=randomUUID();s.launchTransactions.add(transaction.promise);s.launchControllers.add(controller);atomic(inflightPath(s,id),{ownerId:s.ownerId,pid:process.pid,process:processIdentity(),nodeId:id,token:inflightToken});const contextPath=join(s.meta.treeDir,"context",`${id}.json`);atomic(contextPath,{...s.meta,ownerId:id});try{if(isCancelled(s)){const r=writeResult(s,node,failedResult("tree_cancelled","subagent tree cancellation requested",childMetadata));pending.resolve(r);return s.children.get(id)!;}const {launchPiSubagent}=await import("./pi-extension/subagents/launch.ts");const effectiveAutoExit=role?role.autoExit??false:true;const running=await launchPiSubagent({kind:"fresh",id,name:o.name,task:o.task,agent:o.agent,cwd:o.cwd,worktree:o.worktree,parent:{cwd:parent.cwd,invocationCwd:process.cwd(),sessionFile:parent.sessionFile,sessionId:parent.sessionId,sessionDir:parent.sessionDir,agentDir:parent.agentDir},runtimePlan:plan,environment:{PI_SUBAGENT_TREE_CONTEXT:contextPath,PI_SUBAGENT_TREE_OWNER:"1"},signal:controller.signal,behavior:{tools:tools.join(","),skills:o.skills??role?.skills,deniedTools:[],autoExit:effectiveAutoExit,interactive:role?.interactive??!effectiveAutoExit,includeControlTools:false,identity:role?.body,systemPromptMode:role?.systemPromptMode,sessionMode:role?.sessionMode??"standalone",cwd:role?.cwd}});node.surface=running.surface;node.sessionFile=running.sessionFile;node.status="running";writeNode(s,node);if(isCancelled(s)){try{const terminal=await import("./pi-extension/subagents/terminal.ts");terminal.closePane(running.surface);node.open=!(await terminal.waitForPaneAbsence(running.surface,{timeoutMs:5000}));}catch{node.open=true;}const r=writeResult(s,node,failedResult("tree_cancelled","subagent tree cancellation requested",childMetadata));pending.resolve(r);return s.children.get(id)!;}void watch(s,node,running,pending);}catch(e){const r=writeResult(s,node,failedResult(isCancelled(s)?"tree_cancelled":"launch_failed",message(e),childMetadata));pending.resolve(r);maybeFinish(s);return s.children.get(id)!;}finally{clearInflight(s,id,inflightToken);transaction.resolve();s.launchTransactions.delete(transaction.promise);s.launchControllers.delete(controller);}return s.children.get(id)!;}
|
|
143
|
+
async function waitTerminal(s:State){for(;;){const r=readJson<SubagentTreeResult>(join(s.meta.treeDir,"terminal.json"));if(r){adopt(s,r);return r;}await new Promise(r=>setTimeout(r,25));}}
|
|
144
|
+
async function waitBranchAcks(s:State,failures:string[],includeRoot=true):Promise<void>{const owners=new Set<string>();if(includeRoot)owners.add(s.meta.callerId);for(const f of readdirSync(join(s.meta.treeDir,"nodes")).filter(x=>x.endsWith(".json"))){const n=readJson<NodeRecord>(join(s.meta.treeDir,"nodes",f));if(n)owners.add(n.ownerId);}if(!includeRoot)owners.delete(s.meta.callerId);const inspect=()=>{for(const owner of owners){const ack=readJson<{transactionsSettled?:boolean;inflight?:number;unresolvedQueued?:number;synthesized?:boolean;remainingPids?:number[]}>(join(s.meta.treeDir,"branches",`${owner}.cancelled.json`));if(!ack){failures.push(`branch cancellation acknowledgement missing: ${owner}`);continue;}const pending=pendingBranchEvidence(s,owner);if(ack.transactionsSettled===false)failures.push(`branch cancellation transaction did not settle: ${owner}`);if((ack.inflight??0)>0||pending.inflight>0)failures.push(`branch cancellation reservation remains: ${owner}`);if((ack.unresolvedQueued??0)>0||pending.queued>0)failures.push(`branch cancellation queued launch remains: ${owner}`);if(ack.synthesized&&(pending.inflight>0||pending.queued>0))failures.push(`synthesized branch cancellation evidence is incomplete: ${owner}`);if((ack.remainingPids??[]).length)failures.push(`branch processes remain: ${(ack.remainingPids??[]).join(",")}`);}};const deadline=Date.now()+CANCEL_TIMEOUT_MS;while(Date.now()<deadline){const missing=[...owners].some(owner=>!existsSync(join(s.meta.treeDir,"branches",`${owner}.cancelled.json`)));if(!missing){inspect();return;}await new Promise(r=>setTimeout(r,25));}inspect();}
|
|
145
|
+
async function cancelWork(s:State){const mux=await import("./pi-extension/subagents/terminal.ts");atomic(join(s.meta.treeDir,"cancel.request"),{process:processIdentity(),at:Date.now()});const failures:string[]=[],pids=new Set<number>();await cancelBranch(s);await waitBranchAcks(s,failures);const deadline=Date.now()+CANCEL_TIMEOUT_MS;for(const row of rows(s)){const n=readJson<NodeRecord>(join(s.meta.treeDir,"nodes",`${row.nodeId}.json`));if(!n||n.open===false)continue;if(!n.surface){if(n.status==="running"){failures.push(`missing pane identity for ${n.nodeId}`);n.open=true;}else n.open=false;const result=writeResult(s,n,failedResult("tree_cancelled",n.status==="running"?"running child lost its pane":"queued child cancelled",n.metadata));s.pending.get(n.nodeId)?.resolve(result);continue;}try{const info=mux.getPaneProcessInfo(n.surface);if(!info.pids.length&&n.status!=="settled")failures.push(`missing process identity for ${n.nodeId}`);for(const pid of info.pids)pids.add(pid);}catch(e){if(!/pane_not_found|pane not found/i.test(message(e)))failures.push(message(e));}try{mux.closePane(n.surface);}catch{ /* another branch may have closed it */ }}for(const row of rows(s)){const n=readJson<NodeRecord>(join(s.meta.treeDir,"nodes",`${row.nodeId}.json`));if(!n?.surface||n.open===false)continue;try{let absent=false;for(let attempt=0;attempt<3&&!absent;attempt++){if(attempt>0)try{mux.closePane(n.surface);}catch{}absent=await mux.waitForPaneAbsence(n.surface,{timeoutMs:5000});}n.open=!absent;writeNode(s,n);if(!absent)failures.push(`pane ${n.surface} remains`);}catch(e){failures.push(message(e));}}try{const left=await mux.waitForProcessesExit([...pids],{timeoutMs:5000});if(left.length)failures.push(`processes remain: ${left.join(",")}`);}catch(e){failures.push(message(e));}while(Date.now()<deadline&&pendingInflightRecords(s).some(x=>typeof x.pid!=="number"||processAlive(x.pid)))await new Promise(r=>setTimeout(r,25));if(pendingInflightRecords(s).some(x=>typeof x.pid!=="number"||processAlive(x.pid)))failures.push("in-flight launch reservation remains");const error=failures.length?{code:"cancel_termination_failed",message:bounded(failures.join("; "),MAX_ERROR)}:undefined;if(!error)for(const inFlight of inflightRecords(s)){if(typeof inFlight.pid!=="number"||!processAlive(inFlight.pid)){try{unlinkSync(inFlight.path);}catch{}}}const terminal:SubagentTreeResult={terminalId:randomUUID(),state:error?"failed":"cancelled",callerMetadata:s.meta.callerMetadata,rootResult:failedResult(error?error.code:"tree_cancelled",error?.message??"tree cancelled"),...(error?{error}:{}),nodes:error?rows(s).map(r=>({...r,open:true})):rows(s)};await finish(s,terminal);return s.terminal??terminal;}
|
|
146
|
+
function cancel(s:State):Promise<SubagentTreeResult>{const existing=readJson<SubagentTreeResult>(join(s.meta.treeDir,"terminal.json"));if(existing){adopt(s,existing);return Promise.resolve(existing);}if(s.cancellation)return s.cancellation;if(currentTerminalIntent(s)==="completed"){s.cancellation=waitTerminal(s);return s.cancellation;}if(!claimTerminalIntent(s,"cancelled")){if(currentTerminalIntent(s)!=="cancelled"){s.cancellation=waitTerminal(s);return s.cancellation;}}s.cancellationRequested=true;s.cancellation=cancelWork(s);return s.cancellation;}
|
|
147
|
+
function sync(s:State){const terminal=readJson<SubagentTreeResult>(join(s.meta.treeDir,"terminal.json"));if(terminal)adopt(s,terminal);for(const [id,p] of s.pending){const n=readJson<NodeRecord>(nodePath(s,id));const r=n?readJson<SubagentChildResult>(n.resultPath):undefined;if(r)p.resolve(deepFreeze(r));}if(isCancelled(s)&&!s.branchCancellation&&s.ownerId!==s.meta.callerId)void cancelBranch(s);maybeFinish(s);}
|
|
148
|
+
function makeState(meta:TreeMeta,pi:ExtensionAPI,ctx:ExtensionContext,ownerId:string):State{claimBranch(meta,ownerId);let resolveTree!:(r:SubagentTreeResult)=>void;const treePromise=new Promise<SubagentTreeResult>(r=>{resolveTree=r;});const s:State={meta,ownerId,pi,ctx,boundSessionFile:ownerSession(meta,ownerId)??context(ctx).sessionFile,children:new Map(),pending:new Map(),launchTransactions:new Set(),launchControllers:new Set(),callbackVersion:0,deliverySerial:Promise.resolve(),resolveTree,treePromise,cancellationRequested:false};const dir=join(meta.treeDir,"nodes");if(existsSync(dir))for(const f of readdirSync(dir).filter(x=>x.endsWith(".json"))){const n=readJson<NodeRecord>(join(dir,f));if(n?.ownerId===ownerId){const p=deferred<SubagentChildResult>();s.pending.set(n.nodeId,p);const r=readJson<SubagentChildResult>(n.resultPath);if(r)p.resolve(deepFreeze(r));s.children.set(n.nodeId,Object.freeze({nodeId:n.nodeId,parentId:n.parentId,result:p.promise}));}}const terminal=readJson<SubagentTreeResult>(join(meta.treeDir,"terminal.json"));if(terminal)adopt(s,terminal);const timer=setInterval(()=>sync(s),100);timer.unref();return s;}
|
|
149
|
+
function readonlyChildren(s:State):ReadonlyMap<string,SubagentChildHandle>{const view={get size(){return s.children.size;},get:(key:string)=>s.children.get(key),has:(key:string)=>s.children.has(key),keys:()=>s.children.keys(),values:()=>s.children.values(),entries:()=>s.children.entries(),forEach:(fn:(value:SubagentChildHandle,key:string,map:ReadonlyMap<string,SubagentChildHandle>)=>void)=>s.children.forEach((value,key)=>fn(value,key,view)),[Symbol.iterator]:()=>s.children[Symbol.iterator]()};return Object.freeze(view) as ReadonlyMap<string,SubagentChildHandle>;}
|
|
150
|
+
function expose(s:State):SubagentTreeHandle{return Object.freeze({get callerId(){return s.meta.callerId;},get treeId(){return s.meta.treeId;},get ownershipToken(){return s.meta.ownershipToken;},get ownerId(){return s.ownerId;},get children(){return readonlyChildren(s);},get result(){sync(s);return s.treePromise;},launchChild:(o:LaunchChildOptions)=>launch(s,o),bindFinalCallback:(cb:(r:SubagentTreeResult)=>void|Promise<void>)=>{if(s.ownerId!==s.meta.callerId)throw new SubagentTreeOwnershipError("only the original caller may bind the final callback");s.callbackVersion++;s.callback=cb;if(s.terminal)void deliver(s,s.terminal);},cancel:()=>cancel(s)});}
|
|
151
|
+
/** Create a caller-owned tree. Importing this module performs no registration. */
|
|
152
|
+
export function createSubagentTree(options:CreateSubagentTreeOptions):SubagentTreeHandle{const baseMetadata=metadata(options.metadata === undefined ? null : options.metadata),c=context(options.ctx),treeId=randomUUID(),callerId=randomUUID(),token=randomUUID(),treeDir=join(c.sessionDir,"artifacts",c.sessionId,"subagent-trees",treeId),callerMetadata=deepFreeze(identityMetadata(baseMetadata,{callerId,treeId} as TreeMeta,callerId,callerId)),meta:TreeMeta={version:2,treeId,callerId,ownershipToken:token,artifactDir:join(c.sessionDir,"artifacts",c.sessionId),treeDir,callerMetadata,boundSessionFile:c.sessionFile,acceptedSessionFiles:{[callerId]:[c.sessionFile]}};mkdirSync(join(treeDir,"nodes"),{recursive:true});atomic(join(treeDir,"tree.json"),meta);const s=makeState(meta,options.pi,options.ctx,callerId);registry().set(stateKey(treeId,callerId),s);return expose(s);}
|
|
153
|
+
/** Reattach a caller or claim a child branch using its package context. */
|
|
154
|
+
export function attachSubagentTree(options:AttachSubagentTreeOptions):SubagentTreeHandle{const contextPath=process.env.PI_SUBAGENT_TREE_CONTEXT,ctxMeta=contextPath?readJson<TreeMeta&{ownerId?:string}>(contextPath):undefined,values=[options.callerId,options.treeId,options.ownershipToken],has=values.some(x=>x!==undefined);if(has&&values.some(x=>x===undefined))throw new SubagentTreeOwnershipError("callerId, treeId, and ownershipToken must be supplied together");const treeId=options.treeId??ctxMeta?.treeId,callerId=options.callerId??ctxMeta?.callerId,token=options.ownershipToken??ctxMeta?.ownershipToken;if(!treeId||!callerId||!token)throw new SubagentTreeOwnershipError("tree credentials are required");if(ctxMeta&&(treeId!==ctxMeta.treeId||callerId!==ctxMeta.callerId||token!==ctxMeta.ownershipToken))throw new SubagentTreeOwnershipError("invalid tree credentials");const meta=readJson<TreeMeta>(join(ctxMeta?.treeDir??"","tree.json"))??(ctxMeta??findTreeMeta(context(options.ctx).sessionDir,treeId));if(!meta||meta.treeId!==treeId||meta.callerId!==callerId||meta.ownershipToken!==token)throw new SubagentTreeOwnershipError("invalid tree credentials");const ownerId=ctxMeta?.ownerId??callerId;if(ownerId!==callerId&&!readJson<NodeRecord>(join(meta.treeDir,"nodes",`${ownerId}.json`)))throw new SubagentTreeOwnershipError("unknown branch owner");const keyValue=stateKey(treeId,ownerId),existing=registry().get(keyValue)??[...registry().values()].find(v=>v.meta.treeId===treeId&&v.meta.callerId===callerId&&v.ownerId===ownerId),freshSession=context(options.ctx).sessionFile,prior=existing?.boundSessionFile??ownerSession(meta,ownerId),initialContext=Boolean(ctxMeta&&!existing),knownSession=meta.acceptedSessionFiles?.[ownerId]?.includes(freshSession)===true;if(!initialContext&&!knownSession&&options.forkLineage===undefined&&header(freshSession)&&prior!==freshSession)throw new SubagentTreeOwnershipError("fork lineage is required for a fork session");const fresh=initialContext||knownSession?freshSession:validateLineage(meta,ownerId,options.ctx,options.forkLineage,prior);if(initialContext){const claimPath=`${contextPath}.claim`;if(!exclusiveAtomic(claimPath,{process:processIdentity(),ownerId,token:randomUUID()}))throw new SubagentTreeOwnershipError("package tree context was already claimed");}const s=existing??makeState(meta,options.pi,options.ctx,ownerId);s.pi=options.pi;s.ctx=options.ctx;s.boundSessionFile=fresh;meta.acceptedSessionFiles??={};meta.acceptedSessionFiles[ownerId]??=[];if(!meta.acceptedSessionFiles[ownerId].includes(fresh))meta.acceptedSessionFiles[ownerId].push(fresh);if(ownerId===callerId){meta.boundSessionFile=fresh;atomic(join(meta.treeDir,"tree.json"),meta);}else{const node=readJson<NodeRecord>(join(meta.treeDir,"nodes",`${ownerId}.json`));if(node){node.sessionFile=fresh;atomic(join(meta.treeDir,"nodes",`${ownerId}.json`),node);}}registry().set(keyValue,s);return expose(s);}
|
|
155
|
+
function deepFreeze<T>(value:T):T{if(!value||typeof value!=="object"||Object.isFrozen(value))return value;for(const child of Object.values(value as Record<string,unknown>))deepFreeze(child);return Object.freeze(value);}
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-replicate",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "One Pi agent makes copies of itself to work on independent tasks in parallel",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"herdr",
|
|
9
|
+
"subagents",
|
|
10
|
+
"agents",
|
|
11
|
+
"worktrees"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Giuseppe Rodriguez",
|
|
16
|
+
"url": "https://github.com/giuseppecrj"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/alexshpunt/pi-agent-replicate.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/alexshpunt/pi-agent-replicate/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/alexshpunt/pi-agent-replicate#readme",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"types": "./index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"import": "./index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"changelog": "auto-changelog",
|
|
39
|
+
"lint": "oxlint pi-extension test",
|
|
40
|
+
"test": "node --experimental-strip-types --test test/replica-directory-settings.test.ts test/replica-shared-work.test.ts test/replica-startup-cleanup.test.ts test/replica-startup-error.test.ts test/reported-launch.test.ts test/session-work-lifecycle.test.ts test/session-work.test.ts test/test.ts test/replication.test.ts test/delayed-output.test.ts test/replica-branch.test.ts test/source-state.test.ts test/replica-work.test.ts test/replica-background.test.ts test/launch.test.ts test/lineage.test.ts test/session-binding.test.ts test/session-binding-reads.test.ts test/runtime-routing.test.ts test/release-workflow.test.ts test/settled-contract.test.ts test/settled-observer.test.ts test/public-subagent-tree-api.test.ts test/package-skill.test.js test/result-file.test.ts test/delivery-payload.test.ts test/status-framing.test.ts test/delivery-log.test.ts test/delivery-truth.test.ts test/sidecar-ack.test.ts test/delivery-uniformity.test.ts test/delivery-recovery.test.ts test/watchdog.test.ts test/delivery-storm.test.ts",
|
|
41
|
+
"test:integration": "node --experimental-strip-types test/integration/run-isolated.ts",
|
|
42
|
+
"test:integration:live": "PI_TEST_LIVE=1 node --experimental-strip-types test/integration/run-isolated.ts",
|
|
43
|
+
"version": "auto-changelog --package && git add CHANGELOG.md"
|
|
44
|
+
},
|
|
45
|
+
"auto-changelog": {
|
|
46
|
+
"commitLimit": false,
|
|
47
|
+
"template": "keepachangelog",
|
|
48
|
+
"unreleased": true
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.84.4 <0.85.0",
|
|
52
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4 <0.85.0",
|
|
53
|
+
"@earendil-works/pi-tui": ">=0.84.4 <0.85.0",
|
|
54
|
+
"@sinclair/typebox": "*"
|
|
55
|
+
},
|
|
56
|
+
"pi": {
|
|
57
|
+
"extensions": [
|
|
58
|
+
"./pi-extension/subagents/index.ts"
|
|
59
|
+
],
|
|
60
|
+
"image": "https://raw.githubusercontent.com/alexshpunt/pi-agent-replicate/main/assets/banner.png"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
64
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
65
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
66
|
+
"@sinclair/typebox": "^0.34.52",
|
|
67
|
+
"@types/node": "^26.6.4",
|
|
68
|
+
"auto-changelog": "^2.6.0",
|
|
69
|
+
"oxlint": "^1.73.0",
|
|
70
|
+
"typescript": "^7.0.2"
|
|
71
|
+
}
|
|
72
|
+
}
|