@henryqw/pi-subagent 3.0.3 → 4.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 +8 -4
- package/README.md +54 -62
- package/dist/ephemeral.d.ts +52 -0
- package/dist/ephemeral.js +651 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +15 -10
- package/docs/adr/001-composable-ephemeral-execution.md +19 -0
- package/docs/orchestration.md +342 -0
- package/examples/roles/implementer.md +14 -0
- package/examples/roles/reviewer.md +11 -0
- package/examples/roles/scout.md +17 -0
- package/examples/roles/synthesizer.md +18 -0
- package/extensions/config.ts +6 -9
- package/extensions/result-transport.ts +189 -0
- package/extensions/role-tools.ts +3 -3
- package/extensions/subagent.ts +366 -581
- package/extensions/workflow.ts +186 -0
- package/package.json +5 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { PROFILE_NAMES, THINKING_LEVELS } from "@henryqw/pi-task-models";
|
|
3
|
+
import { Type, type Static } from "typebox";
|
|
4
|
+
import { Check } from "typebox/value";
|
|
5
|
+
|
|
6
|
+
export const MAX_WORKFLOW_ENTRIES = 8;
|
|
7
|
+
|
|
8
|
+
const RoleSchema = Type.String({ minLength: 1, description: "Configured Subagent role name" });
|
|
9
|
+
const TaskSchema = Type.String({ minLength: 1, description: "Bounded task packet" });
|
|
10
|
+
const ModelSchema = Type.String({ minLength: 1, description: "Designated model as provider/modelId; overrides modelClass" });
|
|
11
|
+
const ModelClassSchema = StringEnum(PROFILE_NAMES, { description: "Task model profile" });
|
|
12
|
+
const ThinkingSchema = StringEnum(THINKING_LEVELS, { description: "Task thinking-level override" });
|
|
13
|
+
|
|
14
|
+
export const DelegationSchema = Type.Object({
|
|
15
|
+
role: RoleSchema,
|
|
16
|
+
task: TaskSchema,
|
|
17
|
+
model: Type.Optional(ModelSchema),
|
|
18
|
+
modelClass: Type.Optional(ModelClassSchema),
|
|
19
|
+
thinking: Type.Optional(ThinkingSchema),
|
|
20
|
+
}, { additionalProperties: false });
|
|
21
|
+
|
|
22
|
+
export const WorkflowSchema = Type.Object({
|
|
23
|
+
role: Type.Optional(RoleSchema),
|
|
24
|
+
task: Type.Optional(TaskSchema),
|
|
25
|
+
model: Type.Optional(ModelSchema),
|
|
26
|
+
modelClass: Type.Optional(ModelClassSchema),
|
|
27
|
+
thinking: Type.Optional(ThinkingSchema),
|
|
28
|
+
tasks: Type.Optional(Type.Array(DelegationSchema, {
|
|
29
|
+
minItems: 1,
|
|
30
|
+
maxItems: MAX_WORKFLOW_ENTRIES,
|
|
31
|
+
description: "Independent delegations to run concurrently",
|
|
32
|
+
})),
|
|
33
|
+
chain: Type.Optional(Type.Array(DelegationSchema, {
|
|
34
|
+
minItems: 1,
|
|
35
|
+
maxItems: MAX_WORKFLOW_ENTRIES,
|
|
36
|
+
description: "Dependent delegations to run sequentially",
|
|
37
|
+
})),
|
|
38
|
+
background: Type.Optional(Type.Boolean({ description: "Run the selected workflow without blocking" })),
|
|
39
|
+
}, {
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
description: "Exactly one mode: role and task, tasks, or chain",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type Delegation = Static<typeof DelegationSchema>;
|
|
45
|
+
export type WorkflowMode = "single" | "parallel" | "chain";
|
|
46
|
+
export type ParsedWorkflow =
|
|
47
|
+
| { mode: "single"; background: boolean; delegations: [Delegation] }
|
|
48
|
+
| { mode: "parallel"; background: boolean; delegations: Delegation[] }
|
|
49
|
+
| { mode: "chain"; background: boolean; delegations: Delegation[] };
|
|
50
|
+
|
|
51
|
+
type WorkflowInput = Static<typeof WorkflowSchema>;
|
|
52
|
+
|
|
53
|
+
const DELEGATION_KEYS = ["role", "task", "model", "modelClass", "thinking"] as const;
|
|
54
|
+
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
|
|
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 }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasDelegation(value: WorkflowInput): value is WorkflowInput & Delegation {
|
|
72
|
+
return Object.hasOwn(value, "role") && Object.hasOwn(value, "task");
|
|
73
|
+
}
|
|
74
|
+
|
|
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");
|
|
80
|
+
if (Number(single) + Number(parallel) + Number(chain) !== 1) {
|
|
81
|
+
throw new Error("workflow must select exactly one mode: role and task, tasks, or chain.");
|
|
82
|
+
}
|
|
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")] };
|
|
95
|
+
}
|
|
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
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type WorkflowEntry = {
|
|
109
|
+
id: string;
|
|
110
|
+
mode: WorkflowMode;
|
|
111
|
+
index: number;
|
|
112
|
+
delegation: Delegation;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function identifyWorkflowEntries(toolCallId: string, workflow: ParsedWorkflow): WorkflowEntry[] {
|
|
116
|
+
return workflow.delegations.map((delegation, index) => ({
|
|
117
|
+
id: `${toolCallId}:${workflow.mode}:${index}`,
|
|
118
|
+
mode: workflow.mode,
|
|
119
|
+
index,
|
|
120
|
+
delegation,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type DelegationExecution<T> =
|
|
125
|
+
| { ok: true; assistantOutput: string; result: T }
|
|
126
|
+
| { ok: false; result: T };
|
|
127
|
+
|
|
128
|
+
export type DelegationRunner<T> = (
|
|
129
|
+
entry: WorkflowEntry,
|
|
130
|
+
) => DelegationExecution<T> | Promise<DelegationExecution<T>>;
|
|
131
|
+
|
|
132
|
+
export type WorkflowEntryOutcome<T> =
|
|
133
|
+
| { status: "succeeded"; entry: WorkflowEntry; assistantOutput: string; result: T }
|
|
134
|
+
| { status: "failed"; entry: WorkflowEntry; result: T }
|
|
135
|
+
| { status: "rejected"; entry: WorkflowEntry; reason: unknown };
|
|
136
|
+
|
|
137
|
+
async function runEntry<T>(entry: WorkflowEntry, run: DelegationRunner<T>): Promise<WorkflowEntryOutcome<T>> {
|
|
138
|
+
try {
|
|
139
|
+
const execution = await run(entry);
|
|
140
|
+
return execution.ok
|
|
141
|
+
? { status: "succeeded", entry, assistantOutput: execution.assistantOutput, result: execution.result }
|
|
142
|
+
: { status: "failed", entry, result: execution.result };
|
|
143
|
+
} catch (reason) {
|
|
144
|
+
return { status: "rejected", entry, reason };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Run only foreground policy. Callback failures are `rejected`; parent aborts are rethrown after started work settles. */
|
|
149
|
+
export async function runForegroundWorkflow<T>(
|
|
150
|
+
toolCallId: string,
|
|
151
|
+
workflow: ParsedWorkflow,
|
|
152
|
+
run: DelegationRunner<T>,
|
|
153
|
+
signal?: AbortSignal,
|
|
154
|
+
): Promise<WorkflowEntryOutcome<T>[]> {
|
|
155
|
+
if (workflow.background) throw new Error("Background workflows cannot use foreground orchestration.");
|
|
156
|
+
signal?.throwIfAborted();
|
|
157
|
+
const entries = identifyWorkflowEntries(toolCallId, workflow);
|
|
158
|
+
if (workflow.mode === "single") {
|
|
159
|
+
const outcome = await runEntry(entries[0]!, run);
|
|
160
|
+
signal?.throwIfAborted();
|
|
161
|
+
return [outcome];
|
|
162
|
+
}
|
|
163
|
+
if (workflow.mode === "parallel") {
|
|
164
|
+
const outcomes = await Promise.all(entries.map((entry) => runEntry(entry, run)));
|
|
165
|
+
signal?.throwIfAborted();
|
|
166
|
+
return outcomes;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const outcomes: WorkflowEntryOutcome<T>[] = [];
|
|
170
|
+
let previous = "";
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const chained = {
|
|
173
|
+
...entry,
|
|
174
|
+
delegation: {
|
|
175
|
+
...entry.delegation,
|
|
176
|
+
task: entry.delegation.task.replaceAll("{previous}", () => previous),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const outcome = await runEntry(chained, run);
|
|
180
|
+
signal?.throwIfAborted();
|
|
181
|
+
outcomes.push(outcome);
|
|
182
|
+
if (outcome.status !== "succeeded") break;
|
|
183
|
+
previous = outcome.assistantOutput;
|
|
184
|
+
}
|
|
185
|
+
return outcomes;
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Delegate
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"extensions",
|
|
19
|
+
"docs",
|
|
20
|
+
"examples",
|
|
19
21
|
"README.md",
|
|
20
22
|
"CONTEXT.md",
|
|
21
23
|
"LICENSE"
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
},
|
|
30
32
|
"scripts": {
|
|
31
33
|
"build": "tsc --project tsconfig.build.json",
|
|
34
|
+
"pretypecheck": "npm run build",
|
|
32
35
|
"test": "npm run build && node --test test/*.test.ts",
|
|
33
36
|
"typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck src/*.ts extensions/*.ts test/*.test.ts",
|
|
34
37
|
"prepack": "npm run build",
|