@marcoscale98/pi-extensible-workflows 5.14.1-fork.1

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.
Files changed (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
package/src/herdr.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { execFile } from "node:child_process";
2
+ import { isObject } from "./utils.js";
3
+
4
+ export type HerdrPaneAction = "live";
5
+ export type HerdrAgentState = "idle" | "working" | "blocked" | "unknown";
6
+ export type HerdrAgentStatus = HerdrAgentState | "done";
7
+ export interface HerdrPaneStatus { state: HerdrAgentStatus }
8
+ export interface HerdrPaneRequest { action: HerdrPaneAction; cwd: string; command: string; paneId?: string }
9
+ export interface HerdrWorkspacePaneRequest { cwd: string; workspaceLabel?: string; workspaceId?: string; tabLabel: string; command: string }
10
+ export interface HerdrWorkspacePane { workspaceId: string; tabId: string; paneId: string }
11
+ export type HerdrCommandRunner = (args: readonly string[]) => Promise<string>;
12
+
13
+ export const herdrCommandRunner: HerdrCommandRunner = (args) => new Promise<string>((resolve, reject) => {
14
+ execFile("herdr", [...args], { encoding: "utf8", maxBuffer: 1024 * 1024 }, (error, stdout) => {
15
+ if (error) { reject(new Error(error.message)); return; }
16
+ resolve(stdout);
17
+ });
18
+ });
19
+
20
+ export function herdrPaneId(env: NodeJS.ProcessEnv = process.env): string | undefined {
21
+ if (env.HERDR_ENV !== "1") return undefined;
22
+ const paneId = env.HERDR_PANE_ID?.trim();
23
+ return paneId || undefined;
24
+ }
25
+ export function herdrAvailable(env: NodeJS.ProcessEnv = process.env): boolean { return Boolean(herdrPaneId(env) && env.HERDR_SOCKET_PATH); }
26
+
27
+ function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; }
28
+ function json(value: string): unknown { return JSON.parse(value) as unknown; }
29
+ function record(value: unknown): Record<string, unknown> | undefined { return isObject(value) ? value : undefined; }
30
+ function paneLayout(value: unknown, targetPane: string): { width: number; height: number } {
31
+ const root = record(value);
32
+ const result = record(root?.result);
33
+ const layout = record(result?.layout);
34
+ const rawPanes = layout?.panes;
35
+ if (!Array.isArray(rawPanes)) throw new Error("Herdr returned an invalid pane layout.");
36
+ const panes: unknown[] = rawPanes;
37
+ const pane = panes.find((candidate: unknown) => record(candidate)?.pane_id === targetPane);
38
+ const rect = record(record(pane)?.rect);
39
+ const width = rect?.width;
40
+ const height = rect?.height;
41
+ if (width === undefined || height === undefined || typeof width !== "number" || typeof height !== "number") throw new Error("Herdr returned an invalid target pane geometry.");
42
+ return { width, height };
43
+ }
44
+ function splitPaneId(value: unknown): string {
45
+ const pane = record(record(record(value)?.result)?.pane);
46
+ const paneId = pane?.pane_id;
47
+ if (typeof paneId !== "string" || !paneId) throw new Error("Herdr returned an invalid created pane ID.");
48
+ return paneId;
49
+ }
50
+
51
+ function commandFor(request: HerdrPaneRequest): string {
52
+ const environment = ["PI_CODING_AGENT_DIR", "PI_CODING_AGENT_SESSION_DIR"].flatMap((name) => process.env[name] === undefined ? [] : [`${name}=${shellQuote(process.env[name] ?? "")}`]);
53
+ return `cd ${shellQuote(request.cwd)} && ${environment.length ? `${environment.join(" ")} ` : ""}${request.command}`;
54
+ }
55
+
56
+ export function herdrPaneCommand(request: HerdrPaneRequest): string { return commandFor(request); }
57
+
58
+ export async function openHerdrPane(request: HerdrPaneRequest, runner: HerdrCommandRunner = herdrCommandRunner): Promise<string> {
59
+ const targetPane = request.paneId ?? herdrPaneId();
60
+ if (!targetPane) throw new Error("Pane actions require a Herdr-managed session with HERDR_PANE_ID.");
61
+ if (!request.cwd) throw new Error("Pane actions require a working directory.");
62
+ if (!request.command) throw new Error("Pane action is missing its command.");
63
+ const layout = paneLayout(json(await runner(["pane", "layout", "--pane", targetPane])), targetPane);
64
+ const direction = layout.width > layout.height ? "right" : "down";
65
+ const paneId = splitPaneId(json(await runner(["pane", "split", targetPane, "--direction", direction, "--no-focus"])));
66
+ try {
67
+ await runner(["pane", "run", paneId, commandFor(request)]);
68
+ return paneId;
69
+ } catch (error) {
70
+ await runner(["pane", "close", paneId]).catch(() => undefined);
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ function resourceId(value: unknown, resource: string, field: string): string {
76
+ const result = record(record(value)?.result);
77
+ const item = record(result?.[resource]);
78
+ const id = item?.[field];
79
+ if (typeof id !== "string" || !id) throw new Error(`Herdr returned an invalid ${resource} ID.`);
80
+ return id;
81
+ }
82
+
83
+ export async function openHerdrWorkspacePane(request: HerdrWorkspacePaneRequest, runner: HerdrCommandRunner = herdrCommandRunner): Promise<HerdrWorkspacePane> {
84
+ if (!request.cwd || !request.tabLabel || !request.command || !request.workspaceId && !request.workspaceLabel) throw new Error("Herdr workspace pane is missing required data.");
85
+ const created = json(await runner(request.workspaceId
86
+ ? ["tab", "create", "--workspace", request.workspaceId, "--cwd", request.cwd, "--label", request.tabLabel, "--no-focus"]
87
+ : ["workspace", "create", "--cwd", request.cwd, "--label", request.workspaceLabel ?? "", "--no-focus"]));
88
+ const workspaceId = request.workspaceId ?? resourceId(created, "workspace", "workspace_id");
89
+ let tabId = resourceId(created, "tab", "tab_id");
90
+ let paneId = resourceId(created, "root_pane", "pane_id");
91
+ try {
92
+ if (!request.workspaceId) {
93
+ await runner(["tab", "rename", tabId, request.workspaceLabel ?? ""]);
94
+ const tab = json(await runner(["tab", "create", "--workspace", workspaceId, "--cwd", request.cwd, "--label", request.tabLabel, "--no-focus"]));
95
+ tabId = resourceId(tab, "tab", "tab_id");
96
+ paneId = resourceId(tab, "root_pane", "pane_id");
97
+ }
98
+ await runner(["pane", "run", paneId, commandFor({ action: "live", cwd: request.cwd, command: request.command })]);
99
+ return { workspaceId, tabId, paneId };
100
+ } catch (error) {
101
+ await runner(request.workspaceId ? ["tab", "close", tabId] : ["workspace", "close", workspaceId]).catch(() => undefined);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ export async function openHerdrLivePane(request: HerdrPaneRequest | HerdrWorkspacePaneRequest, runner: HerdrCommandRunner = herdrCommandRunner): Promise<string | HerdrWorkspacePane> {
107
+ if ("workspaceLabel" in request || "workspaceId" in request) return openHerdrWorkspacePane(request, runner);
108
+ return openHerdrPane({ ...request, action: "live" }, runner);
109
+ }
110
+
111
+ function hasPiProcess(value: unknown, originatingEntrypoint?: string): boolean {
112
+ const result = record(record(value)?.result);
113
+ const info = record(result?.process_info);
114
+ const processes = info?.foreground_processes;
115
+ if (!Array.isArray(processes)) return false;
116
+ return processes.some((candidate) => {
117
+ const process = record(candidate);
118
+ const name = process?.name;
119
+ const argv0 = process?.argv0;
120
+ const argv = process?.argv;
121
+ const commandLine = process?.cmdline;
122
+ if (originatingEntrypoint) {
123
+ if (Array.isArray(argv) && argv.some((value) => value === originatingEntrypoint) || typeof commandLine === "string" && commandLine.includes(originatingEntrypoint)) return true;
124
+ return name === "pi" || argv0 === "pi" || Array.isArray(argv) && argv[0] === "pi";
125
+ }
126
+ return name === "pi" || argv0 === "pi" || Array.isArray(argv) && argv[0] === "pi" || typeof commandLine === "string" && commandLine.includes("/bin/pi");
127
+ });
128
+ }
129
+ function isHerdrStatus(value: unknown): value is HerdrAgentStatus { return value === "idle" || value === "working" || value === "blocked" || value === "unknown" || value === "done"; }
130
+ function herdrAgentStatus(value: unknown): HerdrPaneStatus | undefined {
131
+ const result = record(record(value)?.result);
132
+ const agent = record(result?.agent);
133
+ const rawState = agent?.agent_status;
134
+ if (typeof rawState !== "string") return undefined;
135
+ const state: HerdrAgentStatus = isHerdrStatus(rawState) ? rawState : "unknown";
136
+ return { state };
137
+ }
138
+ export async function waitForHerdrPane(paneId: string, runner: HerdrCommandRunner = herdrCommandRunner, options: { signal?: AbortSignal; settled?: Promise<void>; intervalMs?: number; startupTimeoutMs?: number; exitGraceMs?: number; originatingEntrypoint?: string; onStatus?: (state: HerdrAgentStatus) => void | Promise<void> } = {}): Promise<"closed" | "exited" | "idle" | "settled" | "aborted"> {
139
+ const intervalMs = options.intervalMs ?? 250;
140
+ const startupTimeoutMs = options.startupTimeoutMs ?? 10000;
141
+ const exitGraceMs = options.exitGraceMs ?? 5000;
142
+ const startedAt = Date.now();
143
+ const monitorController = new AbortController();
144
+ const isAborted = (): boolean => monitorController.signal.aborted;
145
+ let markAborted = (): void => {};
146
+ const aborted = new Promise<"aborted">((resolve) => { markAborted = () => { resolve("aborted"); }; });
147
+ const abortMonitor = (): void => { monitorController.abort(); markAborted(); };
148
+ if (options.signal?.aborted) abortMonitor();
149
+ else options.signal?.addEventListener("abort", abortMonitor, { once: true });
150
+ const delay = (): Promise<void> => new Promise((resolve) => {
151
+ if (isAborted()) { resolve(); return; }
152
+ const timer = setTimeout(finish, intervalMs);
153
+ function finish(): void { clearTimeout(timer); monitorController.signal.removeEventListener("abort", finish); resolve(); }
154
+ monitorController.signal.addEventListener("abort", finish, { once: true });
155
+ });
156
+ const poll = async (): Promise<"closed" | "exited" | "idle" | "aborted"> => {
157
+ let sawPi = false;
158
+ let sawWorking = false;
159
+ let missingSince: number | undefined;
160
+ for (;;) {
161
+ if (isAborted()) return "aborted";
162
+ let piRunning: boolean;
163
+ try {
164
+ const output = await runner(["pane", "process-info", "--pane", paneId]);
165
+ if (isAborted()) return "aborted";
166
+ piRunning = hasPiProcess(json(output), options.originatingEntrypoint);
167
+ } catch {
168
+ if (isAborted()) return "aborted";
169
+ return "closed";
170
+ }
171
+ if (!piRunning) {
172
+ if (sawPi) {
173
+ missingSince ??= Date.now();
174
+ if (Date.now() - missingSince >= exitGraceMs) return "exited";
175
+ }
176
+ } else {
177
+ missingSince = undefined;
178
+ if (!sawPi) { sawPi = true; }
179
+ try {
180
+ const output = await runner(["agent", "get", paneId]);
181
+ if (isAborted()) return "aborted";
182
+ const status = herdrAgentStatus(json(output));
183
+ if (status) {
184
+ if (status.state === "working") sawWorking = true;
185
+ const shouldHandBack = options.settled === undefined && sawWorking && (status.state === "idle" || status.state === "done");
186
+ try { await options.onStatus?.(status.state); } catch { /* Status notifications must not suppress pane handback. */ }
187
+ if (isAborted()) return "aborted";
188
+ if (shouldHandBack) return "idle";
189
+ }
190
+ } catch {
191
+ if (isAborted()) return "aborted";
192
+ /* The process monitor remains authoritative when no agent report is available. */
193
+ }
194
+ }
195
+ if (!piRunning && !sawPi && Date.now() - startedAt >= startupTimeoutMs) throw new Error("Herdr pane did not start Pi.");
196
+ await delay();
197
+ }
198
+ };
199
+ const polling = poll();
200
+ try {
201
+ const result = await Promise.race(options.settled ? [polling, aborted, options.settled.then(() => "settled" as const)] : [polling, aborted]);
202
+ if (result === "settled" || result === "aborted") monitorController.abort();
203
+ return result;
204
+ } finally {
205
+ monitorController.abort();
206
+ options.signal?.removeEventListener("abort", abortMonitor);
207
+ }
208
+ }
209
+
210
+ export interface HerdrAgentReporter { reportSession(session: { sessionId?: string; sessionPath?: string }, sessionStartSource?: string): Promise<void>; reportState(state: HerdrAgentState, message?: string, session?: { sessionId?: string; sessionPath?: string }): Promise<void>; release(): Promise<void> }
211
+
212
+ export function createHerdrAgentReporter(paneId: string, agent: string, runner: HerdrCommandRunner = herdrCommandRunner): HerdrAgentReporter {
213
+ const source = "herdr:pi-extensible-workflows";
214
+ let sequence = Date.now() * 1000;
215
+ let released = false;
216
+ let queue = Promise.resolve();
217
+ const next = () => ++sequence;
218
+ const sessionArgs = (session?: { sessionId?: string; sessionPath?: string }): string[] => session?.sessionPath ? ["--agent-session-path", session.sessionPath] : session?.sessionId ? ["--agent-session-id", session.sessionId] : [];
219
+ const retry = async (operation: () => Promise<string>) => {
220
+ try { await operation(); } catch { try { await operation(); } catch { /* Herdr reporting must not stop the Pi session. */ } }
221
+ };
222
+ const enqueue = (operation: () => Promise<void>) => {
223
+ const result = queue.then(operation);
224
+ queue = result.then(() => undefined, () => undefined);
225
+ return result;
226
+ };
227
+ return {
228
+ async reportSession(session, sessionStartSource) {
229
+ await enqueue(() => retry(() => runner(["pane", "report-agent-session", paneId, "--source", source, "--agent", agent, "--seq", String(next()), ...(sessionStartSource ? ["--session-start-source", sessionStartSource] : []), ...sessionArgs(session)])));
230
+ },
231
+ async reportState(state, message, session) {
232
+ await enqueue(() => retry(() => runner(["pane", "report-agent", paneId, "--source", source, "--agent", agent, "--state", state, "--seq", String(next()), ...(message ? ["--message", message] : []), ...sessionArgs(session)])));
233
+ },
234
+ async release() {
235
+ if (released) return;
236
+ released = true;
237
+ await enqueue(() => retry(() => runner(["pane", "release-agent", paneId, "--source", source, "--agent", agent, "--seq", String(next())])));
238
+ },
239
+ };
240
+ }
@@ -0,0 +1,346 @@
1
+ import { join } from "node:path";
2
+ import { DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
3
+ import { structuralPath as operationPath, type PersistedRun, type RunStore } from "./persistence.js";
4
+ import { ERROR_CODES, WorkflowError, type JsonValue, type WorkflowErrorCode, type WorkflowFailureAgent, type WorkflowFailureDiagnostics, type WorkflowMetadata, type WorkflowSiblingAgent } from "./types.js";
5
+ import { errorCode, errorText, fail, isWorkflowAuthored, object, SerialLane } from "./utils.js";
6
+ const FAILURE_DELIVERY_STATES: ReadonlySet<PersistedRun["state"]> = new Set(["failed", "stopped", "interrupted", "budget_exhausted"]);
7
+
8
+ export type ForegroundDetachResult = { runId: string; state: "running"; detached: true; run: PersistedRun };
9
+ export type ForegroundDelivery = { store: RunStore; detached: boolean; detach: () => Promise<ForegroundDetachResult> };
10
+ export type PendingFailureDiagnostic = { diagnostic: WorkflowFailureDiagnostics; store: RunStore };
11
+
12
+ type ForegroundDeliveryControllerOptions = {
13
+ runs: ReadonlyMap<string, { store: RunStore }>;
14
+ deliver?: (content: string) => void;
15
+ };
16
+
17
+ export class ForegroundDeliveryController {
18
+ readonly foregroundDeliveries = new Map<string, ForegroundDelivery>();
19
+ readonly pendingFailureDiagnostics = new Map<string, PendingFailureDiagnostic>();
20
+ readonly foregroundResumeClaims = new WeakSet<RunStore>();
21
+ readonly terminalDeliveryLanes = new WeakMap<RunStore, SerialLane>();
22
+
23
+ constructor(private readonly options: ForegroundDeliveryControllerOptions) {}
24
+ claimForegroundDelivery = async (store: RunStore, toolCallId: string): Promise<"claimed" | "delivered" | "detached"> => {
25
+ let claim: "claimed" | "delivered" | "detached" = "detached";
26
+ await store.updateState((current) => {
27
+ const delivery = current.delivery;
28
+ if (delivery?.toolCallId !== toolCallId || delivery.mode !== "foreground") return current;
29
+ if (delivery.state === "delivered") { claim = "delivered"; return current; }
30
+ if (delivery.state !== "attached") return current;
31
+ claim = "claimed";
32
+ return { ...current, delivery: { ...delivery, state: "delivered" } };
33
+ });
34
+ return claim;
35
+ };
36
+
37
+ deliverTerminal = (store: RunStore, content: string | (() => string | Promise<string>), failure = false): Promise<void> => {
38
+ const lane = this.terminalDeliveryLanes.get(store) ?? new SerialLane();
39
+ this.terminalDeliveryLanes.set(store, lane);
40
+ return lane.run(async () => {
41
+ let claimed: boolean | undefined;
42
+ await store.updateState((current) => {
43
+ if (failure && !FAILURE_DELIVERY_STATES.has(current.state)) return current;
44
+ if (current.delivery?.state === "delivered") { this.foregroundResumeClaims.delete(store); return current; }
45
+ if (this.foregroundResumeClaims.has(store) && current.delivery?.mode === "foreground" && current.delivery.state === "attached") { this.foregroundResumeClaims.delete(store); return current; }
46
+ if (current.delivery?.mode === "foreground" && current.delivery.state === "attached") { claimed = true; return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } }; }
47
+ if (!current.delivery) { claimed = true; return current; }
48
+ claimed = true;
49
+ return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
50
+ });
51
+ if (claimed !== true) return;
52
+ if (failure && !FAILURE_DELIVERY_STATES.has((await store.load()).run.state)) {
53
+ await store.updateState((current) => !FAILURE_DELIVERY_STATES.has(current.state) && current.delivery?.state === "delivered" ? { ...current, delivery: { ...current.delivery, state: "pending" } } : current);
54
+ return;
55
+ }
56
+ this.options.deliver?.(typeof content === "function" ? await content() : content);
57
+ });
58
+ };
59
+
60
+ foregroundDeliveryCandidates = (runId: string): Array<[string, ForegroundDelivery]> => [...this.foregroundDeliveries.entries()].filter(([, delivery]) => this.options.runs.has(delivery.store.runId) && !delivery.detached && delivery.store.runId === runId);
61
+
62
+ moveForegroundToBackground = async (runId: string): Promise<ForegroundDetachResult> => {
63
+ const candidates = this.foregroundDeliveryCandidates(runId);
64
+ if (!candidates.length) throw new WorkflowError("RUN_NOT_FOUND", `No attached foreground workflow ${runId}`);
65
+ return candidates[0]?.[1].detach() ?? fail("RUN_NOT_FOUND", "No attached foreground workflow is running");
66
+ };
67
+
68
+ isForegroundAttached = (runId: string): boolean => this.foregroundDeliveryCandidates(runId).length > 0;
69
+
70
+ deliverDetachedTerminal = async (toolCallId: string, content: string | (() => string | Promise<string>), failure = false): Promise<void> => {
71
+ const delivery = this.foregroundDeliveries.get(toolCallId);
72
+ if (!delivery) return;
73
+ if ((await delivery.store.load()).run.delivery?.mode !== "background") return;
74
+ this.pendingFailureDiagnostics.delete(toolCallId);
75
+ await this.deliverTerminal(delivery.store, content, failure);
76
+ this.foregroundDeliveries.delete(toolCallId);
77
+ };
78
+ }
79
+
80
+ const workflowFailureDiagnostics = new WeakMap<WorkflowError, WorkflowFailureDiagnostics>();
81
+ export function markWorkflowFailureDiagnostics(error: WorkflowError, diagnostic: WorkflowFailureDiagnostics): void { workflowFailureDiagnostics.set(error, diagnostic); }
82
+
83
+ function workflowDetail(message: string): string {
84
+ const detail = message.trim().replace(new RegExp(`\\b(?:${ERROR_CODES.join("|")})\\b:?\\s*`, "g"), "").replace(/^\s*[A-Z][A-Z0-9_]+:\s*/, "").split("\n").filter((line) => !/^\s*at\s/.test(line)).join("\n").replace(/^Run \S+(?=\s(?:exceeded|is))/i, "Run").replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "the workflow").replace(/^(?:Pi )session \S+(?=\s(?:is|has))/i, "session").replace(/^(Unknown scheduler run|Missing production ownership record|Persisted agent belongs to another run):\s*\S+/i, "$1").replace(/\b(?:runId|sessionId|callSite|occurrence|failedAt|id)[:=]\s*\S+/gi, "").replace(/\s{2,}/g, " ").trim();
85
+ return detail || "No further details were provided";
86
+ }
87
+
88
+ const WORKFLOW_ERROR_PROSE: Record<WorkflowErrorCode, (detail: string) => string> = {
89
+ CONFIG_ERROR: (detail) => `The workflow configuration is invalid: ${detail}.`,
90
+ INVALID_SETTINGS: (detail) => `The workflow settings are invalid: ${detail}.`,
91
+ INVALID_SYNTAX: (detail) => `The workflow source is invalid: ${detail}.`,
92
+ INVALID_METADATA: (detail) => `The workflow metadata is invalid: ${detail}.`,
93
+ DUPLICATE_NAME: (detail) => `The workflow contains a duplicate name: ${detail}.`,
94
+ INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
95
+ REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
96
+ GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
97
+ MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
98
+ UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
99
+ UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
100
+ UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
101
+ RUN_OWNED: (detail) => /already owned|active ownership/.test(detail) ? "The workflow session is already in use." : `The workflow session is already in use: ${detail}.`,
102
+ RUN_NOT_FOUND: (detail) => /^Unknown workflow run\b/.test(detail) ? "The workflow run was not found." : `The workflow run was not found: ${detail}.`,
103
+ RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
104
+ SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
105
+ AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
106
+ AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
107
+ AGENT_RESULT_COLLECTED: (detail) => `The nested agent result was already collected: ${detail}.`,
108
+ RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
109
+ CANCELLED: (detail) => `The workflow was cancelled: ${detail}.`,
110
+ WORKER_UNRESPONSIVE: (detail) => `The workflow worker stopped responding: ${detail}.`,
111
+ WORKTREE_FAILED: (detail) => `The workflow worktree operation failed: ${detail}.`,
112
+ RESUME_INCOMPATIBLE: (detail) => `The workflow cannot resume this run: ${detail}.`,
113
+ BUDGET_EXHAUSTED: (detail) => `The workflow budget was exhausted: ${detail}.`,
114
+ INTERNAL_ERROR: (detail) => `The workflow encountered an internal error: ${detail}.`,
115
+ };
116
+ export function formatWorkflowFailure(error: unknown): string {
117
+ if (isWorkflowAuthored(error)) return errorText(error);
118
+ const code = errorCode(error);
119
+ if (code) return WORKFLOW_ERROR_PROSE[code](workflowDetail(errorText(error)));
120
+ if (error instanceof Error) return error.message || "The workflow failed without an error message.";
121
+ return `The workflow failed with value ${String(error)}.`;
122
+ }
123
+ export function workflowFailedAt(error: unknown): string | undefined { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
124
+ export const DELIVERY_LIMIT_BYTES = 4 * 1024;
125
+ export const WORKFLOW_LOG_ENTRY = "workflow-log";
126
+ export interface WorkflowLogEntry { workflowName: string; message: string }
127
+
128
+ export interface CompletionContextUsage { tokens: number | null; contextWindow: number }
129
+ export interface CompletionDeliveryModel { contextWindow?: number; maxTokens?: number }
130
+ export interface CompletionDeliveryContext {
131
+ getContextUsage?: () => CompletionContextUsage | undefined;
132
+ getModel?: () => CompletionDeliveryModel | undefined;
133
+ model?: CompletionDeliveryModel;
134
+ }
135
+ export interface CompletionDeliveryOptions {
136
+ mode: "foreground" | "background";
137
+ name: string;
138
+ runId: string;
139
+ value: JsonValue;
140
+ resultPath: string;
141
+ resultBytes: number;
142
+ worktrees: readonly { branch: string; path: string }[];
143
+ context?: CompletionDeliveryContext;
144
+ }
145
+ export interface CompletionDeliveryStoreOptions extends Omit<CompletionDeliveryOptions, "worktrees"> {
146
+ store: Pick<RunStore, "changedWorktrees">;
147
+ }
148
+ export interface CompletionDeliveryResult { content: string; inlined: boolean }
149
+
150
+ function positiveFinite(value: number | undefined): value is number { return value !== undefined && Number.isFinite(value) && value > 0; }
151
+ export function completionDescriptor(options: Pick<CompletionDeliveryOptions, "runId" | "resultBytes"> & { resultPath?: string }): string {
152
+ return JSON.stringify({ state: "completed", runId: options.runId, ...(options.resultPath === undefined ? {} : { resultPath: options.resultPath }), resultBytes: options.resultBytes, inlined: false });
153
+ }
154
+ function deliveryEnvelope(mode: CompletionDeliveryOptions["mode"], content: string, runId: string): string {
155
+ const timestamp = Date.now();
156
+ if (mode === "foreground") return JSON.stringify({ role: "toolResult", toolCallId: runId, toolName: "workflow", content: [{ type: "text", text: content }, { type: "text", text: `Workflow run ID: ${runId}` }], isError: false, timestamp });
157
+ return JSON.stringify({ role: "custom", customType: "workflow", content, display: true, timestamp });
158
+ }
159
+ function fitsMainContext(options: CompletionDeliveryOptions, content: string): boolean {
160
+ let usage: CompletionContextUsage | undefined;
161
+ let model: CompletionDeliveryModel | undefined;
162
+ try {
163
+ usage = options.context?.getContextUsage?.();
164
+ model = options.context?.getModel?.() ?? options.context?.model;
165
+ } catch { return false; }
166
+ if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens) || usage.tokens < 0 || !positiveFinite(model?.maxTokens)) return false;
167
+ const contextWindow = positiveFinite(model.contextWindow) ? Math.min(model.contextWindow, usage.contextWindow) : usage.contextWindow;
168
+ if (!positiveFinite(contextWindow)) return false;
169
+ const deliveryTokens = Math.ceil(Buffer.byteLength(deliveryEnvelope(options.mode, content, options.runId), "utf8") / 4);
170
+ return usage.tokens + deliveryTokens + model.maxTokens <= contextWindow;
171
+ }
172
+
173
+ export function completionDelivery(options: CompletionDeliveryOptions): CompletionDeliveryResult {
174
+ const descriptor = completionDescriptor(options);
175
+ if (!Number.isSafeInteger(options.resultBytes) || options.resultBytes < 0 || options.resultBytes > DEFAULT_MAX_BYTES) return { content: descriptor, inlined: false };
176
+ const serialized = JSON.stringify(options.value);
177
+ if (typeof serialized !== "string") return { content: descriptor, inlined: false };
178
+ const locations = options.worktrees.length ? ` Changes: ${options.worktrees.map(({ branch, path }) => `${branch} (${path})`).join(", ")}.` : "";
179
+ const inlineContent = options.mode === "foreground" ? serialized : `Workflow ${options.name} completed: ${serialized}${locations}`;
180
+ if (Buffer.byteLength(inlineContent, "utf8") > DEFAULT_MAX_BYTES) return { content: descriptor, inlined: false };
181
+ return fitsMainContext(options, inlineContent) ? { content: inlineContent, inlined: true } : { content: descriptor, inlined: false };
182
+ }
183
+
184
+ export async function completionDeliveryFromStore(options: CompletionDeliveryStoreOptions): Promise<CompletionDeliveryResult> {
185
+ const { store, ...deliveryOptions } = options;
186
+ try {
187
+ return completionDelivery({ ...deliveryOptions, worktrees: await store.changedWorktrees() });
188
+ } catch {
189
+ // Worktree metadata must not block successful result delivery.
190
+ return { content: completionDescriptor(deliveryOptions), inlined: false };
191
+ }
192
+ }
193
+
194
+ export function utf8Prefix(value: string, maxBytes: number): string {
195
+ const bytes = Buffer.from(value);
196
+ let end = Math.min(bytes.length, maxBytes);
197
+ while (end < bytes.length && end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
198
+ return bytes.subarray(0, end).toString("utf8");
199
+ }
200
+ const DIAGNOSTIC_LIMIT_BYTES = DELIVERY_LIMIT_BYTES - 512;
201
+ export function failureDiagnosticsFrom(error: unknown): WorkflowFailureDiagnostics | undefined {
202
+ return error instanceof WorkflowError ? workflowFailureDiagnostics.get(error) : undefined;
203
+ }
204
+
205
+ function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): WorkflowFailureDiagnostics {
206
+ let bounded: WorkflowFailureDiagnostics = {
207
+ runId: utf8Prefix(value.runId, 128),
208
+ workflowName: utf8Prefix(value.workflowName, 256),
209
+ state: value.state,
210
+ failedAt: value.failedAt === null ? null : utf8Prefix(value.failedAt, 1024),
211
+ error: { code: value.error.code, message: utf8Prefix(value.error.message, 1024) },
212
+ ...(value.failedAgent ? { failedAgent: {
213
+ id: utf8Prefix(value.failedAgent.id, 128),
214
+ ...(value.failedAgent.label ? { label: utf8Prefix(value.failedAgent.label, 128) } : {}),
215
+ ...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
216
+ structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
217
+ attempt: value.failedAgent.attempt,
218
+ ...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
219
+ ...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
220
+ } } : {}),
221
+ completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
222
+ id: utf8Prefix(agent.id, 128),
223
+ ...(agent.label ? { label: utf8Prefix(agent.label, 128) } : {}),
224
+ ...(agent.role ? { role: utf8Prefix(agent.role, 128) } : {}),
225
+ structuralPath: agent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
226
+ })),
227
+ completedSiblingPaths: value.completedSiblingPaths.slice(0, 16).map((path) => path.slice(0, 8).map((part) => utf8Prefix(part, 128))),
228
+ ...(value.retry ? { retry: { sourceRunId: utf8Prefix(value.retry.sourceRunId, 128), action: utf8Prefix(value.retry.action, 256), completedPaths: value.retry.completedPaths.slice(0, 16).map((path) => utf8Prefix(path, 256)), incompletePaths: value.retry.incompletePaths.slice(0, 16).map((path) => utf8Prefix(path, 256)), namedWorktrees: value.retry.namedWorktrees.slice(0, 16).map((name) => utf8Prefix(name, 128)), warning: utf8Prefix(value.retry.warning, 512) } } : {}),
229
+ artifacts: { runDirectory: utf8Prefix(value.artifacts.runDirectory, 1024), statePath: utf8Prefix(value.artifacts.statePath, 1024), journalPath: utf8Prefix(value.artifacts.journalPath, 1024) },
230
+ };
231
+ const size = () => Buffer.byteLength(JSON.stringify(bounded));
232
+ while (size() > DIAGNOSTIC_LIMIT_BYTES) {
233
+ if (bounded.completedSiblingAgents?.length || bounded.completedSiblingPaths.length) {
234
+ bounded = { ...bounded, completedSiblingAgents: bounded.completedSiblingAgents?.slice(0, -1) ?? [], completedSiblingPaths: bounded.completedSiblingPaths.slice(0, -1) };
235
+ continue;
236
+ }
237
+ if (bounded.retry && (bounded.retry.completedPaths.length || bounded.retry.incompletePaths.length || bounded.retry.namedWorktrees.length)) {
238
+ const retry = { ...bounded.retry };
239
+ if (retry.completedPaths.length) retry.completedPaths = retry.completedPaths.slice(0, -1);
240
+ else if (retry.incompletePaths.length) retry.incompletePaths = retry.incompletePaths.slice(0, -1);
241
+ else retry.namedWorktrees = retry.namedWorktrees.slice(0, -1);
242
+ bounded = { ...bounded, retry };
243
+ continue;
244
+ }
245
+ if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } }; continue; }
246
+ if (Buffer.byteLength(bounded.error.message) > 256) { bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } }; continue; }
247
+ if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) { bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) }; continue; }
248
+ if (bounded.failedAgent && bounded.failedAgent.structuralPath.length > 4) { bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.slice(0, 4) } }; continue; }
249
+ if (bounded.failedAgent?.structuralPath.some((part) => Buffer.byteLength(part) > 64)) { bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.map((part) => utf8Prefix(part, 64)) } }; continue; }
250
+ if (Buffer.byteLength(bounded.artifacts.statePath) > 512 || Buffer.byteLength(bounded.artifacts.journalPath) > 512) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, statePath: utf8Prefix(bounded.artifacts.statePath, 512), journalPath: utf8Prefix(bounded.artifacts.journalPath, 512) } }; continue; }
251
+ if (Buffer.byteLength(bounded.workflowName) > 128) { bounded = { ...bounded, workflowName: utf8Prefix(bounded.workflowName, 128) }; continue; }
252
+ break;
253
+ }
254
+ return bounded;
255
+ }
256
+ async function diagnosticNamedWorktrees(store: RunStore, run: PersistedRun): Promise<readonly string[]> {
257
+ const names = new Set<string>();
258
+ try {
259
+ for (const name of await store.validNamedWorktrees()) names.add(name);
260
+ } catch { /* Do not block failure delivery on an invalid worktree record. */ }
261
+ for (const name of run.retry?.namedWorktrees ?? []) {
262
+ try { await store.resolveNamedWorktree(name); names.add(name); } catch { /* Do not advertise stale inherited worktrees. */ }
263
+ }
264
+ return [...names];
265
+ }
266
+ export function incompleteRetryPaths(paths: readonly string[], completedPaths: readonly string[]): string[] {
267
+ return [...new Set(paths)].filter((path) => !completedPaths.some((completedPath) => completedPath === path || completedPath.startsWith(`${path}/`)));
268
+ }
269
+ export async function createWorkflowFailureDiagnostics(store: RunStore, metadata: WorkflowMetadata, error: unknown, run: PersistedRun): Promise<WorkflowFailureDiagnostics> {
270
+ const failedAt = workflowFailedAt(error) ?? null;
271
+ const failedAgents = run.agents.filter((agent) => agent.state === "failed");
272
+ const failedAgentRecord = failedAgents.find((agent) => {
273
+ if (failedAt === null) return false;
274
+ try { return failedAt.includes(`${operationPath("agent", ...(agent.structuralPath ?? []))}/`); } catch { return false; }
275
+ }) ?? failedAgents.at(-1);
276
+ const failedAttempt = failedAgentRecord ? [...(failedAgentRecord.attemptDetails ?? [])].reverse().find((attempt) => attempt.error) ?? failedAgentRecord.attemptDetails?.at(-1) : undefined;
277
+ const failedAgent = failedAgentRecord ? {
278
+ id: failedAgentRecord.id,
279
+ ...(failedAgentRecord.label ?? failedAgentRecord.name ? { label: failedAgentRecord.label ?? failedAgentRecord.name } : {}),
280
+ ...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
281
+ structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
282
+ attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
283
+ ...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
284
+ ...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
285
+ } satisfies WorkflowFailureAgent : undefined;
286
+ const completedSiblingAgents = run.agents.filter((agent) => {
287
+ if (agent.state !== "completed" || agent.id === failedAgentRecord?.id) return false;
288
+ return failedAgentRecord?.parentId === undefined ? agent.parentId === undefined : agent.parentId === failedAgentRecord.parentId;
289
+ }).map((agent) => ({
290
+ id: agent.id,
291
+ ...(agent.label ?? agent.name ? { label: agent.label ?? agent.name } : {}),
292
+ ...(agent.role ? { role: agent.role } : {}),
293
+ structuralPath: [...(agent.structuralPath ?? [])],
294
+ } satisfies WorkflowSiblingAgent));
295
+ const completedSiblingPaths = completedSiblingAgents.map((agent) => [...agent.structuralPath]);
296
+ let journalCompletedPaths: readonly string[] = [];
297
+ try { journalCompletedPaths = (await store.replayableOperations()).map(({ path }) => path); } catch { /* Preserve failure diagnostics when retry history is unavailable. */ }
298
+ const completedPaths = run.retry ? [...new Set([...run.retry.completedPaths, ...journalCompletedPaths])] : journalCompletedPaths.length ? journalCompletedPaths : run.agents.filter((agent) => agent.state === "completed").map((agent) => operationPath("agent", ...(agent.structuralPath ?? [])));
299
+ const namedWorktrees = await diagnosticNamedWorktrees(store, run);
300
+ const retry = run.state === "failed" ? {
301
+ sourceRunId: run.id,
302
+ action: `workflow_retry({ runId: ${JSON.stringify(run.id)} })`,
303
+ completedPaths,
304
+ incompletePaths: incompleteRetryPaths([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])], completedPaths),
305
+ namedWorktrees,
306
+ warning: "Retry re-executes incomplete operations; external side effects before failure are not guaranteed exactly once.",
307
+ } : undefined;
308
+ return boundedWorkflowFailureDiagnostics({
309
+ runId: run.id, workflowName: metadata.name, state: run.state, failedAt,
310
+ error: { code: errorCode(error) ?? "INTERNAL_ERROR", message: errorText(error) || "The workflow failed without an error message." },
311
+ ...(failedAgent ? { failedAgent } : {}), completedSiblingAgents, completedSiblingPaths,
312
+ ...(retry ? { retry } : {}),
313
+ artifacts: { runDirectory: store.directory, statePath: join(store.directory, "state.json"), journalPath: join(store.directory, "journal.json") },
314
+ });
315
+ }
316
+
317
+ export function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string {
318
+ const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.session ? ` session=${diagnostic.failedAgent.session.transport}/${diagnostic.failedAgent.session.sessionId}` : ""}` : "(not persisted)";
319
+ const siblingAgents = diagnostic.completedSiblingAgents;
320
+ const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
321
+ const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
322
+ return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
323
+ }
324
+ function deliveryPart(value: string, maxBytes: number): string { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
325
+ export function formatWorkflowFailureDelivery(diagnostic: WorkflowFailureDiagnostics): string {
326
+ const name = deliveryPart(diagnostic.workflowName, 128);
327
+ const runId = deliveryPart(diagnostic.runId, 128);
328
+ const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
329
+ const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
330
+ const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
331
+ const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
332
+ const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
333
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
334
+ }
335
+ export function formatWorkflowFailureDeliveryFallback(workflowName: string, runId: string, runDirectory: string, error: unknown, retryable = true): string {
336
+ const code = errorCode(error) ?? "INTERNAL_ERROR";
337
+ const failedPath = workflowFailedAt(error);
338
+ const nextAction = retryable && code !== "BUDGET_EXHAUSTED" && code !== "CANCELLED" ? `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })` : "";
339
+ const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
340
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
341
+ }
342
+
343
+ export function serializeWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string { return JSON.stringify(diagnostic); }
344
+ export function isWorkflowFailureDiagnostics(value: unknown): value is WorkflowFailureDiagnostics {
345
+ return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
346
+ }