@relayflows/sdk 2.0.19 → 2.0.21

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 (52) hide show
  1. package/dist/agent-reaper.d.ts +4 -0
  2. package/dist/agent-reaper.d.ts.map +1 -0
  3. package/dist/agent-reaper.js +53 -0
  4. package/dist/agent-reaper.js.map +1 -0
  5. package/dist/authored-flow-error.d.ts +6 -1
  6. package/dist/authored-flow-error.d.ts.map +1 -1
  7. package/dist/authored-flow-error.js.map +1 -1
  8. package/dist/authored-flow-executor.d.ts.map +1 -1
  9. package/dist/authored-flow-executor.js +8 -0
  10. package/dist/authored-flow-executor.js.map +1 -1
  11. package/dist/child-stop.d.ts +1 -1
  12. package/dist/child-stop.d.ts.map +1 -1
  13. package/dist/child-stop.js +68 -12
  14. package/dist/child-stop.js.map +1 -1
  15. package/dist/cli/answer.d.ts.map +1 -1
  16. package/dist/cli/answer.js +2 -1
  17. package/dist/cli/answer.js.map +1 -1
  18. package/dist/cli/run.d.ts.map +1 -1
  19. package/dist/cli/run.js +2 -1
  20. package/dist/cli/run.js.map +1 -1
  21. package/dist/flow-requirements.d.ts +2 -2
  22. package/dist/flow-requirements.d.ts.map +1 -1
  23. package/dist/flow-requirements.js +182 -0
  24. package/dist/flow-requirements.js.map +1 -1
  25. package/dist/human-to.d.ts +45 -0
  26. package/dist/human-to.d.ts.map +1 -0
  27. package/dist/human-to.js +49 -0
  28. package/dist/human-to.js.map +1 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/worker-cli.d.ts +8 -0
  34. package/dist/worker-cli.d.ts.map +1 -1
  35. package/dist/worker-cli.js +63 -8
  36. package/dist/worker-cli.js.map +1 -1
  37. package/dist/worker-usage.d.ts +8 -0
  38. package/dist/worker-usage.d.ts.map +1 -1
  39. package/dist/worker-usage.js +19 -0
  40. package/dist/worker-usage.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/agent-reaper.ts +52 -0
  43. package/src/authored-flow-error.ts +5 -0
  44. package/src/authored-flow-executor.ts +8 -0
  45. package/src/child-stop.ts +62 -12
  46. package/src/cli/answer.ts +2 -1
  47. package/src/cli/run.ts +2 -1
  48. package/src/flow-requirements.ts +143 -2
  49. package/src/human-to.ts +71 -0
  50. package/src/index.ts +1 -0
  51. package/src/worker-cli.ts +59 -7
  52. package/src/worker-usage.ts +13 -0
package/src/worker-cli.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
3
- import { decodeProviderResult, decodeWrapperResult, requirePricedUsage } from './worker-usage.js';
3
+ import { claudeResultOutcome, decodeProviderResult, decodeWrapperResult, requirePricedUsage } from './worker-usage.js';
4
4
  import { openSidechannel, type SidechannelContext } from './pty-sidechannel.js';
5
5
  import { spawn } from 'node:child_process';
6
- import { childStop, ownsProcessGroup } from './child-stop.js';
6
+ import { StringDecoder } from 'node:string_decoder';
7
+ import { childStop } from './child-stop.js';
8
+ import { reapOnExit } from './agent-reaper.js';
7
9
  import {
8
10
  agentExecution,
9
11
  llmExecution,
@@ -134,10 +136,13 @@ export async function runAgentCli(
134
136
  }
135
137
 
136
138
  if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
137
- // Structured provider output carries the authoritative token counts.
139
+ // Structured provider output carries the authoritative token counts. Claude
140
+ // streams it, so its final result is seen when it is emitted rather than
141
+ // only once the process exits — which, after a background task, it may not.
138
142
  const args = [...invocation.args];
139
- args.splice(args.length - 1, 0, ...(kind === 'claude' ? ['--output-format', 'json'] : ['--json']));
140
- return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd), kind), effectiveModel);
143
+ args.splice(args.length - 1, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
144
+ const completion = kind === 'claude' ? claudeResultOutcome : undefined;
145
+ return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion), kind), effectiveModel);
141
146
  }
142
147
  }
143
148
 
@@ -194,6 +199,15 @@ function redactRelayError(message: string): string {
194
199
  return message.replace(/\b(?:at|rk|nt|ot|br|arr)_(?:live_)?[A-Za-z0-9_-]+/g, '[redacted]');
195
200
  }
196
201
 
202
+ /**
203
+ * How long a CLI that has reported its final result may take to exit. Claude
204
+ * Code in print mode waits, after its result, for every background task it
205
+ * started to finish; one that never finishes kept a finished step running
206
+ * until the whole run's deadline. Past this grace the result stands and the
207
+ * tree is stopped.
208
+ */
209
+ export const RESULT_EXIT_GRACE_MS = 30_000;
210
+
197
211
  async function spawnInvocation(
198
212
  cli: string,
199
213
  invocation: CliInvocation,
@@ -201,6 +215,7 @@ async function spawnInvocation(
201
215
  signal?: AbortSignal,
202
216
  sidechannel?: SidechannelContext,
203
217
  cwd?: string,
218
+ completion?: (line: string) => { failed: boolean } | undefined,
204
219
  ): Promise<WorkerCliResult> {
205
220
  let writeInput: (bytes: Buffer) => Promise<boolean> = async () => false;
206
221
  let canDrive = () => false;
@@ -211,7 +226,9 @@ async function spawnInvocation(
211
226
  }, bytes => writeInput(bytes), () => canDrive());
212
227
  if (signal?.aborted) { channel?.close(); signal.throwIfAborted(); }
213
228
  return new Promise((resolve) => {
214
- const ownsGroup = ownsProcessGroup(signal);
229
+ // Always a group of its own off Windows, so every stop — and
230
+ // `reapOnExit` — reaches the whole agent tree, lease-bound or not.
231
+ const ownsGroup = process.platform !== 'win32';
215
232
  const child = spawn(cli, invocation.args, {
216
233
  stdio: ['pipe', 'pipe', 'pipe'], env,
217
234
  detached: ownsGroup,
@@ -232,16 +249,21 @@ async function spawnInvocation(
232
249
  child.stdin.write(bytes, error => resolve(!error));
233
250
  });
234
251
  const stop = childStop(child, ownsGroup);
252
+ const release = ownsGroup ? reapOnExit(stop) : () => {};
235
253
  const stdout: Buffer[] = [];
236
254
  const stderr: Buffer[] = [];
237
255
  let settled = false;
238
256
  let timer: NodeJS.Timeout | undefined;
257
+ let graceTimer: NodeJS.Timeout | undefined;
258
+ const decoder = new StringDecoder('utf8');
259
+ let partialLine = '';
239
260
  const finish = (result: WorkerCliResult): void => {
240
261
  if (settled) return;
241
262
  settled = true;
242
263
  if (inputTimer !== undefined) clearTimeout(inputTimer);
243
264
  channel?.close();
244
265
  if (timer !== undefined) clearTimeout(timer);
266
+ if (graceTimer !== undefined) clearTimeout(graceTimer);
245
267
  signal?.removeEventListener('abort', onAbort);
246
268
  resolve(result);
247
269
  };
@@ -263,13 +285,43 @@ async function spawnInvocation(
263
285
  };
264
286
  signal?.addEventListener('abort', onAbort, { once: true });
265
287
  if (signal?.aborted) onAbort();
266
- child.stdout.on('data', (chunk: Buffer) => { stdout.push(chunk); channel?.publish(chunk); });
288
+ /**
289
+ * The CLI has reported its final result. A normal exit inside the grace
290
+ * settles through `'close'` exactly as before, with the real exit code.
291
+ * Past it, the result settles the step with the exit status that result
292
+ * would have carried, and the stop outlives the settle as the timeout's
293
+ * does.
294
+ */
295
+ const onResult = (outcome: { failed: boolean }): void => {
296
+ if (graceTimer !== undefined || settled) return;
297
+ graceTimer = setTimeout(() => {
298
+ stop.terminate();
299
+ finish({
300
+ exit_code: outcome.failed ? 1 : 0,
301
+ stdout_tail: Buffer.concat(stdout).toString('utf8'),
302
+ stderr_tail: `${Buffer.concat(stderr).toString('utf8')}\nCLI reported its final result but had not exited ${RESULT_EXIT_GRACE_MS}ms later; its process tree was stopped.`.trim(),
303
+ });
304
+ }, RESULT_EXIT_GRACE_MS);
305
+ };
306
+ child.stdout.on('data', (chunk: Buffer) => {
307
+ stdout.push(chunk);
308
+ channel?.publish(chunk);
309
+ if (completion === undefined || graceTimer !== undefined) return;
310
+ const lines = (partialLine + decoder.write(chunk)).split('\n');
311
+ partialLine = lines.pop() ?? '';
312
+ for (const line of lines) {
313
+ const outcome = completion(line);
314
+ if (outcome !== undefined) onResult(outcome);
315
+ }
316
+ });
267
317
  child.stderr.on('data', (chunk: Buffer) => { stderr.push(chunk); channel?.publish(chunk); });
268
318
  child.once('error', (error) => finishOnChildExit({
269
319
  exit_code: null,
270
320
  stdout_tail: Buffer.concat(stdout).toString('utf8'),
271
321
  stderr_tail: error.message,
272
322
  }));
323
+ child.once('error', () => { if (child.pid === undefined) release(); });
324
+ child.once('close', release);
273
325
  child.once('close', (code) => finishOnChildExit({
274
326
  exit_code: code,
275
327
  stdout_tail: Buffer.concat(stdout).toString('utf8'),
@@ -31,6 +31,19 @@ export function decodeProviderResult(result: WorkerCliResult, kind: 'claude' | '
31
31
  return usageResult(result, terminal.usage, text);
32
32
  }
33
33
 
34
+ /**
35
+ * Claude's terminal `stream-json` frame, recognised line by line as it
36
+ * streams. `failed` mirrors the exit status Claude gives that result: an
37
+ * error flag or a non-success subtype is a failed run.
38
+ */
39
+ export function claudeResultOutcome(line: string): { failed: boolean } | undefined {
40
+ if (!line.includes('"result"')) return undefined;
41
+ let frame: unknown;
42
+ try { frame = JSON.parse(line); } catch { return undefined; }
43
+ if (!record(frame) || frame.type !== 'result') return undefined;
44
+ return { failed: frame.is_error === true || (typeof frame.subtype === 'string' && frame.subtype !== 'success') };
45
+ }
46
+
34
47
  /** Optional wrapper result envelope; existing opaque text output stays valid. */
35
48
  export function decodeWrapperResult(result: WorkerCliResult): WorkerCliResult {
36
49
  let value: unknown;