@maxiaochao/pi-toolkit 0.10.1 → 0.10.2
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/global/AGENTS.md +33 -50
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +6 -0
- package/packages/herdr-subagents/README.md +13 -12
- package/packages/herdr-subagents/TESTING.md +2 -2
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +88 -49
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/parameters.ts +16 -13
- package/packages/herdr-subagents/src/queued-runs.ts +3 -3
- package/packages/herdr-subagents/src/tool.ts +39 -6
- package/packages/herdr-subagents/src/validation.ts +16 -29
- package/packages/herdr-subagents/tests/extension-contract.ts +27 -9
- package/packages/herdr-subagents/tests/run-tests.mjs +10 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2026-10-08 - v0.10.2
|
|
2
|
+
|
|
3
|
+
- bundle herdr-subagents v0.3.0: split execution and control tools and unify task runs
|
|
4
|
+
- Bundle the Herdr Subagents execution/control tool split and unified non-empty `tasks` input for single and multi-task runs.
|
|
5
|
+
|
|
1
6
|
## 2026-10-08 - v0.10.1
|
|
2
7
|
|
|
3
8
|
- bundle herdr-subagents v0.2.1: add default thinking configuration and inline Herdr workflow video
|
package/global/AGENTS.md
CHANGED
|
@@ -4,71 +4,54 @@ Behavioral guidelines to reduce common LLM coding mistakes. Project-specific ins
|
|
|
4
4
|
|
|
5
5
|
## 1. Think Before Coding
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Surface material assumptions, interpretations, and tradeoffs.
|
|
8
|
+
- If unresolved ambiguity would materially change the result, ask. When no human is reachable, choose a defensible interpretation, state it, and proceed with verifiable criteria.
|
|
9
|
+
- Prefer the simpler approach. Name conflicting requirements rather than silently compromising between them.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
- If multiple interpretations exist, present them rather than picking one silently.
|
|
11
|
-
- If a simpler approach exists, say so. Push back when warranted.
|
|
12
|
-
- If requirements conflict (durability vs latency, correctness vs speed), name the conflict explicitly. Never average conflicting requirements into something that satisfies neither.
|
|
13
|
-
- When unresolved ambiguity would materially change the result, ask. When no human is reachable (headless, async, subagent), choose the interpretation you can defend, state it as an assumption, and proceed under Rule 4.
|
|
11
|
+
## 2. Minimal Solution
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Understand the problem and relevant code flow before choosing the first rung that works:
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
1. Skip functionality that is speculative or was not requested.
|
|
16
|
+
2. Reuse an existing project abstraction when it encodes the required semantics.
|
|
17
|
+
3. Otherwise use the standard library or a native platform feature.
|
|
18
|
+
4. Otherwise use a direct dependency already declared by the project.
|
|
19
|
+
5. Only then write the minimum custom code that works.
|
|
18
20
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
+
- Introduce abstractions only after repeated use reveals a stable concept.
|
|
22
|
+
- Prefer boring, focused code; review long or deeply nested functions for splitting.
|
|
23
|
+
- Never simplify away explicit requirements, trust-boundary validation, data-loss protection, security, or accessibility.
|
|
21
24
|
- For states excluded by enforced invariants, fail visibly instead of inventing fallback behavior.
|
|
22
|
-
-
|
|
25
|
+
- State the ceiling and upgrade trigger of any deliberate simplification that cuts a real corner.
|
|
23
26
|
|
|
24
|
-
## 3.
|
|
27
|
+
## 3. Skill Boundaries
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
After loading a skill, treat its documented interface as sufficient for normal use. Do not inspect the skill's implementation, installed executable, or package source unless the user asks to review, debug, or modify it; its documented behavior conflicts with observed behavior; or required usage details are missing from the skill.
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## 4. Read, Then Change Surgically
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
- Remove imports, variables, and functions that YOUR changes made unused. Leave pre-existing dead code in place and mention it.
|
|
33
|
+
Before editing, read the affected public API and its immediate callers. For a bug, find every statically discoverable caller and relevant implementation.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
- Find every statically discoverable caller and relevant implementation; a report names a symptom, and patching only the path the ticket mentions leaves sibling callers still broken.
|
|
35
|
-
- If the shared function is wrong, fix it once - do not scatter per-caller guards. Name the sibling callers the fix now affects.
|
|
36
|
-
- If one caller is wrong, fix that caller - do not add defensive handling to a correct shared function.
|
|
35
|
+
Every changed line traces to the user's request or to a defect that request reveals.
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
- Match existing style.
|
|
38
|
+
- Remove imports, variables, and functions that your changes make unused. Leave pre-existing dead code in place and mention it.
|
|
39
|
+
- Fix a faulty shared function once and name affected sibling callers; if only one caller is wrong, fix that caller instead of guarding a correct shared function.
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
## 5. Goal-Driven Execution
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- "Refactor X" -> "Ensure tests pass before and after"
|
|
46
|
-
- "Make X fast" -> "Write and test a simple reference implementation, then optimize against its results"
|
|
43
|
+
- Turn work into checkable outcomes: reproduce bugs before fixing them, preserve behavior during refactors, and optimize against a tested reference implementation.
|
|
44
|
+
- For multi-step or materially ambiguous work, state a brief plan and give every step its own verification.
|
|
45
|
+
- Leave the smallest relevant runnable check for new or changed non-trivial logic. Do not introduce a test framework solely for that check.
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
1. [Step] -> verify: [check]
|
|
47
|
+
## 6. Fail Visibly
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## 5. Read Before You Write
|
|
54
|
-
|
|
55
|
-
**Understand adjacent code before changing it. "Looks orthogonal" is a dangerous assumption.**
|
|
56
|
-
|
|
57
|
-
Before changing a file:
|
|
58
|
-
- Read the file's exports and public API.
|
|
59
|
-
- Read the immediate callers of the function you're modifying.
|
|
60
|
-
|
|
61
|
-
## 6. Fail Visibly, Not Silently
|
|
62
|
-
|
|
63
|
-
**Silent skips are failures.**
|
|
64
|
-
|
|
65
|
-
- "Completed" is wrong if anything was silently skipped.
|
|
49
|
+
- "Completed" is wrong if anything required was silently skipped.
|
|
66
50
|
- "Tests pass" is wrong if any required test was skipped or not run.
|
|
67
|
-
- If a required input, file, or tool is missing
|
|
51
|
+
- If a required input, file, or tool is missing, or a step is impossible under the stated constraints, report what is missing and what you need. Never fabricate it to make a check pass.
|
|
52
|
+
|
|
53
|
+
## 7. Worktree Isolation
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
For a Git-repository task that coordinates substantial changes across multiple files or modules, or implements a feature intended for release, ask before editing whether to isolate it in a worktree. Skip the question when already in a linked worktree.
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
1. Reuse an existing project abstraction when it encodes project semantics. (If it is itself the bug, fix it once - see Rule 3.)
|
|
73
|
-
2. Otherwise use the standard library or a direct dependency already declared by the project.
|
|
74
|
-
3. Only then write it yourself.
|
|
57
|
+
If the user agrees, read the `pi-worktree` skill and establish the worktree before changing code. When no human is reachable, create a worktree only when the task explicitly requests one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2026-10-08 - v0.3.0
|
|
2
|
+
|
|
3
|
+
- split execution and control tools and unify task runs
|
|
4
|
+
- Split execution and control into `herdr_subagents` and `herdr_subagents_control`; require a non-empty `tasks` array for new runs so one-item runs and batches share background and concurrency semantics.
|
|
5
|
+
- Normalize legacy top-level single-task prompts into one-item task arrays before schema validation.
|
|
6
|
+
|
|
1
7
|
## 2026-10-08 - v0.2.1
|
|
2
8
|
|
|
3
9
|
- add default thinking configuration and inline Herdr workflow video
|
|
@@ -29,7 +29,7 @@ pi update npm:@maxiaochao/pi-herdr-subagents
|
|
|
29
29
|
|
|
30
30
|
The full `@maxiaochao/pi-toolkit` package also bundles this extension. Install the full toolkit or this standalone child package, not both.
|
|
31
31
|
|
|
32
|
-
Inside a Herdr workspace, the package registers `herdr_subagents` and `/herdr-subagents`. Outside Herdr (or without a workspace ID), the extension registers
|
|
32
|
+
Inside a Herdr workspace, the package registers `herdr_subagents`, `herdr_subagents_control`, and `/herdr-subagents`. Outside Herdr (or without a workspace ID), the extension registers none of them, so these capabilities are absent from the model context. Start Pi inside Herdr or reload the extension there to enable them.
|
|
33
33
|
|
|
34
34
|
The design replaces hidden print-mode subprocesses with ordinary interactive Pi Agents managed through Herdr, so delegated work stays visible, inspectable, resumable, and durably recorded.
|
|
35
35
|
|
|
@@ -38,16 +38,17 @@ The design replaces hidden print-mode subprocesses with ordinary interactive Pi
|
|
|
38
38
|
Discover the effective personas first:
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
|
-
|
|
41
|
+
herdr_subagents_control({ action: "list" })
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Then run one self-contained task:
|
|
45
45
|
|
|
46
46
|
```text
|
|
47
47
|
herdr_subagents({
|
|
48
|
-
agent: "worker",
|
|
49
48
|
label: "auth-review",
|
|
50
|
-
|
|
49
|
+
tasks: [
|
|
50
|
+
{ name: "auth", agent: "worker", prompt: "Inspect the authentication flow and summarize the risks" }
|
|
51
|
+
]
|
|
51
52
|
})
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -64,21 +65,21 @@ herdr_subagents({
|
|
|
64
65
|
})
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
Set `background: true` to return a stable run ID immediately while the queue continues. One run is dispatched at a time by an extension instance; later runs remain queued and start FIFO. Each background run sends one compact parent notification only when the whole run completes, partially fails, fails, or blocks. Individual successful tasks do not wake the parent model.
|
|
68
|
+
`tasks` is always a non-empty array: one item is a single task and multiple items form a batch. Set `background: true` for either form to return a stable run ID immediately while the queue continues. One run is dispatched at a time by an extension instance; later runs remain queued and start FIFO. Each background run sends one compact parent notification only when the whole run completes, partially fails, fails, or blocks. Individual successful tasks do not wake the parent model.
|
|
68
69
|
|
|
69
|
-
A child missing required information reports `needs-input` with an exact question. The run becomes blocked, keeps its Herdr tab, pane, partial report, and Pi session, and returns control to the parent. Answer through the
|
|
70
|
+
A child missing required information reports `needs-input` with an exact question. The run becomes blocked, keeps its Herdr tab, pane, partial report, and Pi session, and returns control to the parent. Answer through the control tool to continue the original session:
|
|
70
71
|
|
|
71
72
|
```text
|
|
72
|
-
|
|
73
|
+
herdr_subagents_control({ action: "respond", runId: "<run-id>", answer: "Use the main branch." })
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
While a batch is active, its task rows stay below the editor. With an empty editor, press `↓` to enter the list, `↑`/`↓` to choose a task, `Enter` to focus its exact pane, and `Esc` to return. `/herdr-subagents focus <task-number>` remains available as a fallback.
|
|
76
77
|
|
|
77
78
|
## History
|
|
78
79
|
|
|
79
|
-
Run `/herdr-subagents history` or `
|
|
80
|
+
Run `/herdr-subagents history` or `herdr_subagents_control({ action: "history" })` to list compact project-local run and task reports after Herdr tabs close. Full results, described documents, artifacts, and persistent Pi sessions remain separate on disk and are not loaded into parent context.
|
|
80
81
|
|
|
81
|
-
Continue a saved conversation in a new Herdr tab with `
|
|
82
|
+
Continue a saved conversation in a new Herdr tab with `herdr_subagents_control({ action: "resume", runId: "<run-id>", task: 1, prompt: "Follow up..." })`. Remove only a selected archive with `herdr_subagents_control({ action: "cleanup", runId: "<run-id>" })`. History has no automatic count-based eviction. The extension adds the runtime path to local Git excludes when available (and otherwise uses `.pi/.gitignore`) without overwriting unrelated rules.
|
|
82
83
|
|
|
83
84
|
Read-only tasks start FIFO up to the configured concurrency limit. A pane is created only when its task starts, belongs exclusively to that task, and is never reused. Completed panes remain inspectable while another task in the batch is unsettled. Results remain in input order, and one failed task does not cancel its siblings. Any batch containing a write-capable persona is forced to concurrency one.
|
|
84
85
|
|
|
@@ -250,9 +251,9 @@ Override the model for one task:
|
|
|
250
251
|
|
|
251
252
|
```text
|
|
252
253
|
herdr_subagents({
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
tasks: [
|
|
255
|
+
{ name: "authorization", agent: "reviewer", model: "anthropic/claude-opus-4-1:high", prompt: "Review the authorization boundary" }
|
|
256
|
+
]
|
|
256
257
|
})
|
|
257
258
|
```
|
|
258
259
|
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
- Force write-capable batches to serial execution while allowing bounded read-only parallelism.
|
|
17
17
|
- Preserve compact reports, full results, Pi sessions, blocked questions, and historical follow-ups.
|
|
18
18
|
- Recover queued/running work after interruption without retrying post-prompt side effects, including truncated reports and recovery errors.
|
|
19
|
-
- Verify the
|
|
19
|
+
- Verify the split public contracts (`herdr_subagents` with required `tasks`, plus `herdr_subagents_control` with required `action`) and injectable extension wiring, including conditional registration only inside Herdr, Fleet rendering, editor focus ownership, terminal-input passthrough, exact-pane navigation, and stable display while another background run queues.
|
|
20
20
|
- Verify blocked-response live Fleet updates, authoritative stale-field replacement, parent-shutdown cancellation, cleanup notifications, atomic state replacement, non-retried uncertain startup operations, and stalled warnings.
|
|
21
21
|
- Verify resumed batches retain newly allocated pane IDs and historical follow-ups exclude simultaneous session writers.
|
|
22
22
|
|
|
23
23
|
## Latest baseline
|
|
24
24
|
|
|
25
|
-
All
|
|
25
|
+
All 137 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
|
|
@@ -9,8 +9,8 @@ import { RunDispatcher } from "../src/dispatcher.ts";
|
|
|
9
9
|
import { historyText, listSubagentHistory } from "../src/history.ts";
|
|
10
10
|
import { cancelQueuedRuns, loadQueuedRuns, persistQueuedRun, removeQueuedRun } from "../src/queued-runs.ts";
|
|
11
11
|
import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcile.ts";
|
|
12
|
-
import { executeHerdrSubagents, type HerdrSubagentsBatchDetails, type HerdrSubagentsDetails, type
|
|
13
|
-
import { HerdrSubagentsParams } from "../src/parameters.ts";
|
|
12
|
+
import { executeHerdrSubagents, executeHerdrSubagentsControl, type HerdrSubagentsBatchDetails, type HerdrSubagentsControlToolParams, type HerdrSubagentsDetails, type HerdrSubagentsRunParams } from "../src/tool.ts";
|
|
13
|
+
import { HerdrSubagentsControlParams, HerdrSubagentsParams } from "../src/parameters.ts";
|
|
14
14
|
import { HERDR_ACTIONS } from "../src/validation.ts";
|
|
15
15
|
import { FleetController } from "../src/fleet-controller.ts";
|
|
16
16
|
import { isSettledRunStatus } from "../src/records.ts";
|
|
@@ -25,18 +25,32 @@ function statusVisual(status: string): { color: "success" | "warning" | "error"
|
|
|
25
25
|
return { color: "error", glyph: "✗" };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function renderToolCall(args:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
function renderToolCall(args: HerdrSubagentsRunParams, theme: Theme): Text {
|
|
29
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("herdr_subagents"))} ${theme.fg("accent", `${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`)}${theme.fg("muted", ` · concurrency ${args.concurrency ?? "default"}`)}`, 0, 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderControlToolCall(args: HerdrSubagentsControlToolParams, theme: Theme): Text {
|
|
33
|
+
const action = HERDR_ACTIONS[args.action];
|
|
34
|
+
const runId = action.includeRunId ? ` ${theme.fg("muted", String(args.runId ?? ""))}` : "";
|
|
35
|
+
return new Text(`${theme.fg("toolTitle", theme.bold(`herdr_subagents_control ${args.action}`))}${runId}`, 0, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeHerdrSubagentsArguments(args: unknown): HerdrSubagentsRunParams {
|
|
39
|
+
if (!args || typeof args !== "object") return args as HerdrSubagentsRunParams;
|
|
40
|
+
const legacy = args as Record<string, unknown>;
|
|
41
|
+
if (Array.isArray(legacy.tasks) || typeof legacy.prompt !== "string" || legacy.action !== undefined) return args as HerdrSubagentsRunParams;
|
|
42
|
+
if (Object.keys(legacy).some((key) => !["prompt", "agent", "label", "model", "background"].includes(key))) return args as HerdrSubagentsRunParams;
|
|
43
|
+
const label = typeof legacy.label === "string" ? legacy.label : undefined;
|
|
44
|
+
return {
|
|
45
|
+
tasks: [{
|
|
46
|
+
name: label || "task",
|
|
47
|
+
prompt: legacy.prompt,
|
|
48
|
+
...(typeof legacy.agent === "string" ? { agent: legacy.agent } : {}),
|
|
49
|
+
...(typeof legacy.model === "string" ? { model: legacy.model } : {}),
|
|
50
|
+
}],
|
|
51
|
+
...(label ? { label } : {}),
|
|
52
|
+
...(typeof legacy.background === "boolean" ? { background: legacy.background } : {}),
|
|
53
|
+
};
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRenderResultOptions, theme: Theme): Container | Text {
|
|
@@ -49,20 +63,20 @@ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRender
|
|
|
49
63
|
return container;
|
|
50
64
|
}
|
|
51
65
|
const details = result.details as HerdrSubagentsDetails | undefined;
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
container.addChild(new
|
|
61
|
-
|
|
66
|
+
if (details?.agent) {
|
|
67
|
+
const container = new Container();
|
|
68
|
+
container.addChild(new Text(`${theme.fg("toolTitle", theme.bold(details.agent))} ${theme.fg(statusVisual(details.status).color, details.status)}`, 0, 0));
|
|
69
|
+
container.addChild(new Text(details.summary, 0, 0));
|
|
70
|
+
if (details.documents.length > 0) {
|
|
71
|
+
container.addChild(new Spacer(1));
|
|
72
|
+
for (const document of details.documents) container.addChild(new Text(theme.fg("muted", `${document.description}: ${document.path}`), 0, 0));
|
|
73
|
+
}
|
|
74
|
+
if (details.recordDirectory) container.addChild(new Text(theme.fg("dim", `Record: ${details.recordDirectory}`), 0, 0));
|
|
75
|
+
if (details.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${details.errorMessage}`), 0, 0));
|
|
76
|
+
return container;
|
|
62
77
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return container;
|
|
78
|
+
const content = result.content[0];
|
|
79
|
+
return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
|
|
@@ -104,6 +118,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
104
118
|
export interface HerdrSubagentsExtensionDependencies {
|
|
105
119
|
herdr?: HerdrAutomation;
|
|
106
120
|
execute?: typeof executeHerdrSubagents;
|
|
121
|
+
executeControl?: typeof executeHerdrSubagentsControl;
|
|
107
122
|
isEditor?: (value: unknown) => boolean;
|
|
108
123
|
createFleetWidget?: (lines: string[]) => Container;
|
|
109
124
|
}
|
|
@@ -139,6 +154,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
139
154
|
if (process.env.PI_HERDR_SUBAGENTS_CHILD === "1" || process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) return;
|
|
140
155
|
const herdr = dependencies.herdr ?? new CliHerdrAutomation();
|
|
141
156
|
const execute = dependencies.execute ?? executeHerdrSubagents;
|
|
157
|
+
const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
|
|
142
158
|
const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
|
|
143
159
|
const dispatcher = new RunDispatcher();
|
|
144
160
|
const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget);
|
|
@@ -177,17 +193,17 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
177
193
|
pi.registerTool({
|
|
178
194
|
name: "herdr_subagents",
|
|
179
195
|
label: "Herdr Subagents",
|
|
180
|
-
description: "Run one
|
|
196
|
+
description: "Run one or more ordered tasks in visible interactive Herdr Pi Agents. Use one tasks item for a single task. Independent read-only tasks may run concurrently; write-capable runs are serialized. Foreground calls block, while background calls return a run ID immediately. Complete results and Pi sessions stay in project-local records.",
|
|
181
197
|
promptSnippet: "Run focused work in visible Herdr subagents",
|
|
182
|
-
promptGuidelines: ["Call action=list once before the first execution. Submit
|
|
198
|
+
promptGuidelines: ["Call herdr_subagents_control with action=list once before the first execution. Submit an ordered tasks list; use one item for a single task and concurrency only for independent read-only tasks. Foreground calls block until the run settles; background calls return a run ID immediately."],
|
|
183
199
|
parameters: HerdrSubagentsParams,
|
|
200
|
+
prepareArguments: normalizeHerdrSubagentsArguments,
|
|
184
201
|
|
|
185
|
-
async execute(_toolCallId, params:
|
|
202
|
+
async execute(_toolCallId, params: HerdrSubagentsRunParams, signal, onUpdate, ctx) {
|
|
186
203
|
const generation = sessionGeneration;
|
|
187
|
-
const runId =
|
|
188
|
-
const executionId = runId ?? params.runId ?? randomUUID();
|
|
204
|
+
const runId = randomUUID();
|
|
189
205
|
const controller = new AbortController();
|
|
190
|
-
runControllers.set(
|
|
206
|
+
runControllers.set(runId, controller);
|
|
191
207
|
const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(!params.background && signal ? [signal] : [])]);
|
|
192
208
|
const executeRun = async () => {
|
|
193
209
|
const result = await execute(params, runSignal, (update) => {
|
|
@@ -201,21 +217,6 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
201
217
|
fleet.sync(result.details);
|
|
202
218
|
return result;
|
|
203
219
|
};
|
|
204
|
-
if (!params.tasks) {
|
|
205
|
-
const promise = executeRun();
|
|
206
|
-
inFlight.add(promise);
|
|
207
|
-
const result = await promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); });
|
|
208
|
-
if (params.action === "respond") {
|
|
209
|
-
const details = result.details as { runId?: string; status?: string };
|
|
210
|
-
if (details.status === "blocked") fleet.sync(result.details);
|
|
211
|
-
else if (isSettledRunStatus(details.status)) {
|
|
212
|
-
fleet.clearRun(params.runId);
|
|
213
|
-
if (params.runId) dispatcher.release(params.runId);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return result;
|
|
217
|
-
}
|
|
218
|
-
if (!runId) throw new Error("Batch run ID was not allocated.");
|
|
219
220
|
if (params.background) await persistQueuedRun(ctx.cwd, runId, params, { sessionId: ownerSessionId, processId: process.pid });
|
|
220
221
|
const promise = dispatcher.submit(runId, async () => {
|
|
221
222
|
const result = await executeRun().finally(async () => { if (params.background) await removeQueuedRun(ctx.cwd, runId); });
|
|
@@ -227,7 +228,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
227
228
|
return result;
|
|
228
229
|
});
|
|
229
230
|
inFlight.add(promise);
|
|
230
|
-
void promise.finally(() => { inFlight.delete(promise); runControllers.delete(
|
|
231
|
+
void promise.finally(() => { inFlight.delete(promise); runControllers.delete(runId); }).catch(() => undefined);
|
|
231
232
|
if (params.background) {
|
|
232
233
|
void promise.then((result) => {
|
|
233
234
|
if (generation !== sessionGeneration) return;
|
|
@@ -248,6 +249,44 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
248
249
|
renderResult: renderToolResult,
|
|
249
250
|
});
|
|
250
251
|
|
|
252
|
+
pi.registerTool({
|
|
253
|
+
name: "herdr_subagents_control",
|
|
254
|
+
label: "Herdr Subagents Control",
|
|
255
|
+
description: "Inspect Herdr personas and run history, answer a blocked task, resume a saved task, or remove a selected archived run. Use herdr_subagents to start new work.",
|
|
256
|
+
promptSnippet: "Inspect and control Herdr subagent runs",
|
|
257
|
+
parameters: HerdrSubagentsControlParams,
|
|
258
|
+
|
|
259
|
+
async execute(_toolCallId, params: HerdrSubagentsControlToolParams, signal, onUpdate, ctx) {
|
|
260
|
+
const executionId = params.runId ?? randomUUID();
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
runControllers.set(executionId, controller);
|
|
263
|
+
const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(signal ? [signal] : [])]);
|
|
264
|
+
const promise = executeControl(params, runSignal, (update) => {
|
|
265
|
+
const details = update.details as HerdrSubagentsBatchDetails | undefined;
|
|
266
|
+
if (details?.activity) fleet.sync(details);
|
|
267
|
+
const updateText = update.content.find((item) => item.type === "text")?.text;
|
|
268
|
+
const updateDetails = update.details as { warning?: string };
|
|
269
|
+
if (updateDetails.warning || updateText?.includes("cleanup failed")) ctx.ui.notify(updateDetails.warning ?? updateText ?? "Subagent warning", "warning");
|
|
270
|
+
onUpdate?.(update);
|
|
271
|
+
}, ctx, { agentsDirectory: packageAgentsDir, owner: { sessionId: ownerSessionId, processId: process.pid } });
|
|
272
|
+
inFlight.add(promise);
|
|
273
|
+
const result = await promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); });
|
|
274
|
+
fleet.sync(result.details);
|
|
275
|
+
if (params.action === "respond") {
|
|
276
|
+
const details = result.details as { status?: string };
|
|
277
|
+
if (details.status === "blocked") fleet.sync(result.details);
|
|
278
|
+
else if (isSettledRunStatus(details.status)) {
|
|
279
|
+
fleet.clearRun(params.runId);
|
|
280
|
+
if (params.runId) dispatcher.release(params.runId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
renderCall: renderControlToolCall,
|
|
287
|
+
renderResult: renderToolResult,
|
|
288
|
+
});
|
|
289
|
+
|
|
251
290
|
pi.registerCommand("herdr-subagents", {
|
|
252
291
|
description: "Inspect effective subagent agents, models, and settings",
|
|
253
292
|
getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
+
const HerdrSubagentsTaskParams = Type.Object({
|
|
5
|
+
name: Type.String({ minLength: 1, maxLength: 48, description: "Short task name shown in the pane and task list." }),
|
|
6
|
+
prompt: Type.String({ minLength: 1, description: "Self-contained prompt for this task." }),
|
|
7
|
+
agent: Type.Optional(Type.String({ minLength: 1, description: "Persona for this task. Defaults to worker." })),
|
|
8
|
+
model: Type.Optional(Type.String({ minLength: 1, description: "Optional model override for this task." })),
|
|
9
|
+
}, { additionalProperties: false });
|
|
10
|
+
|
|
4
11
|
export const HerdrSubagentsParams = Type.Object({
|
|
5
|
-
|
|
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
|
+
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 simultaneously active Agents. Write-capable tasks force concurrency one." })),
|
|
15
|
+
background: Type.Optional(Type.Boolean({ description: "Return a run ID immediately while the run continues in this parent session." })),
|
|
16
|
+
}, { additionalProperties: false });
|
|
17
|
+
|
|
18
|
+
export const HerdrSubagentsControlParams = Type.Object({
|
|
19
|
+
action: StringEnum(["list", "respond", "history", "resume", "cleanup"] as const, { description: "Control operation to perform." }),
|
|
6
20
|
runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by respond, resume, or cleanup." })),
|
|
7
21
|
answer: Type.Optional(Type.String({ minLength: 1, description: "Answer sent to a blocked task when action is respond." })),
|
|
8
22
|
task: Type.Optional(Type.Integer({ minimum: 1, description: "One-based task number selected for respond or resume." })),
|
|
9
|
-
prompt: Type.Optional(Type.String({ minLength: 1, description: "
|
|
10
|
-
agent: Type.Optional(Type.String({ minLength: 1, description: "Persona for single execution. Defaults to worker." })),
|
|
11
|
-
label: Type.Optional(Type.String({ minLength: 1, maxLength: 48, description: "Short Herdr run-tab label." })),
|
|
12
|
-
model: Type.Optional(Type.String({ minLength: 1, description: "Optional Pi model override, for example provider/model:high." })),
|
|
13
|
-
tasks: Type.Optional(Type.Array(Type.Object({
|
|
14
|
-
name: Type.String({ minLength: 1, maxLength: 48, description: "Short task name shown in the pane and task list." }),
|
|
15
|
-
prompt: Type.String({ minLength: 1, description: "Self-contained prompt for this task." }),
|
|
16
|
-
agent: Type.Optional(Type.String({ minLength: 1, description: "Persona for this task. Defaults to worker." })),
|
|
17
|
-
model: Type.Optional(Type.String({ minLength: 1, description: "Optional model override for this task." })),
|
|
18
|
-
}), { minItems: 1, description: "Ordered tasks for bounded-concurrency batch execution." })),
|
|
19
|
-
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Maximum simultaneously active Agents in a batch." })),
|
|
20
|
-
background: Type.Optional(Type.Boolean({ description: "Return a run ID immediately while the batch continues in this parent session." })),
|
|
23
|
+
prompt: Type.Optional(Type.String({ minLength: 1, description: "Follow-up prompt used by resume." })),
|
|
21
24
|
}, { additionalProperties: false });
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type { HerdrSubagentsRunParams } from "./tool.ts";
|
|
5
5
|
import { isProcessAlive, ownerRecord, type OwnerIdentity } from "./ownership.ts";
|
|
6
6
|
import { writeJsonAtomic } from "./state.ts";
|
|
7
7
|
import { findRunDirectory } from "./run-locator.ts";
|
|
8
8
|
|
|
9
|
-
export interface QueuedRunRecord { runId: string; cwd: string; ownerSessionId?: string; ownerProcessId?: number; params:
|
|
9
|
+
export interface QueuedRunRecord { runId: string; cwd: string; ownerSessionId?: string; ownerProcessId?: number; params: HerdrSubagentsRunParams; queuedAt: string; }
|
|
10
10
|
function queueDirectory(cwd: string): string { return join(cwd, ".pi", "herdr-subagents", "queue"); }
|
|
11
11
|
|
|
12
|
-
export async function persistQueuedRun(cwd: string, runId: string, params:
|
|
12
|
+
export async function persistQueuedRun(cwd: string, runId: string, params: HerdrSubagentsRunParams, owner?: OwnerIdentity): Promise<void> {
|
|
13
13
|
const directory = queueDirectory(cwd);
|
|
14
14
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
15
15
|
await writeJsonAtomic(join(directory, `${runId}.json`), { runId, cwd, ...ownerRecord(owner), params: { ...params, background: true }, queuedAt: new Date().toISOString() });
|
|
@@ -4,11 +4,27 @@ import { runHerdrSubagents, type HerdrSubagentsResult } from "./runner.ts";
|
|
|
4
4
|
import { runHerdrSubagentsBatch, type BatchResult, type BatchTask } from "./batch-runner.ts";
|
|
5
5
|
import { respondToBlockedTask } from "./blocking.ts";
|
|
6
6
|
import { cleanSubagentRun, historyText, listSubagentHistory, resumeHistoricalTask } from "./history.ts";
|
|
7
|
-
import { HERDR_ACTIONS,
|
|
7
|
+
import { HERDR_ACTIONS, validateControlParams, validateRunParams } from "./validation.ts";
|
|
8
8
|
import type { OwnerIdentity } from "./ownership.ts";
|
|
9
9
|
|
|
10
10
|
export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent?: string; model?: string; }
|
|
11
11
|
|
|
12
|
+
export interface HerdrSubagentsRunParams {
|
|
13
|
+
tasks: HerdrSubagentsTaskParams[];
|
|
14
|
+
label?: string;
|
|
15
|
+
concurrency?: number;
|
|
16
|
+
background?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HerdrSubagentsControlToolParams {
|
|
20
|
+
action: "list" | "respond" | "history" | "resume" | "cleanup";
|
|
21
|
+
runId?: string;
|
|
22
|
+
answer?: string;
|
|
23
|
+
task?: number;
|
|
24
|
+
prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Internal superset used by shared execution helpers. */
|
|
12
28
|
export interface HerdrSubagentsToolParams {
|
|
13
29
|
action?: "list" | "respond" | "history" | "resume" | "cleanup";
|
|
14
30
|
prompt?: string;
|
|
@@ -201,17 +217,34 @@ async function executeSingle({ params, signal, onUpdate, ctx, dependencies, disc
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
export async function executeHerdrSubagents(
|
|
204
|
-
params: HerdrSubagentsToolParams,
|
|
220
|
+
params: HerdrSubagentsRunParams | HerdrSubagentsToolParams,
|
|
205
221
|
signal: AbortSignal | undefined,
|
|
206
222
|
onUpdate: ((response: ToolResponse) => void) | undefined,
|
|
207
223
|
ctx: HerdrSubagentsToolContext,
|
|
208
224
|
dependencies: HerdrSubagentsToolDependencies,
|
|
209
225
|
): Promise<ToolResponse> {
|
|
210
|
-
|
|
226
|
+
if (params.action) return executeHerdrSubagentsControl(params as HerdrSubagentsControlToolParams, signal, onUpdate, ctx, dependencies);
|
|
227
|
+
if (params.prompt && !params.tasks) {
|
|
228
|
+
const discovery = loadSubagentConfiguration(ctx.cwd, dependencies.agentsDirectory, modelId(ctx), dependencies.configurationPaths);
|
|
229
|
+
return executeSingle({ params, signal, onUpdate, ctx, dependencies, discovery });
|
|
230
|
+
}
|
|
231
|
+
const runParams = params as HerdrSubagentsRunParams;
|
|
232
|
+
const validationError = validateRunParams(runParams);
|
|
211
233
|
if (validationError) return { content: [{ type: "text", text: validationError }], details: { errorMessage: validationError }, isError: true };
|
|
212
234
|
const discovery = loadSubagentConfiguration(ctx.cwd, dependencies.agentsDirectory, modelId(ctx), dependencies.configurationPaths);
|
|
213
|
-
const execution = { params, signal, onUpdate, ctx, dependencies, discovery };
|
|
214
|
-
|
|
235
|
+
const execution = { params: runParams, signal, onUpdate, ctx, dependencies, discovery };
|
|
236
|
+
return executeBatch(execution);
|
|
237
|
+
}
|
|
215
238
|
|
|
216
|
-
|
|
239
|
+
export async function executeHerdrSubagentsControl(
|
|
240
|
+
params: HerdrSubagentsControlToolParams,
|
|
241
|
+
signal: AbortSignal | undefined,
|
|
242
|
+
onUpdate: ((response: ToolResponse) => void) | undefined,
|
|
243
|
+
ctx: HerdrSubagentsToolContext,
|
|
244
|
+
dependencies: HerdrSubagentsToolDependencies,
|
|
245
|
+
): Promise<ToolResponse> {
|
|
246
|
+
const validationError = validateControlParams(params);
|
|
247
|
+
if (validationError) return { content: [{ type: "text", text: validationError }], details: { errorMessage: validationError }, isError: true };
|
|
248
|
+
const discovery = loadSubagentConfiguration(ctx.cwd, dependencies.agentsDirectory, modelId(ctx), dependencies.configurationPaths);
|
|
249
|
+
return actionHandlers[params.action]({ params, signal, onUpdate, ctx, dependencies, discovery });
|
|
217
250
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
const executionFields = ["prompt", "agent", "label", "model", "tasks", "concurrency", "background"] as const;
|
|
1
|
+
import type { HerdrSubagentsControlToolParams, HerdrSubagentsRunParams } from "./tool.ts";
|
|
4
2
|
|
|
5
3
|
export const HERDR_ACTIONS = {
|
|
6
4
|
list: { allowed: ["action"], includeRunId: false },
|
|
@@ -8,31 +6,20 @@ export const HERDR_ACTIONS = {
|
|
|
8
6
|
cleanup: { allowed: ["action", "runId"], includeRunId: true },
|
|
9
7
|
respond: { allowed: ["action", "runId", "answer", "task"], includeRunId: true },
|
|
10
8
|
resume: { allowed: ["action", "runId", "task", "prompt"], includeRunId: true },
|
|
11
|
-
} as const satisfies Record<string, { allowed: readonly (keyof
|
|
9
|
+
} as const satisfies Record<string, { allowed: readonly (keyof HerdrSubagentsControlToolParams)[]; includeRunId: boolean }>;
|
|
10
|
+
|
|
11
|
+
export function validateRunParams(params: HerdrSubagentsRunParams): string | undefined {
|
|
12
|
+
if (!params.tasks?.length) return "Provide a non-empty tasks list.";
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
12
15
|
|
|
13
|
-
export function
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
if (params.prompt && params.tasks) return "Provide either prompt or tasks, not both.";
|
|
25
|
-
if (params.prompt) {
|
|
26
|
-
const extra = extraField(["prompt", "agent", "label", "model"]);
|
|
27
|
-
if (extra) return `Field '${extra}' is not valid for single-task execution.`;
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
if (params.tasks) {
|
|
31
|
-
if (!params.tasks.length) return "tasks must contain at least one task.";
|
|
32
|
-
const extra = extraField(["tasks", "label", "concurrency", "background"]);
|
|
33
|
-
if (extra) return `Field '${extra}' is not valid for batch execution.`;
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
const supplied = executionFields.find((key) => present(key));
|
|
37
|
-
return supplied ? `Field '${supplied}' requires prompt or tasks.` : "Provide an action, prompt, or non-empty tasks list.";
|
|
16
|
+
export function validateControlParams(params: HerdrSubagentsControlToolParams): string | undefined {
|
|
17
|
+
const allowed = HERDR_ACTIONS[params.action]?.allowed;
|
|
18
|
+
if (!allowed) return "Provide a valid control action.";
|
|
19
|
+
const extra = Object.keys(params).find((key) => !allowed.includes(key as keyof HerdrSubagentsControlToolParams));
|
|
20
|
+
if (extra) return `Field '${extra}' is not valid with action '${params.action}'.`;
|
|
21
|
+
if (params.action === "cleanup" && !params.runId?.trim()) return "runId is required for action 'cleanup'.";
|
|
22
|
+
if (params.action === "respond" && (!params.runId?.trim() || !params.answer?.trim())) return "runId and answer are required for action 'respond'.";
|
|
23
|
+
if (params.action === "resume" && (!params.runId?.trim() || !params.task || !params.prompt?.trim())) return "runId, task, and prompt are required for action 'resume'.";
|
|
24
|
+
return undefined;
|
|
38
25
|
}
|
|
@@ -9,6 +9,7 @@ function harness(execute: (...args: any[]) => Promise<any>) {
|
|
|
9
9
|
const handlers = new Map<string, (...args: any[]) => any>();
|
|
10
10
|
const commands: string[] = [];
|
|
11
11
|
let tool: Record<string, any> | undefined;
|
|
12
|
+
let controlTool: Record<string, any> | undefined;
|
|
12
13
|
let terminalInput: ((data: string) => { consume?: boolean } | undefined) | undefined;
|
|
13
14
|
let widgetCalls = 0;
|
|
14
15
|
let renderedLines: string[] = [];
|
|
@@ -26,13 +27,17 @@ function harness(execute: (...args: any[]) => Promise<any>) {
|
|
|
26
27
|
};
|
|
27
28
|
const pi = {
|
|
28
29
|
on(name: string, handler: (...args: any[]) => any) { handlers.set(name, handler); },
|
|
29
|
-
registerTool(definition: Record<string, any>) {
|
|
30
|
+
registerTool(definition: Record<string, any>) {
|
|
31
|
+
if (definition.name === "herdr_subagents") tool = definition;
|
|
32
|
+
if (definition.name === "herdr_subagents_control") controlTool = definition;
|
|
33
|
+
},
|
|
30
34
|
registerCommand(name: string) { commands.push(name); },
|
|
31
35
|
sendMessage() {},
|
|
32
36
|
};
|
|
33
37
|
registerHerdrSubagents(pi as never, {
|
|
34
38
|
herdr: { async focusPane(paneId: string) { focusedPanes.push(paneId); } } as never,
|
|
35
39
|
execute: execute as never,
|
|
40
|
+
executeControl: execute as never,
|
|
36
41
|
isEditor: (value) => value === editor,
|
|
37
42
|
createFleetWidget(lines) { renderedLines = lines; return new Container(); },
|
|
38
43
|
});
|
|
@@ -40,6 +45,7 @@ function harness(execute: (...args: any[]) => Promise<any>) {
|
|
|
40
45
|
handlers,
|
|
41
46
|
commands,
|
|
42
47
|
get tool() { return tool; },
|
|
48
|
+
get controlTool() { return controlTool; },
|
|
43
49
|
get terminalInput() { return terminalInput; },
|
|
44
50
|
get widgetCalls() { return widgetCalls; },
|
|
45
51
|
get renderedLines() { return renderedLines; },
|
|
@@ -73,7 +79,8 @@ async function verifyPublicUiWiring() {
|
|
|
73
79
|
if (!responding.tool) throw new Error("herdr_subagents was not registered for UI wiring test");
|
|
74
80
|
const renderedBlocked = responding.tool.renderResult({ content: [{ type: "text", text: "needs input" }], details: { agent: "explorer", status: "blocked", ok: false, summary: "Need input.", documents: [] } }, { isPartial: false }, { fg: (_color: string, text: string) => text, bold: (text: string) => text }).render(100).join("\n");
|
|
75
81
|
if (!renderedBlocked.includes("blocked") || renderedBlocked.includes("failed")) throw new Error("single-task blocked result did not render its authoritative status");
|
|
76
|
-
|
|
82
|
+
if (!responding.controlTool) throw new Error("herdr_subagents_control was not registered for UI wiring test");
|
|
83
|
+
await responding.controlTool.execute("call", { action: "respond", runId: "blocked-run", answer: "main" }, undefined, undefined, { cwd, ui: responding.ui });
|
|
77
84
|
if (!responding.renderedLines.some((line) => line.includes("second") && line.includes("blocked"))) throw new Error("blocked response did not refresh Fleet rows");
|
|
78
85
|
if (!responding.terminalInput?.("\u001b[B")?.consume || !responding.terminalInput?.("\u001b[B")?.consume || !responding.terminalInput?.("\r")?.consume) throw new Error("Fleet terminal input was not consumed while selecting");
|
|
79
86
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -167,7 +174,7 @@ try {
|
|
|
167
174
|
if (inactiveHerdr.tool || inactiveHerdr.commands.length || inactiveHerdr.handlers.size) throw new Error("Subagents registered with HERDR_ENV=0");
|
|
168
175
|
process.env.HERDR_ENV = "1";
|
|
169
176
|
const inside = harness(async () => { throw new Error("unreachable"); });
|
|
170
|
-
if (!inside.tool || !inside.commands.includes("herdr-subagents") || !inside.handlers.has("session_start")) throw new Error("Subagents did not register inside Herdr");
|
|
177
|
+
if (!inside.tool || !inside.controlTool || !inside.commands.includes("herdr-subagents") || !inside.handlers.has("session_start")) throw new Error("Subagents did not register both tools inside Herdr");
|
|
171
178
|
process.env.PI_HERDR_SUBAGENTS_CHILD = "1";
|
|
172
179
|
const child = harness(async () => { throw new Error("unreachable"); });
|
|
173
180
|
if (child.tool || child.commands.length || child.handlers.size) throw new Error("Subagents registered inside a child Agent");
|
|
@@ -183,10 +190,10 @@ try {
|
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
export default function (_pi: ExtensionAPI) {
|
|
186
|
-
|
|
193
|
+
const tools = new Map<string, Record<string, any>>();
|
|
187
194
|
const pi = {
|
|
188
195
|
on() {},
|
|
189
|
-
registerTool(definition: Record<string, any>) {
|
|
196
|
+
registerTool(definition: Record<string, any>) { tools.set(definition.name, definition); },
|
|
190
197
|
registerCommand() {},
|
|
191
198
|
sendMessage() {},
|
|
192
199
|
};
|
|
@@ -207,19 +214,30 @@ export default function (_pi: ExtensionAPI) {
|
|
|
207
214
|
if (previousChild === undefined) delete process.env.PI_HERDR_SUBAGENTS_CHILD;
|
|
208
215
|
else process.env.PI_HERDR_SUBAGENTS_CHILD = previousChild;
|
|
209
216
|
}
|
|
210
|
-
|
|
217
|
+
const tool = tools.get("herdr_subagents");
|
|
218
|
+
const controlTool = tools.get("herdr_subagents_control");
|
|
219
|
+
if (!tool || !controlTool) throw new Error("Herdr execution and control tools were not both registered");
|
|
211
220
|
const schema = tool.parameters as Record<string, any>;
|
|
212
221
|
if (schema.type !== "object" || schema.anyOf || schema.oneOf) throw new Error("herdr_subagents parameters must be one flat object schema");
|
|
213
|
-
const
|
|
214
|
-
for (const name of required) {
|
|
222
|
+
for (const name of ["tasks", "label", "concurrency", "background"]) {
|
|
215
223
|
if (!schema.properties?.[name]) throw new Error(`schema is missing property '${name}'`);
|
|
216
224
|
if (!schema.properties[name].description) throw new Error(`schema property '${name}' has no description`);
|
|
217
225
|
}
|
|
226
|
+
if (!schema.required?.includes("tasks")) throw new Error("herdr_subagents tasks must be required");
|
|
227
|
+
const normalized = tool.prepareArguments?.({ prompt: "inspect", agent: "explorer", label: "probe", background: true });
|
|
228
|
+
if (normalized?.tasks?.length !== 1 || normalized.tasks[0].name !== "probe" || normalized.tasks[0].prompt !== "inspect" || normalized.tasks[0].agent !== "explorer" || normalized.background !== true) {
|
|
229
|
+
throw new Error("legacy single-task arguments were not normalized into a one-item background run");
|
|
230
|
+
}
|
|
218
231
|
for (const name of ["name", "prompt", "agent", "model"]) {
|
|
219
232
|
const property = schema.properties.tasks.items.properties[name];
|
|
220
233
|
if (!property?.description) throw new Error(`nested task property '${name}' has no description`);
|
|
221
234
|
}
|
|
222
|
-
|
|
235
|
+
const controlSchema = controlTool.parameters as Record<string, any>;
|
|
236
|
+
for (const name of ["action", "runId", "answer", "task", "prompt"]) {
|
|
237
|
+
if (!controlSchema.properties?.[name]?.description) throw new Error(`control schema property '${name}' is missing or undocumented`);
|
|
238
|
+
}
|
|
239
|
+
if (!controlSchema.required?.includes("action")) throw new Error("herdr_subagents_control action must be required");
|
|
240
|
+
if (!/Foreground calls block/.test(tool.description) || !/background calls return a run ID immediately/.test(tool.description)) {
|
|
223
241
|
throw new Error("tool description does not distinguish foreground and background execution");
|
|
224
242
|
}
|
|
225
243
|
}
|
|
@@ -15,7 +15,7 @@ import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcil
|
|
|
15
15
|
import { continueQueuedRun } from "../src/continue-run.ts";
|
|
16
16
|
import { CliHerdrAutomation } from "../src/herdr.ts";
|
|
17
17
|
import { fleetEditorHasFocus, FleetSelection, fleetLines } from "../src/fleet.ts";
|
|
18
|
-
import { HERDR_ACTIONS,
|
|
18
|
+
import { HERDR_ACTIONS, validateControlParams, validateRunParams } from "../src/validation.ts";
|
|
19
19
|
import { resultPathForAttempt } from "../src/result-path.ts";
|
|
20
20
|
import { isSettledRunStatus, readTaskRecord } from "../src/records.ts";
|
|
21
21
|
import { RunPaneAllocator } from "../src/run-pane-allocator.ts";
|
|
@@ -136,22 +136,18 @@ function latestRunDirectory() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
|
-
check("
|
|
139
|
+
check("run validation accepts one or more tasks", validateRunParams({ tasks: [{ name: "one", prompt: "do it" }], concurrency: 2, background: true }) === undefined);
|
|
140
|
+
check("control validation accepts supported calls", [
|
|
140
141
|
{ action: "list" }, { action: "history" }, { action: "cleanup", runId: "run" },
|
|
141
142
|
{ action: "respond", runId: "run", answer: "yes", task: 1 },
|
|
142
143
|
{ action: "resume", runId: "run", task: 1, prompt: "continue" },
|
|
143
|
-
|
|
144
|
-
{ tasks: [{ name: "one", prompt: "do it" }], concurrency: 2, background: true },
|
|
145
|
-
].every((params) => validateToolParams(params) === undefined));
|
|
144
|
+
].every((params) => validateControlParams(params) === undefined));
|
|
146
145
|
check("all supported actions have shared validation and rendering metadata", ["list", "history", "cleanup", "respond", "resume"].every((action) => action in HERDR_ACTIONS));
|
|
147
|
-
check("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
validateToolParams({ prompt: "one", runId: "run" }),
|
|
153
|
-
validateToolParams({ prompt: "one", task: 1 }),
|
|
154
|
-
validateToolParams({ tasks: [{ name: "one", prompt: "one" }], answer: "extra" }),
|
|
146
|
+
check("split validation rejects missing tasks and invalid control fields", [
|
|
147
|
+
validateRunParams({ tasks: [] }),
|
|
148
|
+
validateControlParams({ action: "list", prompt: "not allowed" }),
|
|
149
|
+
validateControlParams({ action: "respond", runId: "run" }),
|
|
150
|
+
validateControlParams({ action: "resume", runId: "run", task: 1 }),
|
|
155
151
|
].every((message) => typeof message === "string" && message.length > 0));
|
|
156
152
|
const fleet = new FleetSelection();
|
|
157
153
|
const fleetBatch = { label: "reviews", activity: [
|
|
@@ -554,7 +550,7 @@ try {
|
|
|
554
550
|
const historyHerdr = fakeHerdr();
|
|
555
551
|
const historicalResume = await executeHerdrSubagents({ action: "resume", runId: backgroundId, task: 1, prompt: "follow up" }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: historyHerdr });
|
|
556
552
|
const historyStartRequest = historyHerdr.calls.find(([name]) => name === "startAgent")[1];
|
|
557
|
-
check("historical resume preserves the saved Pi session and persona prompt", !historicalResume.isError && historyHerdr.calls[0][0] === "createTab" && historyStartRequest.args.includes(stableHistory.tasks[0].sessionFile) && historyStartRequest.args.includes(
|
|
553
|
+
check("historical resume preserves the saved Pi session and persona prompt", !historicalResume.isError && historyHerdr.calls[0][0] === "createTab" && historyStartRequest.args.includes(stableHistory.tasks[0].sessionFile) && historyStartRequest.args.includes(stableHistory.tasks[0].model) && historyStartRequest.args.some((value) => String(value).includes("subagent_report")) && existsSync(join(stableHistory.tasks[0].recordDirectory, "turns", "02.json")) && readFileSync(join(stableHistory.tasks[0].recordDirectory, "followup-system-prompt.md"), "utf8").includes("Investigate the assigned question without modifying files"));
|
|
558
554
|
const historicalTaskRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
|
|
559
555
|
const historicalTurnRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "turns", "02.json"), "utf8"));
|
|
560
556
|
check("historical task and turn retain their tab identity", historicalTaskRecord.tabId === historicalResume.details.tabId && historicalTurnRecord.tabId === historicalResume.details.tabId);
|