@henryqw/pi-subagent 8.0.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +2 -4
- package/README.md +1 -1
- package/dist/index.d.ts +0 -38
- package/dist/index.js +0 -230
- package/docs/orchestration.md +0 -2
- package/package.json +1 -2
package/CONTEXT.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Provide validated built-in and user Roles, shared task-model Pi launch policy, generic
|
|
5
|
+
Provide validated built-in and user Roles, shared task-model Pi launch policy, generic `delegate_task` delegation, and package-owned `delegate_flow` Git orchestration. Main plans and orchestrates; `delegate_task` remains a flat bounded delegation tool, while Flow uses the effective Implementer and only invokes its effective Reviewer for explicit judgment criteria after authoritative validation. The bundled Main-side Skill adds no runtime behavior or changes generic fallback.
|
|
6
6
|
|
|
7
7
|
## Domain glossary
|
|
8
8
|
|
|
@@ -20,12 +20,11 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
|
|
|
20
20
|
- **Resource Policy**: Role ownership of base tools, extensions, and Skill names, plus explicit caller additions of tools, extensions, and environment through `createRoleLaunch`.
|
|
21
21
|
- **Pi Launch**: reusable `{env,args}` policy for one Role, resolved model route, explicit caller resources, and project trust.
|
|
22
22
|
- **Ephemeral Executor**: mechanism that receives a prepared Pi Launch, runs one bounded Delegated Task in one no-session child process, and returns its result without discovering resources or composing a Workflow.
|
|
23
|
-
- **Managed Subagent**: Pi agent hosted in a reconciled Herdr tab or pane; lifecycle orchestration remains with the caller.
|
|
24
23
|
|
|
25
24
|
## Invariants
|
|
26
25
|
|
|
27
26
|
- One Delegated Task creates one ephemeral child process and no saved session. Execution ends at the first hard budget: attempted turn 51 by default (`maxTurns`, positive safe integer) or `deadline = min(last recognized Pi JSON event + idle timeout, child start + maximum runtime)` (recognized Pi events renew; raw bytes do not; max always terminates). A terminal turn 50 succeeds; attempted continuation rejects with `turn_limit`, accumulated usage, and bounded output. On continuing `turn_end`, the existing child Role tool extension steers the fixed convergence warning once at 80% of completed turns and once at 80% of maximum runtime, combining thresholds first due together and never starting a warning timer or extra turn. After direct Pi exits, inherited stdout/stderr drain until EOF unless an escaped descendant holds them past short inactivity or a one-second hard deadline. Configurable in `~/.pi/agent/config/pi-subagent/pi-subagent.json` (`maxTurns` default 50; `timeout.idleMinutes`/`maxMinutes` defaults 10/30).
|
|
28
|
-
- Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/pi-subagent.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout.
|
|
27
|
+
- Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/pi-subagent.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout.
|
|
29
28
|
- Ambient child extensions and Skills stay disabled. Every Role requires `tools`, `extensions`, and `skills` YAML arrays, and every launch installs the Role tool policy. `tools: []` activates no base built-ins but does activate all tools from explicitly selected trusted extension bundles and explicit caller tool additions; `skills: []` selects no separately named Role Skills but trusted selected extension Skills still load; `extensions: []` selects no Role extension bundle. A Role/caller explicitly selected extension is a trusted atomic capability bundle: all tools it registers and all Skills supplied through its Pi package metadata or dynamic `resources_discover` load alongside separately named Role Skills. This intentionally includes the extension's executable lifecycle/prompt behavior; pi-subagent does not infer or externally narrow undocumented dependencies, and loading an extension is not sandboxing. Scope children by selecting fewer trusted extensions; finer granularity requires separate entry points/configuration or an upstream split. Explicit Role/caller tool names still verify against the final filtered registry, while parent-only recursive orchestration tools remain excluded.
|
|
30
29
|
- Role Skill names resolve through Main's effective Pi Skill registry; unavailable names warn and skip without blocking delegation. Explicit Role/caller tool names verify against the final filtered child registry after explicit provider `session_start` handlers, and unavailable names fail before the first turn.
|
|
31
30
|
- Main policy populates direct `model` and `thinking` only for explicit user overrides; otherwise it selects only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This has no provenance tracking or runtime enforcement. An omitted class uses pi-subagent's local `pi-subagent/delegateTask` Model Task declaration (default `fast`); library callers select a Role plus their own Model Task declaration.
|
|
@@ -35,4 +34,3 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
|
|
|
35
34
|
- Flow is memory-only. Only a post-rebase commit drop produces a no-op (`base === tip`); it validates, skips Reviewer and merge, then cleans up ordinarily. Initial zero-commit implementations block. Implementer, validation, or reviewer blocks allow one `delegate_flow_continue({ guidance, modelClass? })` repair in the same worktree; omission retains the Unit's current class and presence replaces it for that repair. A second block is terminal. Rebase and evidence/Reviewer/infrastructure failures retain worktrees. A reported fast-forward failure retains its worktree unless Main is clean at the exact integrated tip, which completes with the merge diagnostic as a warning. Cleanup uses non-forced worktree removal and branch deletion; cleanup refusal is a completion warning.
|
|
36
35
|
- Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge validation. It never changes generic `delegate_task` Role resolution, isolation, non-Git fallback, or ordinary direct plan/file review.
|
|
37
36
|
- Numbered Codex routes prefer Main's active account slot and explicitly load the multi-Codex child extension.
|
|
38
|
-
- Generic Herdr host functions validate workspace ownership and provisioning identity while callers retain domain state, prompts, and lifecycle decisions.
|
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ Scope a child by selecting fewer trusted extensions. Finer-grained selection req
|
|
|
252
252
|
|
|
253
253
|
## Library API
|
|
254
254
|
|
|
255
|
-
The package root exports Role loading and launch resolution, `createEphemeralSubagentExecutor`,
|
|
255
|
+
The package root exports Role loading and launch resolution, `createEphemeralSubagentExecutor`, and worktree helpers.
|
|
256
256
|
|
|
257
257
|
The executor is for code already running inside active Pi. It does not provide standalone Node.js Pi discovery or launch support.
|
|
258
258
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type HerdrExecutor } from "@henryqw/pi-herdr";
|
|
3
2
|
import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
4
3
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
5
4
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
@@ -60,40 +59,3 @@ export declare function resolveTaskRoute(ctx: ExtensionContext, profileName: Pro
|
|
|
60
59
|
export declare function resolveRoleSkills(pi: Pick<ExtensionAPI, "getCommands">, role: Role): ResolvedRoleSkills;
|
|
61
60
|
export declare function createRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: Pick<ExtensionContext, "isProjectTrusted">, input: CreateRoleLaunchInput): ResolvedRoleLaunch;
|
|
62
61
|
export declare function resolveRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveRoleLaunchInput): ResolvedRoleLaunch;
|
|
63
|
-
export interface ManagedSubagentHost {
|
|
64
|
-
cwd: string;
|
|
65
|
-
workspaceId: string;
|
|
66
|
-
}
|
|
67
|
-
export interface ManagedSubagentCommandOptions {
|
|
68
|
-
cwd: string;
|
|
69
|
-
}
|
|
70
|
-
export type ManagedSubagentExecutor = HerdrExecutor<ManagedSubagentCommandOptions>;
|
|
71
|
-
export interface ManagedSubagentHostOptions {
|
|
72
|
-
execute: ManagedSubagentExecutor;
|
|
73
|
-
delay?: (milliseconds: number) => Promise<void>;
|
|
74
|
-
}
|
|
75
|
-
export interface ManagedSubagentTab {
|
|
76
|
-
tabId: string;
|
|
77
|
-
paneId: string;
|
|
78
|
-
}
|
|
79
|
-
export declare function managedSubagentName(workspaceId: string, ...identity: string[]): string;
|
|
80
|
-
export declare function managedSubagentWorkspaceId(cwd: string, mainPane: string, options: ManagedSubagentHostOptions): Promise<string>;
|
|
81
|
-
/** Returns pane ID to Herdr status for agents owned by this workspace. */
|
|
82
|
-
export declare function listManagedSubagents(host: ManagedSubagentHost, options: ManagedSubagentHostOptions): Promise<Map<string, string>>;
|
|
83
|
-
export declare function createManagedSubagentTab(host: ManagedSubagentHost, cwd: string, launch: PiLaunch, label: string, options: ManagedSubagentHostOptions): Promise<ManagedSubagentTab>;
|
|
84
|
-
export declare function reconcileManagedSubagentTab(host: ManagedSubagentHost, input: {
|
|
85
|
-
tabId?: string;
|
|
86
|
-
paneId?: string;
|
|
87
|
-
cwd: string;
|
|
88
|
-
launch: PiLaunch;
|
|
89
|
-
label: string;
|
|
90
|
-
}, options: ManagedSubagentHostOptions): Promise<ManagedSubagentTab>;
|
|
91
|
-
export declare function findManagedSubagentTab(host: ManagedSubagentHost, label: string, options: ManagedSubagentHostOptions): Promise<ManagedSubagentTab | undefined>;
|
|
92
|
-
export declare function managedSubagentTabExists(host: ManagedSubagentHost, tabId: string, options: ManagedSubagentHostOptions): Promise<boolean>;
|
|
93
|
-
export declare function reconcileManagedSubagentPane(host: ManagedSubagentHost, tabId: string, rootPaneId: string, cwd: string, launch: PiLaunch, label: string, options: ManagedSubagentHostOptions): Promise<string>;
|
|
94
|
-
export declare function startManagedSubagent(host: ManagedSubagentHost, agent: string, pane: string, launch: PiLaunch, options: ManagedSubagentHostOptions, hooks?: {
|
|
95
|
-
beforeStart?: () => Promise<void>;
|
|
96
|
-
onStarted?: () => Promise<void>;
|
|
97
|
-
}): Promise<"existing" | "started">;
|
|
98
|
-
export declare function promptManagedSubagent(host: ManagedSubagentHost, agent: string, prompt: string | Record<string, unknown>, options: ManagedSubagentHostOptions): Promise<void>;
|
|
99
|
-
export declare function retireManagedSubagentTab(host: ManagedSubagentHost, tabId: string, options: ManagedSubagentHostOptions): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
3
2
|
import { isAbsolute, join } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { createHerdrClient, herdrCommandFailure, hasHerdrErrorCode, startPiAgent } from "@henryqw/pi-herdr";
|
|
7
5
|
import { modelReference, orderedProfileRoutes, readTaskModelsConfig, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
8
6
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
9
7
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
|
|
@@ -197,231 +195,3 @@ export function resolveRoleLaunch(pi, ctx, input) {
|
|
|
197
195
|
route: resolveConfiguredTaskRoute(ctx, input.task, input.agentDir),
|
|
198
196
|
});
|
|
199
197
|
}
|
|
200
|
-
function launchEnvironmentArgs(launch) {
|
|
201
|
-
return Object.entries(launch.env).flatMap(([key, value]) => {
|
|
202
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0")) {
|
|
203
|
-
throw new Error(`Invalid launch environment: ${key}`);
|
|
204
|
-
}
|
|
205
|
-
return ["--env", `${key}=${value}`];
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
export function managedSubagentName(workspaceId, ...identity) {
|
|
209
|
-
const parts = [nonEmptyString(workspaceId, "Herdr workspace id"), ...identity.map((part, index) => nonEmptyString(part, `Subagent identity ${index}`))];
|
|
210
|
-
if (!identity.length)
|
|
211
|
-
throw new Error("Managed Subagent identity is required");
|
|
212
|
-
return `subagent-${createHash("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 23)}`;
|
|
213
|
-
}
|
|
214
|
-
export async function managedSubagentWorkspaceId(cwd, mainPane, options) {
|
|
215
|
-
const paneId = nonEmptyString(mainPane, "recorded main Herdr pane");
|
|
216
|
-
const pane = (await listPanes(cwd, options))
|
|
217
|
-
.map((entry, index) => object(entry, `Herdr pane ${index}`))
|
|
218
|
-
.find((entry) => entry.pane_id === paneId);
|
|
219
|
-
if (!pane)
|
|
220
|
-
throw new Error(`Recorded main Herdr pane is missing: ${paneId}`);
|
|
221
|
-
return nonEmptyString(pane.workspace_id, "recorded main Herdr workspace");
|
|
222
|
-
}
|
|
223
|
-
/** Returns pane ID to Herdr status for agents owned by this workspace. */
|
|
224
|
-
export async function listManagedSubagents(host, options) {
|
|
225
|
-
const workspaceId = nonEmptyString(host.workspaceId, "recorded Herdr workspace");
|
|
226
|
-
const response = object(await createHerdrClient(options.execute).json(["agent", "list"], { cwd: host.cwd }), "Herdr agent list response");
|
|
227
|
-
const result = object(response.result, "Herdr agent list result");
|
|
228
|
-
return new Map(array(result.agents, "Herdr agents").flatMap((entry, index) => {
|
|
229
|
-
const agent = object(entry, `Herdr agent ${index}`);
|
|
230
|
-
if (nonEmptyString(agent.workspace_id, `Herdr agent ${index} workspace`) !== workspaceId)
|
|
231
|
-
return [];
|
|
232
|
-
return [[
|
|
233
|
-
nonEmptyString(agent.pane_id, `Herdr agent ${index} pane`),
|
|
234
|
-
nonEmptyString(agent.agent_status, `Herdr agent ${index} status`),
|
|
235
|
-
]];
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
export async function createManagedSubagentTab(host, cwd, launch, label, options) {
|
|
239
|
-
const response = await createHerdrClient(options.execute).json([
|
|
240
|
-
"tab", "create", "--workspace", nonEmptyString(host.workspaceId, "recorded Herdr workspace"), "--cwd", cwd,
|
|
241
|
-
...launchEnvironmentArgs(launch), "--label", nonEmptyString(label, "Herdr tab label"), "--no-focus",
|
|
242
|
-
], { cwd: host.cwd });
|
|
243
|
-
const result = object(object(response, "Herdr tab response").result, "Herdr tab result");
|
|
244
|
-
return {
|
|
245
|
-
tabId: nonEmptyString(object(result.tab, "Herdr tab").tab_id, "Herdr tab id"),
|
|
246
|
-
paneId: nonEmptyString(object(result.root_pane, "Herdr root pane").pane_id, "Herdr root pane id"),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
export async function reconcileManagedSubagentTab(host, input, options) {
|
|
250
|
-
if (input.tabId && input.paneId && await managedSubagentTabExists(host, input.tabId, options)) {
|
|
251
|
-
return { tabId: input.tabId, paneId: input.paneId };
|
|
252
|
-
}
|
|
253
|
-
return await findManagedSubagentTab(host, input.label, options)
|
|
254
|
-
?? await createManagedSubagentTab(host, input.cwd, input.launch, input.label, options);
|
|
255
|
-
}
|
|
256
|
-
export async function findManagedSubagentTab(host, label, options) {
|
|
257
|
-
const workspaceId = nonEmptyString(host.workspaceId, "recorded Herdr workspace");
|
|
258
|
-
const matches = (await listTabs(host.cwd, options))
|
|
259
|
-
.map((entry, index) => object(entry, `Herdr tab ${index}`))
|
|
260
|
-
.filter((tab, index) => nonEmptyString(tab.workspace_id, `Herdr tab ${index} workspace`) === workspaceId)
|
|
261
|
-
.filter((tab) => tab.label === label);
|
|
262
|
-
if (matches.length > 1)
|
|
263
|
-
throw new Error(`Multiple Herdr tabs match provisioning identity: ${label}`);
|
|
264
|
-
if (!matches.length)
|
|
265
|
-
return undefined;
|
|
266
|
-
const tabId = nonEmptyString(object(matches[0], "Herdr tab").tab_id, "Herdr tab id");
|
|
267
|
-
const panes = (await listPanes(host.cwd, options))
|
|
268
|
-
.filter((entry, index) => object(entry, `Herdr pane ${index}`).tab_id === tabId);
|
|
269
|
-
if (panes.length !== 1)
|
|
270
|
-
throw new Error(`Provisioned Herdr tab ${tabId} must contain exactly one root pane`);
|
|
271
|
-
return { tabId, paneId: nonEmptyString(object(panes[0], "Herdr pane").pane_id, "Herdr pane id") };
|
|
272
|
-
}
|
|
273
|
-
export async function managedSubagentTabExists(host, tabId, options) {
|
|
274
|
-
const expected = nonEmptyString(host.workspaceId, "recorded Herdr workspace");
|
|
275
|
-
const id = nonEmptyString(tabId, "Herdr tab id");
|
|
276
|
-
const tab = (await listTabs(host.cwd, options))
|
|
277
|
-
.map((entry, index) => object(entry, `Herdr tab ${index}`))
|
|
278
|
-
.find((entry) => entry.tab_id === id);
|
|
279
|
-
if (!tab)
|
|
280
|
-
return false;
|
|
281
|
-
const actual = nonEmptyString(tab.workspace_id, `Herdr tab ${id} workspace`);
|
|
282
|
-
if (actual !== expected)
|
|
283
|
-
throw new Error(`Herdr tab ${id} belongs to workspace ${actual}, expected initiating workspace ${expected}`);
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
export async function reconcileManagedSubagentPane(host, tabId, rootPaneId, cwd, launch, label, options) {
|
|
287
|
-
const tab = nonEmptyString(tabId, "Herdr tab id");
|
|
288
|
-
const workspace = nonEmptyString(host.workspaceId, "recorded Herdr workspace");
|
|
289
|
-
const ownerTab = (await listTabs(host.cwd, options))
|
|
290
|
-
.map((entry, index) => object(entry, `Herdr tab ${index}`))
|
|
291
|
-
.find((entry) => entry.tab_id === tab);
|
|
292
|
-
if (!ownerTab)
|
|
293
|
-
throw new Error(`Herdr tab is missing: ${tab}`);
|
|
294
|
-
const ownerWorkspace = nonEmptyString(ownerTab.workspace_id, `Herdr tab ${tab} workspace`);
|
|
295
|
-
if (ownerWorkspace !== workspace)
|
|
296
|
-
throw new Error(`Herdr tab ${tab} belongs to workspace ${ownerWorkspace}, expected initiating workspace ${workspace}`);
|
|
297
|
-
const root = nonEmptyString(rootPaneId, "Herdr root pane");
|
|
298
|
-
const panes = (await listPanes(host.cwd, options)).map((entry, index) => object(entry, `Herdr pane ${index}`));
|
|
299
|
-
const owner = panes.find((pane) => pane.pane_id === root);
|
|
300
|
-
if (!owner)
|
|
301
|
-
throw new Error(`Herdr root pane is missing: ${root}`);
|
|
302
|
-
if (owner.tab_id !== tab)
|
|
303
|
-
throw new Error(`Herdr root pane ${root} does not belong to tab ${tab}`);
|
|
304
|
-
const siblings = panes.filter((pane) => pane.tab_id === tab && pane.pane_id !== root);
|
|
305
|
-
const named = siblings.filter((pane) => pane.label === label);
|
|
306
|
-
if (named.length > 1)
|
|
307
|
-
throw new Error(`Multiple Herdr panes match provisioning identity: ${label}`);
|
|
308
|
-
if (named.length)
|
|
309
|
-
return nonEmptyString(named[0].pane_id, "Herdr Subagent pane id");
|
|
310
|
-
if (siblings.length > 1)
|
|
311
|
-
throw new Error(`Provisioned Herdr tab ${tab} has multiple Subagent panes`);
|
|
312
|
-
if (siblings.length)
|
|
313
|
-
return nonEmptyString(siblings[0].pane_id, "Herdr Subagent pane id");
|
|
314
|
-
const herdr = createHerdrClient(options.execute);
|
|
315
|
-
const response = await herdr.json([
|
|
316
|
-
"pane", "split", "--pane", root, "--direction", "right", "--cwd", cwd,
|
|
317
|
-
...launchEnvironmentArgs(launch), "--no-focus",
|
|
318
|
-
], { cwd: host.cwd });
|
|
319
|
-
const result = object(object(response, "Herdr pane response").result, "Herdr pane result");
|
|
320
|
-
const pane = nonEmptyString(object(result.pane, "Herdr Subagent pane").pane_id, "Herdr Subagent pane id");
|
|
321
|
-
await herdr.run(["pane", "rename", pane, nonEmptyString(label, "Herdr pane label")], { cwd: host.cwd });
|
|
322
|
-
return pane;
|
|
323
|
-
}
|
|
324
|
-
export async function startManagedSubagent(host, agent, pane, launch, options, hooks = {}) {
|
|
325
|
-
assertAgentName(agent);
|
|
326
|
-
const name = nonEmptyString(agent, "Herdr agent name");
|
|
327
|
-
const paneId = nonEmptyString(pane, "Herdr agent pane");
|
|
328
|
-
const existing = await getManagedSubagent(host, name, options);
|
|
329
|
-
if (existing) {
|
|
330
|
-
assertAgentPane(name, paneId, existing);
|
|
331
|
-
return "existing";
|
|
332
|
-
}
|
|
333
|
-
await hooks.beforeStart?.();
|
|
334
|
-
const herdr = createHerdrClient(options.execute);
|
|
335
|
-
const startArgs = ["agent", "start", name, "--kind", "pi", "--pane", paneId, "--", ...launch.args];
|
|
336
|
-
const result = await startPiAgent(herdr, {
|
|
337
|
-
name,
|
|
338
|
-
pane: paneId,
|
|
339
|
-
args: launch.args,
|
|
340
|
-
options: { cwd: host.cwd },
|
|
341
|
-
delay: options.delay,
|
|
342
|
-
});
|
|
343
|
-
if (result.code === 0 && !result.killed) {
|
|
344
|
-
await hooks.onStarted?.();
|
|
345
|
-
return "started";
|
|
346
|
-
}
|
|
347
|
-
if (hasHerdrErrorCode(result, "agent_name_taken")) {
|
|
348
|
-
const raced = await getManagedSubagent(host, name, options);
|
|
349
|
-
if (!raced)
|
|
350
|
-
throw new Error(`Herdr agent ${name} reported agent_name_taken but could not be found; refusing to start a duplicate`);
|
|
351
|
-
assertAgentPane(name, paneId, raced);
|
|
352
|
-
return "existing";
|
|
353
|
-
}
|
|
354
|
-
throw new Error(herdrCommandFailure(startArgs, result));
|
|
355
|
-
}
|
|
356
|
-
export async function promptManagedSubagent(host, agent, prompt, options) {
|
|
357
|
-
assertAgentName(agent);
|
|
358
|
-
const text = typeof prompt === "string" ? nonEmptyString(prompt, "Subagent prompt") : JSON.stringify(prompt);
|
|
359
|
-
await createHerdrClient(options.execute).run(["agent", "prompt", agent, text], { cwd: host.cwd });
|
|
360
|
-
}
|
|
361
|
-
export async function retireManagedSubagentTab(host, tabId, options) {
|
|
362
|
-
const id = nonEmptyString(tabId, "Herdr tab id");
|
|
363
|
-
try {
|
|
364
|
-
if (!(await managedSubagentTabExists(host, id, options)))
|
|
365
|
-
return;
|
|
366
|
-
await createHerdrClient(options.execute).run(["tab", "close", id], { cwd: host.cwd });
|
|
367
|
-
}
|
|
368
|
-
catch (error) {
|
|
369
|
-
if (!(await confirmsTabAbsent(host, id, options)))
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function assertAgentName(agent) {
|
|
374
|
-
if (!/^[a-z][a-z0-9_-]{0,31}$/.test(agent))
|
|
375
|
-
throw new Error(`Invalid Herdr agent name: ${agent}`);
|
|
376
|
-
}
|
|
377
|
-
function object(value, label) {
|
|
378
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
379
|
-
throw new Error(`${label} must be an object`);
|
|
380
|
-
return value;
|
|
381
|
-
}
|
|
382
|
-
function array(value, label) {
|
|
383
|
-
if (!Array.isArray(value))
|
|
384
|
-
throw new Error(`${label} must be an array`);
|
|
385
|
-
return value;
|
|
386
|
-
}
|
|
387
|
-
function nonEmptyString(value, label) {
|
|
388
|
-
if (typeof value !== "string")
|
|
389
|
-
throw new Error(`${label} must be a string`);
|
|
390
|
-
if (!value.trim() || value.includes("\0"))
|
|
391
|
-
throw new Error(`${label} must not be empty`);
|
|
392
|
-
return value;
|
|
393
|
-
}
|
|
394
|
-
async function listTabs(cwd, options) {
|
|
395
|
-
const response = await createHerdrClient(options.execute).json(["tab", "list"], { cwd });
|
|
396
|
-
return array(object(object(response, "Herdr tab list response").result, "Herdr tab list result").tabs, "Herdr tabs");
|
|
397
|
-
}
|
|
398
|
-
async function listPanes(cwd, options) {
|
|
399
|
-
const response = await createHerdrClient(options.execute).json(["pane", "list"], { cwd });
|
|
400
|
-
return array(object(object(response, "Herdr pane list response").result, "Herdr pane list result").panes, "Herdr panes");
|
|
401
|
-
}
|
|
402
|
-
async function getManagedSubagent(host, name, options) {
|
|
403
|
-
const arguments_ = ["agent", "get", name];
|
|
404
|
-
const result = await createHerdrClient(options.execute).exec(arguments_, { cwd: host.cwd });
|
|
405
|
-
if (result.code !== 0 || result.killed) {
|
|
406
|
-
if (hasHerdrErrorCode(result, "agent_not_found"))
|
|
407
|
-
return undefined;
|
|
408
|
-
throw new Error(herdrCommandFailure(arguments_, result));
|
|
409
|
-
}
|
|
410
|
-
const response = object(JSON.parse(result.stdout), "Herdr agent get response");
|
|
411
|
-
return object(object(response.result, "Herdr agent get result").agent, `Herdr agent ${name}`);
|
|
412
|
-
}
|
|
413
|
-
function assertAgentPane(name, expected, agent) {
|
|
414
|
-
const actual = typeof agent.pane_id === "string" ? agent.pane_id : "missing";
|
|
415
|
-
if (actual !== expected) {
|
|
416
|
-
throw new Error(`Herdr agent name collision for ${name}: expected pane ${expected}, found ${actual}; refusing to reuse or replace it`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
async function confirmsTabAbsent(host, tabId, options) {
|
|
420
|
-
try {
|
|
421
|
-
const result = await createHerdrClient(options.execute).exec(["tab", "get", tabId], { cwd: host.cwd });
|
|
422
|
-
return !result.killed && result.code !== 0 && hasHerdrErrorCode(result, "tab_not_found");
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
}
|
package/docs/orchestration.md
CHANGED
|
@@ -262,8 +262,6 @@ Activity text is limited to 4 KiB per field. An invalid `toolCallId` or `toolNam
|
|
|
262
262
|
|
|
263
263
|
The low-level executor does not interpret `Role.isolation`, discover resources, compose modes, create shared state, or promote child failure outcomes to tool errors. A direct caller that wants worktrees must call `createChildWorktree` after the permit, choose the returned `cwd`, call `finalizeChildWorktree` on every exit path, and preserve its recovery payload.
|
|
264
264
|
|
|
265
|
-
Generic managed Herdr exports (`managedSubagentWorkspaceId`, reconciliation helpers, `startManagedSubagent`, prompting/listing, and retirement) consume the same launch policy for durable workers. They intentionally contain no workflow prompts, semantic state, or retry policy.
|
|
266
|
-
|
|
267
265
|
## JavaScript composition
|
|
268
266
|
|
|
269
267
|
The examples below use caller-selected `Role` objects and the `runRole` function returned by the package's initializer. A consuming Pi extension calls the initializer once at startup:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@henryqw/pi-herdr": "^0.4.0",
|
|
68
67
|
"@henryqw/pi-multi-codex": "^0.3.8",
|
|
69
68
|
"@henryqw/pi-task-models": "^3.0.0"
|
|
70
69
|
}
|