@relayflows/sdk 2.0.14 → 2.0.16
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-artifacts.d.ts +20 -0
- package/dist/agent-artifacts.d.ts.map +1 -0
- package/dist/agent-artifacts.js +81 -0
- package/dist/agent-artifacts.js.map +1 -0
- package/dist/authored-flow-executor.d.ts +33 -3
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +56 -10
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +11 -4
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +2 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +2 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +5 -1
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +19 -1
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +14 -3
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.js +6 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +30 -0
- package/dist/cli/cloud-deploy.d.ts.map +1 -0
- package/dist/cli/cloud-deploy.js +162 -0
- package/dist/cli/cloud-deploy.js.map +1 -0
- package/dist/cli/cloud-run.d.ts +3 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +37 -5
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-sync.d.ts +13 -0
- package/dist/cli/cloud-sync.d.ts.map +1 -0
- package/dist/cli/cloud-sync.js +39 -0
- package/dist/cli/cloud-sync.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +2 -26
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +20 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +77 -15
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +82 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +70 -0
- package/dist/cloud-deploy.d.ts.map +1 -0
- package/dist/cloud-deploy.js +181 -0
- package/dist/cloud-deploy.js.map +1 -0
- package/dist/cloud-http.d.ts +37 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +98 -8
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +23 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +30 -1
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-sync.d.ts +66 -0
- package/dist/cloud-sync.d.ts.map +1 -0
- package/dist/cloud-sync.js +287 -0
- package/dist/cloud-sync.js.map +1 -0
- package/dist/failure-kinds.d.ts +8 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +8 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/provider-trigger-contract.d.ts +27 -0
- package/dist/provider-trigger-contract.d.ts.map +1 -0
- package/dist/provider-trigger-contract.js +0 -0
- package/dist/provider-trigger-contract.js.map +1 -0
- package/package.json +2 -2
- package/src/agent-artifacts.ts +76 -0
- package/src/authored-flow-executor.ts +77 -15
- package/src/authored-node-runner.ts +11 -4
- package/src/authored-root.ts +2 -2
- package/src/authored-step-output.ts +6 -1
- package/src/authored-worker-step.ts +19 -1
- package/src/cli/check-triggers.ts +14 -5
- package/src/cli/check.ts +6 -1
- package/src/cli/cloud-deploy.ts +157 -0
- package/src/cli/cloud-run.ts +37 -6
- package/src/cli/cloud-sync.ts +37 -0
- package/src/cli/direct-run.ts +2 -25
- package/src/cli/run.ts +86 -16
- package/src/cli.ts +75 -12
- package/src/cloud-deploy.ts +236 -0
- package/src/cloud-http.ts +129 -9
- package/src/cloud-run.ts +47 -1
- package/src/cloud-sync.ts +315 -0
- package/src/failure-kinds.ts +8 -0
- package/src/index.ts +8 -0
- package/src/provider-trigger-contract.ts +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import {
|
|
3
|
+
deployToCloud, listCloudDeployments, parseAgentHarnesses, parseRepository, parseTriggerSource, undeployFromCloud,
|
|
4
|
+
type FlowTriggerSource,
|
|
5
|
+
} from '../cloud-deploy.js';
|
|
6
|
+
import type { CliIo } from '../cli.js';
|
|
7
|
+
|
|
8
|
+
export interface CloudDeployArgs {
|
|
9
|
+
command: 'cloud-deploy';
|
|
10
|
+
value: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
on: string[];
|
|
13
|
+
approver: string | undefined;
|
|
14
|
+
name: string | undefined;
|
|
15
|
+
agents: string | undefined;
|
|
16
|
+
draft: boolean;
|
|
17
|
+
json: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
22
|
+
* --approver <handle> [--name <n>] [--json]`
|
|
23
|
+
*
|
|
24
|
+
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
25
|
+
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
26
|
+
* file bucket, this one creates a hosted listener. The positional decides.
|
|
27
|
+
*/
|
|
28
|
+
export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs | undefined {
|
|
29
|
+
let value: string | undefined;
|
|
30
|
+
let repo: string | undefined;
|
|
31
|
+
let approver: string | undefined;
|
|
32
|
+
let name: string | undefined;
|
|
33
|
+
let agents: string | undefined;
|
|
34
|
+
let draft = false;
|
|
35
|
+
let json = false;
|
|
36
|
+
const on: string[] = [];
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i]!;
|
|
39
|
+
if (arg === '--json') {
|
|
40
|
+
if (json) return undefined;
|
|
41
|
+
json = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === '--draft') {
|
|
45
|
+
if (draft) return undefined;
|
|
46
|
+
draft = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === '--agents') {
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (agents !== undefined || next === undefined || next.startsWith('-')) return undefined;
|
|
52
|
+
agents = next;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
|
|
57
|
+
const next = args[i + 1];
|
|
58
|
+
if (next === undefined || next.startsWith('-')) return undefined;
|
|
59
|
+
i += 1;
|
|
60
|
+
if (arg === '--on') { on.push(next); continue; }
|
|
61
|
+
if (arg === '--repo') { if (repo !== undefined) return undefined; repo = next; continue; }
|
|
62
|
+
if (arg === '--approver') { if (approver !== undefined) return undefined; approver = next; continue; }
|
|
63
|
+
if (name !== undefined) return undefined;
|
|
64
|
+
name = next;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
68
|
+
value = arg;
|
|
69
|
+
}
|
|
70
|
+
if (value === undefined || repo === undefined || on.length === 0) return undefined;
|
|
71
|
+
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, json };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function describeSource(source: FlowTriggerSource): string {
|
|
75
|
+
const settings = Object.entries(source.settings).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
76
|
+
return settings ? `${source.provider} ${settings}` : source.provider;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
80
|
+
try {
|
|
81
|
+
if (args.approver === undefined) {
|
|
82
|
+
throw new CloudFlowError('invalid_input',
|
|
83
|
+
'--approver <handle> is required: every launched run receives it as input.approver for f.human.');
|
|
84
|
+
}
|
|
85
|
+
const deployment = await deployToCloud({
|
|
86
|
+
path: args.value,
|
|
87
|
+
repository: parseRepository(args.repo),
|
|
88
|
+
sources: args.on.map(parseTriggerSource),
|
|
89
|
+
approver: args.approver,
|
|
90
|
+
draft: args.draft,
|
|
91
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
92
|
+
...(args.agents === undefined ? {} : { agents: parseAgentHarnesses(args.agents) }),
|
|
93
|
+
});
|
|
94
|
+
if (args.json) {
|
|
95
|
+
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`);
|
|
99
|
+
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
100
|
+
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
101
|
+
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
102
|
+
io.stdout(deployment.status === 'draft'
|
|
103
|
+
? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
|
|
104
|
+
: 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
|
|
105
|
+
return 0;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return reportCloudFailure(error, args.json, io);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runCloudDeploymentsCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
|
|
112
|
+
try {
|
|
113
|
+
const deployments = await listCloudDeployments();
|
|
114
|
+
if (json) {
|
|
115
|
+
io.stdout(JSON.stringify({ ok: true, deployments }));
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (deployments.length === 0) {
|
|
119
|
+
io.stdout('No flow deployments in this workspace.');
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
for (const d of deployments) {
|
|
123
|
+
const repo = d.repository ? ` ${d.repository.owner}/${d.repository.name}` : '';
|
|
124
|
+
io.stdout(`${d.agentId} ${d.status} ${JSON.stringify(d.name)}${repo}`);
|
|
125
|
+
for (const source of d.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return reportCloudFailure(error, json, io);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function runCloudUndeployCli({ agentId, json }: { agentId: string; json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
|
|
134
|
+
try {
|
|
135
|
+
await undeployFromCloud(agentId);
|
|
136
|
+
io.stdout(json ? JSON.stringify({ ok: true, agentId, status: 'deleted' }) : `UNDEPLOYED ${agentId}`);
|
|
137
|
+
return 0;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return reportCloudFailure(error, json, io);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
144
|
+
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
145
|
+
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
146
|
+
// The deploy routes take a browser session or a `cli:auth` token. A
|
|
147
|
+
// deployment (CI) token gets 403 `session_required`; say what fixes it.
|
|
148
|
+
if (error instanceof CloudFlowError && error.status === 403) {
|
|
149
|
+
message += ' Deploying needs an interactive `cli:auth` credential: run `agent-relay cloud login` '
|
|
150
|
+
+ '(a deployment token can run flows but not deploy them).';
|
|
151
|
+
}
|
|
152
|
+
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
153
|
+
else io.stderr(`${code}: ${message}`);
|
|
154
|
+
return error instanceof CloudFlowError
|
|
155
|
+
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code) || error.status === 403 || error.status === 401)
|
|
156
|
+
? 2 : 1;
|
|
157
|
+
}
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
-
import { runInCloud, waitForCloudFlowRun } from '../cloud-run.js';
|
|
2
|
+
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
|
|
3
|
+
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
|
+
import { snapshotJsonValue } from '../json-value.js';
|
|
3
5
|
import type { CliIo } from '../cli.js';
|
|
4
6
|
|
|
5
7
|
/** Presentation only: the central CLI parser owns argv; the SDK owns the lifecycle. */
|
|
6
8
|
export async function runCloudCli(
|
|
7
|
-
{ value: path, json, wait }: {
|
|
9
|
+
{ value: path, json, wait, input, syncCode }: {
|
|
10
|
+
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean;
|
|
11
|
+
},
|
|
8
12
|
io: CliIo,
|
|
9
13
|
): Promise<0 | 1 | 2> {
|
|
10
14
|
const controller = new AbortController();
|
|
@@ -12,12 +16,36 @@ export async function runCloudCli(
|
|
|
12
16
|
process.once('SIGINT', abort);
|
|
13
17
|
process.once('SIGTERM', abort);
|
|
14
18
|
let runId: string | undefined;
|
|
19
|
+
// Flips at the run submission. An interruption before it — during prepare,
|
|
20
|
+
// packing or upload — admitted nothing and is safe to retry; only an
|
|
21
|
+
// interrupted submission has unknown admission.
|
|
22
|
+
let submitting = false;
|
|
15
23
|
try {
|
|
16
|
-
const
|
|
24
|
+
const options: RunInCloudOptions = { signal: controller.signal, onSubmit: () => { submitting = true; } };
|
|
25
|
+
if (isAuthoredFlowPath(path)) {
|
|
26
|
+
// Same parse as a local direct run, so a file-or-inline argument means
|
|
27
|
+
// the same thing on both sides of `--cloud`.
|
|
28
|
+
try {
|
|
29
|
+
options.input = snapshotJsonValue(parseDirectInput(input), 'Cloud authored input');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// The tree is the invoking directory, as with v1: the flow path is where
|
|
36
|
+
// the body lives, not the boundary of what the run may read.
|
|
37
|
+
if (syncCode) options.syncCode = { root: process.cwd() };
|
|
38
|
+
const receipt = await runInCloud({ path }, options);
|
|
17
39
|
runId = receipt.runId;
|
|
18
40
|
if (!json) {
|
|
19
41
|
io.stdout(`ACCEPTED ${receipt.runId} (${receipt.status})`);
|
|
20
42
|
io.stdout(receipt.apiUrl);
|
|
43
|
+
if (receipt.synced) {
|
|
44
|
+
io.stdout(`SYNCED ${receipt.synced.files} files (${receipt.synced.bytes} bytes); pull changes with: flows sync ${receipt.runId}`);
|
|
45
|
+
for (const link of receipt.synced.skippedLinks) {
|
|
46
|
+
io.stderr(`WARNING [sync_link_skipped] ${link} points outside the synced tree and was not uploaded`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
21
49
|
}
|
|
22
50
|
if (!wait) {
|
|
23
51
|
if (json) io.stdout(JSON.stringify({ ok: true, ...receipt }));
|
|
@@ -29,16 +57,19 @@ export async function runCloudCli(
|
|
|
29
57
|
else io.stdout(`${run.status.toUpperCase()} ${run.runId} completionReason: ${'completionReason' in run ? run.completionReason : 'unavailable'}`);
|
|
30
58
|
return ok ? 0 : 1;
|
|
31
59
|
} catch (error) {
|
|
32
|
-
const code = controller.signal.aborted
|
|
60
|
+
const code = controller.signal.aborted
|
|
61
|
+
? runId ? 'observation_aborted' : submitting ? 'admission_unknown' : 'submission_aborted'
|
|
33
62
|
: error instanceof CloudFlowError ? error.code : 'cloud_run_failed';
|
|
34
63
|
const message = controller.signal.aborted
|
|
35
64
|
? runId ? 'Stopped observing; the hosted run has not been cancelled.'
|
|
36
|
-
:
|
|
65
|
+
: submitting
|
|
66
|
+
? 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
|
|
67
|
+
: 'Interrupted before the run was submitted; nothing was admitted. Safe to run again.'
|
|
37
68
|
: error instanceof Error ? error.message : 'Cloud run failed.';
|
|
38
69
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message, ...(runId ? { runId } : {}) }));
|
|
39
70
|
else io.stderr(`${code}: ${message}${runId ? ` (run ${runId})` : ''}`);
|
|
40
71
|
return error instanceof CloudFlowError
|
|
41
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
72
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported'].includes(error.code)
|
|
42
73
|
|| (runId === undefined && error.code === 'http_error' && [401, 403].includes(error.status ?? 0))) ? 2 : 1;
|
|
43
74
|
} finally {
|
|
44
75
|
process.off('SIGINT', abort);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import { applyCloudPatch, downloadCloudPatch, patchedPaths } from '../cloud-sync.js';
|
|
3
|
+
import type { CliIo } from '../cli.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `flows sync <run-id>`: fetch the diff a hosted run left in its synced tree
|
|
7
|
+
* and apply it here. The patch is the sandbox's own `git diff` against the
|
|
8
|
+
* uploaded baseline, so applying it reproduces exactly what the run's steps
|
|
9
|
+
* wrote — no re-execution, no re-upload.
|
|
10
|
+
*/
|
|
11
|
+
export async function runCloudSyncCli(
|
|
12
|
+
{ runId, json, root }: { runId: string; json: boolean; root: string },
|
|
13
|
+
io: CliIo,
|
|
14
|
+
): Promise<0 | 1 | 2> {
|
|
15
|
+
try {
|
|
16
|
+
const { patch, hasChanges } = await downloadCloudPatch(runId, {});
|
|
17
|
+
if (!hasChanges || !patch.trim()) {
|
|
18
|
+
if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: false, applied: false }));
|
|
19
|
+
else io.stdout(`NO CHANGES ${runId}`);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
applyCloudPatch(root, patch);
|
|
23
|
+
const files = patchedPaths(patch);
|
|
24
|
+
if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: true, applied: true, files }));
|
|
25
|
+
else {
|
|
26
|
+
io.stdout(`APPLIED ${runId}: ${files.length} file${files.length === 1 ? '' : 's'}${files.length ? `\n ${files.join('\n ')}` : ''}`);
|
|
27
|
+
io.stdout('Applied to the working tree, uncommitted: review with git diff before keeping it.');
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const code = error instanceof CloudFlowError ? error.code : 'cloud_sync_failed';
|
|
32
|
+
const message = error instanceof Error ? error.message : 'Cloud sync failed.';
|
|
33
|
+
if (json) io.stdout(JSON.stringify({ ok: false, code, message, runId }));
|
|
34
|
+
else io.stderr(`${code}: ${message} (run ${runId})`);
|
|
35
|
+
return error instanceof CloudFlowError && ['configuration', 'sync_unsupported', 'patch_conflict'].includes(error.code) ? 2 : 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { JournalClient } from '../journal-client.js';
|
|
|
14
14
|
import { inputFailureReport } from './check.js';
|
|
15
15
|
import { checkAuthoredTriggers } from './check-triggers.js';
|
|
16
16
|
import {
|
|
17
|
+
authoredCompletion,
|
|
17
18
|
authoredStepFailure,
|
|
18
19
|
connect,
|
|
19
20
|
emptyReport,
|
|
@@ -99,31 +100,7 @@ export async function runDirectFlow(
|
|
|
99
100
|
`authored flow "${result.name}" completed without a journal step`,
|
|
100
101
|
));
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
exitCode: 3,
|
|
105
|
-
report: {
|
|
106
|
-
...base, ok: false, runId: result.rootRunId, socketPath, status: 'parked',
|
|
107
|
-
completedSteps: result.journalSteps.length,
|
|
108
|
-
diagnostics: [...base.diagnostics, {
|
|
109
|
-
severity: 'parked', kind: 'run_parked',
|
|
110
|
-
message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.`,
|
|
111
|
-
}],
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
exitCode: 0,
|
|
117
|
-
report: {
|
|
118
|
-
...base,
|
|
119
|
-
ok: true,
|
|
120
|
-
runId: result.rootRunId,
|
|
121
|
-
socketPath,
|
|
122
|
-
status: 'completed',
|
|
123
|
-
completionReason: result.completionReason,
|
|
124
|
-
completedSteps: result.journalSteps.length,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
103
|
+
return authoredCompletion('run', base, socketPath, result, result.rootRunId);
|
|
127
104
|
} catch (caught) {
|
|
128
105
|
if (caught instanceof McpPreflightError) return {
|
|
129
106
|
exitCode: 2, report: fromCheckReport('run', caught.report),
|
package/src/cli/run.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
RunStatus,
|
|
23
23
|
} from '../protocol.js';
|
|
24
24
|
import type { StepType } from '../spec.js';
|
|
25
|
+
import type { LoweredCompletionReason } from '../authored-flow-executor.js';
|
|
25
26
|
import {
|
|
26
27
|
checkFlow,
|
|
27
28
|
type CheckReport,
|
|
@@ -36,7 +37,7 @@ export interface ParkedStep {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface RunDiagnostic extends StepFailedDetails {
|
|
39
|
-
severity: 'refusal' | 'failure' | 'parked' | 'warning';
|
|
40
|
+
severity: 'refusal' | 'failure' | 'parked' | 'warning' | 'declined';
|
|
40
41
|
kind: RunFailureKind | RunWarningKind | RunCompletionReason;
|
|
41
42
|
message: string;
|
|
42
43
|
}
|
|
@@ -191,21 +192,7 @@ export async function resumeFlow(
|
|
|
191
192
|
lifecycle: options,
|
|
192
193
|
});
|
|
193
194
|
if (result === undefined) throw new Error('authored root disappeared during resume');
|
|
194
|
-
return
|
|
195
|
-
exitCode: result.completionReason === 'needs_human' ? 3 : 0,
|
|
196
|
-
report: {
|
|
197
|
-
...base,
|
|
198
|
-
ok: result.completionReason === 'success',
|
|
199
|
-
runId,
|
|
200
|
-
socketPath,
|
|
201
|
-
status: result.completionReason === 'needs_human' ? 'parked' : 'completed',
|
|
202
|
-
...(result.completionReason === 'success'
|
|
203
|
-
? { completionReason: 'success' as const }
|
|
204
|
-
: { diagnostics: [{ severity: 'parked' as const, kind: 'run_parked' as const,
|
|
205
|
-
message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.` }] }),
|
|
206
|
-
completedSteps: result.journalSteps.length,
|
|
207
|
-
},
|
|
208
|
-
};
|
|
195
|
+
return authoredCompletion('resume', base, socketPath, result, runId);
|
|
209
196
|
}
|
|
210
197
|
// resumeHelperEffect subsumes the old resumeSlackEffect: it handles the
|
|
211
198
|
// slack effect resume plus every other provider from N's codegen. The
|
|
@@ -309,6 +296,89 @@ export function authoredStepFailure(
|
|
|
309
296
|
};
|
|
310
297
|
}
|
|
311
298
|
|
|
299
|
+
/**
|
|
300
|
+
* An authored body that returned its own terminal verdict, reported as that verdict.
|
|
301
|
+
*
|
|
302
|
+
* Shared by `runDirectFlow` and `resumeFlow` for the reason `authoredStepFailure`
|
|
303
|
+
* is shared: the run and resume paths had already drifted once, and the
|
|
304
|
+
* `needs_human` report was duplicated verbatim in both files.
|
|
305
|
+
*
|
|
306
|
+
* This is NOT `authoredStepFailure`, even though a `step_failed` verdict lands
|
|
307
|
+
* on the same exit code and report shape. That function describes a step that
|
|
308
|
+
* ran and failed, and carries the failing step's evidence. Here every step
|
|
309
|
+
* succeeded and the BODY declared the outcome, so there is no failing step to
|
|
310
|
+
* name — routing this through the other helper would invent one.
|
|
311
|
+
*/
|
|
312
|
+
export function authoredCompletion(
|
|
313
|
+
command: RunCommand,
|
|
314
|
+
base: RunReport,
|
|
315
|
+
socketPath: string,
|
|
316
|
+
result: { name: string; completionReason: LoweredCompletionReason; journalSteps: readonly unknown[] },
|
|
317
|
+
runId: string | undefined,
|
|
318
|
+
): RunExecution {
|
|
319
|
+
const common: RunReport = {
|
|
320
|
+
...fromBase(command, base),
|
|
321
|
+
...(runId === undefined ? {} : { runId }),
|
|
322
|
+
socketPath,
|
|
323
|
+
completedSteps: result.journalSteps.length,
|
|
324
|
+
};
|
|
325
|
+
switch (result.completionReason) {
|
|
326
|
+
case 'success':
|
|
327
|
+
return {
|
|
328
|
+
exitCode: 0,
|
|
329
|
+
report: { ...common, ok: true, status: 'completed', completionReason: 'success' },
|
|
330
|
+
};
|
|
331
|
+
case 'declined':
|
|
332
|
+
return {
|
|
333
|
+
exitCode: 0,
|
|
334
|
+
report: {
|
|
335
|
+
...common, ok: true, status: 'completed', completionReason: 'success',
|
|
336
|
+
diagnostics: [...base.diagnostics, {
|
|
337
|
+
severity: 'declined', kind: 'run_declined',
|
|
338
|
+
message: 'Flow deliberately chose not to act on this input.',
|
|
339
|
+
}],
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
case 'needs_human':
|
|
343
|
+
return {
|
|
344
|
+
exitCode: 3,
|
|
345
|
+
report: {
|
|
346
|
+
...common, ok: false, status: 'parked',
|
|
347
|
+
diagnostics: [...base.diagnostics, {
|
|
348
|
+
severity: 'parked', kind: 'run_parked',
|
|
349
|
+
message: `Flow "${result.name}" needs_human; see the journal for accumulated blockers.`,
|
|
350
|
+
}],
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
case 'step_failed':
|
|
354
|
+
// A declared run failure. Exit 1, not the parked 3: nothing here is
|
|
355
|
+
// waiting for a human to recover it, and exit 3 is the local kit's
|
|
356
|
+
// manual-approval stop. `status: failed` with this `completionReason` is
|
|
357
|
+
// also the only terminal shape Cloud accepts for a non-success run
|
|
358
|
+
// (see cloud-run.ts).
|
|
359
|
+
return {
|
|
360
|
+
exitCode: 1,
|
|
361
|
+
report: {
|
|
362
|
+
...common, ok: false, status: 'failed', completionReason: 'step_failed',
|
|
363
|
+
diagnostics: [...base.diagnostics, {
|
|
364
|
+
severity: 'failure', kind: 'step_failed',
|
|
365
|
+
message: `Flow "${result.name}" declared done("step_failed"): its own checks did not pass. `
|
|
366
|
+
+ 'No step failed, so there is no step-level evidence to inspect; the journal holds '
|
|
367
|
+
+ 'every step the flow ran before it decided.',
|
|
368
|
+
}],
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// Exhaustive by construction. A new lowered completion has to choose its
|
|
373
|
+
// own exit code and wording here; it must not inherit "its own checks did not
|
|
374
|
+
// pass", which would state something the body never declared. Letting an
|
|
375
|
+
// unlisted reason fall through to the failure branch is how a reporting-side
|
|
376
|
+
// copy of the same vocabulary drifts from the executor's — the defect this
|
|
377
|
+
// change exists to close — so it is a compile error, not a wrong report.
|
|
378
|
+
const unreachable: never = result.completionReason;
|
|
379
|
+
throw new Error(`unreachable authored completion: ${String(unreachable)}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
312
382
|
/**
|
|
313
383
|
* Get a live daemon, then open the socket to it.
|
|
314
384
|
*
|
package/src/cli.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { runDirectFlow } from './cli/direct-run.js';
|
|
|
24
24
|
import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
|
|
25
25
|
import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
26
26
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
27
|
+
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
28
|
+
import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
|
|
27
29
|
import { isAuthoredFlowPath } from './direct-input.js';
|
|
28
30
|
import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
|
|
29
31
|
import { parseDigestReference } from './bundle-transport.js';
|
|
@@ -51,7 +53,11 @@ type ParsedArgs =
|
|
|
51
53
|
| BuildArgs
|
|
52
54
|
| DeployArgs
|
|
53
55
|
| { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
|
|
54
|
-
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
|
|
56
|
+
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean }
|
|
57
|
+
| { command: 'sync'; runId: string; json: boolean; root: string }
|
|
58
|
+
| CloudDeployArgs
|
|
59
|
+
| { command: 'deployments'; json: boolean }
|
|
60
|
+
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
55
61
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
56
62
|
| { 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 }
|
|
57
63
|
| { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
@@ -66,12 +72,17 @@ const USAGE = [
|
|
|
66
72
|
'flows add <helper-name|@flows/helper-name>',
|
|
67
73
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
68
74
|
'flows build --verify <bundle-dir>',
|
|
75
|
+
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
|
|
76
|
+
'flows deployments [--json]',
|
|
77
|
+
'flows undeploy [--json] <deployment-id>',
|
|
69
78
|
'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
|
|
70
79
|
'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
|
|
71
80
|
'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
|
|
72
81
|
'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
|
|
73
82
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
74
|
-
'flows run --cloud [--json] [--wait] <flow.yaml|spec.json>',
|
|
83
|
+
'flows run --cloud [--json] [--wait] [--sync-code] <flow.yaml|spec.json>',
|
|
84
|
+
'flows run --cloud [--json] [--wait] [--sync-code] <flow.ts> --input <inline-json-or-file>',
|
|
85
|
+
'flows sync [--json] [--dir <path>] <run-id>',
|
|
75
86
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
|
|
76
87
|
'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
88
|
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
|
|
@@ -116,6 +127,10 @@ export async function runCli(
|
|
|
116
127
|
if (parsed.command === 'serve-webhook') return runServeWebhook(parsed, io);
|
|
117
128
|
|
|
118
129
|
if (parsed.command === 'cloud-run') return runCloudCli(parsed, io);
|
|
130
|
+
if (parsed.command === 'sync') return runCloudSyncCli(parsed, io);
|
|
131
|
+
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
132
|
+
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
133
|
+
if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
|
|
119
134
|
if (parsed.command === 'replay') return replayJournal(parsed, io);
|
|
120
135
|
if (parsed.command === 'build') return runBuild(parsed, io);
|
|
121
136
|
if (parsed.command === 'deploy') return runDeploy(parsed, io);
|
|
@@ -419,17 +434,35 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
419
434
|
if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
|
|
420
435
|
if (command === 'replay') return parseReplayArgs(args.slice(1));
|
|
421
436
|
if (command === 'build') return parseBuildArgs(args.slice(1));
|
|
422
|
-
if (command === 'deploy')
|
|
437
|
+
if (command === 'deploy') {
|
|
438
|
+
// The positional decides the form: an authored source deploys a hosted
|
|
439
|
+
// listener; a digest reference copies a sealed bundle into a file bucket.
|
|
440
|
+
const source = args.slice(1).find(a => !a.startsWith('-') && isAuthoredFlowPath(a));
|
|
441
|
+
return source !== undefined ? parseCloudDeployArgs(args.slice(1)) : parseDeployArgs(args.slice(1));
|
|
442
|
+
}
|
|
443
|
+
if (command === 'undeploy') {
|
|
444
|
+
const rest = args.slice(1).filter(a => a !== '--json');
|
|
445
|
+
const json = args.length - 1 - rest.length;
|
|
446
|
+
if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
|
|
447
|
+
return { command: 'undeploy', agentId: rest[0]!, json: json === 1 };
|
|
448
|
+
}
|
|
449
|
+
if (command === 'deployments') {
|
|
450
|
+
const rest = args.slice(1);
|
|
451
|
+
if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
|
|
452
|
+
return { command: 'deployments', json: rest.length === 1 };
|
|
453
|
+
}
|
|
423
454
|
if (command === 'serve-webhook') return parseWebhookArgs(args.slice(1));
|
|
424
455
|
if (command === 'hn-monitor') return parseHnMonitorArgs(args.slice(1));
|
|
425
456
|
if (command === 'tick') return parseTickArgs(args.slice(1));
|
|
426
457
|
if (command === 'observer') return parseObserverArgs(args.slice(1));
|
|
458
|
+
if (command === 'sync') return parseSyncArgs(args.slice(1));
|
|
427
459
|
if (command !== 'check' && command !== 'run' && command !== 'resume') return undefined;
|
|
428
460
|
|
|
429
461
|
let json = false;
|
|
430
462
|
let watch = false;
|
|
431
463
|
let cloud = false;
|
|
432
464
|
let wait = false;
|
|
465
|
+
let syncCode = false;
|
|
433
466
|
let localAgent = false;
|
|
434
467
|
let allowHumanInfluenced = false;
|
|
435
468
|
let dataDir = DEFAULT_DATA_DIR;
|
|
@@ -443,10 +476,11 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
443
476
|
const positionals: string[] = [];
|
|
444
477
|
for (let index = 1; index < args.length; index += 1) {
|
|
445
478
|
const argument = args[index]!;
|
|
446
|
-
if (argument === '--cloud' || argument === '--wait') {
|
|
447
|
-
if (command !== 'run' || (argument === '--cloud' ? cloud : wait)) return undefined;
|
|
479
|
+
if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code') {
|
|
480
|
+
if (command !== 'run' || (argument === '--cloud' ? cloud : argument === '--wait' ? wait : syncCode)) return undefined;
|
|
448
481
|
if (argument === '--cloud') cloud = true;
|
|
449
|
-
else wait = true;
|
|
482
|
+
else if (argument === '--wait') wait = true;
|
|
483
|
+
else syncCode = true;
|
|
450
484
|
continue;
|
|
451
485
|
}
|
|
452
486
|
if (argument === '--allow-human-influenced') {
|
|
@@ -520,13 +554,15 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
520
554
|
if (bucket !== undefined && (cloud || !parseDigestReference(positionals[0]!))) return undefined;
|
|
521
555
|
if (cloud) {
|
|
522
556
|
// `--cloud` submits the spec to Cloud, so every flag that only describes a
|
|
523
|
-
// local run --
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
557
|
+
// local run -- a data dir, a suppressed daemon, a local agent, a local
|
|
558
|
+
// observer-link opt-out -- describes nothing there and is refused rather
|
|
559
|
+
// than ignored. `--input` is the authored body's argument and travels with
|
|
560
|
+
// the source, so it is accepted exactly where a local run accepts it.
|
|
561
|
+
if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
|
|
562
|
+
if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
563
|
+
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode };
|
|
528
564
|
}
|
|
529
|
-
if (wait) return undefined;
|
|
565
|
+
if (wait || syncCode) return undefined;
|
|
530
566
|
if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
531
567
|
|
|
532
568
|
if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
@@ -579,6 +615,32 @@ function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
|
579
615
|
* directory at all -- the mint is a pure Relaycast API round-trip. No
|
|
580
616
|
* positional argument, no other flags.
|
|
581
617
|
*/
|
|
618
|
+
/** `flows sync [--json] [--dir <path>] <run-id>`: apply a hosted run's patch to a local tree. */
|
|
619
|
+
function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
620
|
+
let json = false;
|
|
621
|
+
let root: string | undefined;
|
|
622
|
+
const positionals: string[] = [];
|
|
623
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
624
|
+
const argument = args[index]!;
|
|
625
|
+
if (argument === '--json') {
|
|
626
|
+
if (json) return undefined;
|
|
627
|
+
json = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (argument === '--dir') {
|
|
631
|
+
const value = args[index + 1];
|
|
632
|
+
if (root !== undefined || value === undefined || value.startsWith('-')) return undefined;
|
|
633
|
+
root = value;
|
|
634
|
+
index += 1;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (argument.startsWith('-')) return undefined;
|
|
638
|
+
positionals.push(argument);
|
|
639
|
+
}
|
|
640
|
+
if (positionals.length !== 1) return undefined;
|
|
641
|
+
return { command: 'sync', runId: positionals[0]!, json, root: root ?? '.' };
|
|
642
|
+
}
|
|
643
|
+
|
|
582
644
|
function parseObserverArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
583
645
|
let dataDir = DEFAULT_DATA_DIR;
|
|
584
646
|
let sawDataDir = false;
|
|
@@ -750,6 +812,7 @@ function diagnosticLabel(severity: string): string {
|
|
|
750
812
|
case 'warning': return 'WARNING';
|
|
751
813
|
case 'failure': return 'FAILED';
|
|
752
814
|
case 'parked': return 'PARKED';
|
|
815
|
+
case 'declined': return 'DECLINED';
|
|
753
816
|
default: return 'REFUSED';
|
|
754
817
|
}
|
|
755
818
|
}
|