@maxiaochao/pi-toolkit 0.11.22 → 0.12.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/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +5 -0
- package/packages/herdr-subagents/README.md +2 -2
- package/packages/herdr-subagents/TESTING.md +3 -1
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +2 -2
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/batch-runner.ts +2 -1
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/tool.ts +2 -2
- package/packages/herdr-subagents/src/validation.ts +3 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +52 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2026-10-15 - v0.12.0
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.6.0: require explicit task Personas and clarify concise tool descriptions
|
|
4
|
+
- Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
|
|
5
|
+
|
|
1
6
|
## 2026-10-15 - v0.11.22
|
|
2
7
|
|
|
3
8
|
- herdr-subagents v0.5.4: preserve configured model and authentication extensions in isolated real-Herdr smoke tests
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Restart Pi after updating. `pi update --extensions` has the same effect when upd
|
|
|
39
39
|
|
|
40
40
|
Each child README owns its feature description, motivation or upstream alignment, examples, screenshots/demos, configuration, and test documentation.
|
|
41
41
|
|
|
42
|
-
Herdr Subagents
|
|
42
|
+
Herdr Subagents tasks require an explicit `agent` Persona; no default `worker` is selected. Personas can opt into local extension files or npm/git Pi packages through `extensions`. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples. The [real Herdr smoke test](packages/herdr-subagents/TESTING.md) preserves the configured analyst model and explicit authentication extensions while isolating unrelated settings.
|
|
43
43
|
|
|
44
44
|
Multiple standalone children may be installed together. `pi-openai-account-pool` may also be installed alongside the complete toolkit because the toolkit does not embed it. Every other child in the table above is embedded, so a second install exposes the same resource twice.
|
|
45
45
|
|
package/package.json
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2026-10-15 - v0.6.0
|
|
2
|
+
|
|
3
|
+
- require explicit task Personas and clarify concise tool descriptions
|
|
4
|
+
- Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
|
|
5
|
+
|
|
1
6
|
## 2026-10-15 - v0.5.4
|
|
2
7
|
|
|
3
8
|
- preserve configured model and authentication extensions in isolated real-Herdr smoke tests
|
|
@@ -26,7 +26,7 @@ The tools and command are registered only when Pi is running inside Herdr with a
|
|
|
26
26
|
|
|
27
27
|
## Execution contract
|
|
28
28
|
|
|
29
|
-
`herdr_subagents` accepts an ordered, non-empty `tasks` array.
|
|
29
|
+
`herdr_subagents` accepts an ordered, non-empty `tasks` array. Every task must explicitly set `agent`; there is no default Persona.
|
|
30
30
|
|
|
31
31
|
- `concurrency: 1` runs tasks strictly in input order.
|
|
32
32
|
- `concurrency: N` runs at most N read-only tasks at once.
|
|
@@ -110,7 +110,7 @@ Starts one ordered batch and waits until every task reaches a terminal state. It
|
|
|
110
110
|
| `concurrency` | No | `defaultConcurrency` | Maximum number of simultaneously active read-only tasks. It is capped by `maxConcurrency`. |
|
|
111
111
|
| `tasks[].name` | Yes | — | Short pane/task name, at most 48 characters. |
|
|
112
112
|
| `tasks[].prompt` | Yes | — | Self-contained task instructions. |
|
|
113
|
-
| `tasks[].agent` |
|
|
113
|
+
| `tasks[].agent` | Yes | — | Effective Persona name; use control `list` to inspect available Personas. |
|
|
114
114
|
| `tasks[].model` | No | Persona/default model | Model override for this task. |
|
|
115
115
|
|
|
116
116
|
A batch containing a write-capable Persona must explicitly or effectively use `concurrency: 1`; an unsafe parallel request is rejected.
|
|
@@ -12,7 +12,9 @@ It verifies:
|
|
|
12
12
|
- bounded read-only parallelism;
|
|
13
13
|
- ordered results despite concurrent completion;
|
|
14
14
|
- sibling continuation after failure and all-failed aggregation;
|
|
15
|
-
- explicit
|
|
15
|
+
- required explicit Personas in the tool schema and execution, including legacy single-task argument normalization;
|
|
16
|
+
- rejection of missing, blank, invalid, and unknown Personas before creating runs, while custom Personas remain supported;
|
|
17
|
+
- explicit rejection of parallel write-capable tasks with the conflicting task, Persona, and corrective action;
|
|
16
18
|
- waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
|
|
17
19
|
- explicit failure when the process exits without a report;
|
|
18
20
|
- parent cancellation of running and queued tasks;
|
|
@@ -42,7 +42,7 @@ export function normalizeHerdrSubagentsArguments(args: unknown): HerdrSubagentsR
|
|
|
42
42
|
tasks: [{
|
|
43
43
|
name: label || "task",
|
|
44
44
|
prompt: legacy.prompt,
|
|
45
|
-
|
|
45
|
+
agent: legacy.agent as string,
|
|
46
46
|
...(typeof legacy.model === "string" ? { model: legacy.model } : {}),
|
|
47
47
|
}],
|
|
48
48
|
...(label ? { label } : {}),
|
|
@@ -148,7 +148,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
148
148
|
name: "herdr_subagents",
|
|
149
149
|
...(exposure ? { exposure } : {}),
|
|
150
150
|
label: "Herdr Subagents",
|
|
151
|
-
description: "Run
|
|
151
|
+
description: "Run ordered tasks in visible Herdr Pi panes, with an explicit agent for each task. concurrency 1 is serial; higher values require read-only Personas. Returns after all tasks complete, fail, or cancel; full results and sessions remain in project-local records.",
|
|
152
152
|
promptSnippet: "Run focused work in visible Herdr subagents",
|
|
153
153
|
parameters: HerdrSubagentsParams,
|
|
154
154
|
prepareArguments: normalizeHerdrSubagentsArguments,
|
|
@@ -92,7 +92,8 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
92
92
|
const runId = options.runId ?? randomUUID();
|
|
93
93
|
const requestedConcurrency = Math.max(1, Math.floor(options.concurrency));
|
|
94
94
|
if (requestedConcurrency > 1 && tasks.some((task) => task.access === "write")) {
|
|
95
|
-
|
|
95
|
+
const writers = tasks.filter((task) => task.access === "write").map((task) => `${JSON.stringify(task.name)} (agent=${task.agent})`).join(", ");
|
|
96
|
+
throw new Error(`Write-capable tasks require concurrency 1: ${writers}. Use concurrency: 1, or select a read-only Persona for read-only work.`);
|
|
96
97
|
}
|
|
97
98
|
const effectiveConcurrency = Math.min(requestedConcurrency, options.maxConcurrency, tasks.length);
|
|
98
99
|
const runDirectory = join(options.cwd, ".pi", "herdr-subagents", "runs", `${timestamp().replace(/[-:.]/g, "").replace("Z", "Z-")}${runId.slice(0, 8)}`);
|
|
@@ -4,14 +4,14 @@ import { Type } from "typebox";
|
|
|
4
4
|
const HerdrSubagentsTaskParams = Type.Object({
|
|
5
5
|
name: Type.String({ minLength: 1, maxLength: 48, description: "Short task name shown in the pane and task list." }),
|
|
6
6
|
prompt: Type.String({ minLength: 1, description: "Self-contained prompt for this task." }),
|
|
7
|
-
agent: Type.
|
|
7
|
+
agent: Type.String({ minLength: 1, description: "Persona name, use control list for available Personas." }),
|
|
8
8
|
model: Type.Optional(Type.String({ minLength: 1, description: "Optional model override for this task." })),
|
|
9
9
|
}, { additionalProperties: false });
|
|
10
10
|
|
|
11
11
|
export const HerdrSubagentsParams = Type.Object({
|
|
12
12
|
tasks: Type.Array(HerdrSubagentsTaskParams, { minItems: 1, description: "Ordered tasks to execute. Use one item for a single task; independent read-only tasks may run concurrently." }),
|
|
13
13
|
label: Type.Optional(Type.String({ minLength: 1, maxLength: 48, description: "Short Herdr run-tab label." })),
|
|
14
|
-
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Maximum
|
|
14
|
+
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Maximum active Agents. Values above 1 require read-only Personas; write-capable Personas require 1 or the batch is rejected." })),
|
|
15
15
|
}, { additionalProperties: false });
|
|
16
16
|
|
|
17
17
|
export const HerdrSubagentsControlParams = Type.Object({
|
|
@@ -7,7 +7,7 @@ import type { OwnerIdentity } from "./ownership.ts";
|
|
|
7
7
|
import { cancelActiveSubagentRuns } from "./reconcile.ts";
|
|
8
8
|
import { getSubagentRunStatus, runStatusText } from "./status.ts";
|
|
9
9
|
|
|
10
|
-
export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent
|
|
10
|
+
export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent: string; model?: string; }
|
|
11
11
|
|
|
12
12
|
export interface HerdrSubagentsRunParams {
|
|
13
13
|
tasks: HerdrSubagentsTaskParams[];
|
|
@@ -140,7 +140,7 @@ async function executeBatch({ params, signal, onUpdate, ctx, dependencies, disco
|
|
|
140
140
|
const tasks = params.tasks as HerdrSubagentsTaskParams[];
|
|
141
141
|
const resolved: BatchTask[] = [];
|
|
142
142
|
for (const task of tasks) {
|
|
143
|
-
const personaName = task.agent
|
|
143
|
+
const personaName = task.agent.trim();
|
|
144
144
|
const persona = discovery.personas.find((candidate) => candidate.name === personaName);
|
|
145
145
|
if (!persona) return errorResponse(`Unknown persona '${personaName}'.\n\n${listText(discovery)}`);
|
|
146
146
|
resolved.push({ name: task.name, prompt: task.prompt, agent: persona.name, access: persona.access, model: task.model?.trim() || persona.model, thinking: persona.thinking ?? (persona.modelSource === "parent session" && !task.model ? ctx.thinkingLevel : undefined), tools: persona.tools, skills: persona.skills, extensions: persona.extensions, systemPrompt: persona.systemPrompt });
|
|
@@ -10,6 +10,9 @@ export const HERDR_ACTIONS = {
|
|
|
10
10
|
|
|
11
11
|
export function validateRunParams(params: HerdrSubagentsRunParams): string | undefined {
|
|
12
12
|
if (!params.tasks?.length) return "Provide a non-empty tasks list.";
|
|
13
|
+
for (const [index, task] of params.tasks.entries()) {
|
|
14
|
+
if (typeof task.agent !== "string" || !task.agent.trim()) return `tasks[${index}].agent is required; select a Persona explicitly (use herdr_subagents_control action=list).`;
|
|
15
|
+
}
|
|
13
16
|
return undefined;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
|
|
6
7
|
import registerSubagentReport from "../extensions/subagent-report.ts";
|
|
7
|
-
import { registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
|
|
8
|
+
import { normalizeHerdrSubagentsArguments, registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
|
|
8
9
|
import { applyTaskStatus } from "../src/records.ts";
|
|
10
|
+
import { executeHerdrSubagents } from "../src/tool.ts";
|
|
9
11
|
|
|
10
12
|
process.env.HERDR_ENV = "1";
|
|
11
13
|
process.env.HERDR_WORKSPACE_ID = "test-workspace";
|
|
@@ -92,6 +94,45 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
|
|
|
92
94
|
return { result, herdr };
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
{
|
|
98
|
+
const herdr = fakeHerdr();
|
|
99
|
+
const execute = (params) => executeHerdrSubagents(params, undefined, undefined, { cwd: root }, {
|
|
100
|
+
agentsDirectory: fileURLToPath(new URL("../agents/", import.meta.url)),
|
|
101
|
+
configurationPaths: { globalConfig: join(root, "global-config.json") },
|
|
102
|
+
herdr,
|
|
103
|
+
});
|
|
104
|
+
const missing = { name: "review", prompt: "Read-only review" };
|
|
105
|
+
for (const params of [
|
|
106
|
+
{ tasks: [missing] },
|
|
107
|
+
...["", " \t", null, 42].map((agent) => ({ tasks: [{ ...missing, agent }] })),
|
|
108
|
+
normalizeHerdrSubagentsArguments({ prompt: "Read-only review" }),
|
|
109
|
+
normalizeHerdrSubagentsArguments({ prompt: "Read-only review", agent: 42 }),
|
|
110
|
+
]) {
|
|
111
|
+
const response = await execute(params);
|
|
112
|
+
assert.equal(response.isError, true, "Missing or invalid agent must not silently select worker");
|
|
113
|
+
assert.match(response.content[0].text, /tasks\[0\]\.agent.*required/);
|
|
114
|
+
}
|
|
115
|
+
const laterMissing = await execute({ tasks: [{ ...missing, agent: "analyst" }, missing], concurrency: 2 });
|
|
116
|
+
assert.match(laterMissing.content[0].text, /tasks\[1\]\.agent.*required/);
|
|
117
|
+
const unknown = await execute({ tasks: [{ ...missing, agent: "unknown-persona" }] });
|
|
118
|
+
assert.equal(unknown.isError, true);
|
|
119
|
+
assert.match(unknown.content[0].text, /Unknown persona 'unknown-persona'/);
|
|
120
|
+
assert.deepEqual(herdr.starts, [], "Invalid requests must not start an Agent");
|
|
121
|
+
assert.equal(existsSync(join(root, ".pi", "herdr-subagents", "runs")), false, "Invalid requests must not create run records");
|
|
122
|
+
|
|
123
|
+
const serial = await execute(normalizeHerdrSubagentsArguments({ prompt: "make change", agent: "worker" }));
|
|
124
|
+
assert.equal(serial.details.status, "completed", "Explicit worker must still run serially through legacy argument normalization");
|
|
125
|
+
const parallel = await execute({ tasks: ["analyst", "reviewer", "explorer"].map((agent) => ({ ...missing, agent })), concurrency: 2 });
|
|
126
|
+
assert.equal(parallel.details.status, "completed");
|
|
127
|
+
assert.equal(parallel.details.effectiveConcurrency, 2);
|
|
128
|
+
|
|
129
|
+
const projectAgents = join(root, ".pi", "herdr-subagents", "agents");
|
|
130
|
+
mkdirSync(projectAgents, { recursive: true });
|
|
131
|
+
writeFileSync(join(projectAgents, "auditor.md"), "---\nname: auditor\ndescription: Custom read-only reviewer\naccess: read\n---\nReview the task.\n");
|
|
132
|
+
const custom = await execute({ tasks: [{ ...missing, agent: " auditor " }], concurrency: 2 });
|
|
133
|
+
assert.equal(custom.details.status, "completed", "Custom Persona names must remain valid and trim whitespace");
|
|
134
|
+
}
|
|
135
|
+
|
|
95
136
|
{
|
|
96
137
|
const { result, herdr } = await run([task("one", "first slow"), task("two", "second")], 1);
|
|
97
138
|
assert.equal(result.status, "completed");
|
|
@@ -146,7 +187,12 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
|
|
|
146
187
|
assert.deepEqual(result.activity.map((entry) => entry.status), ["cancelled", "cancelled"]);
|
|
147
188
|
}
|
|
148
189
|
|
|
149
|
-
await assert.rejects(() => run([task("writer", "edit", "write"), task("reader", "read")], 2),
|
|
190
|
+
await assert.rejects(() => run([task("writer", "edit", "write"), task("reader", "read")], 2), (error) => {
|
|
191
|
+
assert.match(error.message, /writer.*worker/);
|
|
192
|
+
assert.match(error.message, /concurrency: 1/);
|
|
193
|
+
assert.match(error.message, /read-only Persona/);
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
150
196
|
|
|
151
197
|
const terminal = { id: "task", runId: "run", order: 1, status: "completed" };
|
|
152
198
|
assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are immutable/);
|
|
@@ -184,6 +230,9 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
184
230
|
});
|
|
185
231
|
assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
|
|
186
232
|
assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
|
|
233
|
+
const taskSchema = registeredTools.get("herdr_subagents").parameters.properties.tasks.items;
|
|
234
|
+
assert.ok(taskSchema.required.includes("agent"), "The model-facing schema must require agent");
|
|
235
|
+
assert.equal(taskSchema.properties.agent.enum, undefined, "Custom Persona names must remain valid");
|
|
187
236
|
assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
|
|
188
237
|
assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
|
|
189
238
|
mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
|