@henryqw/pi-subagent 3.1.0 → 4.0.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.
@@ -47,4 +47,6 @@ export interface EphemeralSubagentExecutor {
47
47
  */
48
48
  export declare function createEphemeralSubagentExecutor(options: EphemeralSubagentExecutorOptions): EphemeralSubagentExecutor;
49
49
  export declare function capEphemeralSubagentOutput(text: string): string;
50
+ export declare function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined;
51
+ export declare function formatDuration(milliseconds: number): string;
50
52
  export {};
package/dist/ephemeral.js CHANGED
@@ -294,7 +294,7 @@ function usageFrom(value) {
294
294
  function sumOptional(left, right) {
295
295
  return left === undefined && right === undefined ? undefined : (left ?? 0) + (right ?? 0);
296
296
  }
297
- function addUsage(left, right) {
297
+ export function addUsage(left, right) {
298
298
  if (!left)
299
299
  return right;
300
300
  if (!right)
@@ -318,7 +318,7 @@ function addUsage(left, right) {
318
318
  },
319
319
  };
320
320
  }
321
- function formatDuration(milliseconds) {
321
+ export function formatDuration(milliseconds) {
322
322
  const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
323
323
  const hours = Math.floor(seconds / 3_600);
324
324
  const minutes = Math.floor(seconds % 3_600 / 60);
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type HerdrExecutor } from "@henryqw/pi-herdr";
3
3
  import { type AvailableModel, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
4
- export { capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
4
+ export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
5
5
  export { createChildWorktree, finalizeChildWorktree, worktreeContextNote, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
6
+ export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
7
+ export declare const CHILD_EXCLUDED_TOOLS = "delegate_task,ask_question,auto_dag_execute,auto_dag_acknowledge";
6
8
  export interface Role {
7
9
  name: string;
8
10
  description: string;
@@ -36,7 +38,6 @@ export interface ResolvedRoleSkills {
36
38
  paths: string[];
37
39
  missing: string[];
38
40
  }
39
- export declare const isProfileName: (value: unknown) => value is ProfileName;
40
41
  export declare function loadRoles(agentDir?: string): Role[];
41
42
  export declare function resolveTaskRoute(ctx: ExtensionContext, profileName: ProfileName, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute;
42
43
  export declare function resolveRoleSkills(pi: Pick<ExtensionAPI, "getCommands">, role: Role): ResolvedRoleSkills;
@@ -58,7 +59,6 @@ export interface ManagedSubagentTab {
58
59
  tabId: string;
59
60
  paneId: string;
60
61
  }
61
- export declare function launchEnvironmentArgs(launch: PiLaunch): string[];
62
62
  export declare function managedSubagentName(workspaceId: string, ...identity: string[]): string;
63
63
  export declare function managedSubagentWorkspaceId(cwd: string, mainPane: string, options: ManagedSubagentHostOptions): Promise<string>;
64
64
  /** Returns pane ID to Herdr status for agents owned by this workspace. */
package/dist/index.js CHANGED
@@ -3,16 +3,15 @@ import { readFileSync, readdirSync } from "node:fs";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
6
- import { createHerdrClient, herdrCommandFailure, hasHerdrErrorCode } from "@henryqw/pi-herdr";
7
- import { modelReference, orderedProfileRoutes, PROFILE_NAMES, readTaskModelsConfig, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
8
- export { capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, } from "./ephemeral.js";
6
+ import { createHerdrClient, herdrCommandFailure, hasHerdrErrorCode, startPiAgent } from "@henryqw/pi-herdr";
7
+ import { modelReference, orderedProfileRoutes, readTaskModelsConfig, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
8
+ export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, } from "./ephemeral.js";
9
9
  export { createChildWorktree, finalizeChildWorktree, worktreeContextNote, } from "./worktree.js";
10
10
  const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
11
11
  const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
12
12
  const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
13
- const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
14
- const CHILD_EXCLUDED_TOOLS = "delegate_task,ask_question,auto_dag_execute,auto_dag_acknowledge";
15
- export const isProfileName = (value) => typeof value === "string" && PROFILE_NAMES.includes(value);
13
+ export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
14
+ export const CHILD_EXCLUDED_TOOLS = "delegate_task,ask_question,auto_dag_execute,auto_dag_acknowledge";
16
15
  const cleanText = (value, field, source) => {
17
16
  if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
18
17
  throw new Error(`${source}: ${field} must be non-empty text.`);
@@ -176,7 +175,7 @@ export function resolveRoleLaunch(pi, ctx, input) {
176
175
  route: resolveConfiguredTaskRoute(ctx, taskId, input.agentDir),
177
176
  });
178
177
  }
179
- export function launchEnvironmentArgs(launch) {
178
+ function launchEnvironmentArgs(launch) {
180
179
  return Object.entries(launch.env).flatMap(([key, value]) => {
181
180
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0")) {
182
181
  throw new Error(`Invalid launch environment: ${key}`);
@@ -310,27 +309,27 @@ export async function startManagedSubagent(host, agent, pane, launch, options, h
310
309
  return "existing";
311
310
  }
312
311
  await hooks.beforeStart?.();
313
- const arguments_ = ["agent", "start", name, "--kind", "pi", "--pane", paneId, "--", ...launch.args];
314
312
  const herdr = createHerdrClient(options.execute);
315
- for (let attempt = 1; attempt <= 5; attempt += 1) {
316
- const result = await herdr.exec(arguments_, { cwd: host.cwd });
317
- if (result.code === 0 && !result.killed) {
318
- await hooks.onStarted?.();
319
- return "started";
320
- }
321
- if (hasHerdrErrorCode(result, "agent_name_taken")) {
322
- const raced = await getManagedSubagent(host, name, options);
323
- if (!raced)
324
- throw new Error(`Herdr agent ${name} reported agent_name_taken but could not be found; refusing to start a duplicate`);
325
- assertAgentPane(name, paneId, raced);
326
- return "existing";
327
- }
328
- if (!hasHerdrErrorCode(result, "agent_pane_busy") || attempt === 5) {
329
- throw new Error(herdrCommandFailure(arguments_, result));
330
- }
331
- await (options.delay ?? delay)(250);
313
+ const startArgs = ["agent", "start", name, "--kind", "pi", "--pane", paneId, "--", ...launch.args];
314
+ const result = await startPiAgent(herdr, {
315
+ name,
316
+ pane: paneId,
317
+ args: launch.args,
318
+ options: { cwd: host.cwd },
319
+ delay: options.delay,
320
+ });
321
+ if (result.code === 0 && !result.killed) {
322
+ await hooks.onStarted?.();
323
+ return "started";
332
324
  }
333
- throw new Error(`Herdr agent ${name} could not be started`);
325
+ if (hasHerdrErrorCode(result, "agent_name_taken")) {
326
+ const raced = await getManagedSubagent(host, name, options);
327
+ if (!raced)
328
+ throw new Error(`Herdr agent ${name} reported agent_name_taken but could not be found; refusing to start a duplicate`);
329
+ assertAgentPane(name, paneId, raced);
330
+ return "existing";
331
+ }
332
+ throw new Error(herdrCommandFailure(startArgs, result));
334
333
  }
335
334
  export async function promptManagedSubagent(host, agent, prompt, options) {
336
335
  assertAgentName(agent);
@@ -404,6 +403,3 @@ async function confirmsTabAbsent(host, tabId, options) {
404
403
  return false;
405
404
  }
406
405
  }
407
- async function delay(milliseconds) {
408
- await new Promise((done) => { setTimeout(done, milliseconds); });
409
- }
@@ -27,10 +27,7 @@ const positive = (value: unknown): value is number =>
27
27
  // every child immediately instead of applying the configured deadline.
28
28
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
29
29
  export const DEFAULT_TIMEOUT_CONFIG = { idleMinutes: 10, maxMinutes: 30 } as const;
30
- const TIMEOUT_FIELDS: Array<[keyof SubagentTimeoutConfig, string]> = [
31
- ["idleMinutes", "minutes"],
32
- ["maxMinutes", "minutes"],
33
- ];
30
+ const TIMEOUT_FIELDS = ["idleMinutes", "maxMinutes"] as const;
34
31
 
35
32
  /**
36
33
  * All pi-subagent config lives in its existing extension-named directory;
@@ -88,20 +85,20 @@ export function readSubagentConfig(agentDir = getAgentDir()): LoadedSubagentConf
88
85
  } else {
89
86
  const timeoutRecord = record.timeout as Record<string, unknown>;
90
87
  const timeout: SubagentTimeoutConfig = {};
91
- for (const [key, unit] of TIMEOUT_FIELDS) {
88
+ for (const key of TIMEOUT_FIELDS) {
92
89
  const value = timeoutRecord[key];
93
90
  if (value === undefined) continue;
94
91
  if (!positive(value)) {
95
- problems.push(`timeout.${key} must be a positive number of ${unit}, got ${JSON.stringify(value)}`);
92
+ problems.push(`timeout.${key} must be a positive number of minutes, got ${JSON.stringify(value)}`);
96
93
  } else if (value * 60_000 > MAX_TIMER_DELAY_MS) {
97
- problems.push(`timeout.${key} exceeds the maximum supported delay of ${MAX_TIMER_DELAY_MS} ms, got ${JSON.stringify(value)} ${unit}`);
94
+ problems.push(`timeout.${key} exceeds the maximum supported delay of ${MAX_TIMER_DELAY_MS} ms, got ${JSON.stringify(value)} minutes`);
98
95
  } else {
99
96
  timeout[key] = value;
100
97
  }
101
98
  }
102
99
  for (const key of Object.keys(timeoutRecord)) {
103
- if (!TIMEOUT_FIELDS.some(([known]) => known === key)) {
104
- problems.push(`unknown timeout.${key}; expected ${TIMEOUT_FIELDS.map(([known]) => known).join(", ")}`);
100
+ if (!TIMEOUT_FIELDS.includes(key as keyof SubagentTimeoutConfig)) {
101
+ problems.push(`unknown timeout.${key}; expected ${TIMEOUT_FIELDS.join(", ")}`);
105
102
  }
106
103
  }
107
104
  if (Object.keys(timeout).length) config.timeout = timeout;
@@ -1,5 +1,6 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai";
2
2
  import {
3
+ addUsage,
3
4
  capEphemeralSubagentOutput,
4
5
  type EphemeralSubagentResult,
5
6
  type WorktreePayload,
@@ -63,31 +64,6 @@ function splitEvidence(text: string): [string, string] {
63
64
  return [preview, text.slice(preview.length)];
64
65
  }
65
66
 
66
- function sumOptional(left: number | undefined, right: number | undefined): number | undefined {
67
- return left === undefined && right === undefined ? undefined : (left ?? 0) + (right ?? 0);
68
- }
69
-
70
- function addUsage(left: Usage | undefined, right: Usage): Usage {
71
- const cacheWrite1h = sumOptional(left?.cacheWrite1h, right.cacheWrite1h);
72
- const reasoning = sumOptional(left?.reasoning, right.reasoning);
73
- return {
74
- input: (left?.input ?? 0) + right.input,
75
- output: (left?.output ?? 0) + right.output,
76
- cacheRead: (left?.cacheRead ?? 0) + right.cacheRead,
77
- cacheWrite: (left?.cacheWrite ?? 0) + right.cacheWrite,
78
- ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
79
- ...(reasoning === undefined ? {} : { reasoning }),
80
- totalTokens: (left?.totalTokens ?? 0) + right.totalTokens,
81
- cost: {
82
- input: (left?.cost.input ?? 0) + right.cost.input,
83
- output: (left?.cost.output ?? 0) + right.cost.output,
84
- cacheRead: (left?.cost.cacheRead ?? 0) + right.cost.cacheRead,
85
- cacheWrite: (left?.cost.cacheWrite ?? 0) + right.cost.cacheWrite,
86
- total: (left?.cost.total ?? 0) + right.cost.total,
87
- },
88
- };
89
- }
90
-
91
67
  function label(kind: TransportKind, failed: boolean): string {
92
68
  if (kind === "update") return "Workflow update.";
93
69
  if (kind === "background") return `Background workflow ${failed ? "failed" : "succeeded"}.`;
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { CHILD_EXCLUDED_TOOLS, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
2
3
 
3
- export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
4
- const CHILD_EXCLUDED_TOOLS = new Set(["delegate_task", "ask_question", "auto_dag_execute", "auto_dag_acknowledge"]);
4
+ const childExcludedTools = new Set(CHILD_EXCLUDED_TOOLS.split(","));
5
5
 
6
6
  function configuredTools(value: unknown): string[] | undefined {
7
7
  if (value === undefined) return;
@@ -29,6 +29,6 @@ export default function roleTools(pi: ExtensionAPI): void {
29
29
  const extensionTools = pi.getAllTools()
30
30
  .filter((tool) => !["builtin", "sdk", "inline"].includes(tool.sourceInfo.source))
31
31
  .map((tool) => tool.name);
32
- pi.setActiveTools([...new Set([...selected, ...extensionTools])].filter((name) => !CHILD_EXCLUDED_TOOLS.has(name)));
32
+ pi.setActiveTools([...new Set([...selected, ...extensionTools])].filter((name) => !childExcludedTools.has(name)));
33
33
  });
34
34
  }
@@ -17,6 +17,7 @@ import {
17
17
  createRoleLaunch,
18
18
  EphemeralSubagentError,
19
19
  finalizeChildWorktree,
20
+ formatDuration,
20
21
  loadRoles,
21
22
  resolveTaskRoute,
22
23
  worktreeContextNote,
@@ -45,8 +46,6 @@ import {
45
46
  type WorkflowEntry,
46
47
  } from "./workflow.ts";
47
48
 
48
- export { capOutput };
49
-
50
49
  const SUBAGENT_TASK = "pi-subagent/delegateTask";
51
50
  const WIDGET_KEY = "subagent-status";
52
51
  const WIDGET_INTERVAL_MS = 80;
@@ -89,13 +88,6 @@ function formatTokens(tokens: number): string {
89
88
  return `${(tokens / 1_000_000).toFixed(1)}M`;
90
89
  }
91
90
 
92
- function formatElapsed(startedAt: number, finishedAt = Date.now()): string {
93
- const seconds = Math.max(0, Math.floor((finishedAt - startedAt) / 1_000));
94
- const hours = Math.floor(seconds / 3_600);
95
- const minutes = Math.floor(seconds % 3_600 / 60);
96
- return hours ? `${hours}h ${minutes}m` : minutes ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
97
- }
98
-
99
91
  function statusGlyph(status: WidgetStatus, spinnerIndex: number, theme: Theme): string {
100
92
  switch (status) {
101
93
  case "working": return theme.fg("accent", SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]!);
@@ -123,7 +115,7 @@ function renderWidgetRows(
123
115
  const visible = items.slice(0, MAX_WIDGET_ROWS);
124
116
  if (!visible.length) return [];
125
117
  const tokens = visible.map((item) => formatTokens(item.tokens));
126
- const elapsed = visible.map((item) => formatElapsed(item.startedAt, item.finishedAt ?? now));
118
+ const elapsed = visible.map((item) => formatDuration((item.finishedAt ?? now) - item.startedAt));
127
119
  const tokenWidth = Math.max(...tokens.map(visibleWidth));
128
120
  const elapsedWidth = Math.max(...elapsed.map(visibleWidth));
129
121
  const fixedWidth = 1 + 8 + tokenWidth + elapsedWidth;
@@ -1,6 +1,7 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
- import { PROFILE_NAMES, THINKING_LEVELS, type ProfileName, type ThinkingLevel } from "@henryqw/pi-task-models";
2
+ import { PROFILE_NAMES, THINKING_LEVELS } from "@henryqw/pi-task-models";
3
3
  import { Type, type Static } from "typebox";
4
+ import { Check } from "typebox/value";
4
5
 
5
6
  export const MAX_WORKFLOW_ENTRIES = 8;
6
7
 
@@ -47,78 +48,61 @@ export type ParsedWorkflow =
47
48
  | { mode: "parallel"; background: boolean; delegations: Delegation[] }
48
49
  | { mode: "chain"; background: boolean; delegations: Delegation[] };
49
50
 
50
- const DELEGATION_KEYS = ["role", "task", "model", "modelClass", "thinking"] as const;
51
- const TOP_LEVEL_KEYS = [...DELEGATION_KEYS, "tasks", "chain", "background"] as const;
52
-
53
- function record(value: unknown, path: string): Record<string, unknown> {
54
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object.`);
55
- return value as Record<string, unknown>;
56
- }
57
-
58
- function rejectUnknown(value: Record<string, unknown>, allowed: readonly string[], path: string): void {
59
- const unknown = Object.keys(value).find((key) => !allowed.includes(key));
60
- if (unknown) throw new Error(`${path} contains unknown property ${JSON.stringify(unknown)}.`);
61
- }
51
+ type WorkflowInput = Static<typeof WorkflowSchema>;
62
52
 
63
- function text(value: unknown, path: string): string {
64
- if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
65
- throw new Error(`${path} must be non-empty text without NUL.`);
66
- }
67
- return value.trim();
68
- }
53
+ const DELEGATION_KEYS = ["role", "task", "model", "modelClass", "thinking"] as const;
69
54
 
70
- function enumValue<T extends string>(value: unknown, values: readonly T[], path: string): T {
71
- if (typeof value !== "string" || !values.includes(value as T)) {
72
- throw new Error(`${path} must be one of: ${values.join(", ")}.`);
73
- }
74
- return value as T;
55
+ function text(value: string, path: string): string {
56
+ const normalized = value.trim();
57
+ if (!normalized || value.includes("\0")) throw new Error(`${path} must be non-empty text without NUL.`);
58
+ return normalized;
75
59
  }
76
60
 
77
- function parseDelegation(value: unknown, path: string, extraKeys: readonly string[] = []): Delegation {
78
- const input = record(value, path);
79
- rejectUnknown(input, [...DELEGATION_KEYS, ...extraKeys], path);
80
- if (!Object.hasOwn(input, "role") || !Object.hasOwn(input, "task")) {
81
- throw new Error(`${path} requires both role and task.`);
82
- }
83
- const delegation: Delegation = {
84
- role: text(input.role, `${path}.role`),
85
- task: text(input.task, `${path}.task`),
61
+ function normalizeDelegation(value: Delegation, path: string): Delegation {
62
+ return {
63
+ role: text(value.role, `${path}.role`),
64
+ task: text(value.task, `${path}.task`),
65
+ ...(value.model === undefined ? {} : { model: text(value.model, `${path}.model`) }),
66
+ ...(value.modelClass === undefined ? {} : { modelClass: value.modelClass }),
67
+ ...(value.thinking === undefined ? {} : { thinking: value.thinking }),
86
68
  };
87
- if (Object.hasOwn(input, "model")) delegation.model = text(input.model, `${path}.model`);
88
- if (Object.hasOwn(input, "modelClass")) {
89
- delegation.modelClass = enumValue(input.modelClass, PROFILE_NAMES, `${path}.modelClass`) as ProfileName;
90
- }
91
- if (Object.hasOwn(input, "thinking")) {
92
- delegation.thinking = enumValue(input.thinking, THINKING_LEVELS, `${path}.thinking`) as ThinkingLevel;
93
- }
94
- return delegation;
95
69
  }
96
70
 
97
- function parseDelegations(value: unknown, path: "tasks" | "chain"): Delegation[] {
98
- if (!Array.isArray(value)) throw new Error(`${path} must be an array.`);
99
- if (value.length < 1 || value.length > MAX_WORKFLOW_ENTRIES) {
100
- throw new Error(`${path} must contain 1 to ${MAX_WORKFLOW_ENTRIES} delegations.`);
101
- }
102
- return value.map((delegation, index) => parseDelegation(delegation, `${path}[${index}]`));
71
+ function hasDelegation(value: WorkflowInput): value is WorkflowInput & Delegation {
72
+ return Object.hasOwn(value, "role") && Object.hasOwn(value, "task");
103
73
  }
104
74
 
105
- export function parseWorkflow(value: unknown): ParsedWorkflow {
106
- const input = record(value, "workflow");
107
- rejectUnknown(input, TOP_LEVEL_KEYS, "workflow");
108
- const background = Object.hasOwn(input, "background") ? input.background : false;
109
- if (typeof background !== "boolean") throw new Error("workflow.background must be a boolean.");
110
-
111
- const single = DELEGATION_KEYS.some((key) => Object.hasOwn(input, key));
112
- const parallel = Object.hasOwn(input, "tasks");
113
- const chain = Object.hasOwn(input, "chain");
75
+ function workflowMode(value: unknown): WorkflowMode | undefined {
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
77
+ const single = DELEGATION_KEYS.some((key) => Object.hasOwn(value, key));
78
+ const parallel = Object.hasOwn(value, "tasks");
79
+ const chain = Object.hasOwn(value, "chain");
114
80
  if (Number(single) + Number(parallel) + Number(chain) !== 1) {
115
81
  throw new Error("workflow must select exactly one mode: role and task, tasks, or chain.");
116
82
  }
117
- if (single) {
118
- return { mode: "single", background, delegations: [parseDelegation(input, "workflow", ["background"])] };
83
+ return single ? "single" : parallel ? "parallel" : "chain";
84
+ }
85
+
86
+ export function parseWorkflow(value: unknown): ParsedWorkflow {
87
+ const mode = workflowMode(value);
88
+ if (!Check(WorkflowSchema, value)) throw new Error("workflow must match the declared tool schema.");
89
+ if (!mode) throw new Error("workflow must select exactly one mode: role and task, tasks, or chain.");
90
+ const input = value;
91
+ const background = input.background ?? false;
92
+ if (mode === "single") {
93
+ if (!hasDelegation(input)) throw new Error("workflow requires both role and task.");
94
+ return { mode, background, delegations: [normalizeDelegation(input, "workflow")] };
119
95
  }
120
- if (parallel) return { mode: "parallel", background, delegations: parseDelegations(input.tasks, "tasks") };
121
- return { mode: "chain", background, delegations: parseDelegations(input.chain, "chain") };
96
+ if (mode === "parallel") return {
97
+ mode,
98
+ background,
99
+ delegations: input.tasks!.map((delegation, index) => normalizeDelegation(delegation, `tasks[${index}]`)),
100
+ };
101
+ return {
102
+ mode,
103
+ background,
104
+ delegations: input.chain!.map((delegation, index) => normalizeDelegation(delegation, `chain[${index}]`)),
105
+ };
122
106
  }
123
107
 
124
108
  export type WorkflowEntry = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "3.1.0",
3
+ "version": "4.0.1",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsc --project tsconfig.build.json",
34
+ "pretypecheck": "npm run build",
34
35
  "test": "npm run build && node --test test/*.test.ts",
35
36
  "typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck src/*.ts extensions/*.ts test/*.test.ts",
36
37
  "prepack": "npm run build",
@@ -59,7 +60,7 @@
59
60
  ]
60
61
  },
61
62
  "dependencies": {
62
- "@henryqw/pi-herdr": "^0.1.1",
63
+ "@henryqw/pi-herdr": "^0.4.0",
63
64
  "@henryqw/pi-multi-codex": "^0.3.8",
64
65
  "@henryqw/pi-task-models": "^1.0.0"
65
66
  }