@relayflows/sdk 2.0.36 → 2.0.38
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 +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -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 +2 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +2 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +3 -2
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +2 -0
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +6 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +5 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +2 -0
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-preflight.d.ts +1 -1
- package/dist/authored-preflight.d.ts.map +1 -1
- package/dist/authored-preflight.js +2 -2
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-root.d.ts +2 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +146 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-verdict.d.ts +2 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +2 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +3 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.js.map +1 -1
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +28 -10
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +3 -2
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +79 -42
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +11 -3
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +106 -17
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -2
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +27 -9
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.js.map +1 -1
- package/dist/decimal-dollars.d.ts +12 -0
- package/dist/decimal-dollars.d.ts.map +1 -0
- package/dist/decimal-dollars.js +39 -0
- package/dist/decimal-dollars.js.map +1 -0
- package/dist/effect-channel.d.ts.map +1 -1
- package/dist/effect-channel.js +2 -0
- package/dist/effect-channel.js.map +1 -1
- package/dist/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.js.map +1 -1
- package/dist/helper-instruction.d.ts +3 -0
- package/dist/helper-instruction.d.ts.map +1 -0
- package/dist/helper-instruction.js +3 -0
- package/dist/helper-instruction.js.map +1 -0
- package/dist/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- package/dist/journal-client.d.ts +5 -2
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +12 -4
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts +2 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +10 -3
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent-environment.d.ts +13 -0
- package/dist/local-agent-environment.d.ts.map +1 -0
- package/dist/local-agent-environment.js +104 -0
- package/dist/local-agent-environment.js.map +1 -0
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +2 -2
- package/dist/local-agent.js.map +1 -1
- package/dist/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +30 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/reported-cost-total.d.ts +41 -0
- package/dist/reported-cost-total.d.ts.map +1 -0
- package/dist/reported-cost-total.js +63 -0
- package/dist/reported-cost-total.js.map +1 -0
- package/dist/reported-cost.d.ts +24 -0
- package/dist/reported-cost.d.ts.map +1 -0
- package/dist/reported-cost.js +49 -0
- package/dist/reported-cost.js.map +1 -0
- package/dist/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.js.map +1 -0
- package/dist/run-state.d.ts +9 -11
- package/dist/run-state.d.ts.map +1 -1
- package/dist/run-state.js +15 -38
- package/dist/run-state.js.map +1 -1
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +118 -42
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +8 -3
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.js.map +1 -1
- package/dist/yaml-helper-effect.d.ts.map +1 -1
- package/dist/yaml-helper-effect.js +2 -1
- package/dist/yaml-helper-effect.js.map +1 -1
- package/dist/yaml-helpers.d.ts.map +1 -1
- package/dist/yaml-helpers.js +2 -1
- package/dist/yaml-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-mcp.ts +2 -0
- package/src/authored-node-entry.ts +8 -1
- package/src/authored-node-runner.ts +5 -3
- package/src/authored-plugin-effect.ts +2 -0
- package/src/authored-preflight.ts +2 -2
- package/src/authored-root.ts +157 -7
- package/src/authored-verdict.ts +2 -1
- package/src/authored-worker-step.ts +3 -1
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +31 -7
- package/src/cli/cli-probe.ts +80 -42
- package/src/cli/direct-run.ts +11 -2
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +118 -14
- package/src/cli/step-failure.ts +18 -0
- package/src/cloud-run.ts +9 -2
- package/src/communication/local.ts +3 -2
- package/src/communication/worker.ts +29 -11
- package/src/compile.ts +28 -13
- package/src/decimal-dollars.ts +38 -0
- package/src/effect-channel.ts +2 -0
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/helper-instruction.ts +2 -0
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/journal-client.ts +14 -5
- package/src/llm-worker.ts +11 -4
- package/src/local-agent-environment.ts +92 -0
- package/src/local-agent.ts +2 -1
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +31 -0
- package/src/reported-cost-total.ts +82 -0
- package/src/reported-cost.ts +51 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/run-state.ts +25 -37
- package/src/worker-cli.ts +126 -39
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +14 -5
- package/src/wrapper-session.ts +89 -56
- package/src/yaml-helper-effect.ts +2 -1
- package/src/yaml-helpers.ts +2 -1
package/src/worker-cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js
|
|
|
4
4
|
import { claudeResultOutcome, decodeProviderResult, decodeWrapperResult, requirePricedUsage } from './worker-usage.js';
|
|
5
5
|
import { openSidechannel, type SidechannelContext } from './pty-sidechannel.js';
|
|
6
6
|
import { openTranscriptWriter, transcriptPath, type TranscriptDigest, type TranscriptFile, type TranscriptWriter } from './agent-transcript.js';
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
7
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
8
8
|
import { StringDecoder } from 'node:string_decoder';
|
|
9
9
|
import { childStop } from './child-stop.js';
|
|
10
10
|
import { reapOnExit } from './agent-reaper.js';
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
AgentRelayTransportError,
|
|
30
30
|
type AgentTransport,
|
|
31
31
|
} from './agent-relay-transport.js';
|
|
32
|
+
import { pinCliAlias } from './cli/pinned-cli-alias.js';
|
|
32
33
|
|
|
33
34
|
/** Present only when a dispatched agent step carries a journaled wake context. */
|
|
34
35
|
export const WAKE_CONTEXT_ENV = 'RELAYFLOW_WAKE_CONTEXT';
|
|
@@ -40,6 +41,11 @@ export const WAKE_CONTEXT_ENV = 'RELAYFLOW_WAKE_CONTEXT';
|
|
|
40
41
|
*/
|
|
41
42
|
export const MODEL_ENV = 'RELAYFLOW_MODEL';
|
|
42
43
|
|
|
44
|
+
/** Preserve a multicall adapter's authored basename while executing pinned bytes. */
|
|
45
|
+
export function cliInvocationArgv0(cli: string, cliIdentity?: string): string {
|
|
46
|
+
return basename(cliIdentity ?? cli);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export interface WorkerCliResult {
|
|
44
50
|
relay_task?: import('./agent-relay-receipt.js').RelayTaskReceipt;
|
|
45
51
|
tokens_input?: number;
|
|
@@ -91,13 +97,16 @@ export async function runAgentCli(
|
|
|
91
97
|
cwd?: string,
|
|
92
98
|
transport: AgentTransport = 'direct',
|
|
93
99
|
relayContext?: AgentRelayContext,
|
|
100
|
+
processEnvironment: NodeJS.ProcessEnv = process.env,
|
|
101
|
+
cliIdentity?: string,
|
|
94
102
|
): Promise<WorkerCliResult> {
|
|
95
103
|
signal?.throwIfAborted();
|
|
96
104
|
if (signal !== undefined && process.platform === 'win32') {
|
|
97
105
|
throw new Error('Lease-bound agent execution requires macOS or Linux process-group cancellation; Windows is unsupported.');
|
|
98
106
|
}
|
|
99
|
-
const kind = cliAdapterKind(cli);
|
|
100
|
-
const effectiveModel = resolveCliModel(cli, model);
|
|
107
|
+
const kind = cliAdapterKind(cliIdentity ?? cli);
|
|
108
|
+
const effectiveModel = resolveCliModel(cliIdentity ?? cli, model);
|
|
109
|
+
const argv0 = cliInvocationArgv0(cli, cliIdentity);
|
|
101
110
|
|
|
102
111
|
if (mode === 'agent' && transport === 'relay') {
|
|
103
112
|
return runViaAgentRelay(kind, instruction, wakeContext, effectiveModel, relayContext, cwd, signal);
|
|
@@ -134,7 +143,7 @@ export async function runAgentCli(
|
|
|
134
143
|
if (kind === 'relayflows-wrapper-v1') {
|
|
135
144
|
// The closed allowlist admits no ambient RELAYFLOW_* value; the four
|
|
136
145
|
// discovery names are set from this dispatch, exactly as for a direct spawn.
|
|
137
|
-
const wrapperEnv = wrapperEnvironment(
|
|
146
|
+
const wrapperEnv = wrapperEnvironment(processEnvironment);
|
|
138
147
|
applyStepEnvironment(wrapperEnv, sidechannel);
|
|
139
148
|
return requirePricedUsage(decodeWrapperResult(await runWrapperSession(
|
|
140
149
|
cli,
|
|
@@ -145,10 +154,11 @@ export async function runAgentCli(
|
|
|
145
154
|
wrapperLimits,
|
|
146
155
|
signal,
|
|
147
156
|
cwd,
|
|
157
|
+
argv0,
|
|
148
158
|
)), effectiveModel);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
const env: NodeJS.ProcessEnv = { ...
|
|
161
|
+
const env: NodeJS.ProcessEnv = { ...processEnvironment };
|
|
152
162
|
delete env[WAKE_CONTEXT_ENV];
|
|
153
163
|
delete env[MODEL_ENV];
|
|
154
164
|
// Where this attempt's journal is, so the agent can run `flows status` on
|
|
@@ -175,7 +185,7 @@ export async function runAgentCli(
|
|
|
175
185
|
const args = [...invocation.args];
|
|
176
186
|
args.splice(args.length - 1, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
|
|
177
187
|
const completion = kind === 'claude' ? claudeResultOutcome : undefined;
|
|
178
|
-
return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion), kind, env), effectiveModel);
|
|
188
|
+
return requirePricedUsage(decodeProviderResult(await spawnInvocation(cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion, argv0), kind, env), effectiveModel);
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
|
|
@@ -314,40 +324,71 @@ async function spawnInvocation(
|
|
|
314
324
|
sidechannel?: SidechannelContext,
|
|
315
325
|
cwd?: string,
|
|
316
326
|
completion?: (line: string) => { failed: boolean } | undefined,
|
|
327
|
+
argv0?: string,
|
|
317
328
|
): Promise<WorkerCliResult> {
|
|
318
329
|
let writeInput: (bytes: Buffer) => Promise<boolean> = async () => false;
|
|
319
330
|
let canDrive = () => false;
|
|
320
331
|
let driven = false;
|
|
332
|
+
// Prove and pin the authored identity before opening any per-attempt
|
|
333
|
+
// resources. If pinning fails there is then nothing else to unwind.
|
|
334
|
+
const pinned = await pinCliAlias(cli, argv0 ?? basename(cli));
|
|
335
|
+
if (signal?.aborted) {
|
|
336
|
+
pinned.release();
|
|
337
|
+
signal.throwIfAborted();
|
|
338
|
+
}
|
|
321
339
|
// The transcript file lives beside the PTY socket and is named by attempt.
|
|
322
340
|
// Its absence (no data dir, no attempt, unwritable dir) costs the step
|
|
323
341
|
// nothing: the digest is built from the buffered frames regardless. Opened
|
|
324
342
|
// BEFORE the sidechannel: once `onReady` has fired, a drive peer's HELLO may
|
|
325
343
|
// arrive at any time, and nothing may sit between that and the spawn that
|
|
326
344
|
// arms `canDrive`.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
onDrive() { driven = true; sidechannel.onDrive(); },
|
|
332
|
-
}, bytes => writeInput(bytes), () => canDrive());
|
|
333
|
-
// Tee the transcript into bounded tail files beside the socket. Evidence
|
|
334
|
-
// for `flows status --tail`, never the record; a failure here is a warning.
|
|
335
|
-
const tails = sidechannel === undefined ? undefined : openTails(sidechannel);
|
|
336
|
-
if (signal?.aborted) {
|
|
345
|
+
let writer: TranscriptWriter | undefined;
|
|
346
|
+
let channel: Awaited<ReturnType<typeof openSidechannel>> | undefined;
|
|
347
|
+
let tails: ReturnType<typeof openTails>;
|
|
348
|
+
const closeBeforeSpawn = async (): Promise<void> => {
|
|
337
349
|
channel?.close();
|
|
338
|
-
|
|
339
|
-
|
|
350
|
+
const closing = Promise.allSettled([
|
|
351
|
+
...(writer === undefined ? [] : [writer.close()]),
|
|
352
|
+
...(tails === undefined ? [] : [tails.stdout.close(), tails.stderr.close()]),
|
|
353
|
+
]);
|
|
354
|
+
pinned.release();
|
|
355
|
+
const deadline = new Promise<void>((done) => setTimeout(done, TAIL_CLOSE_TIMEOUT_MS).unref?.());
|
|
356
|
+
await Promise.race([closing, deadline]);
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
writer = sidechannel?.attempt === undefined ? undefined
|
|
360
|
+
: await openTranscriptWriter(transcriptPath(sidechannel, sidechannel.attempt), env);
|
|
361
|
+
channel = sidechannel === undefined ? undefined : await openSidechannel({
|
|
362
|
+
...sidechannel,
|
|
363
|
+
onDrive() { driven = true; sidechannel.onDrive(); },
|
|
364
|
+
}, bytes => writeInput(bytes), () => canDrive());
|
|
365
|
+
// Tee the transcript into bounded tail files beside the socket. Evidence
|
|
366
|
+
// for `flows status --tail`, never the record; a failure here is a warning.
|
|
367
|
+
tails = sidechannel === undefined ? undefined : openTails(sidechannel);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
await closeBeforeSpawn();
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
if (signal?.aborted) {
|
|
373
|
+
await closeBeforeSpawn();
|
|
340
374
|
signal.throwIfAborted();
|
|
341
375
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
376
|
+
// Always a group of its own off Windows, so every stop — and
|
|
377
|
+
// `reapOnExit` — reaches the whole agent tree, lease-bound or not.
|
|
378
|
+
const ownsGroup = process.platform !== 'win32';
|
|
379
|
+
let child: ChildProcessWithoutNullStreams;
|
|
380
|
+
try {
|
|
381
|
+
child = spawn(pinned.executable, invocation.args, {
|
|
347
382
|
stdio: ['pipe', 'pipe', 'pipe'], env,
|
|
348
383
|
detached: ownsGroup,
|
|
384
|
+
...(argv0 === undefined ? {} : { argv0 }),
|
|
349
385
|
...(cwd === undefined ? {} : { cwd }),
|
|
350
386
|
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
await closeBeforeSpawn();
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
return new Promise((resolve) => {
|
|
351
392
|
child.stdin.on('error', () => {});
|
|
352
393
|
if (channel === undefined) child.stdin.end();
|
|
353
394
|
canDrive = () => !child.stdin.destroyed && !child.stdin.writableEnded;
|
|
@@ -362,8 +403,7 @@ async function spawnInvocation(
|
|
|
362
403
|
// until they flush; the sidechannel pauses its reader in the meantime.
|
|
363
404
|
child.stdin.write(bytes, error => resolve(!error));
|
|
364
405
|
});
|
|
365
|
-
|
|
366
|
-
const release = ownsGroup ? reapOnExit(stop) : () => {};
|
|
406
|
+
let release = () => {};
|
|
367
407
|
const stdout: Buffer[] = [];
|
|
368
408
|
const stderr: Buffer[] = [];
|
|
369
409
|
let settled = false;
|
|
@@ -412,17 +452,66 @@ async function spawnInvocation(
|
|
|
412
452
|
? result : { ...result, transcript: { file: transcriptFile } });
|
|
413
453
|
}, () => resolve(result));
|
|
414
454
|
};
|
|
455
|
+
let pendingStopResult: {
|
|
456
|
+
result: WorkerCliResult;
|
|
457
|
+
discardTranscript: boolean;
|
|
458
|
+
priority: 'normal' | 'abort';
|
|
459
|
+
} | undefined;
|
|
460
|
+
const stop = childStop(child, ownsGroup, undefined, (stopError) => {
|
|
461
|
+
if (stopError !== undefined) {
|
|
462
|
+
finish({ exit_code: null, stdout_tail: '', stderr_tail: stopError.message }, true);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
pinned.release();
|
|
467
|
+
} catch (error) {
|
|
468
|
+
finish({
|
|
469
|
+
exit_code: null,
|
|
470
|
+
stdout_tail: '',
|
|
471
|
+
stderr_tail: `CLI invocation alias cleanup failed: ${String(error)}`,
|
|
472
|
+
}, true);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (pendingStopResult !== undefined) {
|
|
476
|
+
finish(pendingStopResult.result, pendingStopResult.discardTranscript);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
release = ownsGroup ? reapOnExit(stop, () => {
|
|
480
|
+
try { pinned.release(); } catch { /* host exit cannot report another result */ }
|
|
481
|
+
}) : () => {};
|
|
482
|
+
const finishAfterStop = (
|
|
483
|
+
result: WorkerCliResult,
|
|
484
|
+
action: 'kill' | 'terminate',
|
|
485
|
+
discardTranscript = false,
|
|
486
|
+
priority: 'normal' | 'abort' = 'normal',
|
|
487
|
+
): void => {
|
|
488
|
+
// Lease loss may replace an earlier result while its stop is pending;
|
|
489
|
+
// once it does, no later result-grace or execution timer may replace it.
|
|
490
|
+
if (pendingStopResult !== undefined) {
|
|
491
|
+
if (pendingStopResult.priority === 'abort' || priority === 'normal') return;
|
|
492
|
+
}
|
|
493
|
+
pendingStopResult = { result, discardTranscript, priority };
|
|
494
|
+
stop[action]();
|
|
495
|
+
};
|
|
415
496
|
const onAbort = (): void => {
|
|
416
|
-
|
|
417
|
-
|
|
497
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
498
|
+
timer = undefined;
|
|
499
|
+
if (graceTimer !== undefined) clearTimeout(graceTimer);
|
|
500
|
+
graceTimer = undefined;
|
|
501
|
+
finishAfterStop(
|
|
502
|
+
{ exit_code: null, stdout_tail: '', stderr_tail: 'Agent execution aborted: lease ownership lost.' },
|
|
503
|
+
'kill',
|
|
504
|
+
true,
|
|
505
|
+
'abort',
|
|
506
|
+
);
|
|
418
507
|
};
|
|
419
508
|
/**
|
|
420
509
|
* Same invariant as `wrapper-session.ts`: `'close'` and `'error'` are
|
|
421
510
|
* evidence about the DIRECT CHILD, so they may not settle over a pending
|
|
422
511
|
* escalation, and only `maySettleOnChildExit` may drop one. This settle
|
|
423
|
-
* carries no deadline of its own because it needs none —
|
|
424
|
-
* settles
|
|
425
|
-
*
|
|
512
|
+
* carries no deadline of its own because it needs none — a stop-owned
|
|
513
|
+
* result now settles from the shared confirmation callback, while a normal
|
|
514
|
+
* child exit can only defer to a `'close'` we are still going to get.
|
|
426
515
|
*/
|
|
427
516
|
const finishOnChildExit = (result: WorkerCliResult): void => {
|
|
428
517
|
if (!stop.maySettleOnChildExit()) return;
|
|
@@ -440,12 +529,11 @@ async function spawnInvocation(
|
|
|
440
529
|
const onResult = (outcome: { failed: boolean }): void => {
|
|
441
530
|
if (graceTimer !== undefined || settled) return;
|
|
442
531
|
graceTimer = setTimeout(() => {
|
|
443
|
-
|
|
444
|
-
finish({
|
|
532
|
+
finishAfterStop({
|
|
445
533
|
exit_code: outcome.failed ? 1 : 0,
|
|
446
534
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|
|
447
535
|
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(),
|
|
448
|
-
});
|
|
536
|
+
}, 'terminate');
|
|
449
537
|
}, RESULT_EXIT_GRACE_MS);
|
|
450
538
|
};
|
|
451
539
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
@@ -482,15 +570,14 @@ async function spawnInvocation(
|
|
|
482
570
|
}));
|
|
483
571
|
if (invocation.timeoutMs > 0) {
|
|
484
572
|
timer = setTimeout(() => {
|
|
485
|
-
// The stop
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
finish({
|
|
573
|
+
// The stop owns this settle: a direct-child event is not proof that its
|
|
574
|
+
// group is gone, and an unprovable forced stop must fail the step rather
|
|
575
|
+
// than arriving after a successful result has already won the race.
|
|
576
|
+
finishAfterStop({
|
|
490
577
|
exit_code: null,
|
|
491
578
|
stdout_tail: Buffer.concat(stdout).toString('utf8'),
|
|
492
579
|
stderr_tail: `CLI invocation timed out after ${invocation.timeoutMs}ms.`,
|
|
493
|
-
});
|
|
580
|
+
}, 'terminate');
|
|
494
581
|
}, invocation.timeoutMs);
|
|
495
582
|
}
|
|
496
583
|
});
|
package/src/worker-lease.ts
CHANGED
|
@@ -2,8 +2,12 @@ import { JournalProtocolError, type JournalClient } from './journal-client.js';
|
|
|
2
2
|
import type { StepDispatchEvent } from './protocol.js';
|
|
3
3
|
|
|
4
4
|
export class WorkerLeaseLostError extends Error {
|
|
5
|
-
constructor(
|
|
6
|
-
|
|
5
|
+
constructor(
|
|
6
|
+
readonly reason: 'already_expired' | 'renewal_expired' | 'completion_expired',
|
|
7
|
+
message: string,
|
|
8
|
+
options?: { cause?: unknown },
|
|
9
|
+
) {
|
|
10
|
+
super(message, options);
|
|
7
11
|
this.name = 'WorkerLeaseLostError';
|
|
8
12
|
}
|
|
9
13
|
}
|
|
@@ -36,13 +40,19 @@ export async function withWorkerLease<T>(
|
|
|
36
40
|
): Promise<T> {
|
|
37
41
|
const controller = new AbortController();
|
|
38
42
|
let stopped = false;
|
|
39
|
-
|
|
43
|
+
// Every deadline below is on THIS process's monotonic clock. The daemon
|
|
44
|
+
// issues `lease_deadline_ms` on its own wall clock, and comparing it with
|
|
45
|
+
// ours made the lease exactly as good as the two clocks' agreement: a
|
|
46
|
+
// worker 45s ahead refused every dispatch as already expired, one 75s
|
|
47
|
+
// behind scheduled its first renewal after the daemon had swept the lease
|
|
48
|
+
// (customer rw_3a0fcb71; tests/worker-lease-drift-live.test.ts).
|
|
49
|
+
let latestDeadline = Number.NaN;
|
|
40
50
|
let renewalTimer: NodeJS.Timeout | undefined;
|
|
41
51
|
let expiryTimer: NodeJS.Timeout | undefined;
|
|
42
52
|
let pending: Promise<void> = Promise.resolve();
|
|
43
53
|
const fail = (error: unknown): void => { controller.abort(error); };
|
|
44
54
|
const armExpiry = (deadline: number): number => {
|
|
45
|
-
const remaining = deadline -
|
|
55
|
+
const remaining = deadline - performance.now();
|
|
46
56
|
if (!Number.isFinite(remaining)) {
|
|
47
57
|
throw new Error(`Agent lease is already expired for ${dispatch.run_id}/${dispatch.step_id}.`);
|
|
48
58
|
}
|
|
@@ -57,16 +67,19 @@ export async function withWorkerLease<T>(
|
|
|
57
67
|
return remaining;
|
|
58
68
|
};
|
|
59
69
|
const renew = async (): Promise<void> => {
|
|
70
|
+
// Anchored BEFORE the request: the daemon starts the renewed lease no
|
|
71
|
+
// earlier than it receives this, so the local deadline errs early.
|
|
72
|
+
const sentAt = performance.now();
|
|
60
73
|
const result = await untilAborted(client.stepHeartbeat(
|
|
61
74
|
dispatch.run_id, dispatch.step_id, dispatch.attempt, dispatch.lease_id,
|
|
62
75
|
), controller.signal);
|
|
63
76
|
controller.signal.throwIfAborted();
|
|
64
77
|
// A response handled after local expiry cannot revive ownership, even
|
|
65
78
|
// if its future deadline was issued before this event loop stalled.
|
|
66
|
-
if (
|
|
79
|
+
if (performance.now() >= latestDeadline) {
|
|
67
80
|
throw new WorkerLeaseLostError('renewal_expired', `Agent lease expired before renewal for ${dispatch.run_id}/${dispatch.step_id}.`);
|
|
68
81
|
}
|
|
69
|
-
const remaining = armExpiry(result.lease_deadline_ms);
|
|
82
|
+
const remaining = armExpiry(localDeadline(result.lease_deadline_ms, result.lease_ttl_ms, sentAt));
|
|
70
83
|
if (!stopped) {
|
|
71
84
|
renewalTimer = setTimeout(() => {
|
|
72
85
|
pending = renew().catch(fail);
|
|
@@ -74,10 +87,26 @@ export async function withWorkerLease<T>(
|
|
|
74
87
|
}
|
|
75
88
|
};
|
|
76
89
|
try {
|
|
77
|
-
armExpiry(dispatch.lease_deadline_ms);
|
|
90
|
+
armExpiry(localDeadline(dispatch.lease_deadline_ms, dispatch.lease_ttl_ms, performance.now()));
|
|
78
91
|
// Establish ownership before starting an effectful process.
|
|
79
92
|
await renew();
|
|
80
|
-
|
|
93
|
+
let result: T;
|
|
94
|
+
try {
|
|
95
|
+
result = await execute(controller.signal);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// A body that rejected because the lease was lost under it reports
|
|
98
|
+
// whatever its wait happened to say ("worker wait canceled"). The lease
|
|
99
|
+
// is the cause, and callers branch on it (isLeaseLost): an attempt the
|
|
100
|
+
// kernel will retry must not be recorded as the body's own failure.
|
|
101
|
+
const lost = controller.signal.reason;
|
|
102
|
+
if (isLeaseLost(lost) && error !== lost) {
|
|
103
|
+
if (lost instanceof WorkerLeaseLostError) {
|
|
104
|
+
throw new WorkerLeaseLostError(lost.reason, lost.message, { cause: error });
|
|
105
|
+
}
|
|
106
|
+
throw lost;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
81
110
|
stopped = true;
|
|
82
111
|
if (renewalTimer !== undefined) clearTimeout(renewalTimer);
|
|
83
112
|
// Drain any renewal before the caller sends step.complete. A renewal
|
|
@@ -86,7 +115,7 @@ export async function withWorkerLease<T>(
|
|
|
86
115
|
controller.signal.throwIfAborted();
|
|
87
116
|
// Timer callbacks can be delayed behind a resolved subprocess promise.
|
|
88
117
|
// Check the clock itself before permitting step.complete.
|
|
89
|
-
if (
|
|
118
|
+
if (performance.now() >= latestDeadline) {
|
|
90
119
|
throw new WorkerLeaseLostError('completion_expired', `Agent lease expired before completion for ${dispatch.run_id}/${dispatch.step_id}.`);
|
|
91
120
|
}
|
|
92
121
|
return result;
|
|
@@ -99,6 +128,18 @@ export async function withWorkerLease<T>(
|
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
130
|
|
|
131
|
+
/**
|
|
132
|
+
* A daemon lease deadline on this process's monotonic clock. With the
|
|
133
|
+
* daemon's `lease_ttl_ms` the wall clocks never meet: the duration is added to
|
|
134
|
+
* `anchor`, a moment no later than the daemon started it. An older daemon
|
|
135
|
+
* sends only the absolute deadline, which keeps the old wall-clock comparison
|
|
136
|
+
* -- read NOW, not at `anchor`, or the round trip would be subtracted twice.
|
|
137
|
+
*/
|
|
138
|
+
function localDeadline(daemonDeadlineMs: number, ttlMs: number | undefined, anchor: number): number {
|
|
139
|
+
if (typeof ttlMs === 'number' && Number.isFinite(ttlMs)) return anchor + ttlMs;
|
|
140
|
+
return performance.now() + (daemonDeadlineMs - Date.now());
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
function untilAborted<T>(request: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
103
144
|
return new Promise((resolve, reject) => {
|
|
104
145
|
const abort = (): void => { reject(signal.reason); };
|
package/src/worker.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { communicationInstruction } from './communication/spec.js';
|
|
2
2
|
import { workerSpend } from './worker-spend.js';
|
|
3
|
+
import { reportedCost } from './reported-cost.js';
|
|
3
4
|
import type { WorkerCliResult } from './worker-cli.js';
|
|
4
5
|
import { EventEmitter } from 'node:events';
|
|
5
6
|
import type { JournalClient } from './journal-client.js';
|
|
6
7
|
import type { Pins, StepDispatchEvent } from './protocol.js';
|
|
7
|
-
import type {
|
|
8
|
+
import type { ResolvedKernelAgentStep } from './resolved-cli-identity.js';
|
|
8
9
|
import { runAgentCli } from './worker-cli.js';
|
|
9
10
|
import { agentStepCwd } from './agent-cwd.js';
|
|
10
11
|
import { resolveCliModel } from './cli-adapter.js';
|
|
@@ -31,6 +32,8 @@ export interface AgentWorkerOptions {
|
|
|
31
32
|
* the root a dispatch is measured against is a value, not an assumption.
|
|
32
33
|
*/
|
|
33
34
|
runRoot?: string;
|
|
35
|
+
/** Environment inherited by provider CLIs, including agent-only secrets. */
|
|
36
|
+
environment?: NodeJS.ProcessEnv;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -111,7 +114,7 @@ export class AgentWorker extends EventEmitter {
|
|
|
111
114
|
};
|
|
112
115
|
|
|
113
116
|
private async execute(dispatch: StepDispatchEvent): Promise<void> {
|
|
114
|
-
const spec = dispatch.spec as Partial<
|
|
117
|
+
const spec = dispatch.spec as Partial<ResolvedKernelAgentStep>;
|
|
115
118
|
const helper = helperCall(spec);
|
|
116
119
|
if (helper !== undefined) {
|
|
117
120
|
if (this.options.dataDir === undefined) throw new Error('Helper worker requires a data directory for durable receipts');
|
|
@@ -122,11 +125,14 @@ export class AgentWorker extends EventEmitter {
|
|
|
122
125
|
if (communication) {
|
|
123
126
|
if (!this.options.dataDir) throw new Error('Agent communication requires a worker data directory');
|
|
124
127
|
const { completeCommunicationDispatch } = await import('./communication/worker.js');
|
|
125
|
-
await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir,
|
|
128
|
+
await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir,
|
|
129
|
+
this.options.runRoot, this.options.environment);
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
let humanIntervention = false;
|
|
129
|
-
const effectiveModel = typeof spec.cli === 'string'
|
|
133
|
+
const effectiveModel = typeof spec.cli === 'string'
|
|
134
|
+
? resolveCliModel(spec.cli_identity ?? spec.cli, spec.model)
|
|
135
|
+
: spec.model;
|
|
130
136
|
// Resolved here, before the lease, because this is the process that shares
|
|
131
137
|
// the agent's filesystem. A refusal completes the step the way a missing
|
|
132
138
|
// CLI does — journaled as `worker_error` with the reason — rather than
|
|
@@ -142,9 +148,11 @@ export class AgentWorker extends EventEmitter {
|
|
|
142
148
|
}, cwd.directory ?? (spec.transport === 'relay' ? undefined : this.options.runRoot),
|
|
143
149
|
spec.transport === 'relay' ? 'relay' : 'direct',
|
|
144
150
|
{ runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
|
|
145
|
-
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema }
|
|
151
|
+
dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema },
|
|
152
|
+
this.options.environment, spec.cli_identity)
|
|
146
153
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
|
|
147
154
|
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
155
|
+
const cost = reportedCost(completed, effectiveModel);
|
|
148
156
|
const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
149
157
|
|
|
150
158
|
// Output shape: if the CLI's stdout parses as JSON, promote THAT
|
|
@@ -195,6 +203,7 @@ export class AgentWorker extends EventEmitter {
|
|
|
195
203
|
...(Object.keys(trajectoryTail).length === 0 ? {} : { trajectory_tail: trajectoryTail }),
|
|
196
204
|
...(humanIntervention ? { human_intervention: true } : {}),
|
|
197
205
|
...(usage !== undefined ? { usage } : {}),
|
|
206
|
+
...(cost === undefined ? {} : { reported_cost: cost }),
|
|
198
207
|
started_pins: dispatch.pins,
|
|
199
208
|
end_pins: dispatch.pins,
|
|
200
209
|
},
|