@relayflows/sdk 2.0.21 → 2.0.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.
Files changed (247) hide show
  1. package/dist/agent-artifacts.d.ts +18 -9
  2. package/dist/agent-artifacts.d.ts.map +1 -1
  3. package/dist/agent-artifacts.js +27 -11
  4. package/dist/agent-artifacts.js.map +1 -1
  5. package/dist/agent-transcript.d.ts +186 -0
  6. package/dist/agent-transcript.d.ts.map +1 -0
  7. package/dist/agent-transcript.js +585 -0
  8. package/dist/agent-transcript.js.map +1 -0
  9. package/dist/authored-flow-error.d.ts +33 -1
  10. package/dist/authored-flow-error.d.ts.map +1 -1
  11. package/dist/authored-flow-error.js +20 -1
  12. package/dist/authored-flow-error.js.map +1 -1
  13. package/dist/authored-flow-executor.d.ts.map +1 -1
  14. package/dist/authored-flow-executor.js +4 -1
  15. package/dist/authored-flow-executor.js.map +1 -1
  16. package/dist/authored-node-entry.js +5 -1
  17. package/dist/authored-node-entry.js.map +1 -1
  18. package/dist/authored-node-runner.d.ts +11 -0
  19. package/dist/authored-node-runner.d.ts.map +1 -1
  20. package/dist/authored-node-runner.js +30 -1
  21. package/dist/authored-node-runner.js.map +1 -1
  22. package/dist/authored-root.js +3 -1
  23. package/dist/authored-root.js.map +1 -1
  24. package/dist/authored-source-authority.js +13 -0
  25. package/dist/authored-source-authority.js.map +1 -1
  26. package/dist/authored-step-index.d.ts +82 -0
  27. package/dist/authored-step-index.d.ts.map +1 -0
  28. package/dist/authored-step-index.js +120 -0
  29. package/dist/authored-step-index.js.map +1 -0
  30. package/dist/authored-step-output.d.ts +23 -2
  31. package/dist/authored-step-output.d.ts.map +1 -1
  32. package/dist/authored-step-output.js +47 -5
  33. package/dist/authored-step-output.js.map +1 -1
  34. package/dist/authored-worker-step.d.ts +10 -2
  35. package/dist/authored-worker-step.d.ts.map +1 -1
  36. package/dist/authored-worker-step.js +81 -7
  37. package/dist/authored-worker-step.js.map +1 -1
  38. package/dist/cli/check.d.ts.map +1 -1
  39. package/dist/cli/check.js +67 -12
  40. package/dist/cli/check.js.map +1 -1
  41. package/dist/cli/cloud-read.d.ts +31 -0
  42. package/dist/cli/cloud-read.d.ts.map +1 -0
  43. package/dist/cli/cloud-read.js +482 -0
  44. package/dist/cli/cloud-read.js.map +1 -0
  45. package/dist/cli/run.d.ts +2 -0
  46. package/dist/cli/run.d.ts.map +1 -1
  47. package/dist/cli/run.js +53 -24
  48. package/dist/cli/run.js.map +1 -1
  49. package/dist/cli/status.d.ts +43 -0
  50. package/dist/cli/status.d.ts.map +1 -0
  51. package/dist/cli/status.js +321 -0
  52. package/dist/cli/status.js.map +1 -0
  53. package/dist/cli/step-failure.d.ts +26 -0
  54. package/dist/cli/step-failure.d.ts.map +1 -1
  55. package/dist/cli/step-failure.js +99 -10
  56. package/dist/cli/step-failure.js.map +1 -1
  57. package/dist/cli-commands.d.ts +41 -0
  58. package/dist/cli-commands.d.ts.map +1 -1
  59. package/dist/cli-commands.js +33 -0
  60. package/dist/cli-commands.js.map +1 -1
  61. package/dist/cli.d.ts +4 -2
  62. package/dist/cli.d.ts.map +1 -1
  63. package/dist/cli.js +26 -0
  64. package/dist/cli.js.map +1 -1
  65. package/dist/cloud-http.d.ts +12 -0
  66. package/dist/cloud-http.d.ts.map +1 -1
  67. package/dist/cloud-http.js +14 -6
  68. package/dist/cloud-http.js.map +1 -1
  69. package/dist/cloud-read.d.ts +154 -0
  70. package/dist/cloud-read.d.ts.map +1 -0
  71. package/dist/cloud-read.js +373 -0
  72. package/dist/cloud-read.js.map +1 -0
  73. package/dist/cloud-transcript.d.ts +94 -0
  74. package/dist/cloud-transcript.d.ts.map +1 -0
  75. package/dist/cloud-transcript.js +342 -0
  76. package/dist/cloud-transcript.js.map +1 -0
  77. package/dist/communication/environment.d.ts +3 -0
  78. package/dist/communication/environment.d.ts.map +1 -0
  79. package/dist/communication/environment.js +34 -0
  80. package/dist/communication/environment.js.map +1 -0
  81. package/dist/communication/history.d.ts +6 -0
  82. package/dist/communication/history.d.ts.map +1 -0
  83. package/dist/communication/history.js +38 -0
  84. package/dist/communication/history.js.map +1 -0
  85. package/dist/communication/local.d.ts +6 -0
  86. package/dist/communication/local.d.ts.map +1 -0
  87. package/dist/communication/local.js +56 -0
  88. package/dist/communication/local.js.map +1 -0
  89. package/dist/communication/preflight.d.ts +6 -0
  90. package/dist/communication/preflight.d.ts.map +1 -0
  91. package/dist/communication/preflight.js +37 -0
  92. package/dist/communication/preflight.js.map +1 -0
  93. package/dist/communication/projection.d.ts +8 -0
  94. package/dist/communication/projection.d.ts.map +1 -0
  95. package/dist/communication/projection.js +23 -0
  96. package/dist/communication/projection.js.map +1 -0
  97. package/dist/communication/relay.d.ts +78 -0
  98. package/dist/communication/relay.d.ts.map +1 -0
  99. package/dist/communication/relay.js +89 -0
  100. package/dist/communication/relay.js.map +1 -0
  101. package/dist/communication/session.d.ts +24 -0
  102. package/dist/communication/session.d.ts.map +1 -0
  103. package/dist/communication/session.js +100 -0
  104. package/dist/communication/session.js.map +1 -0
  105. package/dist/communication/spec.d.ts +20 -0
  106. package/dist/communication/spec.d.ts.map +1 -0
  107. package/dist/communication/spec.js +113 -0
  108. package/dist/communication/spec.js.map +1 -0
  109. package/dist/communication/tool.d.ts +3 -0
  110. package/dist/communication/tool.d.ts.map +1 -0
  111. package/dist/communication/tool.js +23 -0
  112. package/dist/communication/tool.js.map +1 -0
  113. package/dist/communication/tools.d.ts +11 -0
  114. package/dist/communication/tools.d.ts.map +1 -0
  115. package/dist/communication/tools.js +65 -0
  116. package/dist/communication/tools.js.map +1 -0
  117. package/dist/communication/worker.d.ts +6 -0
  118. package/dist/communication/worker.d.ts.map +1 -0
  119. package/dist/communication/worker.js +120 -0
  120. package/dist/communication/worker.js.map +1 -0
  121. package/dist/compile.d.ts.map +1 -1
  122. package/dist/compile.js +8 -1
  123. package/dist/compile.js.map +1 -1
  124. package/dist/failure-kinds.d.ts +30 -2
  125. package/dist/failure-kinds.d.ts.map +1 -1
  126. package/dist/failure-kinds.js +9 -0
  127. package/dist/failure-kinds.js.map +1 -1
  128. package/dist/index.d.ts +3 -1
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +2 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/journal-client.d.ts +4 -1
  133. package/dist/journal-client.d.ts.map +1 -1
  134. package/dist/journal-client.js +11 -2
  135. package/dist/journal-client.js.map +1 -1
  136. package/dist/journal-reader.d.ts +1 -1
  137. package/dist/journal-reader.d.ts.map +1 -1
  138. package/dist/journal-reader.js +3 -1
  139. package/dist/journal-reader.js.map +1 -1
  140. package/dist/llm-worker.d.ts.map +1 -1
  141. package/dist/llm-worker.js +12 -1
  142. package/dist/llm-worker.js.map +1 -1
  143. package/dist/permissions-preflight.d.ts +22 -0
  144. package/dist/permissions-preflight.d.ts.map +1 -0
  145. package/dist/permissions-preflight.js +49 -0
  146. package/dist/permissions-preflight.js.map +1 -0
  147. package/dist/preflight.d.ts +13 -2
  148. package/dist/preflight.d.ts.map +1 -1
  149. package/dist/preflight.js +29 -6
  150. package/dist/preflight.js.map +1 -1
  151. package/dist/protocol.d.ts +39 -1
  152. package/dist/protocol.d.ts.map +1 -1
  153. package/dist/pty-sidechannel.d.ts +7 -0
  154. package/dist/pty-sidechannel.d.ts.map +1 -1
  155. package/dist/pty-sidechannel.js.map +1 -1
  156. package/dist/redact.d.ts +16 -0
  157. package/dist/redact.d.ts.map +1 -0
  158. package/dist/redact.js +104 -0
  159. package/dist/redact.js.map +1 -0
  160. package/dist/run-state.d.ts +124 -0
  161. package/dist/run-state.d.ts.map +1 -0
  162. package/dist/run-state.js +368 -0
  163. package/dist/run-state.js.map +1 -0
  164. package/dist/spec.d.ts +24 -1
  165. package/dist/spec.d.ts.map +1 -1
  166. package/dist/spec.js.map +1 -1
  167. package/dist/step-env.d.ts +25 -0
  168. package/dist/step-env.d.ts.map +1 -0
  169. package/dist/step-env.js +34 -0
  170. package/dist/step-env.js.map +1 -0
  171. package/dist/transcript-tail.d.ts +54 -0
  172. package/dist/transcript-tail.d.ts.map +1 -0
  173. package/dist/transcript-tail.js +176 -0
  174. package/dist/transcript-tail.js.map +1 -0
  175. package/dist/worker-cli.d.ts +10 -0
  176. package/dist/worker-cli.d.ts.map +1 -1
  177. package/dist/worker-cli.js +138 -17
  178. package/dist/worker-cli.js.map +1 -1
  179. package/dist/worker-usage.d.ts +7 -2
  180. package/dist/worker-usage.d.ts.map +1 -1
  181. package/dist/worker-usage.js +21 -3
  182. package/dist/worker-usage.js.map +1 -1
  183. package/dist/worker.d.ts +1 -0
  184. package/dist/worker.d.ts.map +1 -1
  185. package/dist/worker.js +41 -7
  186. package/dist/worker.js.map +1 -1
  187. package/dist/wrapper-runtime.d.ts +7 -1
  188. package/dist/wrapper-runtime.d.ts.map +1 -1
  189. package/dist/wrapper-runtime.js +7 -1
  190. package/dist/wrapper-runtime.js.map +1 -1
  191. package/dist/wrapper-session.d.ts +8 -0
  192. package/dist/wrapper-session.d.ts.map +1 -1
  193. package/dist/wrapper-session.js +156 -23
  194. package/dist/wrapper-session.js.map +1 -1
  195. package/package.json +14 -2
  196. package/src/agent-artifacts.ts +27 -11
  197. package/src/agent-transcript.ts +651 -0
  198. package/src/authored-flow-error.ts +17 -0
  199. package/src/authored-flow-executor.ts +4 -1
  200. package/src/authored-node-entry.ts +5 -1
  201. package/src/authored-node-runner.ts +33 -1
  202. package/src/authored-root.ts +2 -1
  203. package/src/authored-source-authority.ts +13 -0
  204. package/src/authored-step-index.ts +157 -0
  205. package/src/authored-step-output.ts +66 -14
  206. package/src/authored-worker-step.ts +88 -15
  207. package/src/cli/check.ts +69 -11
  208. package/src/cli/cloud-read.ts +518 -0
  209. package/src/cli/run.ts +43 -24
  210. package/src/cli/status.ts +349 -0
  211. package/src/cli/step-failure.ts +100 -10
  212. package/src/cli-commands.ts +33 -0
  213. package/src/cli.ts +27 -0
  214. package/src/cloud-http.ts +32 -6
  215. package/src/cloud-read.ts +515 -0
  216. package/src/cloud-transcript.ts +422 -0
  217. package/src/communication/environment.ts +33 -0
  218. package/src/communication/history.ts +38 -0
  219. package/src/communication/local.ts +41 -0
  220. package/src/communication/preflight.ts +34 -0
  221. package/src/communication/projection.ts +23 -0
  222. package/src/communication/relay.ts +102 -0
  223. package/src/communication/session.ts +84 -0
  224. package/src/communication/spec.ts +106 -0
  225. package/src/communication/tool.ts +22 -0
  226. package/src/communication/tools.ts +50 -0
  227. package/src/communication/worker.ts +95 -0
  228. package/src/compile.ts +4 -1
  229. package/src/failure-kinds.ts +31 -1
  230. package/src/index.ts +10 -1
  231. package/src/journal-client.ts +12 -2
  232. package/src/journal-reader.ts +4 -2
  233. package/src/llm-worker.ts +12 -1
  234. package/src/permissions-preflight.ts +53 -0
  235. package/src/preflight.ts +42 -7
  236. package/src/protocol.ts +15 -0
  237. package/src/pty-sidechannel.ts +7 -0
  238. package/src/redact.ts +106 -0
  239. package/src/run-state.ts +468 -0
  240. package/src/spec.ts +18 -1
  241. package/src/step-env.ts +47 -0
  242. package/src/transcript-tail.ts +193 -0
  243. package/src/worker-cli.ts +139 -18
  244. package/src/worker-usage.ts +26 -3
  245. package/src/worker.ts +42 -6
  246. package/src/wrapper-runtime.ts +7 -1
  247. package/src/wrapper-session.ts +159 -24
@@ -206,7 +206,10 @@ export async function executeAuthoredFlow<Input = undefined>(
206
206
  let requestedCompletion: LoweredCompletionReason | undefined;
207
207
 
208
208
  const lowerDeterministic = authoredDeterministicRunner(
209
- definition.name, journal, journalSteps, budget, options.rootRunId,
209
+ definition.name, journal, journalSteps, budget, {
210
+ ...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
211
+ ...(options.dataDir === undefined ? {} : { dataDir: options.dataDir }),
212
+ },
210
213
  );
211
214
 
212
215
  const worker = authoredWorkerRunner(
@@ -81,8 +81,12 @@ try {
81
81
  const message = error instanceof Error ? error.message : 'authored body failed';
82
82
  const prefix = error instanceof AuthoredFlowExecutionError ? `${error.code}: ` : '';
83
83
  send({ type: 'error', message: prefix && message.startsWith(prefix) ? message.slice(prefix.length) : message,
84
+ // `details` is the failing child step's journal evidence. Without it the
85
+ // parent can only re-render the message, and the machine-readable
86
+ // diagnostic loses the command's exit code and output tails.
84
87
  ...(error instanceof AuthoredFlowExecutionError ? { code: error.code,
85
- completionReason: error.completionReason, runId: error.runId } : {}),
88
+ completionReason: error.completionReason, runId: error.runId,
89
+ details: error.details } : {}),
86
90
  ...(error instanceof AuthoredHumanParked ? { wait: error.wait } : {}) });
87
91
  process.exitCode = 1;
88
92
  } finally {
@@ -13,6 +13,7 @@ import {
13
13
  AuthoredFlowExecutionError, AuthoredHumanParked,
14
14
  type AuthoredFlowExecutionErrorCode, type AuthoredHumanWait,
15
15
  } from './authored-flow-error.js';
16
+ import type { StepFailedDetails } from './failure-kinds.js';
16
17
  import { HUMAN_WAIT_ID } from './authored-human.js';
17
18
  import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
18
19
 
@@ -139,7 +140,8 @@ export async function runAuthoredInNode(
139
140
  ? new AuthoredHumanParked(message.wait, rootRunId)
140
141
  : typeof message.code === 'string'
141
142
  ? new AuthoredFlowExecutionError(message.code as AuthoredFlowExecutionErrorCode,
142
- message.message, message.completionReason, message.runId)
143
+ message.message, message.completionReason, message.runId,
144
+ stepFailedFrame(message.details))
143
145
  : new Error(message.message);
144
146
  } else throw new Error('unknown authored runtime message');
145
147
  } catch (error) { stop(error instanceof Error ? error : new Error('invalid authored runtime message')); }
@@ -171,6 +173,36 @@ function isHumanWaitFrame(value: unknown): value is AuthoredHumanWait {
171
173
  && typeof wait.to === 'string' && wait.to !== '';
172
174
  }
173
175
 
176
+ /**
177
+ * Step evidence arriving over IPC, reduced to the fields `StepFailedDetails`
178
+ * declares and the types it declares them as. The child is the same pinned
179
+ * payload the parent hashed, but the frame is still a claim: an unrecognised
180
+ * or mistyped field is dropped rather than trusted, and a malformed `details`
181
+ * yields no details at all. It throws on nothing `JSON.parse` can produce —
182
+ * the run is already failing, and losing the original failure to report a bad
183
+ * evidence frame would replace the answer with a worse one.
184
+ */
185
+ export function stepFailedFrame(value: unknown): StepFailedDetails | undefined {
186
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
187
+ const frame = value as Record<string, unknown>;
188
+ const text = (key: string): string | undefined =>
189
+ typeof frame[key] === 'string' ? (frame[key] as string).slice(0, 8192) : undefined;
190
+ const count = (key: string): number | undefined =>
191
+ typeof frame[key] === 'number' && Number.isSafeInteger(frame[key]) ? frame[key] as number : undefined;
192
+ const details: StepFailedDetails = {};
193
+ for (const [key, parsed] of [
194
+ ['stepId', text('stepId')], ['stepType', text('stepType')],
195
+ ['completionReason', text('completionReason')], ['attempt', count('attempt')],
196
+ ['maxIterations', count('maxIterations')], ['exitCode', count('exitCode')],
197
+ ['stdoutTail', text('stdoutTail')], ['stderrTail', text('stderrTail')],
198
+ ['detail', text('detail')], ['transcriptPath', text('transcriptPath')],
199
+ ['hint', text('hint')], ['journalPath', text('journalPath')],
200
+ ] as const) {
201
+ if (parsed !== undefined) (details as Record<string, unknown>)[key] = parsed;
202
+ }
203
+ return Object.keys(details).length === 0 ? undefined : details;
204
+ }
205
+
174
206
  /** The IPC frame is a claim, not a durable terminal fact or a sandbox boundary. */
175
207
  export async function verifyAuthoredNodeResult(
176
208
  result: AuthoredFlowExecutionResult, metadata: AuthoredRootMetadata,
@@ -15,7 +15,7 @@ import type { RunOutcome, StepDispatchEvent } from './protocol.js';
15
15
  import { SPEC_SCHEMA_VERSION } from './spec.js';
16
16
  import type { RunLifecycleOptions } from './cli/run.js';
17
17
  import { withWorkerLease } from './worker-lease.js';
18
- import { AuthoredHumanParked } from './authored-flow-error.js';
18
+ import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
19
19
  import { readOpenHumanWaits } from './authored-human.js';
20
20
  import { isSurfaceCompletionReason } from './authored-step-output.js';
21
21
 
@@ -234,6 +234,7 @@ async function driveRoot(
234
234
  throw error;
235
235
  }
236
236
  await terminalizeRootFailure(peer, dispatch, error);
237
+ if (error instanceof AuthoredFlowExecutionError) error.rootRunId = dispatch.run_id;
237
238
  throw error;
238
239
  }
239
240
  }
@@ -46,7 +46,20 @@ function installPinnedSourceLoader(sources: Map<string, string>): void {
46
46
  pinnedSourceLoaderInstalled = true;
47
47
  // Node >=22.14 supports register and stripTypeScriptTypes. A loader thread
48
48
  // receives the captured bytes, never reopening mutable authored paths.
49
+ // The `stripTypeScriptTypes` call below makes Node emit
50
+ // `ExperimentalWarning: stripTypeScriptTypes …` on the child's inherited
51
+ // stderr, where a bootstrap that reads the CLI's stderr takes it for the
52
+ // error text. It is emitted on this loader thread, so a listener on the
53
+ // main thread cannot intercept it, and `--disable-warning` on the child
54
+ // would silence every ExperimentalWarning the authored body raises too.
55
+ // Dropping it by its own text, on this thread only, is the narrow form:
56
+ // Node's own printer still handles every other warning.
49
57
  const loader = `
58
+ const emitWarning = process.emitWarning.bind(process);
59
+ process.emitWarning = (warning, ...rest) => {
60
+ if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return;
61
+ emitWarning(warning, ...rest);
62
+ };
50
63
  import {stripTypeScriptTypes} from 'node:module';
51
64
  let sources;
52
65
  export function initialize(data){sources=new Map(data);}
@@ -0,0 +1,157 @@
1
+ import type { JournalClient } from './journal-client.js';
2
+
3
+ /**
4
+ * The root run's durable index of the child runs an authored body opened.
5
+ *
6
+ * An authored flow is not one kernel run: every `f.run`, `f.agent`, `f.llm`
7
+ * and lowered gate is its own single-step child run with its own
8
+ * `<dataDir>/runs/<runId>.sqlite3`. The only thing that ties a child back to
9
+ * the root id a caller (or Cloud) knows is an index — and until now that index
10
+ * existed only in the root's SUCCESS completion output
11
+ * (`authored-root.ts` `driveRoot`). A failed body took
12
+ * `terminalizeRootFailure`, which journals `{ error }` and nothing else, so a
13
+ * failed run named no child at all. A failed run is exactly when its children
14
+ * are worth reading.
15
+ *
16
+ * The completion output could not be made to carry it either: the root is
17
+ * lowered as `type: 'agent'`, and the kernel nulls a failed agent's `output`
18
+ * (`preserve_failure_output`, relayflowd-core/src/machine.rs), preserving only
19
+ * a 2,000-character render in `verification.detail`. So the index is written
20
+ * where a failure cannot erase it — appended to a stream on the ROOT run as
21
+ * each child is opened, using the same `journal.streamAppend` durability the
22
+ * executor already relies on for predicate verdicts.
23
+ *
24
+ * Each record is bounded on its own rather than the index being truncated as
25
+ * one blob: a hundredth child must not push the first one out of the record.
26
+ */
27
+ export const AUTHORED_STEP_STREAM = 'authored-steps';
28
+
29
+ /** Versioned so a reader can tell this record from a future one. */
30
+ const RECORD_KIND = 'relayflows.authored-step.v1';
31
+
32
+ /**
33
+ * Per-field cap. Step ids and run ids are short by construction; the cap
34
+ * exists so a hostile or buggy id cannot make one record unbounded.
35
+ */
36
+ const FIELD_CHARS = 256;
37
+
38
+ /**
39
+ * `admitted` and `completed` are kept distinct on purpose. `admitted` says
40
+ * only "this child run exists and here is where to read it" — it never
41
+ * carries a `completionReason`, because manufacturing one for an attempt
42
+ * that has not completed would turn an unfinished child into a false fact.
43
+ * `completed` is written only from a terminal `step.completed` the journal
44
+ * actually holds.
45
+ */
46
+ export interface AuthoredStepRecord {
47
+ readonly index: typeof RECORD_KIND;
48
+ readonly step: string;
49
+ readonly runId: string;
50
+ readonly state: 'admitted' | 'completed';
51
+ /** Present only on `completed`. */
52
+ readonly completionReason?: string;
53
+ /**
54
+ * The kernel step the completion came from, when it is not the authored
55
+ * operation's own id. A child spec has two steps once a named gate is
56
+ * lowered, so the terminal failure can be `<step>.verify` rather than
57
+ * `<step>` — saying which is the difference between an index and a guess.
58
+ */
59
+ readonly kernelStep?: string;
60
+ }
61
+
62
+ /**
63
+ * Append one record, or do nothing when there is no root to append to.
64
+ *
65
+ * The undefined-root case is the non-durable executor seam (`executeAuthoredFlow`
66
+ * called without `rootRunId`), which has no root run and therefore nothing to
67
+ * index. It is not a silent fallback: there is no durable place for the record
68
+ * to go, and the caller already knows it opted out of one.
69
+ *
70
+ * Append failures propagate (AGENTS.md rule 4). Callers on a path that is
71
+ * already failing must surface the persistence error ALONGSIDE the original
72
+ * failure rather than letting it replace it — see `alsoRecord`.
73
+ */
74
+ export async function recordAuthoredChild(
75
+ journal: JournalClient,
76
+ rootRunId: string | undefined,
77
+ record: Omit<AuthoredStepRecord, 'index'>,
78
+ ): Promise<void> {
79
+ if (rootRunId === undefined) return;
80
+ await journal.streamAppend(rootRunId, AUTHORED_STEP_STREAM, {
81
+ index: RECORD_KIND,
82
+ step: bound(record.step),
83
+ runId: bound(record.runId),
84
+ state: record.state,
85
+ ...(record.completionReason === undefined ? {} : { completionReason: bound(record.completionReason) }),
86
+ ...(record.kernelStep === undefined || record.kernelStep === record.step
87
+ ? {} : { kernelStep: bound(record.kernelStep) }),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Record a child on a path that is already throwing, keeping both facts.
93
+ *
94
+ * A failed append must not be relabelled as successful collection, and it must
95
+ * not replace the failure the caller is about to report. It returns the note
96
+ * to append to that failure's message instead.
97
+ */
98
+ export async function alsoRecord(
99
+ journal: JournalClient,
100
+ rootRunId: string | undefined,
101
+ record: Omit<AuthoredStepRecord, 'index'>,
102
+ ): Promise<string> {
103
+ try {
104
+ await recordAuthoredChild(journal, rootRunId, record);
105
+ return '';
106
+ } catch (error) {
107
+ return `\nCould not persist the step index: ${error instanceof Error ? error.message : String(error)}`;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Fold the root's stream back into one record per authored step.
113
+ *
114
+ * A `completed` record supersedes the `admitted` one for the same step; first
115
+ * admission order is preserved so the index reads in the order the body ran.
116
+ * Records this version does not recognise are skipped rather than trusted.
117
+ */
118
+ export async function readAuthoredStepIndex(
119
+ journal: JournalClient,
120
+ rootRunId: string,
121
+ ): Promise<AuthoredStepRecord[]> {
122
+ const index = new Map<string, AuthoredStepRecord>();
123
+ let offset = 0;
124
+ for (;;) {
125
+ const page = await journal.streamRead(rootRunId, AUTHORED_STEP_STREAM, offset, 1000);
126
+ for (const message of page.messages) {
127
+ // `stream.read` returns either the envelope or the bare message,
128
+ // matching how the executor reads predicate verdicts.
129
+ const raw = (message as { message?: unknown }).message ?? message;
130
+ if (!isAuthoredStepRecord(raw)) continue;
131
+ const previous = index.get(raw.step);
132
+ // An `admitted` record arriving after a `completed` one (a resumed body
133
+ // re-admitting the same child under its stable admission key) must not
134
+ // un-complete it.
135
+ if (previous?.state === 'completed' && raw.state === 'admitted') continue;
136
+ index.set(raw.step, raw);
137
+ }
138
+ if (page.messages.length === 0 || page.next_offset <= offset) break;
139
+ offset = page.next_offset;
140
+ }
141
+ return [...index.values()];
142
+ }
143
+
144
+ function isAuthoredStepRecord(value: unknown): value is AuthoredStepRecord {
145
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
146
+ const record = value as Partial<AuthoredStepRecord>;
147
+ return record.index === RECORD_KIND
148
+ && typeof record.step === 'string' && record.step.length > 0
149
+ && typeof record.runId === 'string' && record.runId.length > 0
150
+ && (record.state === 'admitted' || record.state === 'completed')
151
+ && (record.completionReason === undefined || typeof record.completionReason === 'string')
152
+ && (record.kernelStep === undefined || typeof record.kernelStep === 'string');
153
+ }
154
+
155
+ function bound(value: string): string {
156
+ return value.length <= FIELD_CHARS ? value : `${value.slice(0, FIELD_CHARS)}…`;
157
+ }
@@ -3,6 +3,21 @@ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
3
  import type { JournalClient } from './journal-client.js';
4
4
  import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome } from './protocol.js';
5
5
  import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
6
+ import type { StepFailedDetails } from './failure-kinds.js';
7
+ import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './cli/step-failure.js';
8
+ import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
9
+
10
+ /**
11
+ * What an authored operation needs in order to leave readable evidence:
12
+ * the root run its child index belongs to, and the data dir whose journals
13
+ * a reader would open. Both optional — the non-durable executor seam has no
14
+ * root, and a caller that did not name a data dir still gets the `flows
15
+ * replay <run-id>` hint, just without the on-disk path.
16
+ */
17
+ export interface AuthoredStepContext {
18
+ readonly rootRunId?: string;
19
+ readonly dataDir?: string;
20
+ }
6
21
 
7
22
  /**
8
23
  * Shared by every `f.*` verb that lowers to one kernel step run in isolation:
@@ -18,12 +33,23 @@ import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
18
33
  * terminal state. Given that, a single read from the start of this run's
19
34
  * (small, single-step) journal is enough — no polling here, and no run
20
35
  * outcome ever needs re-checking.
36
+ *
37
+ * A non-success completion used to throw `journal step "run-1" completed with
38
+ * retries_exhausted` and stop there, discarding the evidence it was holding:
39
+ * the kernel PRESERVES a failed deterministic step's `output`
40
+ * (`preserve_failure_output`, relayflowd-core/src/machine.rs), so
41
+ * `{exit_code, stdout_tail, stderr_tail}` — the answer to "which command
42
+ * failed and what did it print" — was in hand and dropped. Four different
43
+ * causes produced that one identical line. It now reads the same extractor
44
+ * `f.agent`/`f.llm` already reach through `classifyOutcome`, so both step
45
+ * families report one grammar instead of two divergent ones.
21
46
  */
22
47
  export async function readCompletedStepOutput(
23
48
  journal: JournalClient,
24
49
  runId: string,
25
50
  stepId: string,
26
51
  journalSteps: AuthoredFlowJournalStep[],
52
+ context: AuthoredStepContext = {},
27
53
  ): Promise<unknown> {
28
54
  const entries = (await journal.journalRead(runId, 1)).entries;
29
55
  const completed = entries.find((entry) => isStepCompleted(entry, stepId));
@@ -34,13 +60,29 @@ export async function readCompletedStepOutput(
34
60
  const reason = completed.payload.completionReason;
35
61
  journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason }));
36
62
  if (reason !== 'success') {
37
- throw new AuthoredFlowExecutionError(
38
- 'step_failed',
39
- `journal step "${stepId}" completed with ${reason}`,
40
- reason,
41
- runId,
42
- );
63
+ let message = `journal step "${stepId}" completed with ${reason}`;
64
+ let details: StepFailedDetails | undefined;
65
+ try {
66
+ details = await stepFailureDetails(journal, runId);
67
+ } catch (error) {
68
+ // Inspection must not erase the already known step failure; the two
69
+ // failures stay separately visible, as in classifyOutcome.
70
+ message += ` Could not inspect the failed step: ${errorMessage(error)}`;
71
+ }
72
+ if (details !== undefined) message += renderStepEvidence(details);
73
+ // Appended whatever inspection found — including nothing. A failure shape
74
+ // this reader does not recognise must still end with somewhere to go.
75
+ const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
76
+ message += renderInspection(where);
77
+ message += await alsoRecord(journal, context.rootRunId, {
78
+ step: stepId, runId, state: 'completed', completionReason: reason,
79
+ ...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
80
+ });
81
+ throw new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
43
82
  }
83
+ await recordAuthoredChild(journal, context.rootRunId, {
84
+ step: stepId, runId, state: 'completed', completionReason: reason,
85
+ });
44
86
  return completed.payload.output;
45
87
  }
46
88
 
@@ -49,15 +91,21 @@ export async function readSuccessfulOutput(
49
91
  outcome: RunOutcome,
50
92
  stepId: string,
51
93
  journalSteps: AuthoredFlowJournalStep[],
94
+ context: AuthoredStepContext = {},
52
95
  ): Promise<string> {
53
- const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps).catch(error => {
54
- if (error instanceof AuthoredFlowExecutionError
55
- && (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
56
- throw new AuthoredFlowExecutionError('lease_exceeded',
57
- `f.run step "${stepId}" exceeded its command timeout.`, error.completionReason, error.runId);
58
- }
59
- throw error;
60
- });
96
+ const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
97
+ .catch(error => {
98
+ if (error instanceof AuthoredFlowExecutionError
99
+ && (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
100
+ // A timeout is still a failed command, and the evidence extracted for
101
+ // it is the same evidence. Re-labelling the error must not delete it.
102
+ throw new AuthoredFlowExecutionError('lease_exceeded',
103
+ `f.run step "${stepId}" exceeded its command timeout.`
104
+ + ` ${error.message.slice(`${error.code}: `.length)}`,
105
+ error.completionReason, error.runId, error.details);
106
+ }
107
+ throw error;
108
+ });
61
109
  if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
62
110
  throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
63
111
  }
@@ -102,6 +150,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
102
150
  return typeof value === 'object' && value !== null && !Array.isArray(value);
103
151
  }
104
152
 
153
+ function errorMessage(error: unknown): string {
154
+ return error instanceof Error ? error.message : String(error);
155
+ }
156
+
105
157
  function protocolViolation(runId: string, detail: string): AuthoredFlowExecutionError {
106
158
  return new AuthoredFlowExecutionError(
107
159
  'journal_protocol_violation',
@@ -3,15 +3,17 @@ import { parseBudget } from './budget.js';
3
3
  import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface';
4
4
  import { compileSpec, toKernelSpec } from './compile.js';
5
5
  import { checkAuthoredFlow } from './cli/check.js';
6
- import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js';
6
+ import { classifyOutcome, type RunLifecycleOptions, type RunReport } from './cli/run.js';
7
7
  import type { PreflightDiagnostic } from './preflight.js';
8
8
  import { AuthoredFlowExecutionError } from './authored-flow-error.js';
9
9
  import type { JournalClient } from './journal-client.js';
10
10
  import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
11
- import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
11
+ import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput, type AuthoredStepContext } from './authored-step-output.js';
12
12
  import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
13
13
  import { snapshotJsonValue } from './json-value.js';
14
14
  import { authoredChildAdmissionKey } from './authored-admission.js';
15
+ import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
16
+ import type { StepFailedDetails } from './failure-kinds.js';
15
17
 
16
18
  const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
17
19
 
@@ -22,6 +24,10 @@ export function authoredWorkerRunner(
22
24
  localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
23
25
  rootRunId?: string,
24
26
  ) {
27
+ const context: AuthoredStepContext = {
28
+ ...(rootRunId === undefined ? {} : { rootRunId }),
29
+ ...(waitOptions.dataDir === undefined ? {} : { dataDir: waitOptions.dataDir }),
30
+ };
25
31
  async function run(step: StepSpec): Promise<unknown> {
26
32
  const id = step.id;
27
33
  const authoring: FlowSpec = { version: SPEC_SCHEMA_VERSION, name: `${definition.name}/${id}`, steps: [step], ...(headerBudget === undefined ? {} : { budget: parseBudget(headerBudget) }) };
@@ -46,6 +52,10 @@ export function authoredWorkerRunner(
46
52
  }
47
53
  const spec = toKernelSpec(resolved);
48
54
  const consume = async (outcome: import('./protocol.js').RunOutcome) => {
55
+ // Written BEFORE the wait, not after it. An agent runs for as long as its
56
+ // lease allows; if this process dies mid-step, this record is the only
57
+ // thing that still names the child run holding the evidence.
58
+ await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
49
59
  // Reuse the declarative CLI's own wait/classification (cli/run.ts) rather
50
60
  // than a hand-rolled poll: `step.completed` and the run's own terminal
51
61
  // state are appended as two SEPARATE actions (kernel/relayflowd-core/src/machine.rs
@@ -69,17 +79,38 @@ export function authoredWorkerRunner(
69
79
  );
70
80
  }
71
81
  if (execution.exitCode !== 0) {
82
+ // `execution.report.completionReason` is the RUN's reason (normally
83
+ // `step_failed`), not the step's. The step's terminal facts are the
84
+ // ones `classifyOutcome` already extracted with `stepFailureDetails`
85
+ // and assigned onto this diagnostic — read them from there rather than
86
+ // re-walking the journal or mislabelling the run reason as a step one.
87
+ const diagnostic = execution.report.diagnostics.at(-1);
88
+ const details = stepDetails(diagnostic);
72
89
  const reason = execution.report.completionReason;
90
+ let message = diagnostic?.message
91
+ ?? `flow "${definition.name}" step "${id}" did not complete successfully `
92
+ + `(status: ${execution.report.status ?? 'unknown'})`;
93
+ // Only a reason the journal actually recorded for a step is indexed;
94
+ // an unfinished or unreadable child is left as `admitted`, never given
95
+ // a manufactured completion.
96
+ if (isSurfaceCompletionReason(details?.completionReason)) {
97
+ journalSteps.push(Object.freeze({
98
+ id, runId: outcome.run_id, completionReason: details.completionReason,
99
+ }));
100
+ message += await alsoRecord(journal, rootRunId, {
101
+ step: id, runId: outcome.run_id, state: 'completed',
102
+ completionReason: details.completionReason,
103
+ ...(details.stepId === undefined ? {} : { kernelStep: details.stepId }),
104
+ });
105
+ }
73
106
  throw new AuthoredFlowExecutionError(
74
- 'step_failed',
75
- execution.report.diagnostics.at(-1)?.message
76
- ?? `flow "${definition.name}" step "${id}" did not complete successfully `
77
- + `(status: ${execution.report.status ?? 'unknown'})`,
107
+ 'step_failed', message,
78
108
  isSurfaceCompletionReason(reason) ? reason : undefined,
79
109
  outcome.run_id,
110
+ details,
80
111
  );
81
112
  }
82
- return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps);
113
+ return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, context);
83
114
  };
84
115
  const admissionKey = authoredChildAdmissionKey(rootRunId, id);
85
116
  return budget === undefined
@@ -173,11 +204,19 @@ export function authoredWorkerRunner(
173
204
  };
174
205
  }
175
206
 
176
- /** Deterministic commands execute inline under their per-invocation lease. */
207
+ /**
208
+ * Deterministic commands execute inline under their per-invocation lease.
209
+ *
210
+ * `context` carries the root run (so each child is indexed on it) and the
211
+ * data dir (so a failure can name the journal on disk). It is the same object
212
+ * the worker runner builds; a failed `f.run` and a failed `f.agent` now report
213
+ * through one grammar.
214
+ */
177
215
  export function authoredDeterministicRunner(
178
216
  name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
179
- rootRunId?: string,
217
+ context: AuthoredStepContext = {},
180
218
  ) {
219
+ const rootRunId = context.rootRunId;
181
220
  return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => {
182
221
  const spec = toKernelSpec(compileSpec({
183
222
  version: SPEC_SCHEMA_VERSION,
@@ -189,11 +228,45 @@ export function authoredDeterministicRunner(
189
228
  }],
190
229
  }));
191
230
  const admissionKey = authoredChildAdmissionKey(rootRunId, id);
192
- if (terminal) return readSuccessfulOutput(
193
- journal, await journal.runStart(spec, undefined, admissionKey), id, journalSteps,
194
- );
195
- return budget.execute(
196
- journal, spec, outcome => readSuccessfulOutput(journal, outcome, id, journalSteps), admissionKey,
197
- );
231
+ const consume = async (outcome: import('./protocol.js').RunOutcome): Promise<string> => {
232
+ await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
233
+ return readSuccessfulOutput(journal, outcome, id, journalSteps, context);
234
+ };
235
+ if (terminal) return consume(await journal.runStart(spec, undefined, admissionKey));
236
+ return budget.execute(journal, spec, consume, admissionKey);
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Lift the step-shaped fields off a run diagnostic.
242
+ *
243
+ * `RunDiagnostic extends StepFailedDetails`, so `classifyOutcome`'s
244
+ * `Object.assign(diagnostic, details)` leaves the extraction right there. The
245
+ * fields are picked explicitly rather than spread, so `severity`, `kind` and
246
+ * `message` — which describe the RUN, not the step — cannot leak into
247
+ * something typed as the step's evidence.
248
+ */
249
+ function stepDetails(
250
+ diagnostic: RunReport['diagnostics'][number] | undefined,
251
+ ): StepFailedDetails | undefined {
252
+ if (diagnostic === undefined) return undefined;
253
+ // `RunDiagnostic` is the only member of this union that extends
254
+ // `StepFailedDetails`; the check-report shapes carry none of these fields,
255
+ // so reading them as optionals yields `undefined` and contributes nothing.
256
+ const found = diagnostic as Partial<StepFailedDetails>;
257
+ const details: StepFailedDetails = {
258
+ ...(found.stepId === undefined ? {} : { stepId: found.stepId }),
259
+ ...(found.stepType === undefined ? {} : { stepType: found.stepType }),
260
+ ...(found.completionReason === undefined ? {} : { completionReason: found.completionReason }),
261
+ ...(found.attempt === undefined ? {} : { attempt: found.attempt }),
262
+ ...(found.maxIterations === undefined ? {} : { maxIterations: found.maxIterations }),
263
+ ...(found.exitCode === undefined ? {} : { exitCode: found.exitCode }),
264
+ ...(found.stdoutTail === undefined ? {} : { stdoutTail: found.stdoutTail }),
265
+ ...(found.stderrTail === undefined ? {} : { stderrTail: found.stderrTail }),
266
+ ...(found.detail === undefined ? {} : { detail: found.detail }),
267
+ ...(found.transcriptPath === undefined ? {} : { transcriptPath: found.transcriptPath }),
268
+ ...(found.hint === undefined ? {} : { hint: found.hint }),
269
+ ...(found.journalPath === undefined ? {} : { journalPath: found.journalPath }),
198
270
  };
271
+ return Object.keys(details).length === 0 ? undefined : details;
199
272
  }