@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
@@ -34,18 +34,33 @@ export async function stepFailureDetails(
34
34
  const snapshot = await client.runGet(runId);
35
35
  let fromSeq = 1;
36
36
  const failures = new Map<string, StepFailedDetails>();
37
+ // `runGet`'s `StepSnapshot` carries only `type`, `state` and an optional
38
+ // lease deadline — neither the attempt number nor the attempt budget. Both
39
+ // are journal facts: the envelope's `attempt` counts the attempt, and
40
+ // `step.attempt.started` records the `max_iterations` the kernel is
41
+ // enforcing (relayflowd-core/src/entry.rs `AttemptStartedPayload`). They are
42
+ // collected on the same walk and reported only when the journal held them.
43
+ const budgets = new Map<string, number>();
37
44
  while (true) {
38
45
  const { entries } = await client.journalRead(runId, fromSeq, 100);
39
46
  if (entries.length === 0) break;
40
47
  for (const raw of entries) {
41
48
  const entry = record(raw);
42
49
  const seq = entry?.['seq'];
43
- if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
50
+ if (entry === undefined || typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
44
51
  throw new Error('invalid journal sequence in step failure inspection');
45
52
  }
46
53
  fromSeq = seq + 1;
47
- const stepId = entry?.['step_id'];
48
- if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string') continue;
54
+ const stepId = entry['step_id'];
55
+ if (typeof stepId !== 'string') continue;
56
+ if (entry['entry_type'] === 'step.attempt.started') {
57
+ const maxIterations = record(entry['payload'])?.['max_iterations'];
58
+ if (typeof maxIterations === 'number' && Number.isSafeInteger(maxIterations) && maxIterations > 0) {
59
+ budgets.set(stepId, maxIterations);
60
+ }
61
+ continue;
62
+ }
63
+ if (entry['entry_type'] !== 'step.completed') continue;
49
64
  // A later completion supersedes an earlier failed attempt.
50
65
  failures.delete(stepId);
51
66
  const payload = record(entry['payload']);
@@ -59,10 +74,14 @@ export async function stepFailureDetails(
59
74
  if (payload['disposition'] !== 'step_done'
60
75
  || typeof completionReason !== 'string' || completionReason === 'success') continue;
61
76
  const stepType = snapshot.steps[stepId]?.type;
77
+ const attempt = entry['attempt'];
78
+ const maxIterations = budgets.get(stepId);
62
79
  failures.set(stepId, {
63
80
  stepId,
64
81
  completionReason,
65
82
  ...(stepType === undefined ? {} : { stepType }),
83
+ ...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
84
+ ...(maxIterations === undefined ? {} : { maxIterations }),
66
85
  ...evidence(payload),
67
86
  });
68
87
  }
@@ -70,6 +89,52 @@ export async function stepFailureDetails(
70
89
  return [...failures.values()].at(-1);
71
90
  }
72
91
 
92
+ /**
93
+ * The evidence half of a `step_failed` diagnostic; `renderInspection` adds the
94
+ * rest. Each field is printed only when the journal actually carried it — an
95
+ * agent step has no exit code to report, and inventing `exit=undefined` (which
96
+ * is what the deterministic-only version printed for one) is worse than
97
+ * silence on that field.
98
+ *
99
+ * `attempt=N/M` is here for `retries_exhausted`, which is the kernel's terminal
100
+ * reason for "the attempt budget was consumed" (kernel/DESIGN.md) and is
101
+ * accurate kernel vocabulary that reads in English as though retries happened.
102
+ * A deterministic step gets the default budget of one, so a single failed
103
+ * attempt with no retry at all terminates as `retries_exhausted`. The
104
+ * vocabulary does not change (AGENTS.md rule 7); printing the facts beside it
105
+ * stops it being misread. Unknown values are omitted rather than defaulted —
106
+ * an absent budget must not be reported as one attempt.
107
+ *
108
+ * Lives next to `StepFailedDetails`'s extractor rather than in `cli/run.ts` so
109
+ * the authored `f.run` path can render the same grammar without importing the
110
+ * run lifecycle (which would be a cycle).
111
+ */
112
+ export function renderStepEvidence(details: StepFailedDetails): string {
113
+ return ` Step ${JSON.stringify(details.stepId)}`
114
+ + (details.stepType === undefined ? '' : ` (${details.stepType})`)
115
+ + ` completionReason: ${details.completionReason}`
116
+ + renderAttempt(details)
117
+ + (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
118
+ + '.'
119
+ + (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
120
+ + (details.stdoutTail ? `\nStdout (last 1,024 bytes):\n${details.stdoutTail}` : '')
121
+ + (details.stderrTail ? `\nStderr (last 1,024 bytes):\n${details.stderrTail}` : '')
122
+ + (details.transcriptPath === undefined ? '' : `\nTranscript: ${details.transcriptPath}`);
123
+ }
124
+
125
+ function renderAttempt(details: StepFailedDetails): string {
126
+ if (details.attempt === undefined) return '';
127
+ return ` attempt=${details.attempt}`
128
+ + (details.maxIterations === undefined ? '' : `/${details.maxIterations}`);
129
+ }
130
+
131
+ /** The `Inspect:`/`Journal:` tail every `step_failed` message ends with. */
132
+ export function renderInspection(where: { hint?: string; journalPath?: string }): string {
133
+ if (where.hint === undefined) return '';
134
+ return `\nInspect: ${where.hint}`
135
+ + (where.journalPath === undefined ? '' : `\nJournal: ${where.journalPath}`);
136
+ }
137
+
73
138
  /**
74
139
  * Where to look, derived from the run id and data dir ALONE.
75
140
  *
@@ -108,28 +173,53 @@ export function inspectionHint(
108
173
  * it does not. A truncated render (the daemon caps at 2,000 chars and appends
109
174
  * a truncation note) will not parse; that falls through to the raw string,
110
175
  * which is still the account of what went wrong.
176
+ *
177
+ * An agent or llm completion now also carries `trajectory_tail.transcript`
178
+ * (agent-transcript.ts) on every attempt. That object is not process-shaped,
179
+ * so it must not shadow the render that is: the first candidate that carries
180
+ * an exit code or a tail wins. The digest contributes what only it has — the
181
+ * failure excerpt the worker picked out of the provider's frames, and the
182
+ * path of the transcript file.
111
183
  */
112
184
  function evidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
113
185
  const detail = record(payload['verification'])?.['detail'];
114
- const structured = record(payload['output'])
115
- ?? record(payload['trajectory_tail'])
116
- ?? (typeof detail === 'string' ? parsed(detail) : undefined);
186
+ const candidates = [
187
+ record(payload['output']),
188
+ record(payload['trajectory_tail']),
189
+ typeof detail === 'string' ? parsed(detail) : undefined,
190
+ ].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
191
+ const structured = candidates.find(processShaped) ?? candidates[0];
117
192
  const exitCode = structured?.['exit_code'];
118
193
  const stdout = structured?.['stdout_tail'];
119
194
  const stderr = structured?.['stderr_tail'];
120
- const structuredShape = typeof exitCode === 'number'
121
- || typeof stdout === 'string' || typeof stderr === 'string';
195
+ const structuredShape = structured !== undefined && processShaped(structured);
196
+ const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
197
+ const failure = record(transcript?.['failure']);
198
+ const excerpt = failure?.['excerpt'];
199
+ const transcriptPath = record(transcript?.['file'])?.['path'];
200
+ // The excerpt is the worker's own pick of the failure; a `stderr` excerpt
201
+ // is the same bytes as `stderrTail`, so it is not printed twice.
202
+ const excerptDetail = typeof excerpt === 'string' && excerpt.length > 0
203
+ && !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
204
+ ? tail(excerpt) : undefined;
122
205
  return {
123
206
  ...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
124
207
  ...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: tail(stdout) } : {}),
125
208
  ...(typeof stderr === 'string' ? { stderrTail: tail(stderr) } : {}),
126
209
  // Keep the daemon's account only when it was NOT just a render of the
127
210
  // fields above — otherwise the same bytes print twice.
128
- ...(typeof detail === 'string' && detail.length > 0 && !structuredShape
129
- ? { detail: tail(detail) } : {}),
211
+ ...(excerptDetail !== undefined ? { detail: excerptDetail }
212
+ : typeof detail === 'string' && detail.length > 0 && !structuredShape
213
+ ? { detail: tail(detail) } : {}),
214
+ ...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
130
215
  };
131
216
  }
132
217
 
218
+ function processShaped(value: Record<string, unknown>): boolean {
219
+ return typeof value['exit_code'] === 'number'
220
+ || typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
221
+ }
222
+
133
223
  function parsed(value: string): Record<string, unknown> | undefined {
134
224
  try {
135
225
  return record(JSON.parse(value));
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_DATA_DIR } from './daemon-connection.js';
2
+ import { DEFAULT_RUN_LIMIT } from './cli/cloud-read.js';
2
3
  import type { ParsedArgs } from './cli.js';
3
4
 
4
5
  /**
@@ -181,6 +182,17 @@ export const CLI_VERBS = [
181
182
  ],
182
183
  variants: ['hn-monitor'],
183
184
  },
185
+ {
186
+ name: 'logs',
187
+ description: 'Read a hosted run’s runner log, or one agent step’s transcript, from Cloud',
188
+ args: [{ name: 'run-id', description: 'Hosted run id, as `flows runs` lists it', required: true }],
189
+ options: [
190
+ { flags: '--step <name>', description: 'Show that agent step’s transcript instead of the runner log' },
191
+ { flags: '--raw', description: 'Print the transcript JSONL unrendered (still redacted)' },
192
+ JSON_OPTION,
193
+ ],
194
+ variants: ['logs'],
195
+ },
184
196
  {
185
197
  name: 'observer',
186
198
  description: 'Mint a read-only observer link without running a flow',
@@ -231,6 +243,15 @@ export const CLI_VERBS = [
231
243
  ],
232
244
  variants: ['run', 'cloud-run'],
233
245
  },
246
+ {
247
+ name: 'runs',
248
+ description: 'List the hosted runs this Cloud credential can read, newest first',
249
+ options: [
250
+ { flags: '--limit <n>', description: 'How many runs to show', defaultValue: DEFAULT_RUN_LIMIT },
251
+ JSON_OPTION,
252
+ ],
253
+ variants: ['runs'],
254
+ },
234
255
  {
235
256
  name: 'schedule',
236
257
  description: 'Register a flow to run in Cloud on a cron or interval, or on the one it declares',
@@ -265,6 +286,18 @@ export const CLI_VERBS = [
265
286
  ],
266
287
  variants: ['serve-webhook'],
267
288
  },
289
+ {
290
+ name: 'status',
291
+ description: 'Show a run’s steps, attempts, leases and last verdicts from its journal, or from Cloud with --cloud',
292
+ args: [{ name: 'run-id', description: 'Run to inspect; defaults to RELAYFLOW_RUN_ID inside a step', required: false }],
293
+ options: [
294
+ JSON_OPTION,
295
+ DATA_DIR_OPTION,
296
+ { flags: '--tail <n>', description: 'Lines of each agent attempt’s transcript tail to show' },
297
+ { flags: '--cloud', description: 'Read the run from Cloud instead of a local journal; needs the run id' },
298
+ ],
299
+ variants: ['status'],
300
+ },
268
301
  {
269
302
  name: 'sync',
270
303
  description: 'Apply a hosted run’s code changes to a local tree (replaces `agent-relay cloud sync`)',
package/src/cli.ts CHANGED
@@ -24,6 +24,12 @@ import { checkAuthoredTriggers } from './cli/check-triggers.js';
24
24
  import { parseWebhookArgs, runServeWebhook } from './cli/serve-webhook.js';
25
25
  import { runDirectFlow } from './cli/direct-run.js';
26
26
  import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
27
+ import { parseStatusArgs, runStatus, type StatusArgs } from './cli/status.js';
28
+ import {
29
+ parseLogsArgs, parseRunsArgs, runCloudLogsCli, runCloudRunsCli, runCloudStatusCli,
30
+ type LogsArgs, type RunsArgs,
31
+ } from './cli/cloud-read.js';
32
+ import { transcriptTailSource } from './transcript-tail.js';
27
33
  import { checkTypeScriptFlow } from './cli/check-typescript.js';
28
34
  import { runCloudCli } from './cli/cloud-run.js';
29
35
  import { runCloudSyncCli } from './cli/cloud-sync.js';
@@ -59,6 +65,7 @@ type CliExitCode = 0 | 1 | 2 | 3;
59
65
  export type ParsedArgs =
60
66
  | { command: 'add'; value: string }
61
67
  | ReplayArgs
68
+ | StatusArgs
62
69
  | BuildArgs
63
70
  | DeployArgs
64
71
  | { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
@@ -74,6 +81,8 @@ export type ParsedArgs =
74
81
  | { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
75
82
  | { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
76
83
  | { command: 'answer'; dataDir: string; json: boolean; spawn: boolean; note: string | undefined; by: string | undefined; runId: string; waitId: string; answer: boolean }
84
+ | RunsArgs
85
+ | LogsArgs
77
86
  | { command: 'observer'; dataDir: string }
78
87
  | { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
79
88
  | { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
@@ -104,6 +113,10 @@ const USAGE = [
104
113
  'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
105
114
  'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
106
115
  'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
116
+ 'flows status [--json] [--data-dir <dir>] [--tail <n>] [<run-id>]',
117
+ 'flows status --cloud [--json] <run-id>',
118
+ 'flows runs [--limit <n>] [--json]',
119
+ 'flows logs [--step <name>] [--raw] [--json] <run-id>',
107
120
  'flows observer [--data-dir <dir>]',
108
121
  'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
109
122
  ].join('\n');
@@ -195,6 +208,17 @@ export async function runCli(
195
208
  if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
196
209
  if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
197
210
  if (parsed.command === 'replay') return replayJournal(parsed, io);
211
+ // Daemon-free like `check`: reads one journal file and nothing else, so it
212
+ // works inside a step of a run whose daemon is gone (kernel/DAEMON-LIFECYCLE.md §4).
213
+ if (parsed.command === 'status') {
214
+ // One verb, two sources. `--cloud` never reaches `runStatus`, so the
215
+ // offline reader stays offline (cli/status.ts).
216
+ return parsed.cloud === true
217
+ ? runCloudStatusCli(parsed, io)
218
+ : runStatus(parsed, io, { tails: transcriptTailSource() });
219
+ }
220
+ if (parsed.command === 'runs') return runCloudRunsCli(parsed, io);
221
+ if (parsed.command === 'logs') return runCloudLogsCli(parsed, io);
198
222
  if (parsed.command === 'answer') {
199
223
  const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
200
224
  ...(parsed.note === undefined ? {} : { note: parsed.note }),
@@ -498,6 +522,9 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
498
522
  if (command === undefined || !CLI_VERB_NAMES.has(command)) return undefined;
499
523
  if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
500
524
  if (command === 'replay') return parseReplayArgs(args.slice(1));
525
+ if (command === 'status') return parseStatusArgs(args.slice(1));
526
+ if (command === 'runs') return parseRunsArgs(args.slice(1));
527
+ if (command === 'logs') return parseLogsArgs(args.slice(1));
501
528
  if (command === 'build') return parseBuildArgs(args.slice(1));
502
529
  if (command === 'deploy') {
503
530
  // The positional decides the form: an authored source deploys a hosted
package/src/cloud-http.ts CHANGED
@@ -21,7 +21,26 @@ export interface CloudRefusal {
21
21
  received?: { packageName?: string; version?: string };
22
22
  }
23
23
 
24
+ /**
25
+ * Narrows a `configuration` failure to the thing that was wrong.
26
+ *
27
+ * `configuration` has always covered every local misconfiguration, and its
28
+ * message says which. A caller that must answer with a *code* — the read
29
+ * verbs print `REFUSED [cloud_auth_missing]` and name `agent-relay cloud
30
+ * login` — cannot get that from prose without matching on it. Optional and
31
+ * additive: every existing `catch` on `code === 'configuration'` is unchanged.
32
+ */
33
+ export type CloudConfigurationReason =
34
+ | 'auth_missing'
35
+ | 'auth_expired'
36
+ | 'url_invalid'
37
+ | 'url_mismatch'
38
+ | 'timeout_invalid';
39
+
24
40
  export class CloudFlowError extends Error {
41
+ /** Set only for `configuration`; see {@link CloudConfigurationReason}. */
42
+ reason?: CloudConfigurationReason;
43
+
25
44
  constructor(
26
45
  readonly code: 'configuration' | 'unsupported_source' | 'invalid_input' | 'invalid_response' | 'http_error'
27
46
  | 'transport_error' | 'transient_error' | 'unsupported_storage_backend' | 'sync_too_large' | 'sync_unsupported'
@@ -35,6 +54,13 @@ export class CloudFlowError extends Error {
35
54
  }
36
55
  }
37
56
 
57
+ /** A `configuration` refusal that also carries its {@link CloudConfigurationReason}. */
58
+ function configurationError(reason: CloudConfigurationReason, message: string): CloudFlowError {
59
+ const error = new CloudFlowError('configuration', message);
60
+ error.reason = reason;
61
+ return error;
62
+ }
63
+
38
64
  /**
39
65
  * The `agent-relay cloud login` credential store. Read only when neither the
40
66
  * `token` option nor `FLOWS_CLOUD_TOKEN` is set, so an explicit credential
@@ -79,7 +105,7 @@ export function cloudConnection(options: CloudConnectionOptions): { baseUrl: str
79
105
  if (login !== undefined) {
80
106
  const expiresAt = login.accessTokenExpiresAt === undefined ? Number.NaN : Date.parse(login.accessTokenExpiresAt);
81
107
  if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
82
- throw new CloudFlowError('configuration',
108
+ throw configurationError('auth_expired',
83
109
  'The agent-relay cloud login has expired. Run `agent-relay cloud login` again, or set FLOWS_CLOUD_TOKEN.');
84
110
  }
85
111
  rawToken = login.accessToken;
@@ -88,7 +114,7 @@ export function cloudConnection(options: CloudConnectionOptions): { baseUrl: str
88
114
  }
89
115
  const token = rawToken?.trim();
90
116
  if (!token || /[\r\n]/u.test(rawToken!) || /^(?:rk|ot)_live_/u.test(token)) {
91
- throw new CloudFlowError('configuration',
117
+ throw configurationError('auth_missing',
92
118
  'Set FLOWS_CLOUD_TOKEN to a scoped Cloud API token (workflow:invoke:write and workflow:runs:read), '
93
119
  + 'or sign in with `agent-relay cloud login`.');
94
120
  }
@@ -96,11 +122,11 @@ export function cloudConnection(options: CloudConnectionOptions): { baseUrl: str
96
122
  try {
97
123
  url = new URL(options.apiUrl ?? process.env['FLOWS_CLOUD_URL'] ?? loginApiUrl ?? 'https://agentrelay.com/cloud');
98
124
  } catch {
99
- throw new CloudFlowError('configuration', 'FLOWS_CLOUD_URL must be an absolute Cloud application base URL.');
125
+ throw configurationError('url_invalid', 'FLOWS_CLOUD_URL must be an absolute Cloud application base URL.');
100
126
  }
101
127
  if (url.protocol !== 'https:'
102
128
  || url.username || url.password || url.search || url.hash || !/^\/[A-Za-z0-9/_-]*$/u.test(url.pathname)) {
103
- throw new CloudFlowError('configuration', 'Cloud URL must use HTTPS and a plain base path.');
129
+ throw configurationError('url_invalid', 'Cloud URL must use HTTPS and a plain base path.');
104
130
  }
105
131
  const baseUrl = `${url.origin}${url.pathname.replace(/\/+$/u, '')}`;
106
132
  // A login-store token is bound to the deployment that issued it. An explicit
@@ -113,7 +139,7 @@ export function cloudConnection(options: CloudConnectionOptions): { baseUrl: str
113
139
  issued = `${login.origin}${login.pathname.replace(/\/+$/u, '')}`;
114
140
  } catch { issued = undefined; }
115
141
  if (issued !== baseUrl) {
116
- throw new CloudFlowError('configuration',
142
+ throw configurationError('url_mismatch',
117
143
  `The agent-relay cloud login was issued for ${loginApiUrl}, not ${baseUrl}. `
118
144
  + 'Set FLOWS_CLOUD_TOKEN for that deployment, or unset FLOWS_CLOUD_URL to use the login.');
119
145
  }
@@ -159,7 +185,7 @@ export async function cloudFetch(
159
185
  const { baseUrl, token } = cloudConnection(options);
160
186
  const timeout = options.requestTimeoutMs ?? 30_000;
161
187
  if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2_147_483_647) {
162
- throw new CloudFlowError('configuration', 'requestTimeoutMs must be a positive 32-bit integer.');
188
+ throw configurationError('timeout_invalid', 'requestTimeoutMs must be a positive 32-bit integer.');
163
189
  }
164
190
  const bearer = init.bearerToken ?? token;
165
191
  if (/[\r\n]/u.test(bearer) || !bearer.trim()) {