@relayflows/sdk 2.0.11 → 2.0.13
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/adapters/base.d.ts +6 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/authored-admission.d.ts +5 -0
- package/dist/authored-admission.d.ts.map +1 -0
- package/dist/authored-admission.js +14 -0
- package/dist/authored-admission.js.map +1 -0
- package/dist/authored-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +18 -9
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +1 -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 +10 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +15 -7
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +20 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +86 -11
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +5 -3
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-mcp.d.ts +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +7 -4
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.d.ts +2 -0
- package/dist/authored-node-entry.d.ts.map +1 -0
- package/dist/authored-node-entry.js +111 -0
- package/dist/authored-node-entry.js.map +1 -0
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -0
- package/dist/authored-node-runner.js +246 -0
- package/dist/authored-node-runner.js.map +1 -0
- package/dist/authored-plugin-effect.d.ts +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +7 -4
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-root.d.ts +38 -0
- package/dist/authored-root.d.ts.map +1 -0
- package/dist/authored-root.js +329 -0
- package/dist/authored-root.js.map +1 -0
- package/dist/authored-runtime-capability.d.ts +6 -0
- package/dist/authored-runtime-capability.d.ts.map +1 -0
- package/dist/authored-runtime-capability.js +40 -0
- package/dist/authored-runtime-capability.js.map +1 -0
- package/dist/authored-source-authority.d.ts +4 -0
- package/dist/authored-source-authority.d.ts.map +1 -0
- package/dist/authored-source-authority.js +61 -0
- package/dist/authored-source-authority.js.map +1 -0
- package/dist/authored-worker-step.d.ts +2 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +11 -6
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts +28 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +34 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +8 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +5 -0
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +46 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +90 -12
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts +40 -0
- package/dist/cli/step-failure.d.ts.map +1 -0
- package/dist/cli/step-failure.js +150 -0
- package/dist/cli/step-failure.js.map +1 -0
- package/dist/cli-adapter.d.ts +16 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +17 -0
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -8
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +11 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +65 -19
- package/dist/cloud-run.js.map +1 -1
- package/dist/daemon-connection.d.ts +7 -0
- package/dist/daemon-connection.d.ts.map +1 -1
- package/dist/daemon-connection.js +7 -0
- package/dist/daemon-connection.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -7
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +6 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- 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/model-pricing.d.ts +7 -6
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +8 -6
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +9 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +36 -18
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +24 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +27 -11
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +6 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +13 -8
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +27 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/claude.ts +1 -0
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +38 -11
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +32 -7
- package/src/authored-flow-loader.ts +96 -13
- package/src/authored-helper-effect.ts +5 -3
- package/src/authored-mcp.ts +6 -4
- package/src/authored-node-entry.ts +90 -0
- package/src/authored-node-runner.ts +219 -0
- package/src/authored-plugin-effect.ts +6 -3
- package/src/authored-root.ts +396 -0
- package/src/authored-runtime-capability.ts +40 -0
- package/src/authored-source-authority.ts +62 -0
- package/src/authored-worker-step.ts +15 -4
- package/src/budget-preflight.ts +52 -25
- package/src/cli/build.ts +7 -0
- package/src/cli/check.ts +3 -1
- package/src/cli/deploy.ts +4 -0
- package/src/cli/direct-run.ts +52 -16
- package/src/cli/run.ts +101 -12
- package/src/cli/step-failure.ts +160 -0
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +9 -9
- package/src/cloud-run.ts +74 -21
- package/src/daemon-connection.ts +8 -0
- package/src/failure-kinds.ts +28 -2
- package/src/journal-client.ts +8 -3
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +8 -6
- package/src/preflight.ts +45 -18
- package/src/protocol.ts +17 -2
- package/src/spec.ts +28 -5
- package/src/worker-cli.ts +7 -5
- package/src/worker-spend.ts +32 -5
- package/src/worker.ts +4 -2
- package/dist/cli/deterministic-failure.d.ts +0 -5
- package/dist/cli/deterministic-failure.d.ts.map +0 -1
- package/dist/cli/deterministic-failure.js +0 -66
- package/dist/cli/deterministic-failure.js.map +0 -1
- package/src/cli/deterministic-failure.ts +0 -69
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEFAULT_DATA_DIR } from '../daemon-connection.js';
|
|
3
|
+
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
4
|
+
import type { JournalClient } from '../journal-client.js';
|
|
5
|
+
|
|
6
|
+
const TAIL_BYTES = 1_024;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read what a failed step left in the journal — for any step type.
|
|
10
|
+
*
|
|
11
|
+
* This was `deterministicFailureDetails`, and its first act was to return
|
|
12
|
+
* `undefined` for any run without a `deterministic` step. That is every
|
|
13
|
+
* `f.agent` and `f.llm` step, because each authored worker call runs as its
|
|
14
|
+
* own single-step kernel run (authored-worker-step.ts). So a failed agent
|
|
15
|
+
* reached the terminal carrying nothing but its taxonomy label — the run said
|
|
16
|
+
* `step_failed` and discarded every account of why, which is the whole reason
|
|
17
|
+
* a local agent failure was undiagnosable.
|
|
18
|
+
*
|
|
19
|
+
* The two step families leave their evidence in different fields, because the
|
|
20
|
+
* kernel preserves `output` on a failed completion only for deterministic
|
|
21
|
+
* steps (`preserve_failure_output`, relayflowd-core/src/machine.rs). For an
|
|
22
|
+
* agent or llm step the worker's `{exit_code, stdout_tail, stderr_tail}` is
|
|
23
|
+
* nulled out of `output` and survives only as the bounded render the daemon
|
|
24
|
+
* captured into `verification.detail` (`worker_failure_detail`,
|
|
25
|
+
* relayflowd/src/engine/remote.rs). Both are read, in that order, and the
|
|
26
|
+
* daemon's render is re-parsed when it carries that same shape: an exit code
|
|
27
|
+
* the daemon stringified on its way into the journal is still an exit code,
|
|
28
|
+
* and printing it as one is the difference between a diagnosis and a blob.
|
|
29
|
+
*/
|
|
30
|
+
export async function stepFailureDetails(
|
|
31
|
+
client: JournalClient,
|
|
32
|
+
runId: string,
|
|
33
|
+
): Promise<StepFailedDetails | undefined> {
|
|
34
|
+
const snapshot = await client.runGet(runId);
|
|
35
|
+
let fromSeq = 1;
|
|
36
|
+
const failures = new Map<string, StepFailedDetails>();
|
|
37
|
+
while (true) {
|
|
38
|
+
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
39
|
+
if (entries.length === 0) break;
|
|
40
|
+
for (const raw of entries) {
|
|
41
|
+
const entry = record(raw);
|
|
42
|
+
const seq = entry?.['seq'];
|
|
43
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
|
|
44
|
+
throw new Error('invalid journal sequence in step failure inspection');
|
|
45
|
+
}
|
|
46
|
+
fromSeq = seq + 1;
|
|
47
|
+
const stepId = entry?.['step_id'];
|
|
48
|
+
if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string') continue;
|
|
49
|
+
// A later completion supersedes an earlier failed attempt.
|
|
50
|
+
failures.delete(stepId);
|
|
51
|
+
const payload = record(entry['payload']);
|
|
52
|
+
if (payload === undefined) continue;
|
|
53
|
+
const completionReason = payload['completionReason'];
|
|
54
|
+
// A terminal completion that is not a success is the failure, whatever
|
|
55
|
+
// its step type. The old predicate also demanded a non-zero `exit_code`,
|
|
56
|
+
// which no agent completion carries and which a deterministic step that
|
|
57
|
+
// exits 0 and then fails its gate does not carry either — both were
|
|
58
|
+
// silently skipped.
|
|
59
|
+
if (payload['disposition'] !== 'step_done'
|
|
60
|
+
|| typeof completionReason !== 'string' || completionReason === 'success') continue;
|
|
61
|
+
const stepType = snapshot.steps[stepId]?.type;
|
|
62
|
+
failures.set(stepId, {
|
|
63
|
+
stepId,
|
|
64
|
+
completionReason,
|
|
65
|
+
...(stepType === undefined ? {} : { stepType }),
|
|
66
|
+
...evidence(payload),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...failures.values()].at(-1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where to look, derived from the run id and data dir ALONE.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately independent of the journal read: a failure whose evidence could
|
|
77
|
+
* not be read, or a failure shape nothing here recognises, must still end with
|
|
78
|
+
* somewhere to go rather than with a dead end. `flows replay` is that command —
|
|
79
|
+
* it already exists and already prints the full journal; nothing ever named it
|
|
80
|
+
* at the moment of failure, which is why the surface looked like it had no way
|
|
81
|
+
* to inspect a finished run.
|
|
82
|
+
*/
|
|
83
|
+
export function inspectionHint(
|
|
84
|
+
runId: string,
|
|
85
|
+
stepId: string | undefined,
|
|
86
|
+
dataDir: string | undefined,
|
|
87
|
+
): { hint: string; journalPath?: string } {
|
|
88
|
+
const at = stepId === undefined ? '' : ` --at ${shellQuote(stepId)}`;
|
|
89
|
+
// Only name a non-default data dir: repeating the default back at an
|
|
90
|
+
// operator who never typed it is noise, and `flows replay` defaults to the
|
|
91
|
+
// same value (cli.ts).
|
|
92
|
+
const dir = dataDir === undefined || dataDir === DEFAULT_DATA_DIR
|
|
93
|
+
? '' : ` --data-dir ${shellQuote(dataDir)}`;
|
|
94
|
+
return {
|
|
95
|
+
hint: `flows replay ${shellQuote(runId)}${at}${dir}`,
|
|
96
|
+
// Left as the operator spelled it rather than resolved: `.relayflowd/...`
|
|
97
|
+
// is what they will recognise in their own working directory.
|
|
98
|
+
...(dataDir === undefined ? {} : { journalPath: join(dataDir, 'runs', `${runId}.sqlite3`) }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pull the process-shaped fields out of whichever field carried them.
|
|
104
|
+
*
|
|
105
|
+
* `verification.detail` is last because it is the daemon's own render rather
|
|
106
|
+
* than the worker's structured report — but for an agent step it is the only
|
|
107
|
+
* thing that survives, so it is parsed when it parses and kept verbatim when
|
|
108
|
+
* it does not. A truncated render (the daemon caps at 2,000 chars and appends
|
|
109
|
+
* a truncation note) will not parse; that falls through to the raw string,
|
|
110
|
+
* which is still the account of what went wrong.
|
|
111
|
+
*/
|
|
112
|
+
function evidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
|
|
113
|
+
const detail = record(payload['verification'])?.['detail'];
|
|
114
|
+
const structured = record(payload['output'])
|
|
115
|
+
?? record(payload['trajectory_tail'])
|
|
116
|
+
?? (typeof detail === 'string' ? parsed(detail) : undefined);
|
|
117
|
+
const exitCode = structured?.['exit_code'];
|
|
118
|
+
const stdout = structured?.['stdout_tail'];
|
|
119
|
+
const stderr = structured?.['stderr_tail'];
|
|
120
|
+
const structuredShape = typeof exitCode === 'number'
|
|
121
|
+
|| typeof stdout === 'string' || typeof stderr === 'string';
|
|
122
|
+
return {
|
|
123
|
+
...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
|
|
124
|
+
...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: tail(stdout) } : {}),
|
|
125
|
+
...(typeof stderr === 'string' ? { stderrTail: tail(stderr) } : {}),
|
|
126
|
+
// Keep the daemon's account only when it was NOT just a render of the
|
|
127
|
+
// fields above — otherwise the same bytes print twice.
|
|
128
|
+
...(typeof detail === 'string' && detail.length > 0 && !structuredShape
|
|
129
|
+
? { detail: tail(detail) } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parsed(value: string): Record<string, unknown> | undefined {
|
|
134
|
+
try {
|
|
135
|
+
return record(JSON.parse(value));
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
142
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
143
|
+
? value as Record<string, unknown> : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function tail(value: string): string {
|
|
147
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
148
|
+
let start = Math.max(0, bytes.length - TAIL_BYTES);
|
|
149
|
+
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
150
|
+
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
151
|
+
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
152
|
+
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
153
|
+
return bytes.subarray(start).toString('utf8')
|
|
154
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shellQuote(value: string): string {
|
|
158
|
+
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
159
|
+
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
160
|
+
}
|
package/src/cli-adapter.ts
CHANGED
|
@@ -28,6 +28,33 @@ export function cliAdapterKind(executable: string): CliAdapterKind {
|
|
|
28
28
|
return resolveAdapterKind(executable);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export type CliModelSource = 'step' | 'named' | 'adapter';
|
|
32
|
+
|
|
33
|
+
export interface ResolvedCliModel {
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly source?: CliModelSource;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the model once, in authoring priority order: step, selected named
|
|
40
|
+
* agent, then the registered CLI adapter's default. Unregistered executables
|
|
41
|
+
* resolve through the wrapper adapter, whose absent default remains undefined.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveCliModelSelection(
|
|
44
|
+
executable: string,
|
|
45
|
+
declarations: Readonly<{ step?: string; named?: string }> = {},
|
|
46
|
+
): ResolvedCliModel {
|
|
47
|
+
if (declarations.step !== undefined) return { model: declarations.step, source: 'step' };
|
|
48
|
+
if (declarations.named !== undefined) return { model: declarations.named, source: 'named' };
|
|
49
|
+
const model = registeredAdapters()[resolveAdapterKind(executable)].defaultModel;
|
|
50
|
+
return model === undefined ? {} : { model, source: 'adapter' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolve a runtime model, where any materialized value is step-owned. */
|
|
54
|
+
export function resolveCliModel(executable: string, model?: string): string | undefined {
|
|
55
|
+
return resolveCliModelSelection(executable, { step: model }).model;
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
/** Prove the adapter command shape before classifying an auth failure. */
|
|
32
59
|
export function adapterIdentification(kind: CliAdapterKind): CliAdapterIdentification {
|
|
33
60
|
return registeredAdapters()[kind].buildIdentification();
|
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { parseDigestReference } from './bundle-transport.js';
|
|
|
30
30
|
import { parseBuildArgs, runBuild, type BuildArgs } from './cli/build.js';
|
|
31
31
|
import { runHnMonitor } from './cli/hn-monitor.js';
|
|
32
32
|
import { runTickRunner } from './cli/tick-runner.js';
|
|
33
|
+
import { DEFAULT_DATA_DIR } from './daemon-connection.js';
|
|
33
34
|
import {
|
|
34
35
|
mintObserverUrl,
|
|
35
36
|
resolveObserverLinkEnv,
|
|
@@ -53,14 +54,13 @@ type ParsedArgs =
|
|
|
53
54
|
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
|
|
54
55
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
55
56
|
| { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
56
|
-
| { command: 'resume'; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
57
|
+
| { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
57
58
|
| { command: 'observer'; dataDir: string }
|
|
58
59
|
| { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
|
|
59
60
|
| { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
|
|
60
61
|
intervalMs: number; epochMs: number | undefined; maxCatchUp: number | undefined;
|
|
61
62
|
pollIntervalMs: number | undefined };
|
|
62
63
|
|
|
63
|
-
const DEFAULT_DATA_DIR = '.relayflowd';
|
|
64
64
|
const USAGE = [
|
|
65
65
|
'Usage:',
|
|
66
66
|
'flows add <helper-name|@flows/helper-name>',
|
|
@@ -74,7 +74,7 @@ const USAGE = [
|
|
|
74
74
|
'flows run --cloud [--json] [--wait] <flow.yaml|spec.json>',
|
|
75
75
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
|
|
76
76
|
'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
|
|
77
|
-
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] <run-id>',
|
|
77
|
+
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
|
|
78
78
|
'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
|
|
79
79
|
'flows observer [--data-dir <dir>]',
|
|
80
80
|
'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
|
|
@@ -191,7 +191,7 @@ export async function runCli(
|
|
|
191
191
|
allowHumanInfluenced: parsed.allowHumanInfluenced,
|
|
192
192
|
onPtyReady: (path: string) => io.stderr(`PTY ${path}`),
|
|
193
193
|
...(parsed.command === 'run' && parsed.reuseFromRunId !== undefined ? { reuseFromRunId: parsed.reuseFromRunId } : {}),
|
|
194
|
-
localAgent: parsed.
|
|
194
|
+
localAgent: parsed.localAgent,
|
|
195
195
|
onProgress: showProgress,
|
|
196
196
|
onWait: (progress: RunProgress) => {
|
|
197
197
|
emitWait(progress, io);
|
|
@@ -299,7 +299,7 @@ async function observerUrlFrom(
|
|
|
299
299
|
if (mint === undefined) return undefined;
|
|
300
300
|
const outcome = await mint;
|
|
301
301
|
if (outcome.warning !== undefined) {
|
|
302
|
-
io.stderr(`[observer] token mint failed: ${outcome.warning};
|
|
302
|
+
io.stderr(`[observer] token mint failed: ${outcome.warning}; continuing without an observer link (the run is unaffected)`);
|
|
303
303
|
}
|
|
304
304
|
return outcome.observerUrl;
|
|
305
305
|
}
|
|
@@ -394,11 +394,11 @@ export async function finalizeObserverLine(
|
|
|
394
394
|
const outcome = await Promise.race([mint, timeout]);
|
|
395
395
|
if (timer !== undefined) clearTimeout(timer);
|
|
396
396
|
if (outcome === TIMED_OUT) {
|
|
397
|
-
io.stderr('[observer] mint did not complete in time;
|
|
397
|
+
io.stderr('[observer] mint did not complete in time; continuing without an observer link (the run is unaffected)');
|
|
398
398
|
return;
|
|
399
399
|
}
|
|
400
400
|
if (outcome.warning !== undefined) {
|
|
401
|
-
io.stderr(`[observer] token mint failed: ${outcome.warning};
|
|
401
|
+
io.stderr(`[observer] token mint failed: ${outcome.warning}; continuing without an observer link (the run is unaffected)`);
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
if (outcome.observerUrl !== undefined) io.stdout(`Observer: ${outcome.observerUrl}`);
|
|
@@ -455,7 +455,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
455
455
|
continue;
|
|
456
456
|
}
|
|
457
457
|
if (argument === '--local-agent') {
|
|
458
|
-
if (command
|
|
458
|
+
if (command === 'check' || localAgent) return undefined;
|
|
459
459
|
localAgent = true;
|
|
460
460
|
continue;
|
|
461
461
|
}
|
|
@@ -534,7 +534,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
534
534
|
? { command, json, watch, value: positionals[0]! }
|
|
535
535
|
: command === 'run'
|
|
536
536
|
? { command, bucket, reuseFromRunId, localAgent, dataDir, input, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! }
|
|
537
|
-
: { command, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
|
|
537
|
+
: { command, localAgent, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
|
package/src/cloud-run.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { RUN_COMPLETION_REASONS } from '@relayflows/surface';
|
|
2
2
|
import type { RunCompletionReason } from './protocol.js';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { setTimeout } from 'node:timers/promises';
|
|
5
6
|
import { parse as parseYaml } from 'yaml';
|
|
6
7
|
import { canonicalize, specHash } from './canonical.js';
|
|
7
8
|
import { CompileError, compileSpec, kernelToAuthoring, toKernelSpec } from './compile.js';
|
|
8
9
|
import type { FlowSpec } from './spec.js';
|
|
10
|
+
import { snapshotJsonValue, type JsonValue } from './json-value.js';
|
|
11
|
+
import { loadAuthoredFlow, type SurfaceModuleAuthority } from './authored-flow-loader.js';
|
|
9
12
|
import {
|
|
10
13
|
CloudFlowError, cloudConnection, cloudRequest, cloudRunId, isCloudRecord,
|
|
11
14
|
type CloudConnectionOptions,
|
|
@@ -15,6 +18,8 @@ export type CloudFlowSource = FlowSpec | { path: string };
|
|
|
15
18
|
export interface RunInCloudOptions extends CloudConnectionOptions {
|
|
16
19
|
/** Existing Cloud workspace to run in; provisioning/authorization remains server-side. */
|
|
17
20
|
workspaceId?: string;
|
|
21
|
+
/** Exact JSON input. Required at runtime for authored .flow.ts; refused for declarative source. */
|
|
22
|
+
input?: JsonValue;
|
|
18
23
|
}
|
|
19
24
|
export interface CloudRunReceipt {
|
|
20
25
|
runId: string;
|
|
@@ -24,8 +29,14 @@ export interface CloudRunReceipt {
|
|
|
24
29
|
/** Authenticated run API resource; this is not a public sharing URL. */
|
|
25
30
|
apiUrl: string;
|
|
26
31
|
}
|
|
32
|
+
export interface CloudAuthoredAuthority {
|
|
33
|
+
readonly schemaVersion: 1;
|
|
34
|
+
readonly sourceSha256: string;
|
|
35
|
+
readonly byteLength: number;
|
|
36
|
+
readonly surface: SurfaceModuleAuthority;
|
|
37
|
+
}
|
|
27
38
|
export type CloudRunState =
|
|
28
|
-
| { runId: string; status: 'pending' | 'running' }
|
|
39
|
+
| { runId: string; status: 'pending' | 'launching' | 'running' }
|
|
29
40
|
| { runId: string; status: 'completed' | 'failed' | 'cancelled'; completionReason: RunCompletionReason };
|
|
30
41
|
|
|
31
42
|
/**
|
|
@@ -38,43 +49,83 @@ export async function runInCloud(
|
|
|
38
49
|
options: RunInCloudOptions = {},
|
|
39
50
|
): Promise<CloudRunReceipt> {
|
|
40
51
|
const { baseUrl } = cloudConnection(options);
|
|
41
|
-
let spec: FlowSpec;
|
|
52
|
+
let spec: FlowSpec | undefined;
|
|
53
|
+
let authored: { source: string; authority: CloudAuthoredAuthority } | undefined;
|
|
54
|
+
const inputPresent = Object.prototype.hasOwnProperty.call(options, 'input');
|
|
55
|
+
let authoredInput: JsonValue | undefined;
|
|
42
56
|
try {
|
|
43
57
|
options.signal?.throwIfAborted();
|
|
44
58
|
if ('path' in flow) {
|
|
45
|
-
if (
|
|
59
|
+
if (/\.flow\.ts$/iu.test(flow.path)) {
|
|
60
|
+
const bytes = await readFile(flow.path);
|
|
61
|
+
const source = bytes.toString('utf8');
|
|
62
|
+
if (!bytes.length || Buffer.from(source, 'utf8').compare(bytes) !== 0) {
|
|
63
|
+
throw new CloudFlowError('invalid_input', 'Authored source must be nonempty, lossless UTF-8.');
|
|
64
|
+
}
|
|
65
|
+
const loaded = await loadAuthoredFlow(flow.path);
|
|
66
|
+
loaded.getDefinition(loaded.handle);
|
|
67
|
+
if (loaded.graph.length !== 1) {
|
|
68
|
+
throw new CloudFlowError('unsupported_source',
|
|
69
|
+
'Cloud authored submission currently accepts one self-contained .flow.ts source without use dependencies.');
|
|
70
|
+
}
|
|
71
|
+
authored = {
|
|
72
|
+
source,
|
|
73
|
+
authority: Object.freeze({
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
76
|
+
byteLength: bytes.length,
|
|
77
|
+
surface: loaded.surfaceAuthority,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
} else if (!/\.(?:ya?ml|json)$/iu.test(flow.path)) {
|
|
46
81
|
throw new CloudFlowError('unsupported_source',
|
|
47
|
-
'Cloud v2
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
spec = compileSpec(parsed);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (!(error instanceof CompileError)) throw error;
|
|
54
|
-
// Prefer the authoring dialect; accept compiled kernel JSON for local-CLI
|
|
55
|
-
// compatibility. If both fail, retain the original authoring diagnostic.
|
|
82
|
+
'Cloud v2 accepts declarative YAML/JSON specs or authored .flow.ts source.');
|
|
83
|
+
} else {
|
|
84
|
+
const parsed: unknown = parseYaml(await readFile(flow.path, 'utf8'));
|
|
56
85
|
try {
|
|
57
|
-
spec = compileSpec(
|
|
58
|
-
} catch {
|
|
59
|
-
throw error;
|
|
86
|
+
spec = compileSpec(parsed);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof CompileError)) throw error;
|
|
89
|
+
// Prefer the authoring dialect; accept compiled kernel JSON for local-CLI
|
|
90
|
+
// compatibility. If both fail, retain the original authoring diagnostic.
|
|
91
|
+
try {
|
|
92
|
+
spec = compileSpec(kernelToAuthoring(parsed));
|
|
93
|
+
} catch {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
60
96
|
}
|
|
61
97
|
}
|
|
62
98
|
} else {
|
|
63
99
|
spec = compileSpec(flow);
|
|
64
100
|
}
|
|
101
|
+
if (authored !== undefined) {
|
|
102
|
+
if (!inputPresent) {
|
|
103
|
+
throw new CloudFlowError('invalid_input', 'Cloud authored .flow.ts requires an explicit JSON input. Pass {} when the flow needs no fields.');
|
|
104
|
+
}
|
|
105
|
+
authoredInput = snapshotJsonValue(options.input, 'Cloud authored input');
|
|
106
|
+
} else if (inputPresent) {
|
|
107
|
+
throw new CloudFlowError('invalid_input', 'Cloud input is supported only for authored .flow.ts source.');
|
|
108
|
+
}
|
|
65
109
|
} catch (error) {
|
|
66
110
|
options.signal?.throwIfAborted();
|
|
67
111
|
if (error instanceof CloudFlowError) throw error;
|
|
68
112
|
throw new CloudFlowError('invalid_input', 'Cannot read or compile the declarative flow. Check the file path and YAML/JSON spec.');
|
|
69
113
|
}
|
|
70
114
|
options.signal?.throwIfAborted();
|
|
71
|
-
const hash =
|
|
115
|
+
const hash = authored === undefined
|
|
116
|
+
? specHash(toKernelSpec(spec!))
|
|
117
|
+
: createHash('sha256').update(canonicalize({
|
|
118
|
+
authority: authored.authority,
|
|
119
|
+
input: authoredInput,
|
|
120
|
+
})).digest('hex');
|
|
72
121
|
const result = await cloudRequest('/api/v1/workflows/run', options, {
|
|
73
122
|
// JSON is a YAML subset. Sending canonical data preserves the exact spec
|
|
74
123
|
// while using the server's existing YAML-to-config admission path.
|
|
75
|
-
workflow: canonicalize(spec),
|
|
76
|
-
fileType: 'yaml',
|
|
124
|
+
workflow: authored?.source ?? canonicalize(spec),
|
|
125
|
+
fileType: authored === undefined ? 'yaml' : 'ts',
|
|
77
126
|
relayflowVersion: 'v2',
|
|
127
|
+
...(authored === undefined ? {} : { authoredAuthority: authored.authority }),
|
|
128
|
+
...(authored === undefined ? {} : { inputs: authoredInput }),
|
|
78
129
|
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
79
130
|
});
|
|
80
131
|
if (!isCloudRecord(result) || (result.status !== 'pending' && result.status !== 'running')) {
|
|
@@ -92,10 +143,12 @@ export async function getCloudFlowRun(
|
|
|
92
143
|
const result = await cloudRequest(`/api/v1/workflows/runs/${runId}`, options);
|
|
93
144
|
if (!isCloudRecord(result) || result.runId !== runId || result.relayflowVersion !== 'v2'
|
|
94
145
|
|| typeof result.status !== 'string'
|
|
95
|
-
|| !['pending', 'running', 'completed', 'failed', 'cancelled'].includes(result.status)) {
|
|
146
|
+
|| !['pending', 'launching', 'running', 'completed', 'failed', 'cancelled'].includes(result.status)) {
|
|
96
147
|
throw new CloudFlowError('invalid_response', 'Cloud returned an invalid v2 run record.');
|
|
97
148
|
}
|
|
98
|
-
if (result.status === 'pending' || result.status === '
|
|
149
|
+
if (result.status === 'pending' || result.status === 'launching' || result.status === 'running') {
|
|
150
|
+
return { runId, status: result.status };
|
|
151
|
+
}
|
|
99
152
|
const report = result.result;
|
|
100
153
|
const reason = isCloudRecord(report) ? report.completionReason : undefined;
|
|
101
154
|
if (typeof reason !== 'string' || !(RUN_COMPLETION_REASONS as readonly string[]).includes(reason)
|
|
@@ -122,7 +175,7 @@ export async function waitForCloudFlowRun(
|
|
|
122
175
|
try {
|
|
123
176
|
const run = await getCloudFlowRun(runId, options);
|
|
124
177
|
failures = 0;
|
|
125
|
-
if (run.status !== 'pending' && run.status !== 'running') return run;
|
|
178
|
+
if (run.status !== 'pending' && run.status !== 'launching' && run.status !== 'running') return run;
|
|
126
179
|
} catch (error) {
|
|
127
180
|
options.signal?.throwIfAborted();
|
|
128
181
|
const transient = error instanceof CloudFlowError && (error.code === 'transient_error'
|
package/src/daemon-connection.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const CONNECTION_FILE = 'connection.json';
|
|
|
41
41
|
export const SOCKET_FILE = 'relayflowd.sock';
|
|
42
42
|
export const DAEMON_LOG_FILE = 'relayflowd.log';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The data dir every verb defaults to. It lives here, next to the other paths
|
|
46
|
+
* derived from it, so the CLI's `--data-dir` default and the failure
|
|
47
|
+
* diagnostic that decides whether to echo `--data-dir` back at an operator
|
|
48
|
+
* cannot drift apart.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_DATA_DIR = '.relayflowd';
|
|
51
|
+
|
|
44
52
|
/** `<data-dir>/connection.json`, exactly the shape in §1. */
|
|
45
53
|
export interface DaemonConnection {
|
|
46
54
|
socket_path: string;
|
package/src/failure-kinds.ts
CHANGED
|
@@ -14,7 +14,6 @@ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [
|
|
|
14
14
|
'mcp_undeclared_server',
|
|
15
15
|
'mcp_unreachable',
|
|
16
16
|
'budget_syntax_invalid',
|
|
17
|
-
'budget_missing_price',
|
|
18
17
|
'cli_missing',
|
|
19
18
|
'cli_unauthenticated',
|
|
20
19
|
'cli_unresolved',
|
|
@@ -61,12 +60,20 @@ export const CHECK_FAILURE_KINDS = [
|
|
|
61
60
|
* `vacuous_gate` is the same principle applied to a declared gate that judges
|
|
62
61
|
* nothing: `schema: {}` and `schema: true` are legal and accepted, but a gate
|
|
63
62
|
* accepting every output must not be reported as if it constrained one.
|
|
63
|
+
*
|
|
64
|
+
* `budget_unmetered` names an LLM/agent step under a dollar budget whose model
|
|
65
|
+
* has no frozen price (including Codex, which selects its own model). A
|
|
66
|
+
* missing price never refuses: the step runs, journals its tokens with
|
|
67
|
+
* `dollars_unmetered: true`, and cannot cross `maxDollars`; token limits still
|
|
68
|
+
* apply. It replaced the `budget_missing_price` refusal (#421), which older
|
|
69
|
+
* `flows check` reports may still show.
|
|
64
70
|
*/
|
|
65
71
|
export const PREFLIGHT_WARNING_KINDS = [
|
|
66
72
|
'unprovable_effects',
|
|
67
73
|
'command_unresolved',
|
|
68
74
|
'command_unprovable',
|
|
69
75
|
'vacuous_gate',
|
|
76
|
+
'budget_unmetered',
|
|
70
77
|
] as const;
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -118,13 +125,32 @@ export type PreflightWarningKind = (typeof PREFLIGHT_WARNING_KINDS)[number];
|
|
|
118
125
|
export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
|
|
119
126
|
export type RunWarningKind = (typeof RUN_WARNING_KINDS)[number];
|
|
120
127
|
|
|
121
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Optional evidence on the existing step_failed diagnostic, not a new kind.
|
|
130
|
+
*
|
|
131
|
+
* Every field is optional because a failure must be reportable on whatever it
|
|
132
|
+
* left behind. A deterministic step leaves `exitCode` plus output tails; an
|
|
133
|
+
* agent or llm step leaves `completionReason` and whatever the daemon captured
|
|
134
|
+
* into `detail` (see cli/step-failure.ts). Absent means "not journaled", never
|
|
135
|
+
* "zero" — an exit code is only ever reported when one was actually recorded.
|
|
136
|
+
*/
|
|
122
137
|
export interface StepFailedDetails {
|
|
123
138
|
stepId?: string;
|
|
139
|
+
/** `deterministic` | `llm` | `agent`, when the run snapshot named one. */
|
|
140
|
+
stepType?: string;
|
|
141
|
+
/** The kernel's per-step reason, e.g. `worker_error`, `retries_exhausted`. */
|
|
142
|
+
completionReason?: string;
|
|
124
143
|
exitCode?: number;
|
|
125
144
|
/** Terminal-safe UTF-8 excerpt, at most 1,024 bytes. */
|
|
145
|
+
stdoutTail?: string;
|
|
146
|
+
/** Terminal-safe UTF-8 excerpt, at most 1,024 bytes. */
|
|
126
147
|
stderrTail?: string;
|
|
148
|
+
/** The daemon's own account, when it was not a render of the fields above. */
|
|
149
|
+
detail?: string;
|
|
150
|
+
/** A runnable `flows replay` invocation for this run. */
|
|
127
151
|
hint?: string;
|
|
152
|
+
/** The on-disk journal for this run, when the data dir is known. */
|
|
153
|
+
journalPath?: string;
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
const CHECK_FAILURE_KIND_SET: ReadonlySet<string> = new Set(CHECK_FAILURE_KINDS);
|
package/src/journal-client.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type Request,
|
|
23
23
|
type Response,
|
|
24
24
|
type ServerEvent,
|
|
25
|
+
type StepUsage,
|
|
25
26
|
} from './protocol.js';
|
|
26
27
|
import type { KernelRunSpec, StepType } from './spec.js';
|
|
27
28
|
|
|
@@ -204,8 +205,12 @@ export class JournalClient extends EventEmitter {
|
|
|
204
205
|
* the run file, and append `run.spawned`. Authoring specs must be compiled
|
|
205
206
|
* with `toKernelSpec` before crossing this journal-protocol boundary.
|
|
206
207
|
*/
|
|
207
|
-
runStart(spec: KernelRunSpec, reuseFromRunId?: string): Promise<VerbContract['run.start']['result']> {
|
|
208
|
-
return this.request('run.start', {
|
|
208
|
+
runStart(spec: KernelRunSpec, reuseFromRunId?: string, admissionKey?: string): Promise<VerbContract['run.start']['result']> {
|
|
209
|
+
return this.request('run.start', {
|
|
210
|
+
spec,
|
|
211
|
+
...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
|
|
212
|
+
...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
|
|
213
|
+
}, null);
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
/** §3 memoized resume. */
|
|
@@ -362,7 +367,7 @@ export class JournalClient extends EventEmitter {
|
|
|
362
367
|
completionReason: CompletionReason,
|
|
363
368
|
extra: {
|
|
364
369
|
output?: unknown;
|
|
365
|
-
usage?:
|
|
370
|
+
usage?: StepUsage;
|
|
366
371
|
started_pins?: Pins;
|
|
367
372
|
end_pins?: Pins;
|
|
368
373
|
effects?: EffectRef[];
|
package/src/llm-worker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { JournalClient } from './journal-client.js';
|
|
|
5
5
|
import type { CompletionReason, StepDispatchEvent } from './protocol.js';
|
|
6
6
|
import type { KernelLlmStep } from './spec.js';
|
|
7
7
|
import { runAgentCli } from './worker-cli.js';
|
|
8
|
+
import { resolveCliModel } from './cli-adapter.js';
|
|
8
9
|
import { withWorkerLease } from './worker-lease.js';
|
|
9
10
|
import { workerInstruction } from './worker-input.js';
|
|
10
11
|
import { jsonSchemaOutputError } from './json-schema.js';
|
|
@@ -49,11 +50,12 @@ export class LlmWorker extends EventEmitter {
|
|
|
49
50
|
const schema = spec.verification?.json_schema;
|
|
50
51
|
const prompt = schema === undefined ? spec.prompt
|
|
51
52
|
: `${spec.prompt}\n\nReturn only a JSON value matching this JSON Schema (no Markdown fences):\n${JSON.stringify(schema)}`;
|
|
53
|
+
const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
|
|
52
54
|
const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
|
|
53
55
|
typeof spec.cli === 'string' && typeof spec.prompt === 'string'
|
|
54
|
-
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context,
|
|
56
|
+
? runAgentCli(spec.cli, workerInstruction(prompt, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'llm')
|
|
55
57
|
: Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'llm step has no declared CLI' }));
|
|
56
|
-
const { result, usage } = workerSpend(completed,
|
|
58
|
+
const { result, usage } = workerSpend(completed, effectiveModel);
|
|
57
59
|
let reason: CompletionReason = result.exit_code === 0 ? 'success' : 'worker_error';
|
|
58
60
|
let output: unknown = result.stdout_tail;
|
|
59
61
|
let detail = result.stderr_tail;
|
package/src/local-agent.ts
CHANGED
|
@@ -3,14 +3,19 @@ import type { JournalClient } from './journal-client.js';
|
|
|
3
3
|
import { AgentWorker } from './worker.js';
|
|
4
4
|
|
|
5
5
|
/** A local worker for stream-only steps; no workspace recovery is claimed. */
|
|
6
|
-
export async function attachLocalAgent(
|
|
6
|
+
export async function attachLocalAgent(
|
|
7
|
+
client: JournalClient,
|
|
8
|
+
dataDir?: string,
|
|
9
|
+
onPtyReady?: (path: string) => void,
|
|
10
|
+
requestedStream?: string,
|
|
11
|
+
): Promise<{
|
|
7
12
|
stream: string;
|
|
8
13
|
readonly failure: unknown;
|
|
9
14
|
close(): Promise<void>;
|
|
10
15
|
}> {
|
|
11
16
|
// A fresh, unconsumed stream has offset zero. The executor declares exactly
|
|
12
17
|
// this stream on its agent steps. No worktree revision is invented.
|
|
13
|
-
const stream = `local-agent-${randomUUID()}`;
|
|
18
|
+
const stream = requestedStream ?? `local-agent-${randomUUID()}`;
|
|
14
19
|
const worker = new AgentWorker(client, {
|
|
15
20
|
workerId: stream,
|
|
16
21
|
capacity: 1,
|
package/src/model-pricing.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export const MODEL_PRICING: Readonly<Record<string, Readonly<{ input: number; output: number }>>> = Object.freeze({
|
|
3
3
|
'claude-sonnet-4-6': Object.freeze({ input: 3, output: 15 }),
|
|
4
4
|
'claude-opus-4-7': Object.freeze({ input: 15, output: 75 }),
|
|
5
|
+
'claude-opus-5': Object.freeze({ input: 5, output: 25 }),
|
|
5
6
|
'codex-medium': Object.freeze({ input: 2, output: 8 }),
|
|
6
7
|
'codex-large': Object.freeze({ input: 5, output: 20 }),
|
|
7
8
|
});
|
|
@@ -13,12 +14,13 @@ export function hasPricing(model: string | undefined): boolean {
|
|
|
13
14
|
/**
|
|
14
15
|
* Cost accounting for a step's declared model.
|
|
15
16
|
*
|
|
16
|
-
* Returns `undefined` for unpriced models
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* preflight
|
|
17
|
+
* Returns `undefined` for unpriced models, meaning "no known dollar cost" —
|
|
18
|
+
* never a zero price. Workers do not omit usage on that signal: `workerSpend`
|
|
19
|
+
* falls back to unmetered usage (reported tokens plus `dollars_unmetered: true`,
|
|
20
|
+
* no `dollars`), so token ceilings still count the step and the journal records
|
|
21
|
+
* its dollars as unknown. An unpriced step is unmetered, not refused:
|
|
22
|
+
* `budgetDiagnostics` warns about it at preflight and it cannot cross
|
|
23
|
+
* `maxDollars`. Codex model ids need no entry here; Codex selects its own model.
|
|
22
24
|
*/
|
|
23
25
|
export function pricedUsage(model: string | undefined, input = 0, output = 0):
|
|
24
26
|
| { tokens_in: number; tokens_out: number; dollars: string }
|