@relayflows/sdk 2.0.13 → 2.0.15

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.
@@ -14,6 +14,8 @@ import { JournalClient } from '../journal-client.js';
14
14
  import { inputFailureReport } from './check.js';
15
15
  import { checkAuthoredTriggers } from './check-triggers.js';
16
16
  import {
17
+ authoredCompletion,
18
+ authoredStepFailure,
17
19
  connect,
18
20
  emptyReport,
19
21
  fromCheckReport,
@@ -98,31 +100,7 @@ export async function runDirectFlow(
98
100
  `authored flow "${result.name}" completed without a journal step`,
99
101
  ));
100
102
  }
101
- if (result.completionReason === 'needs_human') {
102
- return {
103
- exitCode: 3,
104
- report: {
105
- ...base, ok: false, runId: result.rootRunId, socketPath, status: 'parked',
106
- completedSteps: result.journalSteps.length,
107
- diagnostics: [...base.diagnostics, {
108
- severity: 'parked', kind: 'run_parked',
109
- message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.`,
110
- }],
111
- },
112
- };
113
- }
114
- return {
115
- exitCode: 0,
116
- report: {
117
- ...base,
118
- ok: true,
119
- runId: result.rootRunId,
120
- socketPath,
121
- status: 'completed',
122
- completionReason: result.completionReason,
123
- completedSteps: result.journalSteps.length,
124
- },
125
- };
103
+ return authoredCompletion('run', base, socketPath, result, result.rootRunId);
126
104
  } catch (caught) {
127
105
  if (caught instanceof McpPreflightError) return {
128
106
  exitCode: 2, report: fromCheckReport('run', caught.report),
@@ -190,33 +168,12 @@ export async function runDirectFlow(
190
168
  },
191
169
  };
192
170
  }
193
- // A step that ran and failed is a run failure, not a protocol failure.
194
- // Routing it through `protocolFailure` reported `relayflowd could not
195
- // complete the run request` which says the daemon broke and left the
196
- // report with no `status`, so the summary line printed `RUN <id> unknown`
197
- // about a run whose outcome was known exactly. The diagnostic carried up
198
- // from `classifyOutcome` already names the step, its exit code and its
199
- // output tail; this branch is what lets it reach the terminal. Mirrors
200
- // the `McpStepError` branch above, which had this shape all along.
171
+ // A step that ran and failed is a run failure, not a protocol failure. The
172
+ // diagnostic carried up from `classifyOutcome` already names the step, its
173
+ // exit code and its output tail; this branch is what lets it reach the
174
+ // terminal. `resumeFlow` takes the same branch, through the same helper.
201
175
  if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
202
- return {
203
- exitCode: 1,
204
- report: {
205
- ...base,
206
- ok: false,
207
- runId: error.runId,
208
- socketPath,
209
- status: 'failed',
210
- completionReason: 'step_failed',
211
- diagnostics: [...base.diagnostics, {
212
- severity: 'failure',
213
- kind: 'step_failed',
214
- // The `code: ` prefix `AuthoredFlowExecutionError` adds is
215
- // redundant once the diagnostic is labelled `[step_failed]`.
216
- message: error.message.replace(/^step_failed: /, ''),
217
- }],
218
- },
219
- };
176
+ return authoredStepFailure('run', base, socketPath, error);
220
177
  }
221
178
  const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
222
179
  return protocolFailure('run', base, socketPath, error, runId);
package/src/cli/run.ts CHANGED
@@ -22,6 +22,7 @@ import type {
22
22
  RunStatus,
23
23
  } from '../protocol.js';
24
24
  import type { StepType } from '../spec.js';
25
+ import type { LoweredCompletionReason } from '../authored-flow-executor.js';
25
26
  import {
26
27
  checkFlow,
27
28
  type CheckReport,
@@ -191,21 +192,7 @@ export async function resumeFlow(
191
192
  lifecycle: options,
192
193
  });
193
194
  if (result === undefined) throw new Error('authored root disappeared during resume');
194
- return {
195
- exitCode: result.completionReason === 'needs_human' ? 3 : 0,
196
- report: {
197
- ...base,
198
- ok: result.completionReason === 'success',
199
- runId,
200
- socketPath,
201
- status: result.completionReason === 'needs_human' ? 'parked' : 'completed',
202
- ...(result.completionReason === 'success'
203
- ? { completionReason: 'success' as const }
204
- : { diagnostics: [{ severity: 'parked' as const, kind: 'run_parked' as const,
205
- message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.` }] }),
206
- completedSteps: result.journalSteps.length,
207
- },
208
- };
195
+ return authoredCompletion('resume', base, socketPath, result, runId);
209
196
  }
210
197
  // resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
211
198
  // slack effect resume plus every other provider from N's codegen. The
@@ -231,6 +218,14 @@ export async function resumeFlow(
231
218
  return { exitCode: 2, report: { ...base, runId, socketPath,
232
219
  diagnostics: [{ severity: 'refusal', kind: error.code, message: error.message }] } };
233
220
  }
221
+ // Same classification the run path gets. Resuming an authored root whose
222
+ // `f.agent` step failed is a step failure, not a protocol failure, and
223
+ // leaving it on `protocolFailure` meant `flows run` printed the evidence
224
+ // while `flows resume` still printed `protocol_error` and
225
+ // `RUN <id> unknown` for the identical failure.
226
+ if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
227
+ return authoredStepFailure('resume', base, socketPath, error, runId);
228
+ }
234
229
  if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
235
230
  return protocolFailure('resume', base, socketPath, error, runId);
236
231
  }
@@ -258,6 +253,121 @@ export async function resumeFlow(
258
253
  }
259
254
  }
260
255
 
256
+ /**
257
+ * A step that ran and failed, reported as the run failure it is.
258
+ *
259
+ * Shared by `runDirectFlow` and `resumeFlow` on purpose. The first cut of this
260
+ * fix classified the run path and left resume on `protocolFailure`, so `flows
261
+ * run` became diagnosable while `flows resume` still printed `protocol_error`
262
+ * and `RUN <id> unknown` for the same failed step — and the surface doc claimed
263
+ * both were fixed. One function is what stops the two paths drifting again.
264
+ *
265
+ * `protocolFailure` is wrong here twice over: it blames the daemon for a run it
266
+ * drove correctly, and it produces a report with no `status`, which is the
267
+ * whole of what `RUN <id> unknown` ever meant.
268
+ */
269
+ export function authoredStepFailure(
270
+ command: RunCommand,
271
+ base: CheckReport | RunReport,
272
+ socketPath: string,
273
+ error: AuthoredFlowExecutionError,
274
+ fallbackRunId?: string,
275
+ ): RunExecution {
276
+ // The failing step runs as its own kernel run, so the error's run id is the
277
+ // one whose journal holds the evidence. The resume target is the fallback.
278
+ const runId = error.runId ?? fallbackRunId;
279
+ return {
280
+ exitCode: 1,
281
+ report: {
282
+ ...fromBase(command, base),
283
+ ok: false,
284
+ ...(runId === undefined ? {} : { runId }),
285
+ socketPath,
286
+ status: 'failed',
287
+ completionReason: 'step_failed',
288
+ diagnostics: [...base.diagnostics, {
289
+ severity: 'failure',
290
+ kind: 'step_failed',
291
+ // The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
292
+ // redundant once the diagnostic is labelled `[step_failed]`.
293
+ message: error.message.replace(/^step_failed: /, ''),
294
+ }],
295
+ },
296
+ };
297
+ }
298
+
299
+ /**
300
+ * An authored body that returned its own terminal verdict, reported as that verdict.
301
+ *
302
+ * Shared by `runDirectFlow` and `resumeFlow` for the reason `authoredStepFailure`
303
+ * is shared: the run and resume paths had already drifted once, and the
304
+ * `needs_human` report was duplicated verbatim in both files.
305
+ *
306
+ * This is NOT `authoredStepFailure`, even though a `step_failed` verdict lands
307
+ * on the same exit code and report shape. That function describes a step that
308
+ * ran and failed, and carries the failing step's evidence. Here every step
309
+ * succeeded and the BODY declared the outcome, so there is no failing step to
310
+ * name — routing this through the other helper would invent one.
311
+ */
312
+ export function authoredCompletion(
313
+ command: RunCommand,
314
+ base: RunReport,
315
+ socketPath: string,
316
+ result: { name: string; completionReason: LoweredCompletionReason; journalSteps: readonly unknown[] },
317
+ runId: string | undefined,
318
+ ): RunExecution {
319
+ const common: RunReport = {
320
+ ...fromBase(command, base),
321
+ ...(runId === undefined ? {} : { runId }),
322
+ socketPath,
323
+ completedSteps: result.journalSteps.length,
324
+ };
325
+ switch (result.completionReason) {
326
+ case 'success':
327
+ return {
328
+ exitCode: 0,
329
+ report: { ...common, ok: true, status: 'completed', completionReason: 'success' },
330
+ };
331
+ case 'needs_human':
332
+ return {
333
+ exitCode: 3,
334
+ report: {
335
+ ...common, ok: false, status: 'parked',
336
+ diagnostics: [...base.diagnostics, {
337
+ severity: 'parked', kind: 'run_parked',
338
+ message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.`,
339
+ }],
340
+ },
341
+ };
342
+ case 'step_failed':
343
+ // A declared run failure. Exit 1, not the parked 3: nothing here is
344
+ // waiting for a human to recover it, and exit 3 is the local kit's
345
+ // manual-approval stop. `status: failed` with this `completionReason` is
346
+ // also the only terminal shape Cloud accepts for a non-success run
347
+ // (see cloud-run.ts).
348
+ return {
349
+ exitCode: 1,
350
+ report: {
351
+ ...common, ok: false, status: 'failed', completionReason: 'step_failed',
352
+ diagnostics: [...base.diagnostics, {
353
+ severity: 'failure', kind: 'step_failed',
354
+ message: `Flow "${result.name}" declared done("step_failed"): its own checks did not pass. `
355
+ + 'No step failed, so there is no step-level evidence to inspect; the journal holds '
356
+ + 'every step the flow ran before it decided.',
357
+ }],
358
+ },
359
+ };
360
+ }
361
+ // Exhaustive by construction. A fourth lowered completion has to choose its
362
+ // own exit code and wording here; it must not inherit "its own checks did not
363
+ // pass", which would state something the body never declared. Letting an
364
+ // unlisted reason fall through to the failure branch is how a reporting-side
365
+ // copy of the same vocabulary drifts from the executor's — the defect this
366
+ // change exists to close — so it is a compile error, not a wrong report.
367
+ const unreachable: never = result.completionReason;
368
+ throw new Error(`unreachable authored completion: ${String(unreachable)}`);
369
+ }
370
+
261
371
  /**
262
372
  * Get a live daemon, then open the socket to it.
263
373
  *
Binary file