@maxiaochao/pi-toolkit 0.10.21 → 0.10.23
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 +10 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +22 -0
- package/packages/herdr-subagents/README.md +16 -8
- package/packages/herdr-subagents/RESUME-LIFECYCLE-DESIGN.md +84 -0
- package/packages/herdr-subagents/TESTING.md +13 -3
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +4 -17
- package/packages/herdr-subagents/extensions/subagent-report.ts +2 -2
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/batch-runner.ts +56 -15
- package/packages/herdr-subagents/src/blocking.ts +115 -72
- package/packages/herdr-subagents/src/continue-run.ts +22 -7
- package/packages/herdr-subagents/src/fleet.ts +2 -1
- package/packages/herdr-subagents/src/herdr.ts +10 -1
- package/packages/herdr-subagents/src/historical-resume.ts +184 -0
- package/packages/herdr-subagents/src/history.ts +5 -131
- package/packages/herdr-subagents/src/live-agent.ts +99 -35
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/protocol.ts +3 -1
- package/packages/herdr-subagents/src/reconcile.ts +156 -135
- package/packages/herdr-subagents/src/records.ts +50 -32
- package/packages/herdr-subagents/src/resume-state.ts +46 -0
- package/packages/herdr-subagents/src/run-pane-allocator.ts +39 -37
- package/packages/herdr-subagents/src/run-state.ts +120 -0
- package/packages/herdr-subagents/src/status.ts +0 -6
- package/packages/herdr-subagents/src/tab-cleanup.ts +16 -16
- package/packages/herdr-subagents/src/tool.ts +1 -54
- package/packages/herdr-subagents/src/validation.ts +1 -1
- package/packages/herdr-subagents/tests/extension-contract.ts +4 -4
- package/packages/herdr-subagents/tests/run-tests.mjs +579 -60
- package/packages/herdr-subagents/src/runner.ts +0 -234
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { appendFile, mkdir, readFile, rm
|
|
2
|
+
import { appendFile, mkdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { childEnvironment, liveAgentName, promptLiveAgent } from "./live-agent.ts";
|
|
6
|
-
import { writeJsonAtomic, writeTextAtomic } from "./state.ts";
|
|
7
|
-
import { cleanupRunTab } from "./tab-cleanup.ts";
|
|
4
|
+
import type { HerdrAutomation } from "./herdr.ts";
|
|
8
5
|
import type { OwnerIdentity } from "./ownership.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { aggregateTaskStatus, applyRunStatus } from "./run-status.ts";
|
|
11
|
-
import { applyReport, applyTaskStatus, isActiveStatus, normalizeNeedsInputReport, projectTaskActivity, readReport, readRunRecord, readTaskRecord, settleAttemptFailure, type PersistedDocument, type PersistedReport, type RunRecord, type RunStatus, type TaskActivity, type TaskRecord, type TaskStatus } from "./records.ts";
|
|
12
|
-
import { attemptReportPath } from "./attempts.ts";
|
|
13
|
-
import { continuationSystemPrompt } from "./protocol.ts";
|
|
14
|
-
import { isAbortError } from "./errors.ts";
|
|
6
|
+
import { isActiveStatus, readReport, readRunRecord, readTaskRecord, type PersistedDocument, type RunStatus, type TaskActivity, type TaskRecord, type TaskStatus } from "./records.ts";
|
|
15
7
|
import { findRunDirectory } from "./run-locator.ts";
|
|
8
|
+
import { resumeHistoricalTaskConcurrent } from "./historical-resume.ts";
|
|
16
9
|
|
|
17
10
|
export interface HistoricalTask { order: number; id: string; name: string; agent: string; status: TaskStatus; summary?: string; question?: string; documents: PersistedDocument[]; recordDirectory: string; sessionFile: string; model?: string; thinking?: string; tools?: string[]; skills: string[]; access?: string; }
|
|
18
11
|
export interface HistoricalRun { id: string; label: string; status: RunStatus; startedAt: string; completedAt?: string; effectiveConcurrency?: number; cleanupPendingTabIds?: string[]; tasks: HistoricalTask[]; recordDirectory: string; }
|
|
@@ -104,124 +97,5 @@ export interface ResumeHistoricalTaskOptions { herdr?: HerdrAutomation; signal?:
|
|
|
104
97
|
export interface ResumeHistoricalTaskResult { runId: string; label: string; status: TaskStatus; summary: string; documents: PersistedDocument[]; tabId: string; paneId: string; sessionFile: string; activity: TaskActivity[]; }
|
|
105
98
|
|
|
106
99
|
export async function resumeHistoricalTask(cwd: string, runId: string, taskNumber: number, prompt: string, options: ResumeHistoricalTaskOptions = {}): Promise<ResumeHistoricalTaskResult> {
|
|
107
|
-
|
|
108
|
-
if (!runDirectory) throw new Error(`Unknown Subagent run '${runId}'.`);
|
|
109
|
-
const canonicalRunId = (await readRunRecord(join(runDirectory, "run.json"))).id;
|
|
110
|
-
const lockDirectory = join(runDirectory, ".resume.lock");
|
|
111
|
-
try { await mkdir(lockDirectory); }
|
|
112
|
-
catch (error) {
|
|
113
|
-
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Subagent run '${runId}' already has a historical resume in progress (lock: ${lockDirectory}).`);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
try { return await resumeHistoricalTaskLocked(cwd, canonicalRunId, taskNumber, prompt, options); }
|
|
117
|
-
finally { await rmdir(lockDirectory); }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function resumeHistoricalTaskLocked(cwd: string, runId: string, taskNumber: number, prompt: string, options: ResumeHistoricalTaskOptions): Promise<ResumeHistoricalTaskResult> {
|
|
121
|
-
const herdr = options.herdr ?? new CliHerdrAutomation();
|
|
122
|
-
const { signal, onCleanupError } = options;
|
|
123
|
-
const run = (await listSubagentHistory(cwd)).find((candidate) => candidate.id === runId);
|
|
124
|
-
const historical = run?.tasks.find((task) => task.order === taskNumber);
|
|
125
|
-
if (!run || !historical) throw new Error(`Unknown historical task ${runId}#${taskNumber}.`);
|
|
126
|
-
if (isActiveStatus(run.status)) throw new Error(`Subagent run '${runId}' is still active. Use action 'respond' for blocked work or wait for it to settle before resuming history.`);
|
|
127
|
-
if (run.cleanupPendingTabIds?.length) throw new Error(`Subagent run '${runId}' is still waiting to clean up Herdr tabs ${run.cleanupPendingTabIds.join(", ")}.`);
|
|
128
|
-
if (!existsSync(historical.sessionFile)) throw new Error(`Historical task ${runId}#${taskNumber} has no saved Pi session.`);
|
|
129
|
-
const taskFile = join(historical.recordDirectory, "task.json");
|
|
130
|
-
const task = await readTaskRecord(taskFile);
|
|
131
|
-
const attempt = Number(task.attempt ?? 1) + 1;
|
|
132
|
-
const turnsDirectory = join(historical.recordDirectory, "turns");
|
|
133
|
-
await mkdir(turnsDirectory, { recursive: true, mode: 0o700 });
|
|
134
|
-
const initialTurnFile = join(turnsDirectory, "01.json");
|
|
135
|
-
if (!existsSync(initialTurnFile)) {
|
|
136
|
-
const initialReportFile = join(historical.recordDirectory, "report.json");
|
|
137
|
-
let initialReport;
|
|
138
|
-
try { initialReport = await readReport(initialReportFile); }
|
|
139
|
-
catch (error) { initialReport = { status: "failed" as const, summary: `Previous structured report was unavailable: ${error instanceof Error ? error.message : String(error)}`, documents: [] }; }
|
|
140
|
-
await writeJsonAtomic(initialTurnFile, { attempt: 1, prompt: task.prompt, status: task.status, summary: initialReport.summary, question: initialReport.question, error: initialReport.error, documents: initialReport.documents ?? [], sessionFile: historical.sessionFile, startedAt: task.startedAt, completedAt: task.completedAt });
|
|
141
|
-
}
|
|
142
|
-
const turnFile = join(turnsDirectory, `${String(attempt).padStart(2, "0")}.json`);
|
|
143
|
-
const followupPrompt = join(historical.recordDirectory, "followup-system-prompt.md");
|
|
144
|
-
await writeTextAtomic(followupPrompt, `${continuationSystemPrompt(task.systemPrompt, "Continue the requested historical work.", "followup")}\n`);
|
|
145
|
-
let tab: { tabId: string; paneId: string } | undefined;
|
|
146
|
-
let failedAttempt: { status: "cancelled" | "failed"; report: PersistedReport; error: unknown } | undefined;
|
|
147
|
-
const agentName = liveAgentName(historical.id, attempt);
|
|
148
|
-
try {
|
|
149
|
-
let report: PersistedReport;
|
|
150
|
-
let status: "blocked" | "completed" | "failed";
|
|
151
|
-
try {
|
|
152
|
-
const env = childEnvironment(historical.recordDirectory);
|
|
153
|
-
tab = await herdr.createTab({ workspaceId: process.env.HERDR_WORKSPACE_ID ?? "", cwd, label: `SA · ${run.label}`, env, focus: false, signal: options.signal });
|
|
154
|
-
const paneLabel = `${String(historical.order).padStart(2, "0")} · ${historical.name}`;
|
|
155
|
-
await herdr.renamePane(tab.paneId, paneLabel, options.signal);
|
|
156
|
-
applyTaskStatus(task, "running");
|
|
157
|
-
Object.assign(task, { attempt, currentTurnFile: turnFile, tabId: tab.tabId, paneId: tab.paneId, paneLabel, agentName });
|
|
158
|
-
const activeRunFile = join(run.recordDirectory, "run.json");
|
|
159
|
-
const activeRunRecord = await readRunRecord(activeRunFile);
|
|
160
|
-
applyRunStatus(activeRunRecord, "running");
|
|
161
|
-
Object.assign(activeRunRecord, { ...ownerRecord(options.owner), tabId: tab.tabId, paneIds: [...new Set([...(activeRunRecord.paneIds ?? []), tab.paneId])] });
|
|
162
|
-
await Promise.all([
|
|
163
|
-
writeJsonAtomic(taskFile, task),
|
|
164
|
-
writeJsonAtomic(turnFile, { attempt, prompt, status: "running", sessionFile: historical.sessionFile, tabId: tab.tabId, paneId: tab.paneId, agentName, startedAt: new Date().toISOString() }),
|
|
165
|
-
writeJsonAtomic(activeRunFile, activeRunRecord),
|
|
166
|
-
]);
|
|
167
|
-
const runningActivity = await Promise.all(run.tasks.map(async (entry, index) => projectTaskActivity(await readTaskRecord(join(entry.recordDirectory, "task.json")), index)));
|
|
168
|
-
options.onUpdate?.({ runId, label: run.label, status: "running", summary: `${historical.name} is continuing.`, documents: [], tabId: tab.tabId, paneId: tab.paneId, sessionFile: historical.sessionFile, activity: runningActivity });
|
|
169
|
-
const prompted = await promptLiveAgent({ herdr, task: { ...task, id: historical.id, sessionFile: historical.sessionFile }, paneId: tab.paneId, prompt, systemPromptFile: followupPrompt, agentName, signal, stalledWarningMs: options.stalledWarningSeconds ? options.stalledWarningSeconds * 1000 : undefined, onStalled: () => options.onStalled?.(`${historical.name} appears stalled; it is still running.`) });
|
|
170
|
-
task.currentAttemptId = prompted.attemptId;
|
|
171
|
-
report = normalizeNeedsInputReport(prompted.report, Number.POSITIVE_INFINITY);
|
|
172
|
-
if (report !== prompted.report) await Promise.all([
|
|
173
|
-
writeJsonAtomic(join(historical.recordDirectory, "report.json"), report),
|
|
174
|
-
writeJsonAtomic(attemptReportPath(historical.recordDirectory, prompted.attemptId), report),
|
|
175
|
-
]);
|
|
176
|
-
status = applyReport(task, report);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
const settled = await settleAttemptFailure(task, join(historical.recordDirectory, "report.json"), {
|
|
179
|
-
cancelled: isAbortError(error, signal),
|
|
180
|
-
error: error instanceof Error ? error.message : String(error),
|
|
181
|
-
name: `${historical.name} follow-up`,
|
|
182
|
-
replaceReport: true,
|
|
183
|
-
});
|
|
184
|
-
failedAttempt = { ...settled, error };
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
await Promise.all([
|
|
188
|
-
writeJsonAtomic(taskFile, task),
|
|
189
|
-
writeJsonAtomic(turnFile, { attempt, prompt, status, summary: report.summary, documents: report.documents ?? [], sessionFile: historical.sessionFile, tabId: tab.tabId, paneId: tab.paneId, agentName, completedAt: new Date().toISOString(), ...(report.question ? { question: report.question } : {}), ...(report.error ? { error: report.error } : {}) }),
|
|
190
|
-
]);
|
|
191
|
-
const runRecord = await readRunRecord(join(run.recordDirectory, "run.json"));
|
|
192
|
-
const aggregateStatus = aggregateTaskStatus((await listSubagentHistory(cwd)).find((candidate) => candidate.id === runId)?.tasks.map((entry) => entry.status) ?? [status]);
|
|
193
|
-
applyRunStatus(runRecord, aggregateStatus, { question: report.question });
|
|
194
|
-
Object.assign(runRecord, { ...ownerRecord(options.owner), tabId: tab.tabId });
|
|
195
|
-
await writeJsonAtomic(join(run.recordDirectory, "run.json"), runRecord);
|
|
196
|
-
if (status !== "blocked") {
|
|
197
|
-
await cleanupRunTab({ herdr, tabId: tab.tabId, runFile: join(run.recordDirectory, "run.json"), runRecord, signal, onError: onCleanupError });
|
|
198
|
-
}
|
|
199
|
-
const activity = await Promise.all(run.tasks.map(async (entry, index) => projectTaskActivity(await readTaskRecord(join(entry.recordDirectory, "task.json")), index)));
|
|
200
|
-
return { runId, label: run.label, status, summary: report.summary, documents: report.documents ?? [], tabId: tab.tabId, paneId: tab.paneId, sessionFile: historical.sessionFile, activity };
|
|
201
|
-
} catch (error) {
|
|
202
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
-
const completedAt = new Date().toISOString();
|
|
204
|
-
const settled = failedAttempt ?? await settleAttemptFailure(task, join(historical.recordDirectory, "report.json"), {
|
|
205
|
-
cancelled: isAbortError(error, signal),
|
|
206
|
-
error: message,
|
|
207
|
-
name: `${historical.name} follow-up`,
|
|
208
|
-
at: completedAt,
|
|
209
|
-
replaceReport: true,
|
|
210
|
-
});
|
|
211
|
-
const { status, report } = settled;
|
|
212
|
-
task.attempt = attempt;
|
|
213
|
-
const runRecord = await readRunRecord(join(run.recordDirectory, "run.json"));
|
|
214
|
-
const aggregateStatus = status === "cancelled" ? "cancelled" : aggregateTaskStatus(run.tasks.map((entry) => entry.id === historical.id ? status : entry.status));
|
|
215
|
-
applyRunStatus(runRecord, aggregateStatus, { error: message });
|
|
216
|
-
Object.assign(runRecord, { ...ownerRecord(options.owner), ...(tab ? { tabId: tab.tabId } : {}) });
|
|
217
|
-
await Promise.all([
|
|
218
|
-
writeJsonAtomic(taskFile, task),
|
|
219
|
-
writeJsonAtomic(turnFile, { attempt, prompt, status, summary: report.summary, documents: report.documents, error: message, sessionFile: historical.sessionFile, ...(tab ? { tabId: tab.tabId, paneId: tab.paneId } : {}), completedAt }),
|
|
220
|
-
writeJsonAtomic(join(run.recordDirectory, "run.json"), runRecord),
|
|
221
|
-
]);
|
|
222
|
-
const activity = await Promise.all(run.tasks.map(async (entry, index) => projectTaskActivity(await readTaskRecord(join(entry.recordDirectory, "task.json")), index)));
|
|
223
|
-
options.onUpdate?.({ runId, label: run.label, status, summary: report.summary, documents: report.documents, tabId: tab?.tabId ?? "", paneId: tab?.paneId ?? "", sessionFile: historical.sessionFile, activity });
|
|
224
|
-
if (tab) await cleanupRunTab({ herdr, tabId: tab.tabId, runFile: join(run.recordDirectory, "run.json"), runRecord, signal, onError: onCleanupError });
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
100
|
+
return resumeHistoricalTaskConcurrent(cwd, runId, taskNumber, prompt, options);
|
|
227
101
|
}
|
|
@@ -2,9 +2,8 @@ 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 { HerdrAutomation } from "./herdr.ts";
|
|
6
|
-
import type
|
|
7
|
-
import { parseReport } from "./records.ts";
|
|
5
|
+
import type { HerdrAgentState, HerdrAutomation } from "./herdr.ts";
|
|
6
|
+
import { applyTaskRuntime, parseReport, readTaskRecord, type PersistedReport, type TaskRecord, type TaskRuntimeState } from "./records.ts";
|
|
8
7
|
import { attemptReportPath, attemptStatePath, createAttemptId, type AttemptState } from "./attempts.ts";
|
|
9
8
|
import { writeJsonAtomic } from "./state.ts";
|
|
10
9
|
|
|
@@ -64,11 +63,13 @@ export async function promptLiveAgent(options: {
|
|
|
64
63
|
start?: boolean;
|
|
65
64
|
stalledWarningMs?: number;
|
|
66
65
|
onStalled?: () => void;
|
|
67
|
-
|
|
66
|
+
onRuntimeUpdate?: (task: TaskRecord) => void | Promise<void>;
|
|
67
|
+
attemptId?: string;
|
|
68
|
+
}): Promise<{ report: PersistedReport; agentName: string; attemptId: string }> {
|
|
68
69
|
const taskDirectory = dirname(options.task.sessionFile);
|
|
69
70
|
const reportFile = join(taskDirectory, "report.json");
|
|
70
71
|
const taskFile = join(taskDirectory, "task.json");
|
|
71
|
-
const attemptId = createAttemptId();
|
|
72
|
+
const attemptId = options.attemptId ?? createAttemptId();
|
|
72
73
|
const attemptReportFile = attemptReportPath(taskDirectory, attemptId);
|
|
73
74
|
const attemptStateFile = attemptStatePath(taskDirectory, attemptId);
|
|
74
75
|
await Promise.all([mkdir(dirname(attemptReportFile), { recursive: true, mode: 0o700 }), mkdir(dirname(attemptStateFile), { recursive: true, mode: 0o700 })]);
|
|
@@ -85,39 +86,102 @@ export async function promptLiveAgent(options: {
|
|
|
85
86
|
signal: options.signal,
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
|
-
const
|
|
89
|
+
const persistRuntime = async (state: TaskRuntimeState, diagnostic?: string) => {
|
|
90
|
+
const task = await readTaskRecord(taskFile);
|
|
91
|
+
if (task.currentAttemptId !== attemptId || (task.status !== "running" && task.status !== "starting")) return;
|
|
92
|
+
if (task.runtimeState === state && task.runtimeDiagnostic === diagnostic) return;
|
|
93
|
+
applyTaskRuntime(task, state, diagnostic);
|
|
94
|
+
await writeJsonAtomic(taskFile, task);
|
|
95
|
+
await options.onRuntimeUpdate?.(task);
|
|
96
|
+
};
|
|
97
|
+
const readAttemptState = async (): Promise<AttemptState | undefined> => {
|
|
98
|
+
if (!existsSync(attemptStateFile)) return undefined;
|
|
99
|
+
try {
|
|
100
|
+
const value = JSON.parse(await readFile(attemptStateFile, "utf8")) as { attemptId?: unknown; state?: unknown };
|
|
101
|
+
if (value.attemptId === attemptId && (value.state === "working" || value.state === "settled")) return value.state;
|
|
102
|
+
} catch { /* a concurrent atomic replacement will be retried */ }
|
|
103
|
+
return undefined;
|
|
104
|
+
};
|
|
105
|
+
const wait = () => new Promise<void>((resolve, reject) => {
|
|
106
|
+
const onAbort = () => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
if (existsSync(attemptReportFile)) resolve();
|
|
109
|
+
else reject(options.signal?.reason ?? new Error("Subagent attempt aborted."));
|
|
110
|
+
};
|
|
111
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener("abort", onAbort); resolve(); }, 100);
|
|
112
|
+
if (options.signal?.aborted) onAbort();
|
|
113
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
114
|
+
});
|
|
115
|
+
const promptController = new AbortController();
|
|
116
|
+
const abortPrompt = () => promptController.abort(options.signal?.reason ?? new Error("Subagent prompt wait stopped."));
|
|
117
|
+
if (options.signal?.aborted) abortPrompt();
|
|
118
|
+
else options.signal?.addEventListener("abort", abortPrompt, { once: true });
|
|
119
|
+
const promptOutcome = options.herdr.promptAgent({ target: agentName, prompt: options.prompt, signal: promptController.signal })
|
|
120
|
+
.then((result) => ({ result }), (error: unknown) => ({ error }));
|
|
121
|
+
let promptSettled = false;
|
|
122
|
+
let promptStatus: HerdrAgentState | undefined;
|
|
89
123
|
let promptError: unknown;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
const promptSettlementWork = promptOutcome.then(async (outcome) => {
|
|
125
|
+
promptSettled = true;
|
|
126
|
+
if ("error" in outcome) {
|
|
127
|
+
promptError = outcome.error;
|
|
128
|
+
const detail = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
129
|
+
if (/agent_prompt_stalled|timed?\s*out|timeout/i.test(detail)) await persistRuntime("delivery_unknown", detail);
|
|
130
|
+
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.");
|
|
131
|
+
} else if (outcome.result.status === "working" || outcome.result.status === "blocked" || outcome.result.status === "idle" || outcome.result.status === "done") promptStatus = outcome.result.status;
|
|
132
|
+
});
|
|
133
|
+
const stalledTimer = options.stalledWarningMs ? setTimeout(() => options.onStalled?.(), options.stalledWarningMs) : undefined;
|
|
134
|
+
let terminalObservedAt: number | undefined;
|
|
135
|
+
let turnObserved = false;
|
|
136
|
+
try {
|
|
137
|
+
await persistRuntime("working");
|
|
138
|
+
while (!existsSync(attemptReportFile)) {
|
|
139
|
+
await Promise.resolve();
|
|
140
|
+
let agentState = promptStatus;
|
|
141
|
+
let observationDiagnostic: string | undefined;
|
|
142
|
+
try { agentState = await options.herdr.getAgentState(agentName, options.signal); }
|
|
143
|
+
catch (error) { observationDiagnostic = `Could not observe Herdr agent state: ${error instanceof Error ? error.message : String(error)}`; }
|
|
144
|
+
const attemptState = await readAttemptState();
|
|
145
|
+
if (agentState === "working" || agentState === "blocked" || attemptState === "working") turnObserved = true;
|
|
146
|
+
const promptDetail = promptError instanceof Error ? promptError.message : promptError === undefined ? "" : String(promptError);
|
|
147
|
+
if (agentState === "blocked" || /agent_blocked/i.test(promptDetail)) {
|
|
148
|
+
await persistRuntime("waiting_interaction", observationDiagnostic ?? "Herdr is waiting for an approval or interaction; no business input request has been reported.");
|
|
149
|
+
terminalObservedAt = undefined;
|
|
150
|
+
} else if (promptError && /agent_prompt_stalled|timed?\s*out|timeout/i.test(promptDetail)) {
|
|
151
|
+
await persistRuntime("delivery_unknown", observationDiagnostic ?? promptDetail);
|
|
152
|
+
let live = false;
|
|
153
|
+
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
154
|
+
if (attemptState !== "working" || !live) {
|
|
155
|
+
terminalObservedAt ??= Date.now();
|
|
156
|
+
if (Date.now() - terminalObservedAt >= 50) throw promptError;
|
|
157
|
+
}
|
|
158
|
+
} else if (promptError && promptSettled) {
|
|
159
|
+
terminalObservedAt ??= Date.now();
|
|
160
|
+
if (Date.now() - terminalObservedAt >= 50) throw promptError;
|
|
161
|
+
} else if ((promptSettled || turnObserved || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
|
|
162
|
+
if (attemptState === "working") {
|
|
163
|
+
let live = false;
|
|
164
|
+
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
165
|
+
if (!live) throw new Error("Child Pi stopped while its authoritative attempt state was still working and did not submit a subagent_report.");
|
|
166
|
+
}
|
|
167
|
+
terminalObservedAt ??= Date.now();
|
|
168
|
+
if (Date.now() - terminalObservedAt >= 50) throw new Error("Child Pi did not submit a subagent_report before settling.");
|
|
169
|
+
} else {
|
|
170
|
+
await persistRuntime("working", observationDiagnostic);
|
|
171
|
+
terminalObservedAt = undefined;
|
|
172
|
+
if (!observationDiagnostic && agentState === "unknown") {
|
|
173
|
+
let live = false;
|
|
174
|
+
try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
|
|
175
|
+
if (!live) throw new Error("Child Pi stopped without submitting a subagent_report.");
|
|
106
176
|
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error === promptError || (error instanceof Error && error.message.startsWith("Child Pi stopped while"))) throw error;
|
|
109
|
-
// A transient liveness query is not evidence that the child stopped.
|
|
110
177
|
}
|
|
111
|
-
await
|
|
112
|
-
const onAbort = () => { clearTimeout(timer); reject(options.signal?.reason ?? new Error("Subagent attempt aborted.")); };
|
|
113
|
-
const timer = setTimeout(() => { options.signal?.removeEventListener("abort", onAbort); resolve(); }, 100);
|
|
114
|
-
if (options.signal?.aborted) onAbort();
|
|
115
|
-
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
116
|
-
});
|
|
117
|
-
continue;
|
|
178
|
+
await wait();
|
|
118
179
|
}
|
|
119
|
-
|
|
120
|
-
|
|
180
|
+
} finally {
|
|
181
|
+
if (stalledTimer) clearTimeout(stalledTimer);
|
|
182
|
+
promptController.abort(new Error("Authoritative subagent_report observed; stop waiting for prompt acknowledgement."));
|
|
183
|
+
await promptSettlementWork;
|
|
184
|
+
options.signal?.removeEventListener("abort", abortPrompt);
|
|
121
185
|
}
|
|
122
186
|
let value: unknown;
|
|
123
187
|
try { value = JSON.parse(await readFile(attemptReportFile, "utf8")); }
|
|
@@ -127,7 +191,7 @@ export async function promptLiveAgent(options: {
|
|
|
127
191
|
throw new Error("Child Pi submitted an invalid subagent_report.");
|
|
128
192
|
}
|
|
129
193
|
try {
|
|
130
|
-
const report = parseReport(value, "child subagent_report")
|
|
194
|
+
const report = parseReport(value, "child subagent_report");
|
|
131
195
|
if (report.attemptId !== attemptId) throw new Error(`Report belongs to attempt '${report.attemptId ?? "unknown"}', expected '${attemptId}'.`);
|
|
132
196
|
await writeJsonAtomic(reportFile, report);
|
|
133
197
|
return { report, agentName, attemptId };
|
|
@@ -18,7 +18,7 @@ export const HerdrSubagentsParams = Type.Object({
|
|
|
18
18
|
export const HerdrSubagentsControlParams = Type.Object({
|
|
19
19
|
action: StringEnum(["list", "status", "respond", "history", "resume", "cancel", "cleanup"] as const, { description: "Control operation to perform." }),
|
|
20
20
|
runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by status, respond, resume, cancel, or cleanup." })),
|
|
21
|
-
answer: Type.Optional(Type.String({ minLength: 1, description: "
|
|
22
|
-
task: Type.Optional(Type.Integer({ minimum: 1, description: "One-based task number
|
|
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
23
|
prompt: Type.Optional(Type.String({ minLength: 1, description: "Follow-up prompt used by resume." })),
|
|
24
24
|
}, { additionalProperties: false });
|
|
@@ -13,7 +13,9 @@ export function reportProtocolPrompt(systemPrompt: string, kind: ReportPromptKin
|
|
|
13
13
|
"",
|
|
14
14
|
"When this turn is finished, call subagent_report exactly once for this turn.",
|
|
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=
|
|
16
|
+
"Use status=completed when the task is finished.",
|
|
17
|
+
"Use status=blocked with question when you have a specific question or decision the parent agent must answer before you can continue; the run stays open waiting for it.",
|
|
18
|
+
"Use status=failed with error when this task cannot be completed, so the run does not wait for an answer.",
|
|
17
19
|
].filter((line, index) => line || index > 1).join("\n").trim();
|
|
18
20
|
}
|
|
19
21
|
|