@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.
- package/dist/agent-reaper.d.ts +4 -0
- package/dist/agent-reaper.d.ts.map +1 -0
- package/dist/agent-reaper.js +53 -0
- package/dist/agent-reaper.js.map +1 -0
- package/dist/authored-flow-error.d.ts +6 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +8 -0
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/child-stop.d.ts +1 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +68 -12
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/answer.d.ts.map +1 -1
- package/dist/cli/answer.js +2 -1
- package/dist/cli/answer.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/flow-requirements.d.ts +2 -2
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +182 -0
- package/dist/flow-requirements.js.map +1 -1
- package/dist/human-to.d.ts +45 -0
- package/dist/human-to.d.ts.map +1 -0
- package/dist/human-to.js +49 -0
- package/dist/human-to.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/worker-cli.d.ts +8 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +63 -8
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-usage.d.ts +8 -0
- package/dist/worker-usage.d.ts.map +1 -1
- package/dist/worker-usage.js +19 -0
- package/dist/worker-usage.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +52 -0
- package/src/authored-flow-error.ts +5 -0
- package/src/authored-flow-executor.ts +8 -0
- package/src/child-stop.ts +62 -12
- package/src/cli/answer.ts +2 -1
- package/src/cli/run.ts +2 -1
- package/src/flow-requirements.ts +143 -2
- package/src/human-to.ts +71 -0
- package/src/index.ts +1 -0
- package/src/worker-cli.ts +59 -7
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'),
|
package/src/worker-usage.ts
CHANGED
|
@@ -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;
|