@maxiaochao/pi-toolkit 0.11.12 → 0.11.14
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 +11 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/ARCHITECTURE.md +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +6 -0
- package/packages/herdr-subagents/README.md +3 -1
- package/packages/herdr-subagents/TESTING.md +3 -2
- package/packages/herdr-subagents/extensions/subagent-report.ts +18 -5
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/tool.ts +6 -2
- package/packages/herdr-subagents/tests/core-state-machine.mjs +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2026-10-14 - v0.11.14
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.5.1: fix focus-safe run cleanup and compact report output
|
|
4
|
+
- fix Herdr Subagents so completed child Pi panes remain inspectable while focused and close cleanly after focus leaves, without raw ANSI fragments from forced TUI exits
|
|
5
|
+
- keep large Subagent results on disk while showing compact report calls and one result path per task to the parent model
|
|
6
|
+
|
|
7
|
+
## 2026-10-14 - v0.11.13
|
|
8
|
+
|
|
9
|
+
- openai-account-pool v0.1.4: hide the redundant next-session marker when it matches the current account
|
|
10
|
+
- hide the redundant OpenAI Account Pool `★下次` marker when it matches `●当前`
|
|
11
|
+
|
|
1
12
|
## 2026-10-14 - v0.11.12
|
|
2
13
|
|
|
3
14
|
- scope release checks to changed packages and skip unchanged Herdr validation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.14",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -80,7 +80,7 @@ There is no global dispatcher, persisted background queue, blocked response lane
|
|
|
80
80
|
|
|
81
81
|
## Child boundary
|
|
82
82
|
|
|
83
|
-
Each task owns one Herdr pane and one single-task Pi process. Herdr first starts and detects the interactive Pi, then submits the prompt exactly once.
|
|
83
|
+
Each task owns one Herdr pane and one single-task Pi process. Herdr first starts and detects the interactive Pi, then submits the prompt exactly once. At Pi’s final `agent_settled` boundary, the child extension records settled state but leaves the interactive process alive. The atomic report remains the authoritative task result. Early-finishing children remain visible while sibling tasks continue. Once the whole run settles, the orchestrator closes an unfocused owned tab immediately and defers a focused tab until the user leaves it, so Pi can restore the terminal normally.
|
|
84
84
|
|
|
85
85
|
The child writes:
|
|
86
86
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2026-10-14 - v0.5.1
|
|
2
|
+
|
|
3
|
+
- fix focus-safe run cleanup and compact report output
|
|
4
|
+
- keep settled child Pi TUIs alive until focus-aware tab cleanup, preventing abrupt exits and leaked ANSI control sequences in focused panes
|
|
5
|
+
- render report submission compactly and return only each task's complete-result path to the parent model
|
|
6
|
+
|
|
1
7
|
## 2026-10-13 - v0.5.0
|
|
2
8
|
|
|
3
9
|
- simplify lifecycle to one report-first state machine with strict serial and bounded read-only parallel execution
|
|
@@ -34,7 +34,7 @@ The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.we
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Each task starts a single-task interactive Pi process in its own Herdr pane. Herdr first confirms the interactive Pi is ready, then the prompt is delivered exactly once. Prompt acknowledgement never decides task completion. Panes remain ordinary Herdr panes: they can be focused and inspected while work is running.
|
|
37
|
+
Each task starts a single-task interactive Pi process in its own Herdr pane. Herdr first confirms the interactive Pi is ready, then the prompt is delivered exactly once. Prompt acknowledgement never decides task completion. Panes remain ordinary Herdr panes: they can be focused and inspected while work is running or after it settles. An early-finishing child stays at the Pi UI while sibling tasks continue. After the whole run settles, cleanup closes an unfocused run tab immediately or waits for the user to leave a focused run tab.
|
|
38
38
|
|
|
39
39
|
## Result protocol
|
|
40
40
|
|
|
@@ -45,6 +45,8 @@ The child must call `subagent_report` once with a terminal status:
|
|
|
45
45
|
|
|
46
46
|
The complete result is stored on disk. The parent receives only the compact summary and document paths.
|
|
47
47
|
|
|
48
|
+
The child UI renders only the report status and saved size instead of echoing the complete result argument. The parent-facing tool result includes one complete-result path per task; additional consulted-document metadata remains in the durable report.
|
|
49
|
+
|
|
48
50
|
An atomically renamed attempt report is the authoritative completion signal. Herdr states such as `idle`, `blocked`, and `done` are wake-up hints only. A prompt transport error or Pi retry cannot fail a task while its child process remains alive. If the process exits without a valid report, the task fails explicitly.
|
|
49
51
|
|
|
50
52
|
Duplicate identical reports are idempotent. A conflicting report for the same task attempt is rejected. Terminal task records are immutable.
|
|
@@ -17,7 +17,8 @@ It verifies:
|
|
|
17
17
|
- explicit failure when the process exits without a report;
|
|
18
18
|
- parent cancellation of running and queued tasks;
|
|
19
19
|
- immutable terminal task records;
|
|
20
|
-
- idempotent duplicate reports and rejection of conflicting replacements
|
|
20
|
+
- idempotent duplicate reports and rejection of conflicting replacements;
|
|
21
|
+
- keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI.
|
|
21
22
|
|
|
22
23
|
The previous broad lifecycle suite remains available temporarily as `npm run test:legacy --workspace @maxiaochao/pi-herdr-subagents` for migration auditing. Scenarios for background queues, blocked/respond, and historical resume intentionally no longer describe the public contract.
|
|
23
24
|
|
|
@@ -33,4 +34,4 @@ Run the real Herdr smoke test from a Herdr session:
|
|
|
33
34
|
npm run test:real-herdr --workspace @maxiaochao/pi-herdr-subagents
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
For real smoke testing, verify that each child appears in a visible pane
|
|
37
|
+
For real smoke testing, verify that each child appears in a visible pane and can be focused and inspected. An early-finishing child must remain visible while sibling tasks continue. After the whole run settles, a focused child must remain at its restored Pi UI until focus leaves the run tab; the tab must then close automatically. Verify that the parent returns only after all requested tasks settle. Also verify cancellation closes the run tab and does not overwrite a report already atomically published.
|
|
@@ -3,6 +3,7 @@ import net from "node:net";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
7
|
import { Type } from "typebox";
|
|
7
8
|
import { resultPathForAttemptId } from "../src/result-path.ts";
|
|
8
9
|
import { PARENT_PANE_ENV } from "../src/live-agent.ts";
|
|
@@ -75,12 +76,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
76
|
const taskDirectory = process.env.PI_HERDR_SUBAGENTS_TASK_DIR?.trim();
|
|
76
77
|
if (taskDirectory && activeAttemptId) {
|
|
77
78
|
writeAttemptState(taskDirectory, activeAttemptId, "settled");
|
|
78
|
-
const reported = existsSync(attemptReportPath(taskDirectory, activeAttemptId));
|
|
79
|
-
// agent_settled is Pi's final automatic-work boundary. Ending the child here
|
|
80
|
-
// gives the orchestrator an authoritative no-report process-exit signal while
|
|
81
|
-
// still allowing all provider auto-retries to finish first.
|
|
82
|
-
setTimeout(() => process.exit(reported ? 0 : 2), 25);
|
|
83
79
|
}
|
|
80
|
+
// Keep the interactive child alive at Pi's final automatic-work boundary.
|
|
81
|
+
// The orchestrator settles each task from its atomic report. Once every task in
|
|
82
|
+
// the run settles, it closes the owned tab immediately when unfocused or waits
|
|
83
|
+
// for the user to leave it. Force-exiting here can interrupt TUI restoration.
|
|
84
84
|
if (ctx?.isIdle?.() === true) reportHerdrState("idle");
|
|
85
85
|
});
|
|
86
86
|
pi.events.on("herdr:blocked", (data: { active?: boolean; label?: string }) => {
|
|
@@ -103,6 +103,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
103
|
label: "Subagent Report",
|
|
104
104
|
description: "Finish this delegated task by saving its complete result and returning only a compact structured report to the parent.",
|
|
105
105
|
parameters: ReportParams,
|
|
106
|
+
renderCall(params, theme) {
|
|
107
|
+
const resultBytes = Buffer.byteLength(params.result, "utf8");
|
|
108
|
+
const size = resultBytes < 1024 ? `${resultBytes} B` : `${(resultBytes / 1024).toFixed(1)} KiB`;
|
|
109
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagent_report "))}${theme.fg(params.status === "completed" ? "success" : "error", params.status)}${theme.fg("dim", ` · ${size} saved to disk`)}`, 0, 0);
|
|
110
|
+
},
|
|
111
|
+
renderResult(result, { isPartial }, theme) {
|
|
112
|
+
if (isPartial) return new Text(theme.fg("warning", "Saving report..."), 0, 0);
|
|
113
|
+
const report = result.details as { status?: string; summary?: string; documents?: Array<{ path?: string }> } | undefined;
|
|
114
|
+
const resultPath = report?.documents?.[0]?.path;
|
|
115
|
+
const status = report?.status ?? "saved";
|
|
116
|
+
const summary = report?.summary?.replaceAll("\n", " ") ?? "Report saved.";
|
|
117
|
+
return new Text(`${theme.fg(status === "completed" ? "success" : status === "failed" ? "error" : "accent", status)}${theme.fg("dim", ` · ${summary}${resultPath ? `\n${resultPath}` : ""}`)}`, 0, 0);
|
|
118
|
+
},
|
|
106
119
|
async execute(_toolCallId, params) {
|
|
107
120
|
const taskDirectory = process.env.PI_HERDR_SUBAGENTS_TASK_DIR?.trim();
|
|
108
121
|
if (!taskDirectory) throw new Error("PI_HERDR_SUBAGENTS_TASK_DIR is not set");
|
|
@@ -159,8 +159,12 @@ async function executeBatch({ params, signal, onUpdate, ctx, dependencies, disco
|
|
|
159
159
|
onCleanupError: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "batch", runId: dependencies.runId, cleanupError: text } }),
|
|
160
160
|
onUpdate: (partial) => onUpdate?.({ content: [{ type: "text", text: partial.summary }], details: { ...partial, prompts: tasks.map((task) => task.prompt) } }),
|
|
161
161
|
});
|
|
162
|
-
const lines = [result.summary
|
|
163
|
-
|
|
162
|
+
const lines = [result.summary];
|
|
163
|
+
for (const task of result.tasks) {
|
|
164
|
+
lines.push(`- ${task.name}: ${task.status} — ${task.summary}`);
|
|
165
|
+
const resultDocument = task.documents[0];
|
|
166
|
+
if (resultDocument) lines.push(` Result: ${resultDocument.path}`);
|
|
167
|
+
}
|
|
164
168
|
const isError = result.status === "failed" || result.status === "partial" || result.status === "cancelled";
|
|
165
169
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { ...result, prompts: tasks.map((task) => task.prompt) }, ...(isError ? { isError: true } : {}) };
|
|
166
170
|
} catch (error) {
|
|
@@ -170,6 +170,24 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
170
170
|
await reportTool.execute("first", report);
|
|
171
171
|
await reportTool.execute("duplicate", report);
|
|
172
172
|
await assert.rejects(() => reportTool.execute("conflict", { ...report, summary: "different" }), /Conflicting subagent_report/);
|
|
173
|
+
const originalExit = process.exit;
|
|
174
|
+
let exitCode;
|
|
175
|
+
process.exit = (code) => { exitCode = code; };
|
|
176
|
+
try {
|
|
177
|
+
await handlers.get("agent_settled")?.({}, { isIdle: () => true });
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
179
|
+
} finally {
|
|
180
|
+
process.exit = originalExit;
|
|
181
|
+
}
|
|
182
|
+
assert.equal(exitCode, undefined, "settled child must remain alive for focus-aware tab cleanup");
|
|
183
|
+
const attempt = JSON.parse(readFileSync(join(directory, "attempts", `${attemptId}.json`), "utf8"));
|
|
184
|
+
assert.equal(attempt.state, "settled");
|
|
185
|
+
const theme = { fg: (_color, text) => text, bold: (text) => text };
|
|
186
|
+
const renderedCall = reportTool.renderCall({ ...report, result: "FULL RESULT MUST NOT RENDER" }, theme).render(120).join("\n");
|
|
187
|
+
assert.match(renderedCall, /completed.*saved to disk/);
|
|
188
|
+
assert.doesNotMatch(renderedCall, /FULL RESULT MUST NOT RENDER/);
|
|
189
|
+
const renderedResult = reportTool.renderResult(await reportTool.execute("render", report), { isPartial: false }, theme).render(120).join("\n");
|
|
190
|
+
assert.match(renderedResult, /completed.*done/);
|
|
173
191
|
const persisted = JSON.parse(readFileSync(join(directory, "reports", `${attemptId}.json`), "utf8"));
|
|
174
192
|
assert.equal(persisted.summary, "done");
|
|
175
193
|
delete process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
|