@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/cli/run.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type LoweredCompletionReason,
|
|
40
40
|
} from '../authored-completion.js';
|
|
41
41
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from '../worker-slots.js';
|
|
42
|
+
import { localAgentEnvironment } from '../local-agent-environment.js';
|
|
42
43
|
import {
|
|
43
44
|
checkFlow,
|
|
44
45
|
type CheckReport,
|
|
@@ -176,20 +177,22 @@ export async function runFlow(
|
|
|
176
177
|
dataDir: string,
|
|
177
178
|
options: RunLifecycleOptions = {},
|
|
178
179
|
): Promise<RunExecution> {
|
|
180
|
+
const agentEnvironment = localAgentEnvironment();
|
|
179
181
|
const prepared = parseDigestReference(path) ? await prepareDigestRun(path, options.bucket) : undefined;
|
|
180
182
|
if (prepared && 'exitCode' in prepared) return prepared;
|
|
181
|
-
const checked = prepared ?? checkFlow(path);
|
|
183
|
+
const checked = prepared ?? checkFlow(path, { environment: agentEnvironment });
|
|
182
184
|
if (!checked.report.ok || checked.flow === undefined) {
|
|
183
185
|
return { exitCode: 2, report: fromCheckReport('run', checked.report) };
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
return executeCheckedFlow(checked, dataDir, options);
|
|
188
|
+
return executeCheckedFlow(checked, dataDir, options, agentEnvironment);
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
async function executeCheckedFlow(
|
|
190
192
|
checked: ReturnType<typeof checkFlow>,
|
|
191
193
|
dataDir: string,
|
|
192
194
|
options: RunLifecycleOptions,
|
|
195
|
+
agentEnvironment?: NodeJS.ProcessEnv,
|
|
193
196
|
): Promise<RunExecution> {
|
|
194
197
|
const socketPath = socketFor(dataDir);
|
|
195
198
|
// Carry the preflight's diagnostics as a RunReport from here on, so the
|
|
@@ -213,12 +216,12 @@ async function executeCheckedFlow(
|
|
|
213
216
|
if (options.localAgent) {
|
|
214
217
|
localAgent = await attachLocalAgent(
|
|
215
218
|
client, dataDir, options.onPtyReady, undefined, options.agentCapacity,
|
|
216
|
-
undefined, declaredLocalAgentStreams(spec),
|
|
219
|
+
undefined, declaredLocalAgentStreams(spec), agentEnvironment,
|
|
217
220
|
);
|
|
218
221
|
}
|
|
219
222
|
if (options.localAgent && spec.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
220
223
|
const { attachCommunicationWorkers } = await import('../communication/local.js');
|
|
221
|
-
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir);
|
|
224
|
+
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir, agentEnvironment);
|
|
222
225
|
}
|
|
223
226
|
if (options.onJournalEntry !== undefined) client.on('entry', options.onJournalEntry);
|
|
224
227
|
const outcome = await startWatched(client, spec, options);
|
|
@@ -281,6 +284,9 @@ export async function resumeFlow(
|
|
|
281
284
|
dataDir: string,
|
|
282
285
|
options: RunLifecycleOptions = {},
|
|
283
286
|
): Promise<RunExecution> {
|
|
287
|
+
// Resume can load authored source; consume the descriptor before reading the
|
|
288
|
+
// journal so no source evaluation can race credential pickup.
|
|
289
|
+
const agentEnvironment = localAgentEnvironment();
|
|
284
290
|
const socketPath = socketFor(dataDir);
|
|
285
291
|
const base = emptyReport('resume');
|
|
286
292
|
const client = new JournalClient(socketPath);
|
|
@@ -323,11 +329,12 @@ export async function resumeFlow(
|
|
|
323
329
|
if (options.localAgent) {
|
|
324
330
|
authoredAgent = await attachLocalAgent(
|
|
325
331
|
client, dataDir, options.onPtyReady, authoredRoot.localAgentStream, workerCapacity,
|
|
332
|
+
undefined, [], agentEnvironment,
|
|
326
333
|
);
|
|
327
334
|
authoredLlmClient = new JournalClient(socketPath);
|
|
328
335
|
await authoredLlmClient.connect();
|
|
329
336
|
await authoredLlmClient.hello('flows-authored-resume-llm');
|
|
330
|
-
authoredLlm = new LlmWorker(authoredLlmClient, `${authoredAgent.stream}-llm`, workerCapacity);
|
|
337
|
+
authoredLlm = new LlmWorker(authoredLlmClient, `${authoredAgent.stream}-llm`, workerCapacity, agentEnvironment);
|
|
331
338
|
authoredLlm.on('error', onWorkerFailure('resume-llm', error => {
|
|
332
339
|
llmFailure = error;
|
|
333
340
|
client.close();
|
|
@@ -338,6 +345,7 @@ export async function resumeFlow(
|
|
|
338
345
|
dataDir,
|
|
339
346
|
localAgentStream: authoredAgent?.stream,
|
|
340
347
|
...(authoredAgent === undefined ? {} : { workerCapacity }),
|
|
348
|
+
...(agentEnvironment === undefined ? {} : { agentEnvironment }),
|
|
341
349
|
lifecycle: options,
|
|
342
350
|
});
|
|
343
351
|
if (result === undefined) throw new Error('authored root disappeared during resume');
|
|
@@ -356,11 +364,11 @@ export async function resumeFlow(
|
|
|
356
364
|
if (!spec) throw new Error('Cannot attach a local worker: the journaled run spec is missing');
|
|
357
365
|
authoredAgent = await attachLocalAgent(
|
|
358
366
|
client, dataDir, options.onPtyReady, undefined, options.agentCapacity,
|
|
359
|
-
undefined, declaredLocalAgentStreams(spec),
|
|
367
|
+
undefined, declaredLocalAgentStreams(spec), agentEnvironment,
|
|
360
368
|
);
|
|
361
369
|
if (spec?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
362
370
|
const { attachCommunicationWorkers } = await import('../communication/local.js');
|
|
363
|
-
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir);
|
|
371
|
+
communicationWorkers = await attachCommunicationWorkers(spec, socketPath, dataDir, agentEnvironment);
|
|
364
372
|
}
|
|
365
373
|
}
|
|
366
374
|
const onEntry = options.onJournalEntry;
|
|
@@ -400,6 +408,9 @@ export async function resumeFlow(
|
|
|
400
408
|
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
401
409
|
return authoredStepFailure('resume', base, socketPath, error, runId);
|
|
402
410
|
}
|
|
411
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
|
|
412
|
+
return rootLeaseLostReport('resume', base, socketPath, error, runId);
|
|
413
|
+
}
|
|
403
414
|
// An authored resume that parks for want of a worker is a park, reported
|
|
404
415
|
// like the run path's — and with the same remedy, because an authored root
|
|
405
416
|
// admits its worker at run start: resuming again cannot attach one, so the
|
|
@@ -514,6 +525,38 @@ function authoredInputArgument(metadata: AuthoredRootMetadata | undefined): Auth
|
|
|
514
525
|
* drove correctly, and it produces a report with no `status`, which is the
|
|
515
526
|
* whole of what `RUN <id> unknown` ever meant.
|
|
516
527
|
*/
|
|
528
|
+
/**
|
|
529
|
+
* The authored root's lease was lost and not re-dispatched to this process
|
|
530
|
+
* (authored-root.ts driveRootFollowingRetries). Exit 1 because this CLI did
|
|
531
|
+
* not finish the run, but the report says what is true of it: still
|
|
532
|
+
* `running` in the journal, and resumable -- never `protocol_error` with the
|
|
533
|
+
* generic "worker wait canceled" and no run id, which is what it was.
|
|
534
|
+
*/
|
|
535
|
+
export function rootLeaseLostReport(
|
|
536
|
+
command: RunCommand,
|
|
537
|
+
base: CheckReport | RunReport,
|
|
538
|
+
socketPath: string,
|
|
539
|
+
error: AuthoredFlowExecutionError,
|
|
540
|
+
fallbackRunId?: string,
|
|
541
|
+
): RunExecution {
|
|
542
|
+
const rootRunId = error.rootRunId ?? fallbackRunId;
|
|
543
|
+
return {
|
|
544
|
+
exitCode: 1,
|
|
545
|
+
report: {
|
|
546
|
+
...fromBase(command, base),
|
|
547
|
+
ok: false,
|
|
548
|
+
...(rootRunId === undefined ? {} : { runId: rootRunId, rootRunId }),
|
|
549
|
+
socketPath,
|
|
550
|
+
status: 'running',
|
|
551
|
+
diagnostics: [...base.diagnostics, {
|
|
552
|
+
severity: 'failure',
|
|
553
|
+
kind: 'root_lease_lost',
|
|
554
|
+
message: error.message.replace(/^root_lease_lost: /, ''),
|
|
555
|
+
}],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
517
560
|
export function authoredStepFailure(
|
|
518
561
|
command: RunCommand,
|
|
519
562
|
base: CheckReport | RunReport,
|
|
@@ -790,13 +833,27 @@ export async function classifyOutcome(
|
|
|
790
833
|
let parkedStep: ParkedStep | undefined;
|
|
791
834
|
let needsHuman = false;
|
|
792
835
|
let unclassifiedPolls = 0;
|
|
793
|
-
|
|
836
|
+
// `running` too: an authored body re-driven after a lost root lease re-admits
|
|
837
|
+
// its child by admission key and gets the EXISTING run back, mid-attempt.
|
|
838
|
+
// Only `parked` was followed, so that child -- on the attempt 2 the kernel
|
|
839
|
+
// retried it to -- fell straight through to "status running without a
|
|
840
|
+
// classifiable completion" and failed the flow (customer rw_3a0fcb71).
|
|
841
|
+
while (current.status === 'parked' || current.status === 'running') {
|
|
794
842
|
const inspection = await inspectOutOfBandStep(client, current.run_id);
|
|
795
843
|
// A runnable sibling may only be waiting for capacity held by a live
|
|
796
|
-
// attempt. Keep driving until that
|
|
797
|
-
|
|
844
|
+
// attempt or by a retry in backoff. Keep driving until that active work
|
|
845
|
+
// settles before declaring a park.
|
|
846
|
+
if (inspection?.parkedStep !== undefined && (inspection.needsHuman
|
|
847
|
+
|| (inspection.runningStep === undefined && inspection.backoffStep === undefined))) {
|
|
798
848
|
parkedStep = inspection.parkedStep;
|
|
799
849
|
needsHuman = inspection.needsHuman;
|
|
850
|
+
// `run.get` reports the live snapshot as `running` for an ordinary
|
|
851
|
+
// runnable worker wait, even when `run.start`/`run.resume` already
|
|
852
|
+
// classified the outcome as parked. Conversely, a re-admitted run can
|
|
853
|
+
// arrive here with a stale `running` outcome after its worker vanished.
|
|
854
|
+
// The step-level inspection is authoritative that this is a park; do
|
|
855
|
+
// not copy its broader run status and lose that classification.
|
|
856
|
+
current = { ...current, status: 'parked' };
|
|
800
857
|
break;
|
|
801
858
|
}
|
|
802
859
|
if (inspection?.runningStep !== undefined) {
|
|
@@ -804,6 +861,20 @@ export async function classifyOutcome(
|
|
|
804
861
|
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
805
862
|
continue;
|
|
806
863
|
}
|
|
864
|
+
// A step in `backoff` is between attempts: the kernel has scheduled the
|
|
865
|
+
// next one (a dead worker's retry is due at once, but waits for the
|
|
866
|
+
// worker slot the dead attempt held; a failed attempt waits out the
|
|
867
|
+
// retry policy's delay, bounded by its max_backoff_ms). That is progress,
|
|
868
|
+
// not an unclassifiable state -- treating it as one gave up after 2s on a
|
|
869
|
+
// child whose attempt 2 then succeeded (customer rw_3a0fcb71). Once due,
|
|
870
|
+
// the step is dispatched (`running`, followed above) or, with no worker,
|
|
871
|
+
// left `runnable` and reported as a park.
|
|
872
|
+
if (inspection?.backoffStep !== undefined && inspection.runningStep === undefined) {
|
|
873
|
+
throwIfCanceled(options.signal, inspection.backoffStep.id);
|
|
874
|
+
await delay(EXPIRED_LEASE_POLL_MS, options.signal);
|
|
875
|
+
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
807
878
|
if (inspection?.status === 'completed' || inspection?.status === 'failed') {
|
|
808
879
|
current = await client.runResume(current.run_id, command === 'run' || options.allowHumanInfluenced);
|
|
809
880
|
continue;
|
|
@@ -946,6 +1017,8 @@ interface OutOfBandInspection {
|
|
|
946
1017
|
parkedStep?: ParkedStep;
|
|
947
1018
|
needsHuman: boolean;
|
|
948
1019
|
runningStep?: RunningStep;
|
|
1020
|
+
/** A step waiting out a retry backoff: its next attempt is scheduled. */
|
|
1021
|
+
backoffStep?: ParkedStep;
|
|
949
1022
|
}
|
|
950
1023
|
|
|
951
1024
|
interface RunningStep extends ParkedStep {
|
|
@@ -973,6 +1046,9 @@ async function inspectOutOfBandStep(
|
|
|
973
1046
|
const runningEntry = entries.find(([, step]) =>
|
|
974
1047
|
step.type !== 'deterministic' && step.state === 'running',
|
|
975
1048
|
);
|
|
1049
|
+
const backoffEntry = entries.find(([, step]) =>
|
|
1050
|
+
step.type !== 'deterministic' && step.state === 'backoff',
|
|
1051
|
+
);
|
|
976
1052
|
const parkedEntry = humanEntry ?? runnableEntry;
|
|
977
1053
|
const parkedStep = parkedEntry === undefined ? undefined : {
|
|
978
1054
|
id: parkedEntry[0],
|
|
@@ -988,11 +1064,21 @@ async function inspectOutOfBandStep(
|
|
|
988
1064
|
needsHuman: humanEntry !== undefined,
|
|
989
1065
|
...(parkedStep !== undefined ? { parkedStep } : {}),
|
|
990
1066
|
...(runningStep !== undefined ? { runningStep } : {}),
|
|
1067
|
+
...(backoffEntry !== undefined ? { backoffStep: {
|
|
1068
|
+
id: backoffEntry[0],
|
|
1069
|
+
type: backoffEntry[1].type as Extract<StepType, 'llm' | 'agent'>,
|
|
1070
|
+
} } : {}),
|
|
991
1071
|
};
|
|
992
1072
|
}
|
|
993
1073
|
|
|
994
1074
|
// Match kernel/relayflowd/src/server/client.rs: allow the lease sweep to dispatch a retry.
|
|
995
1075
|
export const LEASE_SWEEP_GRACE_MS = 5_000;
|
|
1076
|
+
// How often to re-ask the daemon about a step whose lease looks expired here.
|
|
1077
|
+
const EXPIRED_LEASE_POLL_MS = 250;
|
|
1078
|
+
// A deadline unchanged for a whole lease (relayflowd LEASE_RENEWAL_MS, 30s)
|
|
1079
|
+
// plus the sweep grace was renewed by nobody: the attempt is dead and the
|
|
1080
|
+
// daemon never swept it. Fail rather than hang.
|
|
1081
|
+
const STALE_LEASE_MS = 30_000 + LEASE_SWEEP_GRACE_MS;
|
|
996
1082
|
|
|
997
1083
|
async function waitForRunningStep(
|
|
998
1084
|
client: JournalClient,
|
|
@@ -1010,20 +1096,29 @@ async function waitForRunningStep(
|
|
|
1010
1096
|
stepType: runningStep.type,
|
|
1011
1097
|
leaseDeadlineMs,
|
|
1012
1098
|
});
|
|
1099
|
+
// When the lease deadline last changed, on this process's monotonic clock.
|
|
1100
|
+
// A live worker renews every ~10s, moving the deadline; a dead lease the
|
|
1101
|
+
// daemon has not swept keeps it. That -- not a comparison of the daemon's
|
|
1102
|
+
// deadline with our wall clock -- is what separates the two under skew.
|
|
1103
|
+
let deadlineSeenAt = performance.now();
|
|
1013
1104
|
while (true) {
|
|
1014
1105
|
throwIfCanceled(options.signal, runningStep.id);
|
|
1015
1106
|
const remainingMs = leaseDeadlineMs + LEASE_SWEEP_GRACE_MS - Date.now();
|
|
1016
|
-
if (
|
|
1107
|
+
if (performance.now() - deadlineSeenAt > STALE_LEASE_MS) {
|
|
1017
1108
|
throw new Error(
|
|
1018
1109
|
`worker lease for step "${runningStep.id}" expired at ${leaseDeadlineMs} without completion`,
|
|
1019
1110
|
);
|
|
1020
1111
|
}
|
|
1021
|
-
|
|
1112
|
+
// Past the deadline by THIS clock alone is not an expiry: the daemon may
|
|
1113
|
+
// still hold the lease (skew) or be sweeping it into a retry. Keep
|
|
1114
|
+
// following the step, at a slower poll, until the daemon says otherwise.
|
|
1115
|
+
await delay(remainingMs <= 0 ? EXPIRED_LEASE_POLL_MS : Math.min(50, remainingMs), options.signal);
|
|
1022
1116
|
const snapshot = await client.runGet(runId);
|
|
1023
1117
|
const step = snapshot.steps[runningStep.id];
|
|
1024
1118
|
if (step?.state !== 'running') return;
|
|
1025
1119
|
if (step.lease_deadline_ms !== undefined && step.lease_deadline_ms !== leaseDeadlineMs) {
|
|
1026
1120
|
leaseDeadlineMs = step.lease_deadline_ms;
|
|
1121
|
+
deadlineSeenAt = performance.now();
|
|
1027
1122
|
options.onWait?.({
|
|
1028
1123
|
runId,
|
|
1029
1124
|
stepId: runningStep.id,
|
|
@@ -1090,7 +1185,14 @@ function errorMessage(error: unknown): string {
|
|
|
1090
1185
|
}
|
|
1091
1186
|
|
|
1092
1187
|
function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void {
|
|
1093
|
-
if (signal?.aborted === true)
|
|
1188
|
+
if (signal?.aborted === true) {
|
|
1189
|
+
throw new Error(`waiting for running step "${stepId}" was canceled${cancelCause(signal)}`, { cause: signal.reason });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Why a wait was canceled, when whoever aborted it said (a lost lease does). */
|
|
1194
|
+
function cancelCause(signal: AbortSignal): string {
|
|
1195
|
+
return signal.reason instanceof Error && signal.reason.name !== 'AbortError' ? `: ${signal.reason.message}` : '';
|
|
1094
1196
|
}
|
|
1095
1197
|
|
|
1096
1198
|
export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
@@ -1104,7 +1206,9 @@ export function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1104
1206
|
const cancel = (): void => {
|
|
1105
1207
|
clearTimeout(timer);
|
|
1106
1208
|
signal.removeEventListener('abort', cancel);
|
|
1107
|
-
|
|
1209
|
+
// Carries the abort reason: a bare "worker wait canceled" was all a
|
|
1210
|
+
// lost root lease ever reported (customer rw_3a0fcb71).
|
|
1211
|
+
rejectDelay(new Error(`worker wait canceled${cancelCause(signal)}`, { cause: signal.reason }));
|
|
1108
1212
|
};
|
|
1109
1213
|
if (signal.aborted) cancel();
|
|
1110
1214
|
else signal.addEventListener('abort', cancel, { once: true });
|
package/src/cli/step-failure.ts
CHANGED
|
@@ -63,6 +63,8 @@ export async function stepFailureDetails(
|
|
|
63
63
|
// Keyed by step, so interleaved steps never pool their attempts and a page
|
|
64
64
|
// boundary never splits one step's history.
|
|
65
65
|
const histories = new Map<string, AttemptHistory>();
|
|
66
|
+
// Attempts the kernel itself declared dead (see `unchargedAttempts`).
|
|
67
|
+
const uncharged = new Map<string, number>();
|
|
66
68
|
while (true) {
|
|
67
69
|
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
68
70
|
if (entries.length === 0) break;
|
|
@@ -93,6 +95,7 @@ export async function stepFailureDetails(
|
|
|
93
95
|
// holds every entry; only this diagnostic's candidate is cleared.
|
|
94
96
|
if (payload === undefined || typeof completionReason !== 'string' || completionReason === 'success') {
|
|
95
97
|
histories.delete(stepId);
|
|
98
|
+
uncharged.delete(stepId);
|
|
96
99
|
continue;
|
|
97
100
|
}
|
|
98
101
|
const history = histories.get(stepId) ?? { records: [], causes: [] };
|
|
@@ -101,6 +104,10 @@ export async function stepFailureDetails(
|
|
|
101
104
|
history.records.push(attemptFailure(entry['attempt'], completionReason, payload['disposition'], payload));
|
|
102
105
|
history.causes.push(failureCause(completionReason, payload));
|
|
103
106
|
histories.set(stepId, history);
|
|
107
|
+
if (payload['disposition'] !== 'step_done' && payload['completed_by'] === 'kernel'
|
|
108
|
+
&& (completionReason === 'crashed' || completionReason === 'lease_expired')) {
|
|
109
|
+
uncharged.set(stepId, (uncharged.get(stepId) ?? 0) + 1);
|
|
110
|
+
}
|
|
104
111
|
// A terminal completion that is not a success is the failure, whatever
|
|
105
112
|
// its step type. The old predicate also demanded a non-zero `exit_code`,
|
|
106
113
|
// which no agent completion carries and which a deterministic step that
|
|
@@ -116,6 +123,7 @@ export async function stepFailureDetails(
|
|
|
116
123
|
...(stepType === undefined ? {} : { stepType }),
|
|
117
124
|
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
118
125
|
...(maxIterations === undefined ? {} : { maxIterations }),
|
|
126
|
+
...((uncharged.get(stepId) ?? 0) > 0 ? { unchargedAttempts: uncharged.get(stepId) } : {}),
|
|
119
127
|
...terminalEvidence(payload),
|
|
120
128
|
// A single failed attempt is already fully described by the scalars
|
|
121
129
|
// above; repeating it as a one-element history would add a clause to
|
|
@@ -169,6 +177,16 @@ export function renderStepEvidence(details: StepFailedDetails): string {
|
|
|
169
177
|
|
|
170
178
|
function renderAttempt(details: StepFailedDetails): string {
|
|
171
179
|
if (details.attempt === undefined) return '';
|
|
180
|
+
const uncharged = details.unchargedAttempts ?? 0;
|
|
181
|
+
// `attempt=N/M` compares like with like only when every attempt was charged.
|
|
182
|
+
// A worker that died (crashed / lease_expired) costs no iteration, so a
|
|
183
|
+
// one-iteration step can legitimately run attempt 2 -- printed as
|
|
184
|
+
// `attempt=2/1` it read as the budget being overrun. Say which is which.
|
|
185
|
+
if (uncharged > 0 && uncharged < details.attempt) {
|
|
186
|
+
return ` attempt=${details.attempt}`
|
|
187
|
+
+ ` (iteration ${details.attempt - uncharged}${details.maxIterations === undefined ? '' : `/${details.maxIterations}`};`
|
|
188
|
+
+ ` ${uncharged} earlier attempt${uncharged === 1 ? '' : 's'} lost to a dead worker, not charged)`;
|
|
189
|
+
}
|
|
172
190
|
return ` attempt=${details.attempt}`
|
|
173
191
|
+ (details.maxIterations === undefined ? '' : `/${details.maxIterations}`);
|
|
174
192
|
}
|
package/src/cloud-run.ts
CHANGED
|
@@ -98,6 +98,7 @@ export async function prepareCloudSubmission(
|
|
|
98
98
|
options: { input?: JsonValue; signal?: AbortSignal } = {},
|
|
99
99
|
): Promise<CloudSubmission> {
|
|
100
100
|
let spec: FlowSpec | undefined;
|
|
101
|
+
let compiledKernelInput = false;
|
|
101
102
|
let authored: { source: string; authority: CloudAuthoredAuthority; name: string; schedules: ScheduleTriggerSource[]; extensions: readonly FlowExtensionSubmission[] } | undefined;
|
|
102
103
|
const inputPresent = Object.prototype.hasOwnProperty.call(options, 'input');
|
|
103
104
|
let authoredInput: JsonValue | undefined;
|
|
@@ -150,7 +151,11 @@ export async function prepareCloudSubmission(
|
|
|
150
151
|
// compatibility. If both fail, retain the original authoring diagnostic.
|
|
151
152
|
try {
|
|
152
153
|
spec = compileSpec(kernelToAuthoring(parsed));
|
|
153
|
-
|
|
154
|
+
compiledKernelInput = true;
|
|
155
|
+
} catch (kernelError) {
|
|
156
|
+
if (kernelError instanceof CompileError && kernelError.kind === 'untrusted_cli_identity') {
|
|
157
|
+
throw new CloudFlowError('invalid_input', kernelError.message);
|
|
158
|
+
}
|
|
154
159
|
throw error;
|
|
155
160
|
}
|
|
156
161
|
}
|
|
@@ -176,7 +181,9 @@ export async function prepareCloudSubmission(
|
|
|
176
181
|
const kernel = toKernelSpec(spec!);
|
|
177
182
|
// JSON is a YAML subset. Sending canonical data preserves the exact spec
|
|
178
183
|
// while using the server's existing YAML-to-config admission path.
|
|
179
|
-
|
|
184
|
+
// A compiled input stays in the kernel dialect. kernelToAuthoring refuses
|
|
185
|
+
// serialized cli_identity: only local preflight may mint that proof.
|
|
186
|
+
return { workflow: canonicalize(compiledKernelInput ? kernel : spec), fileType: 'yaml', inputPresent: false, specHash: specHash(kernel),
|
|
180
187
|
name: spec!.name ?? "flow", schedules: [] };
|
|
181
188
|
}
|
|
182
189
|
return {
|
|
@@ -8,7 +8,8 @@ import { loadRelayModules } from './relay.js';
|
|
|
8
8
|
import { checkCommunicationEnvironment, CommunicationEnvironmentError } from './preflight.js';
|
|
9
9
|
import { requireCommunicationCli } from './worker.js';
|
|
10
10
|
|
|
11
|
-
export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath: string, dataDir: string
|
|
11
|
+
export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath: string, dataDir: string,
|
|
12
|
+
environment?: NodeJS.ProcessEnv) {
|
|
12
13
|
const steps = spec.steps.filter(step => step.type === 'agent' && communicationInstruction(step.instruction));
|
|
13
14
|
checkCommunicationEnvironment(spec);
|
|
14
15
|
try { await loadRelayModules(); }
|
|
@@ -28,7 +29,7 @@ export async function attachCommunicationWorkers(spec: KernelRunSpec, socketPath
|
|
|
28
29
|
requireCommunicationCli(step.cli ?? spec.cli);
|
|
29
30
|
if (step.surfaces?.workspace?.length) throw new Error('Local communication workers support stream surfaces only');
|
|
30
31
|
const client = new JournalClient(socketPath);
|
|
31
|
-
const worker = new AgentWorker(client, { workerId: `communication-${randomUUID()}`, dataDir,
|
|
32
|
+
const worker = new AgentWorker(client, { workerId: `communication-${randomUUID()}`, dataDir, environment,
|
|
32
33
|
requiredStreams: [channelName(step.id, '$receipts')],
|
|
33
34
|
pins: { workspace: [], streams: step.surfaces?.streams?.map(({ stream }) => ({ stream, read_offset: 0 })) ?? [] } });
|
|
34
35
|
workers.push({ client, worker });
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { communicationHistory } from './history.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
3
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
5
|
import type { JournalClient } from '../journal-client.js';
|
|
5
6
|
import type { StepDispatchEvent } from '../protocol.js';
|
|
6
|
-
import type {
|
|
7
|
+
import type { ResolvedKernelAgentStep } from '../resolved-cli-identity.js';
|
|
7
8
|
import { withWorkerLease } from '../worker-lease.js';
|
|
8
9
|
import { workerInstruction } from '../worker-input.js';
|
|
9
10
|
import { resolveCliModel } from '../cli-adapter.js';
|
|
@@ -18,13 +19,15 @@ export function requireCommunicationCli(cli: string | undefined): void {
|
|
|
18
19
|
if (!cli?.trim()) throw new Error('Agent communication requires a declared CLI executable');
|
|
19
20
|
}
|
|
20
21
|
export async function completeCommunicationDispatch(client: JournalClient, dispatch: StepDispatchEvent,
|
|
21
|
-
instruction: CommunicationInstruction, dataDir: string, runRoot?: string
|
|
22
|
-
|
|
22
|
+
instruction: CommunicationInstruction, dataDir: string, runRoot?: string,
|
|
23
|
+
environment?: NodeJS.ProcessEnv): Promise<void> {
|
|
24
|
+
const spec = dispatch.spec as ResolvedKernelAgentStep;
|
|
23
25
|
requireCommunicationCli(spec.cli);
|
|
24
26
|
let output: unknown;
|
|
25
27
|
let completionReason: 'success' | 'worker_error' = 'success';
|
|
26
28
|
try {
|
|
27
|
-
output = await withWorkerLease(client, dispatch, signal =>
|
|
29
|
+
output = await withWorkerLease(client, dispatch, signal =>
|
|
30
|
+
run(client, dispatch, instruction, spec, dataDir, signal, runRoot, environment));
|
|
28
31
|
} catch (error) {
|
|
29
32
|
completionReason = 'worker_error';
|
|
30
33
|
output = { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -34,7 +37,8 @@ export async function completeCommunicationDispatch(client: JournalClient, dispa
|
|
|
34
37
|
started_pins: dispatch.pins, end_pins: dispatch.pins });
|
|
35
38
|
}
|
|
36
39
|
async function run(client: JournalClient, dispatch: StepDispatchEvent, instruction: CommunicationInstruction,
|
|
37
|
-
spec:
|
|
40
|
+
spec: ResolvedKernelAgentStep, dataDir: string, lease: AbortSignal, runRoot?: string,
|
|
41
|
+
environment?: NodeJS.ProcessEnv): Promise<unknown> {
|
|
38
42
|
// Same contract as the CLI worker: a declared directory is resolved and held
|
|
39
43
|
// inside the same run root the CLI worker measures against, before anything
|
|
40
44
|
// is spawned. Thrown, not reported, because `completeCommunicationDispatch`
|
|
@@ -50,6 +54,7 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
50
54
|
let pumping: Promise<void> | undefined;
|
|
51
55
|
let receipts = Promise.resolve();
|
|
52
56
|
let unsubscribe: (() => void) | undefined;
|
|
57
|
+
let cliLinkDirectory: string | undefined;
|
|
53
58
|
try {
|
|
54
59
|
let resolve!: (value: unknown) => void;
|
|
55
60
|
let reject!: (reason: unknown) => void;
|
|
@@ -77,10 +82,20 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
77
82
|
void receipts.catch(reject);
|
|
78
83
|
});
|
|
79
84
|
// Relay supplies each CLI's launch flags and injection behavior.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const cliIdentity = spec.cli_identity ?? spec.cli!;
|
|
86
|
+
const argv0 = basename(cliIdentity);
|
|
87
|
+
let command = spec.cli!;
|
|
88
|
+
if (basename(command) !== argv0) {
|
|
89
|
+
const linkRoot = join(dataDir, 'communication');
|
|
90
|
+
await mkdir(linkRoot, { recursive: true, mode: 0o700 });
|
|
91
|
+
cliLinkDirectory = await mkdtemp(join(linkRoot, 'cli-'));
|
|
92
|
+
command = join(cliLinkDirectory, argv0);
|
|
93
|
+
await symlink(spec.cli!, command, 'file');
|
|
94
|
+
}
|
|
95
|
+
handle = await relay.broker.spawnPty({ name, cli: basename(cliIdentity).replace(/\.exe$/i, ''), task: prompt, channels: [], skipRelayPrompt: true,
|
|
96
|
+
model: resolveCliModel(cliIdentity, spec.model), cwd: directory,
|
|
97
|
+
harnessConfig: { runtime: 'pty', command: quote(command), args: [],
|
|
98
|
+
cwd: directory, env: { ...agentEnvironment(cliIdentity, environment ?? process.env),
|
|
84
99
|
RELAYFLOW_COMMUNICATION_SOCKET: tools.path, RELAYFLOW_COMMUNICATION_TOKEN: tools.token },
|
|
85
100
|
delivery: { mode: 'pty-injection', format: 'relay-block' } } });
|
|
86
101
|
const ready = await handle.waitForReady(Math.min(instruction.timeoutMs, 90_000));
|
|
@@ -97,7 +112,10 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
97
112
|
finally {
|
|
98
113
|
try { await receipts; }
|
|
99
114
|
finally { try { await handle?.release('Flow communication attempt ended', { deleteIdentity: true }); }
|
|
100
|
-
finally { try { await tools?.close(); } finally {
|
|
115
|
+
finally { try { await tools?.close(); } finally {
|
|
116
|
+
try { await relay.close(); }
|
|
117
|
+
finally { if (cliLinkDirectory) await rm(cliLinkDirectory, { recursive: true, force: true }); }
|
|
118
|
+
} } }
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
}
|
package/src/compile.ts
CHANGED
|
@@ -24,10 +24,7 @@ import type {
|
|
|
24
24
|
AgentStepSpec,
|
|
25
25
|
DeterministicStepSpec,
|
|
26
26
|
FlowSpec,
|
|
27
|
-
KernelAgentStep,
|
|
28
27
|
KernelRunSpec,
|
|
29
|
-
KernelStepCommon,
|
|
30
|
-
KernelStepSpec,
|
|
31
28
|
KernelTriggerSpec,
|
|
32
29
|
KernelVerificationSpec,
|
|
33
30
|
LlmStepSpec,
|
|
@@ -45,6 +42,13 @@ import { validateSpec, type ValidationResult } from './validate.js';
|
|
|
45
42
|
import { snapshotJsonValue } from './json-value.js';
|
|
46
43
|
import { expandYamlHelpers } from './yaml-helpers.js';
|
|
47
44
|
import { expandCommunication, validateCommunicationTopology } from './communication/spec.js';
|
|
45
|
+
import {
|
|
46
|
+
inheritResolvedCliIdentities,
|
|
47
|
+
resolvedCliIdentities,
|
|
48
|
+
type ResolvedKernelAgentStep,
|
|
49
|
+
type ResolvedKernelStepCommon,
|
|
50
|
+
type ResolvedKernelStepSpec,
|
|
51
|
+
} from './resolved-cli-identity.js';
|
|
48
52
|
|
|
49
53
|
export class CompileError extends Error {
|
|
50
54
|
readonly errors: string[];
|
|
@@ -173,7 +177,7 @@ export function compileSpec(spec: unknown): CompiledFlowSpec {
|
|
|
173
177
|
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
174
178
|
...(input.tools !== undefined ? { tools: input.tools } : {}),
|
|
175
179
|
};
|
|
176
|
-
return flow;
|
|
180
|
+
return inheritResolvedCliIdentities(spec, flow);
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
function compileStep(step: StepSpec): StepSpec {
|
|
@@ -325,6 +329,7 @@ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
|
|
|
325
329
|
// This public boundary is callable without compileSpec. Compile again so
|
|
326
330
|
// runtime casts are validated and all returned schema data is snapshotted.
|
|
327
331
|
const compiled = compileSpec(flow);
|
|
332
|
+
const cliIdentities = resolvedCliIdentities(compiled);
|
|
328
333
|
|
|
329
334
|
return {
|
|
330
335
|
version: compiled.version,
|
|
@@ -347,7 +352,10 @@ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
|
|
|
347
352
|
// reverse order would hand the green gate a source whose barrier it does
|
|
348
353
|
// not wait for.
|
|
349
354
|
steps: lowerNamedGates(lowerStepsGreenGates(compiled.steps))
|
|
350
|
-
.map((step) => toKernelStep(
|
|
355
|
+
.map((step) => toKernelStep(
|
|
356
|
+
resolveNamedAgent(step, compiled.agents),
|
|
357
|
+
cliIdentities?.get(step.id),
|
|
358
|
+
)),
|
|
351
359
|
...(compiled.budget !== undefined ? { budget: toKernelBudget(compiled.budget) } : {}),
|
|
352
360
|
};
|
|
353
361
|
}
|
|
@@ -374,7 +382,7 @@ export function kernelToAuthoring(value: unknown): unknown {
|
|
|
374
382
|
const steps = requireKernelArray(root['steps'], 'spec.steps')
|
|
375
383
|
.map((step, index) => kernelStepToAuthoring(step, `spec.steps[${index}]`));
|
|
376
384
|
const triggers = root['triggers'];
|
|
377
|
-
|
|
385
|
+
const authoring: FlowSpec = {
|
|
378
386
|
...copyDefined(root, ['version', 'name', 'description', 'cli']),
|
|
379
387
|
...(triggers !== undefined
|
|
380
388
|
? {
|
|
@@ -386,7 +394,8 @@ export function kernelToAuthoring(value: unknown): unknown {
|
|
|
386
394
|
...(root['budget'] !== undefined
|
|
387
395
|
? { budget: kernelBudgetToAuthoring(root['budget'], 'spec.budget') }
|
|
388
396
|
: {}),
|
|
389
|
-
};
|
|
397
|
+
} as FlowSpec;
|
|
398
|
+
return authoring;
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
/**
|
|
@@ -443,17 +452,22 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
443
452
|
const unionKeys = [
|
|
444
453
|
'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
|
|
445
454
|
'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
|
|
446
|
-
'cwd', 'recovery_mode', 'surfaces', 'permissions',
|
|
455
|
+
'cwd', 'recovery_mode', 'surfaces', 'permissions', 'cli_identity',
|
|
447
456
|
] as const;
|
|
448
457
|
const step = requireKernelObject(value, unionKeys, at);
|
|
458
|
+
if (step['cli_identity'] !== undefined) {
|
|
459
|
+
throw new CompileError([
|
|
460
|
+
`${at}.cli_identity: host-proved adapter identity is not accepted from serialized input`,
|
|
461
|
+
], 'untrusted_cli_identity');
|
|
462
|
+
}
|
|
449
463
|
const type = step['type'];
|
|
450
464
|
const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input'] as const;
|
|
451
465
|
const typeKeys = type === 'deterministic'
|
|
452
466
|
? ['command', 'timeout_ms', 'lease_ms', 'on_non_zero'] as const
|
|
453
467
|
: type === 'llm'
|
|
454
|
-
? ['prompt', 'model', 'cli'] as const
|
|
468
|
+
? ['prompt', 'model', 'cli', 'cli_identity'] as const
|
|
455
469
|
: type === 'agent'
|
|
456
|
-
? ['instruction', 'cli', 'model', 'cwd', 'recovery_mode', 'surfaces', 'permissions'] as const
|
|
470
|
+
? ['instruction', 'cli', 'model', 'cwd', 'recovery_mode', 'surfaces', 'permissions', 'cli_identity'] as const
|
|
457
471
|
: [];
|
|
458
472
|
assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
|
|
459
473
|
if (step['retry'] !== undefined) validateAuthoringRetryDefaults(step['retry'], `${at}.retry`);
|
|
@@ -596,8 +610,8 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
596
610
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
597
611
|
}
|
|
598
612
|
|
|
599
|
-
function toKernelStep(step: StepSpec):
|
|
600
|
-
const common:
|
|
613
|
+
function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepSpec {
|
|
614
|
+
const common: ResolvedKernelStepCommon = {
|
|
601
615
|
id: step.id,
|
|
602
616
|
depends_on: step.input === undefined ? step.dependsOn ?? []
|
|
603
617
|
: [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])],
|
|
@@ -605,6 +619,7 @@ function toKernelStep(step: StepSpec): KernelStepSpec {
|
|
|
605
619
|
max_iterations: step.maxIterations ?? 1,
|
|
606
620
|
retry: { ...KERNEL_RETRY_DEFAULTS },
|
|
607
621
|
verification: toKernelVerification(step),
|
|
622
|
+
...(cliIdentity !== undefined ? { cli_identity: cliIdentity } : {}),
|
|
608
623
|
...(step.requirements !== undefined ? { requirements: {
|
|
609
624
|
...Object.fromEntries(Object.entries(step.requirements).filter(([key]) => key !== 'expectedDurationMs')),
|
|
610
625
|
...(step.requirements.expectedDurationMs !== undefined ? { expected_duration_ms: step.requirements.expectedDurationMs } : {}),
|
|
@@ -643,7 +658,7 @@ function toKernelStep(step: StepSpec): KernelStepSpec {
|
|
|
643
658
|
};
|
|
644
659
|
}
|
|
645
660
|
case 'agent': {
|
|
646
|
-
const out:
|
|
661
|
+
const out: ResolvedKernelAgentStep = {
|
|
647
662
|
...common,
|
|
648
663
|
type: 'agent',
|
|
649
664
|
instruction: step.instruction,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Exact decimal-dollar arithmetic shared by the journal folds.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sum two decimal dollar strings exactly, at whatever scale they carry — the
|
|
5
|
+
* kernel's budget fold (`relayflowd-core/src/state/budget.rs`) aligns every
|
|
6
|
+
* fractional digit before adding, and it accepts arbitrary precision. Scaling
|
|
7
|
+
* to a fixed six fractional digits dropped a charge of `0.0000001` to zero, so
|
|
8
|
+
* `flows status` underreported spend the kernel had totalled exactly.
|
|
9
|
+
*
|
|
10
|
+
* A value that is not a decimal number is left out of the sum rather than
|
|
11
|
+
* counted as zero, and the caller is told by `malformed`.
|
|
12
|
+
*/
|
|
13
|
+
export function addDollars(left: string, right: string): string {
|
|
14
|
+
const parsed = [left, right].map((value) => /^\d+(?:\.\d+)?$/.test(value) ? value : null);
|
|
15
|
+
const usable = parsed.filter((value): value is string => value !== null);
|
|
16
|
+
if (usable.length === 0) return '0';
|
|
17
|
+
if (usable.length === 1) return normalizeDollars(usable[0]!);
|
|
18
|
+
const scale = Math.max(...usable.map((value) => (value.split('.')[1] ?? '').length));
|
|
19
|
+
const scaled = usable.map((value) => {
|
|
20
|
+
const [whole, fraction = ''] = value.split('.');
|
|
21
|
+
return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
|
|
22
|
+
});
|
|
23
|
+
return fromScaled(scaled[0]! + scaled[1]!, scale);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `12.3400` -> `12.34`, `0.000` -> `0`; the journal's own spelling otherwise. */
|
|
27
|
+
function normalizeDollars(value: string): string {
|
|
28
|
+
const [whole, fraction = ''] = value.split('.');
|
|
29
|
+
return fromScaled(BigInt(`${whole}${fraction}`), fraction.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fromScaled(total: bigint, scale: number): string {
|
|
33
|
+
if (scale === 0) return String(total);
|
|
34
|
+
const digits = String(total).padStart(scale + 1, '0');
|
|
35
|
+
const whole = digits.slice(0, digits.length - scale);
|
|
36
|
+
const fraction = digits.slice(digits.length - scale).replace(/0+$/, '');
|
|
37
|
+
return fraction.length === 0 ? whole : `${whole}.${fraction}`;
|
|
38
|
+
}
|