@relayflows/sdk 2.0.10 → 2.0.12
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/agent-relay-receipt.d.ts +24 -0
- package/dist/agent-relay-receipt.d.ts.map +1 -0
- package/dist/agent-relay-receipt.js +66 -0
- package/dist/agent-relay-receipt.js.map +1 -0
- package/dist/agent-relay-state.d.ts +18 -0
- package/dist/agent-relay-state.d.ts.map +1 -0
- package/dist/agent-relay-state.js +58 -0
- package/dist/agent-relay-state.js.map +1 -0
- package/dist/agent-relay-transport.d.ts +17 -70
- package/dist/agent-relay-transport.d.ts.map +1 -1
- package/dist/agent-relay-transport.js +177 -112
- package/dist/agent-relay-transport.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 +3 -3
- 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 +10 -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 +17 -3
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +31 -24
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/check.js +3 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +18 -14
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +56 -1
- package/dist/cli/run.js.map +1 -1
- 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.js +4 -4
- 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/failure-kinds.d.ts +7 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +5 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +1 -1
- 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 +3 -4
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +4 -4
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +4 -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 +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +5 -4
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts +5 -2
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +43 -46
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts +4 -3
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +13 -4
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +12 -4
- 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/agent-relay-receipt.ts +120 -0
- package/src/agent-relay-state.ts +73 -0
- package/src/agent-relay-transport.ts +242 -156
- package/src/authored-admission.ts +17 -0
- package/src/authored-budget.ts +4 -3
- package/src/authored-flow-error.ts +0 -1
- package/src/authored-flow-executor.ts +27 -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 +41 -25
- package/src/cli/check.ts +3 -1
- package/src/cli/direct-run.ts +24 -16
- package/src/cli/run.ts +58 -1
- package/src/cli-adapter.ts +27 -0
- package/src/cli.ts +5 -5
- package/src/cloud-run.ts +74 -21
- package/src/failure-kinds.ts +5 -1
- package/src/journal-client.ts +6 -2
- package/src/llm-worker.ts +4 -2
- package/src/local-agent.ts +7 -2
- package/src/model-pricing.ts +4 -4
- package/src/preflight.ts +40 -18
- package/src/protocol.ts +2 -0
- package/src/spec.ts +5 -4
- package/src/worker-cli.ts +50 -54
- package/src/worker-spend.ts +15 -4
- package/src/worker.ts +12 -4
package/src/cli/direct-run.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { McpStepError } from '../authored-mcp.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { authoredLocalAgentStream } from '../authored-admission.js';
|
|
2
4
|
import { McpPreflightError } from './check-typescript.js';
|
|
3
5
|
import { attachLocalAgent } from '../local-agent.js';
|
|
4
6
|
import { LlmWorker } from '../llm-worker.js';
|
|
5
7
|
import {
|
|
6
8
|
AuthoredFlowExecutionError,
|
|
7
|
-
executeAuthoredFlow,
|
|
8
9
|
} from '../authored-flow-executor.js';
|
|
10
|
+
import { executeDurableAuthoredFlow } from '../authored-root.js';
|
|
9
11
|
import { AuthoredFlowLoadError } from '../authored-flow-loader.js';
|
|
10
12
|
import { DirectInputError, parseDirectInput } from '../direct-input.js';
|
|
11
13
|
import { JournalClient } from '../journal-client.js';
|
|
@@ -61,10 +63,13 @@ export async function runDirectFlow(
|
|
|
61
63
|
let localLlm: LlmWorker | undefined;
|
|
62
64
|
let llmClient: JournalClient | undefined;
|
|
63
65
|
let llmFailure: unknown;
|
|
66
|
+
const admissionIdentity = options.authoredAdmissionKey
|
|
67
|
+
?? process.env['RELAYFLOW_AUTHORED_ADMISSION_KEY'] ?? randomUUID();
|
|
64
68
|
try {
|
|
65
|
-
const { handle, getDefinition } = checked.loaded;
|
|
66
69
|
if (options.localAgent) {
|
|
67
|
-
localAgent = await attachLocalAgent(
|
|
70
|
+
localAgent = await attachLocalAgent(
|
|
71
|
+
client, dataDir, options.onPtyReady, authoredLocalAgentStream(admissionIdentity),
|
|
72
|
+
);
|
|
68
73
|
// A session owns one worker registration. Keep the workspace-free LLM
|
|
69
74
|
// worker on its own connection so it cannot replace the agent worker.
|
|
70
75
|
llmClient = new JournalClient(socketPath);
|
|
@@ -74,15 +79,19 @@ export async function runDirectFlow(
|
|
|
74
79
|
localLlm.on('error', error => { llmFailure = error; client.close(); });
|
|
75
80
|
await localLlm.attach();
|
|
76
81
|
}
|
|
77
|
-
const result = await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const result = await executeDurableAuthoredFlow(
|
|
83
|
+
checked.loaded, client, input,
|
|
84
|
+
{
|
|
85
|
+
dataDir,
|
|
86
|
+
admissionKey: admissionIdentity,
|
|
87
|
+
localAgentStream: localAgent?.stream,
|
|
88
|
+
lifecycle: {
|
|
89
|
+
onProgress: options.onProgress,
|
|
90
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
91
|
+
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
86
95
|
const terminal = result.journalSteps.at(-1);
|
|
87
96
|
if (terminal === undefined) {
|
|
88
97
|
return protocolFailure('run', base, socketPath, new Error(
|
|
@@ -93,7 +102,7 @@ export async function runDirectFlow(
|
|
|
93
102
|
return {
|
|
94
103
|
exitCode: 3,
|
|
95
104
|
report: {
|
|
96
|
-
...base, ok: false, runId:
|
|
105
|
+
...base, ok: false, runId: result.rootRunId, socketPath, status: 'parked',
|
|
97
106
|
completedSteps: result.journalSteps.length,
|
|
98
107
|
diagnostics: [...base.diagnostics, {
|
|
99
108
|
severity: 'parked', kind: 'run_parked',
|
|
@@ -107,7 +116,7 @@ export async function runDirectFlow(
|
|
|
107
116
|
report: {
|
|
108
117
|
...base,
|
|
109
118
|
ok: true,
|
|
110
|
-
runId:
|
|
119
|
+
runId: result.rootRunId,
|
|
111
120
|
socketPath,
|
|
112
121
|
status: 'completed',
|
|
113
122
|
completionReason: result.completionReason,
|
|
@@ -144,7 +153,7 @@ export async function runDirectFlow(
|
|
|
144
153
|
&& (error.code === 'helper_slack.credential_missing'
|
|
145
154
|
|| error.code === 'helper_slack.mount_required'
|
|
146
155
|
|| error.code === 'budget_syntax_invalid'
|
|
147
|
-
|| error.code === '
|
|
156
|
+
|| error.code === 'unsupported_promise_lifecycle'
|
|
148
157
|
|| error.code === 'unsupported_header'
|
|
149
158
|
|| error.code === 'agent_cli_unresolved'
|
|
150
159
|
|| error.code === 'llm_cli_unresolved'
|
|
@@ -157,7 +166,6 @@ export async function runDirectFlow(
|
|
|
157
166
|
error.code === 'helper_slack.credential_missing'
|
|
158
167
|
|| error.code === 'helper_slack.mount_required'
|
|
159
168
|
|| error.code === 'budget_syntax_invalid'
|
|
160
|
-
|| error.code === 'budget_missing_price'
|
|
161
169
|
) ? error.code : 'invalid_spec',
|
|
162
170
|
message: error.message,
|
|
163
171
|
}, path)),
|
package/src/cli/run.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { RunFailureKind, RunWarningKind, StepFailedDetails } from '../failu
|
|
|
14
14
|
import { deterministicFailureDetails } from './deterministic-failure.js';
|
|
15
15
|
import { JournalClient, JournalProtocolError } from '../journal-client.js';
|
|
16
16
|
import { attachLocalAgent } from '../local-agent.js';
|
|
17
|
+
import { LlmWorker } from '../llm-worker.js';
|
|
18
|
+
import { readAuthoredRootMetadata, resumeDurableAuthoredFlow } from '../authored-root.js';
|
|
17
19
|
import type {
|
|
18
20
|
RunCompletionReason,
|
|
19
21
|
RunOutcome,
|
|
@@ -68,6 +70,8 @@ export interface RunProgress {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface RunLifecycleOptions {
|
|
73
|
+
/** Caller-owned hosted invocation identity for authored-root start recovery. */
|
|
74
|
+
authoredAdmissionKey?: string;
|
|
71
75
|
bucket?: string;
|
|
72
76
|
allowHumanInfluenced?: boolean;
|
|
73
77
|
onPtyReady?: (path: string) => void;
|
|
@@ -151,7 +155,50 @@ export async function resumeFlow(
|
|
|
151
155
|
const connected = await connect(client, 'resume', dataDir, base, options);
|
|
152
156
|
if (connected !== undefined) return connected;
|
|
153
157
|
|
|
158
|
+
let authoredAgent: Awaited<ReturnType<typeof attachLocalAgent>> | undefined;
|
|
159
|
+
let authoredLlm: LlmWorker | undefined;
|
|
160
|
+
let authoredLlmClient: JournalClient | undefined;
|
|
154
161
|
try {
|
|
162
|
+
const authoredRoot = await readAuthoredRootMetadata(client, runId);
|
|
163
|
+
if (authoredRoot !== undefined) {
|
|
164
|
+
if (authoredRoot.localAgentStream !== undefined && !options.localAgent) {
|
|
165
|
+
throw new Error('authored root requires --local-agent to resume its pinned worker surface');
|
|
166
|
+
}
|
|
167
|
+
if (authoredRoot.localAgentStream === undefined && options.localAgent) {
|
|
168
|
+
throw new Error('authored root was started without a local agent worker surface');
|
|
169
|
+
}
|
|
170
|
+
if (options.localAgent) {
|
|
171
|
+
authoredAgent = await attachLocalAgent(
|
|
172
|
+
client, dataDir, options.onPtyReady, authoredRoot.localAgentStream,
|
|
173
|
+
);
|
|
174
|
+
authoredLlmClient = new JournalClient(socketPath);
|
|
175
|
+
await authoredLlmClient.connect();
|
|
176
|
+
await authoredLlmClient.hello('flows-authored-resume-llm');
|
|
177
|
+
authoredLlm = new LlmWorker(authoredLlmClient, `${authoredAgent.stream}-llm`);
|
|
178
|
+
await authoredLlm.attach();
|
|
179
|
+
}
|
|
180
|
+
const result = await resumeDurableAuthoredFlow(runId, client, {
|
|
181
|
+
dataDir,
|
|
182
|
+
localAgentStream: authoredAgent?.stream,
|
|
183
|
+
lifecycle: options,
|
|
184
|
+
});
|
|
185
|
+
if (result === undefined) throw new Error('authored root disappeared during resume');
|
|
186
|
+
return {
|
|
187
|
+
exitCode: result.completionReason === 'needs_human' ? 3 : 0,
|
|
188
|
+
report: {
|
|
189
|
+
...base,
|
|
190
|
+
ok: result.completionReason === 'success',
|
|
191
|
+
runId,
|
|
192
|
+
socketPath,
|
|
193
|
+
status: result.completionReason === 'needs_human' ? 'parked' : 'completed',
|
|
194
|
+
...(result.completionReason === 'success'
|
|
195
|
+
? { completionReason: 'success' as const }
|
|
196
|
+
: { diagnostics: [{ severity: 'parked' as const, kind: 'run_parked' as const,
|
|
197
|
+
message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.` }] }),
|
|
198
|
+
completedSteps: result.journalSteps.length,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
155
202
|
// resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
|
|
156
203
|
// slack effect resume plus every other provider from N's codegen. The
|
|
157
204
|
// second call the earlier rebase left is a stale reference from before
|
|
@@ -166,6 +213,10 @@ export async function resumeFlow(
|
|
|
166
213
|
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
167
214
|
diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } };
|
|
168
215
|
}
|
|
216
|
+
if (error instanceof AuthoredFlowExecutionError && error.code === 'unsupported_promise_lifecycle') {
|
|
217
|
+
return { exitCode: 2, report: { ...base, runId, socketPath,
|
|
218
|
+
diagnostics: [{ severity: 'refusal', kind: 'invalid_spec', message: error.message }] } };
|
|
219
|
+
}
|
|
169
220
|
if (error instanceof AuthoredFlowExecutionError
|
|
170
221
|
&& (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required'
|
|
171
222
|
|| error.code === 'helper_provider.mount_required' || error.code === 'helper_provider.unsupported')) {
|
|
@@ -189,7 +240,13 @@ export async function resumeFlow(
|
|
|
189
240
|
},
|
|
190
241
|
};
|
|
191
242
|
} finally {
|
|
192
|
-
|
|
243
|
+
try {
|
|
244
|
+
await authoredLlm?.close();
|
|
245
|
+
authoredLlmClient?.close();
|
|
246
|
+
await authoredAgent?.close();
|
|
247
|
+
} finally {
|
|
248
|
+
client.close();
|
|
249
|
+
}
|
|
193
250
|
}
|
|
194
251
|
}
|
|
195
252
|
|
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
|
@@ -53,7 +53,7 @@ type ParsedArgs =
|
|
|
53
53
|
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
|
|
54
54
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
55
55
|
| { 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 }
|
|
56
|
+
| { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
57
57
|
| { command: 'observer'; dataDir: string }
|
|
58
58
|
| { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
|
|
59
59
|
| { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
|
|
@@ -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);
|
|
@@ -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/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,17 @@ 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). Pricing
|
|
66
|
+
* is light enforcement: the step runs and simply contributes no dollars.
|
|
64
67
|
*/
|
|
65
68
|
export const PREFLIGHT_WARNING_KINDS = [
|
|
66
69
|
'unprovable_effects',
|
|
67
70
|
'command_unresolved',
|
|
68
71
|
'command_unprovable',
|
|
69
72
|
'vacuous_gate',
|
|
73
|
+
'budget_unmetered',
|
|
70
74
|
] as const;
|
|
71
75
|
|
|
72
76
|
/**
|
package/src/journal-client.ts
CHANGED
|
@@ -204,8 +204,12 @@ export class JournalClient extends EventEmitter {
|
|
|
204
204
|
* the run file, and append `run.spawned`. Authoring specs must be compiled
|
|
205
205
|
* with `toKernelSpec` before crossing this journal-protocol boundary.
|
|
206
206
|
*/
|
|
207
|
-
runStart(spec: KernelRunSpec, reuseFromRunId?: string): Promise<VerbContract['run.start']['result']> {
|
|
208
|
-
return this.request('run.start', {
|
|
207
|
+
runStart(spec: KernelRunSpec, reuseFromRunId?: string, admissionKey?: string): Promise<VerbContract['run.start']['result']> {
|
|
208
|
+
return this.request('run.start', {
|
|
209
|
+
spec,
|
|
210
|
+
...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
|
|
211
|
+
...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
|
|
212
|
+
}, null);
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
/** §3 memoized resume. */
|
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
|
});
|
|
@@ -15,10 +16,9 @@ export function hasPricing(model: string | undefined): boolean {
|
|
|
15
16
|
*
|
|
16
17
|
* Returns `undefined` for unpriced models — callers should omit `usage`
|
|
17
18
|
* from the journal payload rather than sending nulls that break the kernel
|
|
18
|
-
* wire schema.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* preflight was meant to prevent.
|
|
19
|
+
* wire schema. An unpriced step is unmetered, not refused: it contributes no
|
|
20
|
+
* dollars to a budget, and `budgetDiagnostics` warns about it at preflight.
|
|
21
|
+
* Codex model ids need no entry here; Codex selects its own model.
|
|
22
22
|
*/
|
|
23
23
|
export function pricedUsage(model: string | undefined, input = 0, output = 0):
|
|
24
24
|
| { tokens_in: number; tokens_out: number; dollars: string }
|