@maxiaochao/pi-toolkit 0.10.0 → 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 +16 -0
- package/README.md +0 -1
- package/global/AGENTS.md +33 -50
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +15 -0
- package/packages/herdr-subagents/README.md +166 -37
- package/packages/herdr-subagents/TESTING.md +5 -4
- package/packages/herdr-subagents/docs/demo.webm +0 -0
- package/packages/herdr-subagents/docs/screenshot.png +0 -0
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +89 -50
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/config.ts +9 -5
- 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 +21 -19
- package/packages/herdr-subagents/docs/demo.gif +0 -0
- package/skills/calldiff/LICENSE +0 -21
- package/skills/calldiff/NOTICE +0 -19
- package/skills/calldiff/SKILL.md +0 -121
|
@@ -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> {
|
|
@@ -92,7 +106,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
92
106
|
}
|
|
93
107
|
if (section === "settings") {
|
|
94
108
|
const settings = configuration.settings;
|
|
95
|
-
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel };
|
|
109
|
+
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking };
|
|
96
110
|
const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`);
|
|
97
111
|
if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
|
|
98
112
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -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 })),
|
|
@@ -10,6 +10,7 @@ export interface SubagentConfig {
|
|
|
10
10
|
maxConcurrency: number;
|
|
11
11
|
stalledWarningSeconds: number;
|
|
12
12
|
defaultModel?: string;
|
|
13
|
+
defaultThinking?: string;
|
|
13
14
|
personas: Record<string, PersonaConfig>;
|
|
14
15
|
}
|
|
15
16
|
export interface EffectivePersona extends AgentDefinition {
|
|
@@ -33,6 +34,7 @@ interface ConfigFile {
|
|
|
33
34
|
maxConcurrency?: number;
|
|
34
35
|
stalledWarningSeconds?: number;
|
|
35
36
|
defaultModel?: string;
|
|
37
|
+
defaultThinking?: string;
|
|
36
38
|
personas?: Record<string, PersonaConfig>;
|
|
37
39
|
}
|
|
38
40
|
export interface ConfigurationPaths {
|
|
@@ -64,9 +66,11 @@ function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, so
|
|
|
64
66
|
result[key] = value as number;
|
|
65
67
|
sources[key] = source;
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
for (const key of ["defaultModel", "defaultThinking"] as const) {
|
|
70
|
+
const value = input[key];
|
|
71
|
+
if (value === undefined) continue;
|
|
72
|
+
if (typeof value === "string" && value.trim()) { result[key] = value.trim(); sources[key] = source; }
|
|
73
|
+
else diagnostics.push(`${source} ${key} must be a non-empty string`);
|
|
70
74
|
}
|
|
71
75
|
if (input.personas !== undefined) {
|
|
72
76
|
if (!input.personas || typeof input.personas !== "object" || Array.isArray(input.personas)) diagnostics.push(`${source} personas must be an object`);
|
|
@@ -115,8 +119,8 @@ export function loadSubagentConfiguration(cwd: string, packageAgentsDirectory: s
|
|
|
115
119
|
const projectPersona = projectInput.personas?.[definition.name] ?? {};
|
|
116
120
|
const model = projectPersona.model ?? globalPersona.model ?? definition.model ?? settings.defaultModel ?? parentModel;
|
|
117
121
|
const modelSource = projectPersona.model ? "project persona" : globalPersona.model ? "global persona" : definition.model ? `${source} persona` : settings.defaultModel ? settingSources.defaultModel : parentModel ? "parent session" : "unresolved";
|
|
118
|
-
const thinking = projectPersona.thinking ?? globalPersona.thinking ?? definition.thinking;
|
|
119
|
-
const thinkingSource = projectPersona.thinking ? "project persona" : globalPersona.thinking ? "global persona" : definition.thinking ? `${source} persona` : "default";
|
|
122
|
+
const thinking = projectPersona.thinking ?? globalPersona.thinking ?? definition.thinking ?? settings.defaultThinking;
|
|
123
|
+
const thinkingSource = projectPersona.thinking ? "project persona" : globalPersona.thinking ? "global persona" : definition.thinking ? `${source} persona` : settings.defaultThinking ? settingSources.defaultThinking : "default";
|
|
120
124
|
const skills = projectPersona.skills ?? globalPersona.skills ?? definition.skills ?? [];
|
|
121
125
|
const skillsSource = projectPersona.skills ? "project persona" : globalPersona.skills ? "global persona" : definition.skills ? `${source} persona` : "none";
|
|
122
126
|
return { ...definition, source, access: definition.access ?? "write", model, modelSource, thinking, thinkingSource, skills, skillsSource };
|
|
@@ -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
|
}
|