@maxiaochao/pi-toolkit 0.11.6 → 0.11.8
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/README.md +2 -1
- package/package.json +5 -3
- package/packages/herdr-subagents/ARCHITECTURE.md +82 -378
- package/packages/herdr-subagents/CHANGELOG.md +4 -0
- package/packages/herdr-subagents/README.md +52 -321
- package/packages/herdr-subagents/TESTING.md +36 -53
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +24 -158
- package/packages/herdr-subagents/extensions/subagent-report.ts +21 -7
- package/packages/herdr-subagents/package.json +4 -3
- package/packages/herdr-subagents/src/batch-runner.ts +5 -3
- package/packages/herdr-subagents/src/herdr.ts +11 -0
- package/packages/herdr-subagents/src/live-agent.ts +49 -114
- package/packages/herdr-subagents/src/parameters.ts +2 -6
- package/packages/herdr-subagents/src/protocol.ts +4 -5
- package/packages/herdr-subagents/src/records.ts +2 -0
- package/packages/herdr-subagents/src/tool.ts +9 -51
- package/packages/herdr-subagents/src/validation.ts +0 -4
- package/packages/herdr-subagents/tests/core-state-machine.mjs +178 -0
- package/packages/herdr-subagents/tests/extension-sources-load.mjs +7 -3
- package/packages/openai-account-pool/CHANGELOG.md +11 -0
- package/packages/openai-account-pool/README.md +67 -0
- package/packages/openai-account-pool/TESTING.md +28 -0
- package/packages/openai-account-pool/docs/demo.gif +0 -0
- package/packages/openai-account-pool/docs/screenshot.png +0 -0
- package/packages/openai-account-pool/extensions/openai-account-pool.ts +368 -0
- package/packages/openai-account-pool/package.json +45 -0
- package/packages/openai-account-pool/src/auth.ts +418 -0
- package/packages/openai-account-pool/src/availability.ts +230 -0
- package/packages/openai-account-pool/src/command-completions.ts +18 -0
- package/packages/openai-account-pool/src/enrollment.ts +339 -0
- package/packages/openai-account-pool/src/lease.ts +128 -0
- package/packages/openai-account-pool/src/lock.ts +171 -0
- package/packages/openai-account-pool/src/login-slot.ts +158 -0
- package/packages/openai-account-pool/src/policy.ts +176 -0
- package/packages/openai-account-pool/src/provider.ts +511 -0
- package/packages/openai-account-pool/src/status-widget.ts +62 -0
- package/packages/openai-account-pool/src/store.ts +259 -0
- package/packages/openai-account-pool/src/usage.ts +291 -0
- package/packages/openai-account-pool/tests/command-completions.mjs +10 -0
- package/packages/openai-account-pool/tests/enrollment.mjs +322 -0
- package/packages/openai-account-pool/tests/failover-stream.mjs +554 -0
- package/packages/openai-account-pool/tests/helpers/fixtures.mjs +40 -0
- package/packages/openai-account-pool/tests/helpers/native-credential-store.mjs +58 -0
- package/packages/openai-account-pool/tests/helpers/pi-runtime.mjs +24 -0
- package/packages/openai-account-pool/tests/helpers/refresh-child.mjs +29 -0
- package/packages/openai-account-pool/tests/integration-login-ownership.mjs +299 -0
- package/packages/openai-account-pool/tests/login-slot.mjs +131 -0
- package/packages/openai-account-pool/tests/multiprocess.mjs +73 -0
- package/packages/openai-account-pool/tests/selection.mjs +286 -0
- package/packages/openai-account-pool/tests/store-ownership.mjs +369 -0
- package/packages/openai-account-pool/tests/usage.mjs +428 -0
|
@@ -2,9 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, rename } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
import { attemptReportPath, attemptStatePath, createAttemptId
|
|
5
|
+
import type { HerdrAutomation } from "./herdr.ts";
|
|
6
|
+
import { parseReport, type PersistedReport, type TaskRecord } from "./records.ts";
|
|
7
|
+
import { attemptReportPath, attemptStatePath, createAttemptId } from "./attempts.ts";
|
|
8
8
|
import { writeJsonAtomic } from "./state.ts";
|
|
9
9
|
|
|
10
10
|
export interface LiveTaskRecord {
|
|
@@ -33,14 +33,11 @@ export function childEnvironment(taskDirectory: string, environment: NodeJS.Proc
|
|
|
33
33
|
|
|
34
34
|
export function childPiArgs(task: LiveTaskRecord, systemPromptFile: string): string[] {
|
|
35
35
|
const args = [
|
|
36
|
-
"--approve",
|
|
37
|
-
"--session", task.sessionFile,
|
|
36
|
+
"--approve", "--session", task.sessionFile,
|
|
38
37
|
"--name", `Subagent: ${task.name ?? task.agent ?? "worker"}`,
|
|
39
|
-
"--no-extensions",
|
|
40
|
-
"--extension", "builtin:codemode",
|
|
38
|
+
"--no-extensions", "--extension", "builtin:codemode",
|
|
41
39
|
"--extension", fileURLToPath(new URL("../extensions/subagent-report.ts", import.meta.url)),
|
|
42
|
-
"--no-skills",
|
|
43
|
-
"--append-system-prompt", systemPromptFile,
|
|
40
|
+
"--no-skills", "--append-system-prompt", systemPromptFile,
|
|
44
41
|
];
|
|
45
42
|
for (const extension of task.extensions ?? []) args.push("--extension", extension);
|
|
46
43
|
if (task.model) args.push("--model", task.model);
|
|
@@ -51,8 +48,17 @@ export function childPiArgs(task: LiveTaskRecord, systemPromptFile: string): str
|
|
|
51
48
|
return args;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
export function liveAgentName(taskId: string,
|
|
55
|
-
return `herdr-subagent-${taskId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 12)}
|
|
51
|
+
export function liveAgentName(taskId: string, _attempt = 1): string {
|
|
52
|
+
return `herdr-subagent-${taskId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 12)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function pause(signal?: AbortSignal): Promise<void> {
|
|
56
|
+
await new Promise<void>((resolve, reject) => {
|
|
57
|
+
const onAbort = () => { clearTimeout(timer); reject(signal?.reason ?? new Error("Subagent wait aborted.")); };
|
|
58
|
+
const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, 250);
|
|
59
|
+
if (signal?.aborted) onAbort();
|
|
60
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
});
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export async function promptLiveAgent(options: {
|
|
@@ -75,121 +81,52 @@ export async function promptLiveAgent(options: {
|
|
|
75
81
|
const taskFile = join(taskDirectory, "task.json");
|
|
76
82
|
const attemptId = options.attemptId ?? createAttemptId();
|
|
77
83
|
const attemptReportFile = attemptReportPath(taskDirectory, attemptId);
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
await Promise.all([
|
|
85
|
+
mkdir(dirname(attemptReportFile), { recursive: true, mode: 0o700 }),
|
|
86
|
+
mkdir(dirname(attemptStatePath(taskDirectory, attemptId)), { recursive: true, mode: 0o700 }),
|
|
87
|
+
]);
|
|
80
88
|
const durableTask = JSON.parse(await readFile(taskFile, "utf8")) as Record<string, unknown>;
|
|
81
89
|
durableTask.currentAttemptId = attemptId;
|
|
82
90
|
await writeJsonAtomic(taskFile, durableTask);
|
|
83
|
-
const agentName = options.agentName ?? liveAgentName(options.task.id
|
|
84
|
-
if (options.start !== false) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
kind: "pi",
|
|
88
|
-
paneId: options.paneId,
|
|
89
|
-
args: childPiArgs(options.task, options.systemPromptFile),
|
|
90
|
-
signal: options.signal,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
const persistRuntime = async (state: TaskRuntimeState, diagnostic?: string) => {
|
|
94
|
-
const task = await readTaskRecord(taskFile);
|
|
95
|
-
if (task.currentAttemptId !== attemptId || (task.status !== "running" && task.status !== "starting")) return;
|
|
96
|
-
if (task.runtimeState === state && task.runtimeDiagnostic === diagnostic) return;
|
|
97
|
-
applyTaskRuntime(task, state, diagnostic);
|
|
98
|
-
await writeJsonAtomic(taskFile, task);
|
|
99
|
-
await options.onRuntimeUpdate?.(task);
|
|
100
|
-
};
|
|
101
|
-
const readAttemptState = async (): Promise<AttemptState | undefined> => {
|
|
102
|
-
if (!existsSync(attemptStateFile)) return undefined;
|
|
103
|
-
try {
|
|
104
|
-
const value = JSON.parse(await readFile(attemptStateFile, "utf8")) as { attemptId?: unknown; state?: unknown };
|
|
105
|
-
if (value.attemptId === attemptId && (value.state === "working" || value.state === "settled")) return value.state;
|
|
106
|
-
} catch { /* a concurrent atomic replacement will be retried */ }
|
|
107
|
-
return undefined;
|
|
108
|
-
};
|
|
109
|
-
const wait = () => new Promise<void>((resolve, reject) => {
|
|
110
|
-
const onAbort = () => {
|
|
111
|
-
clearTimeout(timer);
|
|
112
|
-
if (existsSync(attemptReportFile)) resolve();
|
|
113
|
-
else reject(options.signal?.reason ?? new Error("Subagent attempt aborted."));
|
|
114
|
-
};
|
|
115
|
-
const timer = setTimeout(() => { options.signal?.removeEventListener("abort", onAbort); resolve(); }, 100);
|
|
116
|
-
if (options.signal?.aborted) onAbort();
|
|
117
|
-
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
91
|
+
const agentName = options.agentName ?? liveAgentName(options.task.id);
|
|
92
|
+
if (options.start !== false) await options.herdr.startAgent({
|
|
93
|
+
name: agentName, kind: "pi", paneId: options.paneId,
|
|
94
|
+
args: childPiArgs(options.task, options.systemPromptFile), signal: options.signal,
|
|
118
95
|
});
|
|
96
|
+
// Delivery is attempted exactly once. Its acknowledgement is not a lifecycle
|
|
97
|
+
// signal: Pi can recover from a transport/provider error in the same process.
|
|
119
98
|
const promptController = new AbortController();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
else options.signal?.addEventListener("abort", abortPrompt, { once: true });
|
|
123
|
-
const promptOutcome = options.herdr.promptAgent({ target: agentName, prompt: options.prompt, signal: promptController.signal })
|
|
124
|
-
.then((result) => ({ result }), (error: unknown) => ({ error }));
|
|
125
|
-
let promptSettled = false;
|
|
126
|
-
let promptStatus: HerdrAgentState | undefined;
|
|
127
|
-
let promptError: unknown;
|
|
128
|
-
const promptSettlementWork = promptOutcome.then(async (outcome) => {
|
|
129
|
-
promptSettled = true;
|
|
130
|
-
if ("error" in outcome) {
|
|
131
|
-
promptError = outcome.error;
|
|
132
|
-
const detail = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
133
|
-
if (/agent_prompt_stalled|timed?\s*out|timeout/i.test(detail)) await persistRuntime("delivery_unknown", detail);
|
|
134
|
-
else if (/agent_blocked/i.test(detail)) await persistRuntime("waiting_interaction", "Herdr is waiting for an approval or interaction; no business input request has been reported.");
|
|
135
|
-
} else if (outcome.result.status === "working" || outcome.result.status === "blocked" || outcome.result.status === "idle" || outcome.result.status === "done") promptStatus = outcome.result.status;
|
|
136
|
-
});
|
|
99
|
+
const promptSignal = options.signal ? AbortSignal.any([options.signal, promptController.signal]) : promptController.signal;
|
|
100
|
+
const prompt = options.herdr.promptAgent({ target: agentName, prompt: options.prompt, signal: promptSignal }).catch(() => undefined);
|
|
137
101
|
const stalledTimer = options.stalledWarningMs ? setTimeout(() => options.onStalled?.(), options.stalledWarningMs) : undefined;
|
|
138
|
-
let terminalObservedAt: number | undefined;
|
|
139
102
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try { agentState = await options.herdr.getAgentState(agentName, options.signal); }
|
|
146
|
-
catch (error) { observationDiagnostic = `Could not observe Herdr agent state: ${error instanceof Error ? error.message : String(error)}`; }
|
|
147
|
-
const attemptState = await readAttemptState();
|
|
148
|
-
const promptDetail = promptError instanceof Error ? promptError.message : promptError === undefined ? "" : String(promptError);
|
|
149
|
-
if (agentState === "blocked" || /agent_blocked/i.test(promptDetail)) {
|
|
150
|
-
await persistRuntime("waiting_interaction", observationDiagnostic ?? "Herdr is waiting for an approval or interaction; no business input request has been reported.");
|
|
151
|
-
terminalObservedAt = undefined;
|
|
152
|
-
} else if (promptError && /agent_prompt_stalled|timed?\s*out|timeout/i.test(promptDetail)) {
|
|
153
|
-
await persistRuntime("delivery_unknown", observationDiagnostic ?? promptDetail);
|
|
154
|
-
let live = false;
|
|
155
|
-
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
156
|
-
if (attemptState !== "working" || !live) {
|
|
157
|
-
terminalObservedAt ??= Date.now();
|
|
158
|
-
if (Date.now() - terminalObservedAt >= 50) throw promptError;
|
|
159
|
-
}
|
|
160
|
-
} else if (promptError && promptSettled) {
|
|
161
|
-
terminalObservedAt ??= Date.now();
|
|
162
|
-
if (Date.now() - terminalObservedAt >= 50) throw promptError;
|
|
163
|
-
} else if ((promptSettled || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
|
|
164
|
-
if (attemptState === "working") {
|
|
165
|
-
let live = false;
|
|
166
|
-
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
167
|
-
if (!live) throw new Error("Child Pi stopped while its authoritative attempt state was still working and did not submit a subagent_report.");
|
|
168
|
-
}
|
|
169
|
-
terminalObservedAt ??= Date.now();
|
|
170
|
-
if (Date.now() - terminalObservedAt >= 50) throw new Error("Child Pi did not submit a subagent_report before settling.");
|
|
171
|
-
} else {
|
|
172
|
-
await persistRuntime("working", observationDiagnostic);
|
|
173
|
-
terminalObservedAt = undefined;
|
|
174
|
-
if (!observationDiagnostic && agentState === "unknown") {
|
|
175
|
-
let live = false;
|
|
176
|
-
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
177
|
-
if (!live) throw new Error("Child Pi stopped without submitting a subagent_report.");
|
|
178
|
-
}
|
|
103
|
+
while (true) {
|
|
104
|
+
if (existsSync(attemptReportFile)) break;
|
|
105
|
+
if (options.signal?.aborted) {
|
|
106
|
+
if (existsSync(attemptReportFile)) break;
|
|
107
|
+
throw options.signal.reason ?? new Error("Subagent attempt aborted.");
|
|
179
108
|
}
|
|
180
|
-
|
|
109
|
+
let alive = true;
|
|
110
|
+
try { alive = await options.herdr.isTaskRunning(options.paneId, options.signal); }
|
|
111
|
+
catch { /* an observation failure is not a child failure */ }
|
|
112
|
+
if (!alive) {
|
|
113
|
+
// The child publishes by atomic rename before it exits. Reconcile disk once;
|
|
114
|
+
// there is no timing-based drain and no semantic use of idle/done.
|
|
115
|
+
if (existsSync(attemptReportFile)) break;
|
|
116
|
+
throw new Error("Child Pi exited without submitting a subagent_report.");
|
|
117
|
+
}
|
|
118
|
+
if (options.herdr.waitForAgentChange) await options.herdr.waitForAgentChange(agentName, options.signal).catch(() => undefined);
|
|
119
|
+
else await pause(options.signal);
|
|
181
120
|
}
|
|
182
121
|
} finally {
|
|
183
122
|
if (stalledTimer) clearTimeout(stalledTimer);
|
|
184
|
-
promptController.abort(new Error("
|
|
185
|
-
await promptSettlementWork;
|
|
186
|
-
options.signal?.removeEventListener("abort", abortPrompt);
|
|
123
|
+
promptController.abort(new Error("Task lifecycle settled; stop waiting for prompt acknowledgement."));
|
|
187
124
|
}
|
|
125
|
+
await prompt;
|
|
188
126
|
let value: unknown;
|
|
189
127
|
try { value = JSON.parse(await readFile(attemptReportFile, "utf8")); }
|
|
190
128
|
catch {
|
|
191
129
|
await rename(attemptReportFile, `${reportFile}.invalid-${Date.now()}`).catch(() => undefined);
|
|
192
|
-
if (promptError) throw new Error(`${promptError instanceof Error ? promptError.message : String(promptError)}; Child Pi also submitted an invalid subagent_report.`);
|
|
193
130
|
throw new Error("Child Pi submitted an invalid subagent_report.");
|
|
194
131
|
}
|
|
195
132
|
try {
|
|
@@ -197,10 +134,8 @@ export async function promptLiveAgent(options: {
|
|
|
197
134
|
if (report.attemptId !== attemptId) throw new Error(`Report belongs to attempt '${report.attemptId ?? "unknown"}', expected '${attemptId}'.`);
|
|
198
135
|
await writeJsonAtomic(reportFile, report);
|
|
199
136
|
return { report, agentName, attemptId };
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
137
|
+
} catch (error) {
|
|
202
138
|
await rename(attemptReportFile, `${reportFile}.invalid-${Date.now()}`).catch(() => undefined);
|
|
203
|
-
|
|
204
|
-
throw new Error(promptError ? `${promptError instanceof Error ? promptError.message : String(promptError)}; ${detail}` : detail);
|
|
139
|
+
throw new Error(`Child Pi submitted an invalid subagent_report: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
140
|
}
|
|
206
141
|
}
|
|
@@ -12,13 +12,9 @@ export const HerdrSubagentsParams = Type.Object({
|
|
|
12
12
|
tasks: Type.Array(HerdrSubagentsTaskParams, { minItems: 1, description: "Ordered tasks to execute. Use one item for a single task; independent read-only tasks may run concurrently." }),
|
|
13
13
|
label: Type.Optional(Type.String({ minLength: 1, maxLength: 48, description: "Short Herdr run-tab label." })),
|
|
14
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
15
|
}, { additionalProperties: false });
|
|
17
16
|
|
|
18
17
|
export const HerdrSubagentsControlParams = Type.Object({
|
|
19
|
-
action: StringEnum(["list", "status", "
|
|
20
|
-
runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by status,
|
|
21
|
-
answer: Type.Optional(Type.String({ minLength: 1, description: "Message sent to the selected blocked or failed task when action is respond." })),
|
|
22
|
-
task: Type.Optional(Type.Integer({ minimum: 1, description: "One-based task number. Always required for respond, and selects the task for resume." })),
|
|
23
|
-
prompt: Type.Optional(Type.String({ minLength: 1, description: "Follow-up prompt used by resume." })),
|
|
18
|
+
action: StringEnum(["list", "status", "history", "cancel", "cleanup"] as const, { description: "Control operation to perform." }),
|
|
19
|
+
runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by status, cancel, or cleanup." })),
|
|
24
20
|
}, { additionalProperties: false });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReportPromptKind = "initial" | "resumed" | "followup";
|
|
1
|
+
export type ReportPromptKind = "initial" | "resumed" | "followup"; // legacy kinds remain readable
|
|
2
2
|
|
|
3
3
|
const TURN_INTRO: Record<ReportPromptKind, string> = {
|
|
4
4
|
initial: "",
|
|
@@ -11,11 +11,10 @@ export function reportProtocolPrompt(systemPrompt: string, kind: ReportPromptKin
|
|
|
11
11
|
TURN_INTRO[kind],
|
|
12
12
|
systemPrompt.trim(),
|
|
13
13
|
"",
|
|
14
|
-
"
|
|
14
|
+
"Before this process exits, call subagent_report exactly once with a terminal result.",
|
|
15
15
|
"Put the complete final answer in result, a concise parent-facing paragraph in summary, and list any useful document paths.",
|
|
16
|
-
"Use status=completed when the task is finished.",
|
|
17
|
-
"
|
|
18
|
-
"Use status=failed with error when this task cannot be completed, so the run does not wait for an answer.",
|
|
16
|
+
"Use status=completed when the task is finished, or status=failed with error when it cannot be completed.",
|
|
17
|
+
"Do not pause for parent input; make a reasonable assumption or report failed with the missing requirement.",
|
|
19
18
|
].filter((line, index) => line || index > 1).join("\n").trim();
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -252,6 +252,8 @@ export function applyLifecycleStatus<T extends { status: string; question?: stri
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export function applyTaskStatus<T extends TaskRecord | TurnRecord>(record: T, status: TaskStatus, options: { question?: string; error?: string; at?: string } = {}): T {
|
|
255
|
+
const terminal = record.status === "completed" || record.status === "failed" || record.status === "cancelled";
|
|
256
|
+
if (terminal && record.status !== status) throw new Error(`Task is already terminal (${record.status}); terminal tasks are immutable.`);
|
|
255
257
|
const at = options.at ?? new Date().toISOString();
|
|
256
258
|
if ("order" in record) {
|
|
257
259
|
if (record.status === "running" && status !== "running") {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { CliHerdrAutomation, type HerdrAutomation } from "./herdr.ts";
|
|
2
2
|
import { loadSubagentConfiguration, type ConfigurationPaths, type SubagentConfiguration } from "./config.ts";
|
|
3
3
|
import { runHerdrSubagentsBatch, type BatchResult, type BatchTask } from "./batch-runner.ts";
|
|
4
|
-
import {
|
|
5
|
-
import { cleanSubagentRun, historyText, listSubagentHistory, resumeHistoricalTask } from "./history.ts";
|
|
4
|
+
import { cleanSubagentRun, historyText, listSubagentHistory } from "./history.ts";
|
|
6
5
|
import { HERDR_ACTIONS, validateControlParams, validateRunParams } from "./validation.ts";
|
|
7
6
|
import type { OwnerIdentity } from "./ownership.ts";
|
|
8
7
|
import { cancelActiveSubagentRuns } from "./reconcile.ts";
|
|
9
|
-
import { cancelQueuedRuns, loadQueuedRuns } from "./queued-runs.ts";
|
|
10
8
|
import { getSubagentRunStatus, runStatusText } from "./status.ts";
|
|
11
9
|
|
|
12
10
|
export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent?: string; model?: string; }
|
|
@@ -15,28 +13,20 @@ export interface HerdrSubagentsRunParams {
|
|
|
15
13
|
tasks: HerdrSubagentsTaskParams[];
|
|
16
14
|
label?: string;
|
|
17
15
|
concurrency?: number;
|
|
18
|
-
background?: boolean;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export interface HerdrSubagentsControlToolParams {
|
|
22
|
-
action: "list" | "status" | "
|
|
19
|
+
action: "list" | "status" | "history" | "cancel" | "cleanup";
|
|
23
20
|
runId?: string;
|
|
24
|
-
answer?: string;
|
|
25
|
-
task?: number;
|
|
26
|
-
prompt?: string;
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
/** Internal superset used by shared execution helpers. */
|
|
30
24
|
export interface HerdrSubagentsToolParams {
|
|
31
|
-
action?: "list" | "status" | "
|
|
32
|
-
prompt?: string;
|
|
25
|
+
action?: "list" | "status" | "history" | "cancel" | "cleanup";
|
|
33
26
|
label?: string;
|
|
34
27
|
tasks?: HerdrSubagentsTaskParams[];
|
|
35
28
|
concurrency?: number;
|
|
36
|
-
background?: boolean;
|
|
37
29
|
runId?: string;
|
|
38
|
-
answer?: string;
|
|
39
|
-
task?: number;
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
export interface HerdrSubagentsToolContext {
|
|
@@ -51,7 +41,7 @@ export interface HerdrSubagentsToolDependencies {
|
|
|
51
41
|
configurationPaths?: ConfigurationPaths;
|
|
52
42
|
owner?: OwnerIdentity;
|
|
53
43
|
runId?: string;
|
|
54
|
-
cancelRun?: (runId: string) => Promise<{ cancelled: boolean;
|
|
44
|
+
cancelRun?: (runId: string) => Promise<{ cancelled: boolean; cleanupErrors?: string[] }>;
|
|
55
45
|
}
|
|
56
46
|
|
|
57
47
|
export type HerdrSubagentsBatchDetails = BatchResult & { prompts: string[] };
|
|
@@ -127,51 +117,20 @@ const actionHandlers = {
|
|
|
127
117
|
const requestedRunId = (params.runId as string).trim();
|
|
128
118
|
try {
|
|
129
119
|
const runId = (await getSubagentRunStatus(ctx.cwd, requestedRunId))?.runId ?? requestedRunId;
|
|
130
|
-
let result: { cancelled: boolean;
|
|
120
|
+
let result: { cancelled: boolean; cleanupErrors?: string[] };
|
|
131
121
|
if (dependencies.cancelRun) result = await dependencies.cancelRun(runId);
|
|
132
122
|
else {
|
|
133
123
|
const reason = "Subagent run cancelled by user.";
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
await cancelQueuedRuns(ctx.cwd, [runId], { reason, owner: dependencies.owner });
|
|
137
|
-
result = { cancelled: true, queued: true };
|
|
138
|
-
} else {
|
|
139
|
-
const cancellation = await cancelActiveSubagentRuns(ctx.cwd, dependencies.herdr ?? new CliHerdrAutomation(), { reason, owner: dependencies.owner, runId });
|
|
140
|
-
result = { cancelled: cancellation.cancelledRuns > 0, cleanupErrors: cancellation.cleanupErrors };
|
|
141
|
-
}
|
|
124
|
+
const cancellation = await cancelActiveSubagentRuns(ctx.cwd, dependencies.herdr ?? new CliHerdrAutomation(), { reason, owner: dependencies.owner, runId });
|
|
125
|
+
result = { cancelled: cancellation.cancelledRuns > 0, cleanupErrors: cancellation.cleanupErrors };
|
|
142
126
|
}
|
|
143
127
|
const suffix = result.cleanupErrors?.length ? ` Cleanup warnings: ${result.cleanupErrors.join("; ")}` : "";
|
|
144
|
-
const text = result.cancelled ? `Cancelled Subagent run ${runId}.${suffix}` : `Subagent run '${runId}' is not active
|
|
128
|
+
const text = result.cancelled ? `Cancelled Subagent run ${runId}.${suffix}` : `Subagent run '${runId}' is not active.`;
|
|
145
129
|
return { content: [{ type: "text", text }], details: { action: "cancel", runId, ...result }, ...(result.cancelled ? {} : { isError: true }) };
|
|
146
130
|
} catch (error) {
|
|
147
131
|
return errorResponse(error instanceof Error ? error.message : String(error), { action: "cancel", runId: requestedRunId, cancelled: false });
|
|
148
132
|
}
|
|
149
133
|
},
|
|
150
|
-
async resume({ params, signal, onUpdate, ctx, dependencies, discovery }: ToolExecutionContext) {
|
|
151
|
-
try {
|
|
152
|
-
const result = await resumeHistoricalTask(ctx.cwd, params.runId as string, params.task as number, (params.prompt as string).trim(), {
|
|
153
|
-
herdr: dependencies.herdr, signal, owner: dependencies.owner, stalledWarningSeconds: discovery.settings.stalledWarningSeconds,
|
|
154
|
-
onUpdate: (partial) => onUpdate?.({ content: [{ type: "text", text: partial.summary }], details: partial }),
|
|
155
|
-
onStalled: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "resume", runId: params.runId, stalled: true } }),
|
|
156
|
-
onCleanupError: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "resume", runId: params.runId, cleanupError: text } }),
|
|
157
|
-
});
|
|
158
|
-
return { content: [{ type: "text", text: result.summary }], details: result, ...(result.status === "failed" || result.status === "cancelled" ? { isError: true } : {}) };
|
|
159
|
-
} catch (error) { return errorResponse(error instanceof Error ? error.message : String(error)); }
|
|
160
|
-
},
|
|
161
|
-
async respond({ params, signal, onUpdate, ctx, dependencies }: ToolExecutionContext) {
|
|
162
|
-
try {
|
|
163
|
-
const result = await respondToBlockedTask(ctx.cwd, (params.runId as string).trim(), (params.answer as string).trim(), {
|
|
164
|
-
herdr: dependencies.herdr, taskNumber: params.task as number, signal, owner: dependencies.owner,
|
|
165
|
-
onUpdate: (partial) => onUpdate?.({ content: [{ type: "text", text: partial.summary }], details: partial }),
|
|
166
|
-
onStalled: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "respond", runId: params.runId, stalled: true } }),
|
|
167
|
-
onWarning: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "respond", runId: params.runId, warning: text } }),
|
|
168
|
-
onCleanupError: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { action: "respond", runId: params.runId, cleanupError: text } }),
|
|
169
|
-
});
|
|
170
|
-
const blockedControl = result.status === "blocked" ? { allowedActions: ["respond", "cancel"] as const } : {};
|
|
171
|
-
const text = [result.summary, ...(result.question ? [`Question: ${result.question}`] : []), ...(result.status === "blocked" ? [`Run ID: ${result.runId}`, `Answer with herdr_subagents_control({ action: "respond", runId: "${result.runId}", answer: "..." }) or cancel this run.`] : []), ...result.documents.map((document) => `${document.description}: ${document.path}`)].join("\n");
|
|
172
|
-
return { content: [{ type: "text", text }], details: { ...result, ...blockedControl }, ...(result.status === "failed" || result.status === "partial" || result.status === "cancelled" ? { isError: true } : {}) };
|
|
173
|
-
} catch (error) { return errorResponse(error instanceof Error ? error.message : String(error)); }
|
|
174
|
-
},
|
|
175
134
|
async list({ discovery }: ToolExecutionContext) {
|
|
176
135
|
return { content: [{ type: "text", text: listText(discovery) }], details: { action: "list", agents: discovery.personas, diagnostics: discovery.diagnostics, settings: discovery.settings } };
|
|
177
136
|
},
|
|
@@ -201,10 +160,9 @@ async function executeBatch({ params, signal, onUpdate, ctx, dependencies, disco
|
|
|
201
160
|
onUpdate: (partial) => onUpdate?.({ content: [{ type: "text", text: partial.summary }], details: { ...partial, prompts: tasks.map((task) => task.prompt) } }),
|
|
202
161
|
});
|
|
203
162
|
const lines = [result.summary, ...result.tasks.map((task) => `- ${task.name}: ${task.status} — ${task.summary}`)];
|
|
204
|
-
if (result.status === "blocked") lines.push("", `Run ID: ${result.runId}`, `Answer with herdr_subagents_control({ action: "respond", runId: "${result.runId}", answer: "..." }) or cancel this run before starting another foreground run.`);
|
|
205
163
|
if (result.documents.length) lines.push("", "Documents:", ...result.documents.map((document) => `- ${document.description}: ${document.path}`));
|
|
206
164
|
const isError = result.status === "failed" || result.status === "partial" || result.status === "cancelled";
|
|
207
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { ...result, prompts: tasks.map((task) => task.prompt)
|
|
165
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { ...result, prompts: tasks.map((task) => task.prompt) }, ...(isError ? { isError: true } : {}) };
|
|
208
166
|
} catch (error) {
|
|
209
167
|
const text = error instanceof Error ? error.message : String(error);
|
|
210
168
|
const diagnostic = error as { recordDirectory?: string; runId?: string };
|
|
@@ -6,8 +6,6 @@ export const HERDR_ACTIONS = {
|
|
|
6
6
|
status: { allowed: ["action", "runId"], includeRunId: true },
|
|
7
7
|
cancel: { allowed: ["action", "runId"], includeRunId: true },
|
|
8
8
|
cleanup: { allowed: ["action", "runId"], includeRunId: true },
|
|
9
|
-
respond: { allowed: ["action", "runId", "answer", "task"], includeRunId: true },
|
|
10
|
-
resume: { allowed: ["action", "runId", "task", "prompt"], includeRunId: true },
|
|
11
9
|
} as const satisfies Record<string, { allowed: readonly (keyof HerdrSubagentsControlToolParams)[]; includeRunId: boolean }>;
|
|
12
10
|
|
|
13
11
|
export function validateRunParams(params: HerdrSubagentsRunParams): string | undefined {
|
|
@@ -21,7 +19,5 @@ export function validateControlParams(params: HerdrSubagentsControlToolParams):
|
|
|
21
19
|
const extra = Object.keys(params).find((key) => !allowed.includes(key as keyof HerdrSubagentsControlToolParams));
|
|
22
20
|
if (extra) return `Field '${extra}' is not valid with action '${params.action}'.`;
|
|
23
21
|
if ((params.action === "status" || params.action === "cancel" || params.action === "cleanup") && !params.runId?.trim()) return `runId is required for action '${params.action}'.`;
|
|
24
|
-
if (params.action === "respond" && (!params.runId?.trim() || !params.answer?.trim() || !params.task)) return "runId, answer, and task are required for action 'respond'.";
|
|
25
|
-
if (params.action === "resume" && (!params.runId?.trim() || !params.task || !params.prompt?.trim())) return "runId, task, and prompt are required for action 'resume'.";
|
|
26
22
|
return undefined;
|
|
27
23
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
|
|
6
|
+
import registerSubagentReport from "../extensions/subagent-report.ts";
|
|
7
|
+
import { applyTaskStatus } from "../src/records.ts";
|
|
8
|
+
|
|
9
|
+
process.env.HERDR_ENV = "1";
|
|
10
|
+
process.env.HERDR_WORKSPACE_ID = "test-workspace";
|
|
11
|
+
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), "herdr-state-machine-"));
|
|
13
|
+
|
|
14
|
+
function task(name, prompt, access = "read") {
|
|
15
|
+
return { name, prompt, agent: "worker", access, skills: [], systemPrompt: "Finish the delegated task." };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fakeHerdr() {
|
|
19
|
+
let pane = 0;
|
|
20
|
+
let active = 0;
|
|
21
|
+
let maxActive = 0;
|
|
22
|
+
const alive = new Map();
|
|
23
|
+
const starts = [];
|
|
24
|
+
const paneDirectories = new Map();
|
|
25
|
+
const agents = new Map();
|
|
26
|
+
const publish = (directory, report) => {
|
|
27
|
+
const record = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
|
|
28
|
+
const path = join(directory, "reports", `${record.currentAttemptId}.json`);
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, JSON.stringify({ attemptId: record.currentAttemptId, documents: [], reportedAt: new Date().toISOString(), ...report }));
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
get maxActive() { return maxActive; },
|
|
34
|
+
starts,
|
|
35
|
+
async createTab(request) {
|
|
36
|
+
pane += 1;
|
|
37
|
+
const paneId = `pane-${pane}`;
|
|
38
|
+
paneDirectories.set(paneId, request.env.PI_HERDR_SUBAGENTS_TASK_DIR);
|
|
39
|
+
return { tabId: "tab-1", paneId };
|
|
40
|
+
},
|
|
41
|
+
async splitPane(request) {
|
|
42
|
+
pane += 1;
|
|
43
|
+
const paneId = `pane-${pane}`;
|
|
44
|
+
paneDirectories.set(paneId, request.env.PI_HERDR_SUBAGENTS_TASK_DIR);
|
|
45
|
+
return { paneId };
|
|
46
|
+
},
|
|
47
|
+
async startAgent(request) {
|
|
48
|
+
const session = request.args[request.args.indexOf("--session") + 1];
|
|
49
|
+
const directory = dirname(session);
|
|
50
|
+
assert.equal(paneDirectories.get(request.paneId), directory);
|
|
51
|
+
assert.equal(request.args.includes("--print"), false);
|
|
52
|
+
alive.set(request.paneId, true);
|
|
53
|
+
agents.set(request.name, { paneId: request.paneId, directory });
|
|
54
|
+
},
|
|
55
|
+
async promptAgent(request) {
|
|
56
|
+
const { paneId, directory } = agents.get(request.target);
|
|
57
|
+
starts.push(request.prompt);
|
|
58
|
+
active += 1;
|
|
59
|
+
maxActive = Math.max(maxActive, active);
|
|
60
|
+
if (request.prompt.includes("transport retry")) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
publish(directory, { status: "completed", summary: `${request.prompt} result` });
|
|
63
|
+
alive.set(paneId, false);
|
|
64
|
+
active -= 1;
|
|
65
|
+
}, 35);
|
|
66
|
+
throw new Error("WebSocket closed 1000");
|
|
67
|
+
}
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, request.prompt.includes("slow") ? 35 : 10));
|
|
69
|
+
if (!request.prompt.includes("omit report")) publish(directory, {
|
|
70
|
+
status: request.prompt.includes("fail") ? "failed" : "completed",
|
|
71
|
+
summary: `${request.prompt} result`,
|
|
72
|
+
...(request.prompt.includes("fail") ? { error: "expected failure" } : {}),
|
|
73
|
+
});
|
|
74
|
+
alive.set(paneId, false);
|
|
75
|
+
active -= 1;
|
|
76
|
+
return { status: "done" };
|
|
77
|
+
},
|
|
78
|
+
async getAgentState() { return "unknown"; },
|
|
79
|
+
async waitForAgentChange() { await new Promise((resolve) => setTimeout(resolve, 5)); },
|
|
80
|
+
async isTaskRunning(paneId) { return alive.get(paneId) === true; },
|
|
81
|
+
async renamePane() {}, async renameTab() {}, async focusPane() {},
|
|
82
|
+
async isTabFocused() { return false; }, async waitForTabUnfocused() {},
|
|
83
|
+
async paneExists() { return true; }, async closeTab() {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
|
|
88
|
+
const result = await runHerdrSubagentsBatch(tasks, {
|
|
89
|
+
label: "state-machine-test", concurrency, maxConcurrency: 4, cwd: root, herdr, signal,
|
|
90
|
+
});
|
|
91
|
+
return { result, herdr };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
const { result, herdr } = await run([task("one", "first slow"), task("two", "second")], 1);
|
|
96
|
+
assert.equal(result.status, "completed");
|
|
97
|
+
assert.equal(herdr.maxActive, 1);
|
|
98
|
+
assert.deepEqual(herdr.starts, ["first slow", "second"]);
|
|
99
|
+
assert.deepEqual(result.tasks.map((entry) => entry.status), ["completed", "completed"]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
const { result, herdr } = await run([task("one", "one slow"), task("two", "two slow"), task("three", "three")], 2);
|
|
104
|
+
assert.equal(result.status, "completed");
|
|
105
|
+
assert.equal(herdr.maxActive, 2);
|
|
106
|
+
assert.deepEqual(result.tasks.map((entry) => entry.index), [0, 1, 2]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
const { result } = await run([task("retry", "retry slow")], 1);
|
|
111
|
+
assert.equal(result.status, "completed");
|
|
112
|
+
assert.equal(result.tasks[0].summary, "retry slow result");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
const { result } = await run([task("retry", "transport retry")], 1);
|
|
118
|
+
assert.equal(result.status, "completed");
|
|
119
|
+
assert.equal(result.tasks[0].summary, "transport retry result");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
const { result } = await run([task("missing", "omit report")], 1);
|
|
124
|
+
assert.equal(result.status, "failed");
|
|
125
|
+
assert.match(result.tasks[0].error, /exited without submitting/);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
const { result } = await run([task("bad", "fail"), task("good", "still run")], 2);
|
|
130
|
+
assert.equal(result.status, "partial");
|
|
131
|
+
assert.deepEqual(result.tasks.map((entry) => entry.status), ["failed", "completed"]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
const { result } = await run([task("bad-one", "fail one"), task("bad-two", "fail two")], 2);
|
|
136
|
+
assert.equal(result.status, "failed");
|
|
137
|
+
assert.deepEqual(result.tasks.map((entry) => entry.status), ["failed", "failed"]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
setTimeout(() => controller.abort(new Error("test cancellation")), 8);
|
|
143
|
+
const { result } = await run([task("active", "slow active"), task("queued", "queued")], 1, fakeHerdr(), controller.signal);
|
|
144
|
+
assert.equal(result.status, "cancelled");
|
|
145
|
+
assert.deepEqual(result.activity.map((entry) => entry.status), ["cancelled", "cancelled"]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await assert.rejects(() => run([task("writer", "edit", "write"), task("reader", "read")], 2), /require concurrency 1/);
|
|
149
|
+
|
|
150
|
+
const terminal = { id: "task", runId: "run", order: 1, status: "completed" };
|
|
151
|
+
assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are immutable/);
|
|
152
|
+
|
|
153
|
+
{
|
|
154
|
+
const directory = join(root, "report-idempotency");
|
|
155
|
+
mkdirSync(directory, { recursive: true });
|
|
156
|
+
const attemptId = "11111111-1111-4111-8111-111111111111";
|
|
157
|
+
writeFileSync(join(directory, "task.json"), JSON.stringify({ id: "task", runId: "run", order: 1, status: "running", currentAttemptId: attemptId }));
|
|
158
|
+
const handlers = new Map();
|
|
159
|
+
let reportTool;
|
|
160
|
+
registerSubagentReport({
|
|
161
|
+
registerTool(tool) { reportTool = tool; },
|
|
162
|
+
registerCommand() {}, registerShortcut() {},
|
|
163
|
+
on(name, handler) { handlers.set(name, handler); },
|
|
164
|
+
events: { on() {} },
|
|
165
|
+
async exec() { return { code: 0, stdout: "", stderr: "", killed: false }; },
|
|
166
|
+
});
|
|
167
|
+
process.env.PI_HERDR_SUBAGENTS_TASK_DIR = directory;
|
|
168
|
+
await handlers.get("before_agent_start")?.();
|
|
169
|
+
const report = { status: "completed", summary: "done", result: "full result", documents: [] };
|
|
170
|
+
await reportTool.execute("first", report);
|
|
171
|
+
await reportTool.execute("duplicate", report);
|
|
172
|
+
await assert.rejects(() => reportTool.execute("conflict", { ...report, summary: "different" }), /Conflicting subagent_report/);
|
|
173
|
+
const persisted = JSON.parse(readFileSync(join(directory, "reports", `${attemptId}.json`), "utf8"));
|
|
174
|
+
assert.equal(persisted.summary, "done");
|
|
175
|
+
delete process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log("PASS core Herdr state machine");
|
|
@@ -37,16 +37,20 @@ try {
|
|
|
37
37
|
});
|
|
38
38
|
}`);
|
|
39
39
|
const args = childPiArgs({ id: "probe", sessionFile: join(root, "session.jsonl"), tools: ["read"], extensions: [fileExtension, "./local package", probe] }, join(root, "system.md"));
|
|
40
|
+
// `builtin:codemode` is translated by `herdr agent start` and is covered by
|
|
41
|
+
// the real-Herdr smoke test. Direct Pi has no portable form of that alias.
|
|
42
|
+
const codemode = args.indexOf("builtin:codemode");
|
|
43
|
+
args.splice(codemode - 1, 2);
|
|
40
44
|
writeFileSync(join(root, "system.md"), "Extension source loading probe.");
|
|
41
45
|
const env = { ...process.env, PI_CODING_AGENT_DIR: join(root, "agent"), PI_HERDR_SUBAGENTS_CHILD: "1" };
|
|
42
46
|
for (const key of Object.keys(env)) if (key.startsWith("HERDR_") || key === "PI_HERDR_SUBAGENTS_TASK_DIR" || key === "PI_HERDR_SUBAGENTS_PARENT_PANE_ID") delete env[key];
|
|
43
|
-
const result = spawnSync(process.env.PI_TEST_BINARY?.trim() || "pi", [...args, "--offline"
|
|
47
|
+
const result = spawnSync(process.env.PI_TEST_BINARY?.trim() || "pi", [...args, "--offline"], { cwd: root, env, encoding: "utf8", timeout: 30_000 });
|
|
44
48
|
assert.equal(result.error, undefined, String(result.error));
|
|
45
49
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
46
50
|
const loaded = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
47
51
|
for (const command of ["file-extension", "package-extension", "skill:package-skill", "package-prompt", "parent"]) assert.ok(loaded.commands.includes(command), `Missing ${command}: ${JSON.stringify(loaded)}`);
|
|
48
|
-
assert.deepEqual([...loaded.tools].sort(), ["
|
|
49
|
-
console.log("PASS real Pi loads explicit file and package sources, including package skills/prompts despite discovery flags, without broadening the tool allowlist");
|
|
52
|
+
assert.deepEqual([...loaded.tools].sort(), ["read", "subagent_report"]);
|
|
53
|
+
console.log("PASS real Pi loads explicit file and package sources, including package skills/prompts despite discovery flags, without broadening the direct-Pi tool allowlist");
|
|
50
54
|
} finally {
|
|
51
55
|
rmSync(root, { recursive: true, force: true });
|
|
52
56
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## 2026-10-14 - v0.1.0
|
|
2
|
+
|
|
3
|
+
- add OpenAI Codex account pooling with safe quota failover and live usage status
|
|
4
|
+
- add multi-account failover for the openai-codex provider
|
|
5
|
+
- Add `/codex_pool` with a continue/end loop that adds any number of ChatGPT accounts through the native OAuth flow and keeps an existing single login.
|
|
6
|
+
- Switch accounts on proven subscription limits or proven authorization failures only, before any output reaches the conversation.
|
|
7
|
+
- Share cooldown and probe state between pi processes in the same agent directory, and keep auth.json authoritative for credentials.
|
|
8
|
+
- Show live per-account Codex usage and the last successful account, recover locally cooled accounts with one bounded usage sweep, and report the earliest server-stated recovery time.
|
|
9
|
+
- Keep usage recovery race-safe across processes with freshness-guarded state updates, probe leases, bounded private-endpoint reads, model-scoped estimates, and non-secret failure codes.
|
|
10
|
+
- Show `/codex_pool status` in a temporary colored panel above the editor that appears immediately, displays cached state, refreshes live quota in the background, and removes itself after 15 seconds; keep non-TUI text output and place `●当前` at the end of the account line.
|
|
11
|
+
- Stop returning an identical argument completion after a subcommand is fully typed, so one Enter submits `/codex_pool status` instead of merely accepting `status`.
|