@relayflows/sdk 2.0.55 → 2.0.56
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/authored-broker.d.ts +13 -0
- package/dist/authored-broker.d.ts.map +1 -0
- package/dist/authored-broker.js +75 -0
- package/dist/authored-broker.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 +8 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +6 -0
- 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 +11 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +104 -20
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +42 -3
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +1 -1
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-hooks.d.ts +2 -1
- package/dist/authored-hooks.d.ts.map +1 -1
- package/dist/authored-hooks.js +2 -2
- package/dist/authored-hooks.js.map +1 -1
- package/dist/authored-human.d.ts +18 -0
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +32 -4
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-mcp.d.ts.map +1 -1
- package/dist/authored-mcp.js +3 -2
- package/dist/authored-mcp.js.map +1 -1
- package/dist/authored-node-entry.js +4 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +1 -0
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-plugin-effect.d.ts.map +1 -1
- package/dist/authored-plugin-effect.js +3 -2
- package/dist/authored-plugin-effect.js.map +1 -1
- package/dist/authored-preflight.d.ts.map +1 -1
- package/dist/authored-preflight.js +2 -1
- package/dist/authored-preflight.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +13 -3
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-serial-ordinals.d.ts +41 -0
- package/dist/authored-serial-ordinals.d.ts.map +1 -0
- package/dist/authored-serial-ordinals.js +45 -0
- package/dist/authored-serial-ordinals.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +19 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +11 -2
- package/dist/budget-preflight.js.map +1 -1
- package/dist/bundle-transport.d.ts +2 -0
- package/dist/bundle-transport.d.ts.map +1 -1
- package/dist/bundle-transport.js +4 -0
- package/dist/bundle-transport.js.map +1 -1
- package/dist/cached-flow-runtime.d.ts +3 -0
- package/dist/cached-flow-runtime.d.ts.map +1 -0
- package/dist/cached-flow-runtime.js +37 -0
- package/dist/cached-flow-runtime.js.map +1 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +13 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.js +6 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-mirror-session.d.ts +8 -2
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
- package/dist/cli/cloud-mirror-session.js +20 -27
- package/dist/cli/cloud-mirror-session.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +2 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +19 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +39 -18
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +3 -3
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +15 -9
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-flow-source.d.ts +13 -0
- package/dist/cloud-flow-source.d.ts.map +1 -0
- package/dist/cloud-flow-source.js +55 -0
- package/dist/cloud-flow-source.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +2 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -1
- package/dist/cloud-mirror-transport.js +2 -0
- package/dist/cloud-mirror-transport.js.map +1 -1
- package/dist/cloud-mirror.d.ts.map +1 -1
- package/dist/cloud-mirror.js +10 -1
- package/dist/cloud-mirror.js.map +1 -1
- package/dist/communication/broker-preflight.d.ts +3 -0
- package/dist/communication/broker-preflight.d.ts.map +1 -0
- package/dist/communication/broker-preflight.js +22 -0
- package/dist/communication/broker-preflight.js.map +1 -0
- package/dist/communication/broker-spec.d.ts +20 -0
- package/dist/communication/broker-spec.d.ts.map +1 -0
- package/dist/communication/broker-spec.js +46 -0
- package/dist/communication/broker-spec.js.map +1 -0
- package/dist/communication/broker-worker.d.ts +17 -0
- package/dist/communication/broker-worker.d.ts.map +1 -0
- package/dist/communication/broker-worker.js +214 -0
- package/dist/communication/broker-worker.js.map +1 -0
- package/dist/communication/pty.d.ts +29 -0
- package/dist/communication/pty.d.ts.map +1 -0
- package/dist/communication/pty.js +40 -0
- package/dist/communication/pty.js.map +1 -0
- package/dist/communication/relay.d.ts +18 -0
- package/dist/communication/relay.d.ts.map +1 -1
- package/dist/communication/relay.js +4 -0
- package/dist/communication/relay.js.map +1 -1
- package/dist/communication/skills.d.ts +3 -0
- package/dist/communication/skills.d.ts.map +1 -0
- package/dist/communication/skills.js +17 -0
- package/dist/communication/skills.js.map +1 -0
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +9 -26
- package/dist/communication/worker.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/plugin-manifest.js +1 -1
- package/dist/plugin-manifest.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +2 -1
- package/dist/preflight.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +12 -0
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-broker.ts +73 -0
- package/src/authored-budget.ts +6 -2
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-flow-executor.ts +115 -20
- package/src/authored-flow-loader.ts +43 -3
- package/src/authored-helper-effect.ts +2 -1
- package/src/authored-hooks.ts +4 -3
- package/src/authored-human.ts +35 -4
- package/src/authored-mcp.ts +7 -2
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +1 -0
- package/src/authored-plugin-effect.ts +7 -2
- package/src/authored-preflight.ts +2 -1
- package/src/authored-root.ts +13 -3
- package/src/authored-serial-ordinals.ts +76 -0
- package/src/authored-worker-step.ts +17 -2
- package/src/budget-preflight.ts +9 -1
- package/src/bundle-transport.ts +4 -0
- package/src/cached-flow-runtime.ts +32 -0
- package/src/cli/check.ts +11 -1
- package/src/cli/cloud-deploy.ts +5 -5
- package/src/cli/cloud-mirror-session.ts +23 -29
- package/src/cli/direct-run.ts +2 -1
- package/src/cli/run.ts +18 -3
- package/src/cli.ts +31 -13
- package/src/cloud-deploy.ts +19 -14
- package/src/cloud-flow-source.ts +58 -0
- package/src/cloud-mirror-transport.ts +4 -0
- package/src/cloud-mirror.ts +9 -1
- package/src/communication/broker-preflight.ts +22 -0
- package/src/communication/broker-spec.ts +48 -0
- package/src/communication/broker-worker.ts +198 -0
- package/src/communication/pty.ts +47 -0
- package/src/communication/relay.ts +12 -1
- package/src/communication/skills.ts +16 -0
- package/src/communication/worker.ts +9 -26
- package/src/failure-kinds.ts +2 -0
- package/src/plugin-manifest.ts +1 -1
- package/src/preflight.ts +3 -1
- package/src/worker.ts +11 -0
package/src/authored-root.ts
CHANGED
|
@@ -19,12 +19,13 @@ import { SPEC_SCHEMA_VERSION } from './spec.js';
|
|
|
19
19
|
import type { RunLifecycleOptions } from './cli/run.js';
|
|
20
20
|
import { isLeaseLost, withWorkerLease } from './worker-lease.js';
|
|
21
21
|
import { AuthoredFlowExecutionError, AuthoredHumanParked } from './authored-flow-error.js';
|
|
22
|
-
import { readOpenHumanWaits, resumeCommand } from './authored-human.js';
|
|
22
|
+
import { readHumanWaitIds, readOpenHumanWaits, resumeCommand } from './authored-human.js';
|
|
23
23
|
import { helperReceiptWaitId, releaseHelperReceiptWaits } from './helper-park.js';
|
|
24
24
|
import { readSubscriptionPark } from './authored-subscription-park.js';
|
|
25
25
|
import { localAgentCredentialEnvironment } from './local-agent-environment.js';
|
|
26
26
|
import { completedResult, rootKernelStep, rootStepIfPresent } from './authored-root-readback.js';
|
|
27
27
|
import { parallelPromiseIntrinsicsIntact } from './authored-flow-lifecycle.js';
|
|
28
|
+
import { readAuthoredStepIndex } from './authored-step-index.js';
|
|
28
29
|
|
|
29
30
|
export type DurableAuthoredFlowResult =
|
|
30
31
|
| (AuthoredFlowExecutionResult & { readonly rootRunId: string })
|
|
@@ -391,6 +392,14 @@ async function driveRoot(
|
|
|
391
392
|
// A lost root lease must also stop body reads still inside their budget.
|
|
392
393
|
const releaseReads = journal.scopeReads?.(rootSignal) ?? (() => {});
|
|
393
394
|
try {
|
|
395
|
+
// A re-dispatched serial body replays an earlier attempt; give it that
|
|
396
|
+
// attempt's step index so a reordered resume is refused, not re-run.
|
|
397
|
+
// A first attempt has nothing to replay and reads nothing.
|
|
398
|
+
const parallel = metadata.identityVersion === 2 && metadata.executionMode === 'parallel';
|
|
399
|
+
const journaledSteps = parallel || dispatch.attempt <= 1 ? undefined : [
|
|
400
|
+
...(await readAuthoredStepIndex(journal, dispatch.run_id)).map(record => record.step),
|
|
401
|
+
...await readHumanWaitIds(journal, dispatch.run_id),
|
|
402
|
+
];
|
|
394
403
|
const callerSignal = options.lifecycle?.signal;
|
|
395
404
|
const signal = callerSignal === undefined ? rootSignal : AbortSignal.any([callerSignal, rootSignal]);
|
|
396
405
|
if (process.versions['bun'] !== undefined) {
|
|
@@ -401,6 +410,7 @@ async function driveRoot(
|
|
|
401
410
|
agentEnvironment: localAgentCredentialEnvironment(options.agentEnvironment),
|
|
402
411
|
}),
|
|
403
412
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
413
|
+
...(journaledSteps === undefined ? {} : { journaledSteps }),
|
|
404
414
|
...options.lifecycle, signal,
|
|
405
415
|
});
|
|
406
416
|
}
|
|
@@ -416,8 +426,8 @@ async function driveRoot(
|
|
|
416
426
|
...(options.agentEnvironment === undefined ? {} : { agentEnvironment: options.agentEnvironment }),
|
|
417
427
|
...(options.workerCapacity === undefined ? {} : { workerCapacity: options.workerCapacity }),
|
|
418
428
|
rootRunId: dispatch.run_id,
|
|
419
|
-
executionMode:
|
|
420
|
-
|
|
429
|
+
executionMode: parallel ? 'parallel' : 'serial',
|
|
430
|
+
...(journaledSteps === undefined ? {} : { journaledSteps }),
|
|
421
431
|
extensions: loaded.extensions,
|
|
422
432
|
flowGraph: loaded.graph,
|
|
423
433
|
...options.lifecycle,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serial step ids are `<kind>-<N>`, with ONE ordinal counter shared by every
|
|
5
|
+
* kind and assigned in call order (authored-flow-executor `nextId`). Inside a
|
|
6
|
+
* `Promise.all` that call order follows result arrival: each branch's next
|
|
7
|
+
* call is made when its previous step resolves. A resume replays completed
|
|
8
|
+
* results from the journal, in whatever order they come back, so two
|
|
9
|
+
* branches can take each other's ordinals.
|
|
10
|
+
*
|
|
11
|
+
* The kernel catches part of that: an admission key is bound to its spec, so
|
|
12
|
+
* `run-3` re-requested with a different command is `run_admission_conflict`.
|
|
13
|
+
* It cannot catch a KIND swap. `run-3` and `llm-3` are different admission
|
|
14
|
+
* keys, so a resumed `f.run` that lands on the ordinal an `llm` already
|
|
15
|
+
* holds gets a fresh key and runs again, exiting 0. This guard refuses that
|
|
16
|
+
* from the root's journaled step index before the step is admitted.
|
|
17
|
+
*
|
|
18
|
+
* The root driver reads the index, plus the root's `wait.human` ids, once
|
|
19
|
+
* per re-dispatched attempt (authored-root.ts); a first attempt reads
|
|
20
|
+
* nothing. The guard adds no journal record of its own. It sees what those
|
|
21
|
+
* hold — `f.llm`/`f.agent` from admission, an `f.dispatch` from its child's
|
|
22
|
+
* first step, an `f.human` from its park, other steps from completion — so an
|
|
23
|
+
* ordinal whose only holder was an `f.run` or effect still in flight at the
|
|
24
|
+
* crash is invisible to it. Stable per-branch identities (a new identity
|
|
25
|
+
* version) remove the class.
|
|
26
|
+
*/
|
|
27
|
+
export type SerialOrdinals = ReadonlyMap<number, ReadonlyArray<{ readonly kind: string; readonly step: string }>>;
|
|
28
|
+
|
|
29
|
+
const SERIAL_ID = /^(.+)-([1-9][0-9]*)$/u;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The ordinals journaled directly under `prefix` (a dispatched child's
|
|
33
|
+
* `dispatch-N--`, or '' for the root body). A descendant's step claims its
|
|
34
|
+
* dispatch's ordinal here: `dispatch-3--run-1` holds ordinal 3 for `dispatch`
|
|
35
|
+
* even before the child's receipt is journaled. Lowered `.gate` steps and
|
|
36
|
+
* anything not shaped `<kind>-<N>` hold none.
|
|
37
|
+
*/
|
|
38
|
+
export function serialOrdinals(journaledSteps: readonly string[], prefix: string): SerialOrdinals {
|
|
39
|
+
const ordinals = new Map<number, Array<{ kind: string; step: string }>>();
|
|
40
|
+
for (const step of journaledSteps) {
|
|
41
|
+
if (!step.startsWith(prefix)) continue;
|
|
42
|
+
const local = step.slice(prefix.length);
|
|
43
|
+
const descendant = local.indexOf('--');
|
|
44
|
+
const match = SERIAL_ID.exec(descendant < 0 ? local : local.slice(0, descendant));
|
|
45
|
+
if (match === null || (descendant >= 0 && match[1] !== 'dispatch')) continue;
|
|
46
|
+
const ordinal = Number(match[2]);
|
|
47
|
+
if (!Number.isSafeInteger(ordinal)) continue;
|
|
48
|
+
const holders = ordinals.get(ordinal) ?? [];
|
|
49
|
+
holders.push({ kind: match[1]!, step });
|
|
50
|
+
ordinals.set(ordinal, holders);
|
|
51
|
+
}
|
|
52
|
+
return ordinals;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Refuse `<kind>-<ordinal>` when the journal holds that ordinal under another kind. */
|
|
56
|
+
export function assertSerialOrdinal(ordinals: SerialOrdinals, kind: string, ordinal: number): void {
|
|
57
|
+
const journaled = ordinals.get(ordinal)?.find(holder => holder.kind !== kind);
|
|
58
|
+
if (journaled === undefined) return;
|
|
59
|
+
throw serialReplayOrderMismatch(
|
|
60
|
+
`ordinal ${ordinal} is journaled as ${kindName(journaled.kind)} step "${journaled.step}", `
|
|
61
|
+
+ `but this resume reached it with ${kindName(kind)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function serialReplayOrderMismatch(detail: string): AuthoredFlowExecutionError {
|
|
66
|
+
return new AuthoredFlowExecutionError(
|
|
67
|
+
'serial_replay_order_mismatch',
|
|
68
|
+
`serial replay order mismatch: ${detail}. Concurrent branches resolved in a different order `
|
|
69
|
+
+ 'than the journaled run, so the step was refused instead of executed.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Helper ids embed the root run id (`slack-<root>-N`); name the provider only. */
|
|
74
|
+
function kindName(kind: string): string {
|
|
75
|
+
return kind.replace(/-[0-9A-HJKMNP-TV-Z]{26}$/u, '');
|
|
76
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { authoredBroker } from './authored-broker.js';
|
|
2
|
+
import { brokerInstruction, brokerOptionsError } from './communication/broker-spec.js';
|
|
1
3
|
import { dirname, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
2
4
|
import type { AuthoredBudget } from './authored-budget.js';
|
|
3
5
|
import { parseBudget } from './budget.js';
|
|
@@ -159,14 +161,24 @@ export function authoredWorkerRunner(
|
|
|
159
161
|
onCompleted === undefined ? context : { ...context, onCompleted }, execution.report.status);
|
|
160
162
|
};
|
|
161
163
|
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
164
|
+
const cleanup = step.type === 'agent' && brokerInstruction(step.instruction)?.operation === 'release';
|
|
162
165
|
const admit = async () => budget === undefined
|
|
163
166
|
? consume(await journal.runStart(spec, undefined, admissionKey))
|
|
164
|
-
: budget.execute(journal, spec, consume, admissionKey);
|
|
167
|
+
: budget.execute(journal, spec, consume, admissionKey, false, cleanup);
|
|
165
168
|
return slotScopes === undefined ? admit() : slotScopes[step.type === 'llm' ? 'llm' : 'agent'].run(admit);
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
const broker = authoredBroker(definition, rootRunId, localAgentStream, async step => {
|
|
172
|
+
const output = await run(step);
|
|
173
|
+
if (typeof output === 'object' && output !== null && AGENT_TIMEOUT in output) {
|
|
174
|
+
return { completionReason: 'timeout', summary: String(output[AGENT_TIMEOUT]), artifacts: [] };
|
|
175
|
+
}
|
|
176
|
+
return output;
|
|
177
|
+
});
|
|
168
178
|
return {
|
|
169
179
|
slots,
|
|
180
|
+
message: broker.message,
|
|
181
|
+
release: broker.release,
|
|
170
182
|
/** Refuse every agent/LLM call still waiting for a worker slot (body teardown). */
|
|
171
183
|
stop(reason: unknown): void {
|
|
172
184
|
if (ownsSlots) {
|
|
@@ -177,9 +189,12 @@ export function authoredWorkerRunner(
|
|
|
177
189
|
slotScopes?.llm.close(reason);
|
|
178
190
|
}
|
|
179
191
|
},
|
|
180
|
-
async agent(id: string, options: AgentOptions, verification?: NamedGate): Promise<AgentResult> {
|
|
192
|
+
async agent(id: string, options: AgentOptions, verification?: NamedGate, name = id): Promise<AgentResult> {
|
|
181
193
|
const secrets = validateStepSecrets((options as AgentOptions & { secrets?: unknown }).secrets,
|
|
182
194
|
agentEnvironment ?? process.env);
|
|
195
|
+
if (options.transport === 'broker') return broker.agent(id, name, options, verification, secrets);
|
|
196
|
+
const brokerProblem = brokerOptionsError(options);
|
|
197
|
+
if (brokerProblem) throw new AuthoredFlowExecutionError('agent_cli_unresolved', brokerProblem);
|
|
183
198
|
if (options.workspace !== undefined && localAgentStream !== undefined) {
|
|
184
199
|
throw new AuthoredFlowExecutionError('unsupported_workspace_permission',
|
|
185
200
|
'The local agent worker accepts stream-only steps. Remove workspace or attach a worker that holds its revision pins.');
|
package/src/budget-preflight.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { brokerInstruction } from './communication/broker-spec.js';
|
|
1
2
|
import type { CompiledFlowSpec } from './compile.js';
|
|
2
3
|
import type { PreflightWarning } from './preflight.js';
|
|
3
4
|
import { hasPricing } from './model-pricing.js';
|
|
@@ -34,10 +35,17 @@ export function budgetDiagnostics(
|
|
|
34
35
|
flow: CompiledFlowSpec,
|
|
35
36
|
resolved: ReadonlyMap<string, BudgetStepResolution> = new Map(),
|
|
36
37
|
): PreflightWarning[] {
|
|
37
|
-
|
|
38
|
+
const dollarBudget = flow.budget?.pricing === 'frozen' && flow.budget.maxDollars !== undefined;
|
|
38
39
|
const warnings: PreflightWarning[] = [];
|
|
39
40
|
for (const step of flow.steps) {
|
|
40
41
|
if (step.type === 'deterministic') continue;
|
|
42
|
+
const broker = step.type === 'agent' ? brokerInstruction(step.instruction) : undefined;
|
|
43
|
+
if (broker) {
|
|
44
|
+
if (broker.operation !== 'release') warnings.push({ severity: 'warning', kind: 'budget_unmetered', stepId: step.id,
|
|
45
|
+
message: `Step "${step.id}" uses a broker session that reports no token or dollar usage. Token and dollar ceilings cannot bound its spend; timeout bounds its duration.` });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!dollarBudget) continue;
|
|
41
49
|
const resolution = resolved.get(step.id);
|
|
42
50
|
const model = resolution?.model
|
|
43
51
|
?? step.model ?? (step.type === 'agent' && step.agent !== undefined
|
package/src/bundle-transport.ts
CHANGED
|
@@ -11,6 +11,10 @@ export class BundleFailure extends Error {
|
|
|
11
11
|
constructor(readonly kind: BundleFailureKind, message: string) { super(message); }
|
|
12
12
|
}
|
|
13
13
|
export interface DigestReference { name: string; digest: string }
|
|
14
|
+
/** Bare cloud names exclude file extensions and directory separators. */
|
|
15
|
+
export function isFlowName(value: string): boolean {
|
|
16
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/u.test(value);
|
|
17
|
+
}
|
|
14
18
|
export function parseDigestReference(value: string): DigestReference | undefined {
|
|
15
19
|
// Match the safe single-component name grammar used by sealBundle.
|
|
16
20
|
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)@sha256:([0-9a-f]{64})$/.exec(value);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { mkdir, readFile, realpath, symlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/** Cached single-file flows use the CLI's installed authoring runtime. */
|
|
7
|
+
export async function prepareCachedFlowRuntime(directory: string): Promise<void> {
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const packages = [
|
|
10
|
+
['@relayflows/surface', require.resolve('@relayflows/surface/runtime')],
|
|
11
|
+
['@relayflows/sdk', fileURLToPath(import.meta.url)],
|
|
12
|
+
] as const;
|
|
13
|
+
for (const [name, entry] of packages) {
|
|
14
|
+
let root = dirname(entry);
|
|
15
|
+
for (;;) {
|
|
16
|
+
const manifest = await readFile(join(root, 'package.json'), 'utf8').catch(() => '{}');
|
|
17
|
+
if ((JSON.parse(manifest) as { name?: string }).name === name) break;
|
|
18
|
+
const parent = dirname(root);
|
|
19
|
+
if (parent === root) throw new Error(`Cannot locate installed ${name}.`);
|
|
20
|
+
root = parent;
|
|
21
|
+
}
|
|
22
|
+
const target = join(directory, 'node_modules', name);
|
|
23
|
+
await mkdir(dirname(target), { recursive: true });
|
|
24
|
+
try { await symlink(root, target, 'dir'); }
|
|
25
|
+
catch (error) {
|
|
26
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
27
|
+
if (await realpath(target) !== await realpath(root)) {
|
|
28
|
+
throw new Error(`Cached runtime at ${target} differs from this CLI. Remove that link and retry.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/cli/check.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { checkBrokerEnvironment } from '../communication/broker-preflight.js';
|
|
2
|
+
import { brokerInstruction } from '../communication/broker-spec.js';
|
|
1
3
|
import { validateSpecWithDaemon, type DaemonValidation, type DaemonValidationMode, type DaemonValidationDeps } from '../daemon-spec-validation.js';
|
|
2
4
|
import { communicationInstruction } from '../communication/spec.js';
|
|
3
5
|
import { rememberResolvedCliIdentities } from '../resolved-cli-identity.js';
|
|
@@ -246,6 +248,14 @@ export function checkAuthoredFlow(
|
|
|
246
248
|
effectiveProbeCache,
|
|
247
249
|
)
|
|
248
250
|
: undefined;
|
|
251
|
+
if (flow?.steps.some(step => step.type === 'agent' && brokerInstruction(step.instruction)?.operation === 'spawn')) {
|
|
252
|
+
try { checkBrokerEnvironment(); }
|
|
253
|
+
catch (error) {
|
|
254
|
+
result.ok = false;
|
|
255
|
+
result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
|
|
256
|
+
message: error instanceof Error ? error.message : 'Broker environment could not be checked.' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
249
259
|
if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
250
260
|
try { if (options?.communicationChecked !== true) checkCommunicationEnvironment(flow); }
|
|
251
261
|
catch (error) {
|
|
@@ -520,7 +530,7 @@ function bindResolvedCliPaths(
|
|
|
520
530
|
const resolution = byStep.get(step.id);
|
|
521
531
|
if (resolution === undefined) return step;
|
|
522
532
|
const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
|
|
523
|
-
const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
|
|
533
|
+
const managed = step.type === 'agent' && (communicationInstruction(step.instruction) !== undefined || brokerInstruction(step.instruction) !== undefined);
|
|
524
534
|
const outcome = probeCache.get(cliProbeKey(resolution, managed, step.secrets));
|
|
525
535
|
const executable = outcome !== undefined && 'result' in outcome
|
|
526
536
|
? outcome.result.executable
|
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
98
98
|
value = arg;
|
|
99
99
|
}
|
|
100
100
|
if (value === undefined) return undefined;
|
|
101
|
-
if (flow === undefined &&
|
|
101
|
+
if (flow === undefined && on.length > 0 && repo === undefined) return undefined;
|
|
102
102
|
return { command: 'cloud-deploy', value, flow, repo, on, approver, name, agents, draft, noConnect, json, plugins };
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -119,7 +119,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
119
119
|
// by deployToCloud, not here.
|
|
120
120
|
let harnesses: readonly string[] = agents instanceof CloudFlowError ? [] : agents ?? [];
|
|
121
121
|
try {
|
|
122
|
-
if (args.approver === undefined) {
|
|
122
|
+
if (args.on.length > 0 && args.approver === undefined) {
|
|
123
123
|
throw new CloudFlowError('invalid_input',
|
|
124
124
|
'--approver <handle> is required: every launched run receives it as input.approver for f.human.');
|
|
125
125
|
}
|
|
@@ -128,7 +128,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
128
128
|
if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
|
|
129
129
|
const deployment = await deployToCloud({
|
|
130
130
|
path: args.value,
|
|
131
|
-
repository: parseRepository(args.repo
|
|
131
|
+
...(args.repo === undefined ? {} : { repository: parseRepository(args.repo) }),
|
|
132
132
|
sources: args.on.map(parseTriggerSource),
|
|
133
133
|
approver: args.approver,
|
|
134
134
|
draft: args.draft,
|
|
@@ -144,12 +144,12 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
144
144
|
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
145
145
|
+ (deployment.version === undefined ? '' : ` · ${describeVersionChange(deployment.version)}`));
|
|
146
146
|
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
147
|
-
io.stdout(` repository: ${describeRepository(deployment.repository)}`);
|
|
147
|
+
if (deployment.repository) io.stdout(` repository: ${describeRepository(deployment.repository)}`);
|
|
148
148
|
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
149
149
|
const requires = describeFlowRequirements(deployment.requirements);
|
|
150
150
|
if (requires) io.stdout(` requires: ${requires}`);
|
|
151
151
|
for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
|
|
152
|
-
io.stdout(deployment.status === 'draft'
|
|
152
|
+
io.stdout(deployment.sources.length === 0 ? `Run locally: flows run ${deployment.name}` : deployment.status === 'draft'
|
|
153
153
|
? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
|
|
154
154
|
: 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
|
|
155
155
|
return 0;
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the
|
|
3
|
-
//
|
|
4
|
-
// Opt-in, and deliberately the only opt-in thing on this path.
|
|
5
|
-
//
|
|
6
|
-
// A local run is already watchable by default: `observer-session.ts` beside
|
|
7
|
-
// this one projects the run into its own channel and prints a read-only link,
|
|
8
|
-
// off a workspace key, for free. That is the right default — it is a step
|
|
9
|
-
// projection, it costs nothing, and a run that never joins a workspace is not
|
|
10
|
-
// a defect (RFC-0001 settled decision 7).
|
|
11
|
-
//
|
|
12
|
-
// The dashboard is the richer, hosted view of the same run: the flow source,
|
|
13
|
-
// every step's transcript, the run graph, the logs, and the run sitting in the
|
|
14
|
-
// same history as the hosted ones. It is also the one that *stores* all of
|
|
15
|
-
// that. So it is asked for — `--cloud-mirror`, or `FLOWS_CLOUD_MIRROR=1` for a
|
|
16
|
-
// shell — and never turned on by a login happening to be present. Mirroring
|
|
17
|
-
// someone's local runs because they once signed in is not a default anyone
|
|
18
|
-
// consented to.
|
|
19
|
-
//
|
|
20
|
-
// Best-effort once it is on, like the observer: a registration failure is one
|
|
21
|
-
// labeled stderr line, and nothing here can change a run's exit code or its
|
|
22
|
-
// journal. But it is a *louder* line than it used to be, because the run asked
|
|
23
|
-
// for this and did not get it.
|
|
1
|
+
// Signed-in local runs project their journal onto the Cloud dashboard.
|
|
2
|
+
// Reporting is best-effort and never changes the durable run outcome.
|
|
24
3
|
|
|
25
4
|
import { readFile } from 'node:fs/promises';
|
|
26
5
|
import type { CliIo } from '../cli.js';
|
|
@@ -59,7 +38,7 @@ export interface CloudMirrorSession {
|
|
|
59
38
|
*/
|
|
60
39
|
receipt(): Promise<CloudMirrorReceipt | undefined>;
|
|
61
40
|
/** A journaled entry; the first one names the run for a declarative flow. */
|
|
62
|
-
onJournalEntry(entry: { run_id: string }): void;
|
|
41
|
+
onJournalEntry(entry: { run_id: string; entry_type?: string; step_id?: string | null }): void;
|
|
63
42
|
/** Step transitions, pushed as lifecycle events the run page's stream shows. */
|
|
64
43
|
onProgress(event: ProgressEvent): void;
|
|
65
44
|
/** Publish the final report and the terminal status. Never rejects. */
|
|
@@ -82,7 +61,7 @@ export interface CloudMirrorRequest {
|
|
|
82
61
|
/** Lines the CLI has printed for this run, uploaded as the run's `runner.log`. */
|
|
83
62
|
log: () => readonly string[];
|
|
84
63
|
/** How the dashboard was asked for, so a refusal can name the right switch. */
|
|
85
|
-
requested: 'flag' | 'env';
|
|
64
|
+
requested: 'flag' | 'env' | 'login';
|
|
86
65
|
}
|
|
87
66
|
|
|
88
67
|
/**
|
|
@@ -147,7 +126,14 @@ export function createCloudMirrorSession(
|
|
|
147
126
|
void open(run.runId);
|
|
148
127
|
},
|
|
149
128
|
onJournalEntry(entry) {
|
|
150
|
-
|
|
129
|
+
const opened = open(entry.run_id);
|
|
130
|
+
if (entry.step_id && (entry.entry_type === 'step.attempt.started' || entry.entry_type === 'step.completed')) {
|
|
131
|
+
void opened.then(mirror => mirror?.event({
|
|
132
|
+
eventType: entry.entry_type === 'step.completed' ? 'relayflow.step.completed' : 'relayflow.step.started',
|
|
133
|
+
stepName: entry.step_id!,
|
|
134
|
+
payload: { journalRunId: entry.run_id },
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
151
137
|
},
|
|
152
138
|
async receipt() {
|
|
153
139
|
if (opening === undefined) return undefined;
|
|
@@ -194,6 +180,7 @@ export function mirrorSourceFromPath(
|
|
|
194
180
|
path: string,
|
|
195
181
|
inputArgument: string | undefined,
|
|
196
182
|
dataDir: string,
|
|
183
|
+
named?: { flowId: string; flowVersionId: string; source: string },
|
|
197
184
|
): (runId: string) => Promise<MirrorRunSource> {
|
|
198
185
|
const fromJournal = mirrorSourceFromJournal(dataDir);
|
|
199
186
|
return async (runId) => {
|
|
@@ -203,9 +190,10 @@ export function mirrorSourceFromPath(
|
|
|
203
190
|
// the dashboard, so fall back to what the journal recorded — the same
|
|
204
191
|
// source a resume mirrors.
|
|
205
192
|
if (parseDigestReference(path)) return fromJournal(runId);
|
|
206
|
-
const workflow = await readFile(path, 'utf8');
|
|
193
|
+
const workflow = named?.source ?? await readFile(path, 'utf8');
|
|
207
194
|
if (!isAuthoredFlowPath(path)) return { workflow, fileType: 'yaml' };
|
|
208
|
-
return { workflow, fileType: 'ts', inputs: parseDirectInput(inputArgument)
|
|
195
|
+
return { workflow, fileType: 'ts', inputs: parseDirectInput(inputArgument),
|
|
196
|
+
...(named === undefined ? {} : { flowId: named.flowId, flowVersionId: named.flowVersionId }) };
|
|
209
197
|
};
|
|
210
198
|
}
|
|
211
199
|
|
|
@@ -305,7 +293,13 @@ function completionReport(report: RunReport): Record<string, unknown> {
|
|
|
305
293
|
* missing-login case is the common one and names the two commands that fix
|
|
306
294
|
* it: sign in, or stop asking.
|
|
307
295
|
*/
|
|
308
|
-
function mirrorRefusal(error: unknown, requested: 'flag' | 'env'): string {
|
|
296
|
+
function mirrorRefusal(error: unknown, requested: 'flag' | 'env' | 'login'): string {
|
|
297
|
+
if (requested === 'login') {
|
|
298
|
+
if (error instanceof CloudFlowError && error.reason === 'auth_missing') {
|
|
299
|
+
return 'Not signed in; this run stays local. Sign in with `agent-relay cloud login` to report runs.';
|
|
300
|
+
}
|
|
301
|
+
return `Local run reporting unavailable: ${error instanceof Error ? error.message : String(error)}; the run itself is unaffected.`;
|
|
302
|
+
}
|
|
309
303
|
const asked = requested === 'flag' ? '--cloud-mirror' : `${MIRROR_ENV}=1`;
|
|
310
304
|
if (error instanceof CloudFlowError && error.code === 'configuration') {
|
|
311
305
|
return error.reason === 'auth_missing'
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -221,7 +221,8 @@ export async function runDirectFlow(
|
|
|
221
221
|
// diagnostic carried up from `classifyOutcome` already names the step, its
|
|
222
222
|
// exit code and its output tail; this branch is what lets it reach the
|
|
223
223
|
// terminal. `resumeFlow` takes the same branch, through the same helper.
|
|
224
|
-
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
|
|
224
|
+
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
|
|
225
|
+
|| error.code === 'serial_replay_order_mismatch')) {
|
|
225
226
|
return authoredStepFailure('run', base, socketPath, error);
|
|
226
227
|
}
|
|
227
228
|
if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
|
package/src/cli/run.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { communicationInstruction } from '../communication/spec.js';
|
|
|
7
7
|
import { checkCommunicationEnvironment, CommunicationEnvironmentError } from '../communication/preflight.js';
|
|
8
8
|
import { parseHumanRecipient } from '../human-to.js';
|
|
9
9
|
import { parseDigestReference } from '../bundle-transport.js';
|
|
10
|
+
import { resolveNamedFlow } from '../cloud-flow-source.js';
|
|
10
11
|
import { prepareDigestRun } from './run-digest.js';
|
|
11
12
|
import { attachReuseSummary } from './reuse.js';
|
|
12
13
|
import { resumeHelperEffect } from '../authored-helper-effect.js';
|
|
@@ -47,6 +48,7 @@ import { DEFAULT_LOCAL_AGENT_CAPACITY } from '../worker-slots.js';
|
|
|
47
48
|
import { localAgentEnvironment } from '../local-agent-environment.js';
|
|
48
49
|
import {
|
|
49
50
|
checkFlow,
|
|
51
|
+
inputFailureReport,
|
|
50
52
|
type CheckReport,
|
|
51
53
|
} from './check.js';
|
|
52
54
|
|
|
@@ -182,6 +184,17 @@ export async function runFlow(
|
|
|
182
184
|
dataDir: string,
|
|
183
185
|
options: RunLifecycleOptions = {},
|
|
184
186
|
): Promise<RunExecution> {
|
|
187
|
+
try {
|
|
188
|
+
const named = await resolveNamedFlow(path);
|
|
189
|
+
if (named) {
|
|
190
|
+
const { runDirectFlow } = await import('./direct-run.js');
|
|
191
|
+
return runDirectFlow(named.path, '{}', dataDir, options);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { exitCode: 2, report: fromCheckReport('run', inputFailureReport({
|
|
195
|
+
kind: 'input_unreadable', message: error instanceof Error ? error.message : String(error),
|
|
196
|
+
}, path)) };
|
|
197
|
+
}
|
|
185
198
|
const agentEnvironment = localAgentEnvironment();
|
|
186
199
|
const prepared = parseDigestReference(path) ? await prepareDigestRun(path, options.bucket) : undefined;
|
|
187
200
|
if (prepared && 'exitCode' in prepared) return prepared;
|
|
@@ -432,7 +445,8 @@ export async function resumeFlow(
|
|
|
432
445
|
// leaving it on `protocolFailure` meant `flows run` printed the evidence
|
|
433
446
|
// while `flows resume` still printed `protocol_error` and
|
|
434
447
|
// `RUN <id> unknown` for the identical failure.
|
|
435
|
-
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
|
|
448
|
+
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
|
|
449
|
+
|| error.code === 'serial_replay_order_mismatch')) {
|
|
436
450
|
return authoredStepFailure('resume', base, socketPath, error, runId);
|
|
437
451
|
}
|
|
438
452
|
if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
|
|
@@ -632,10 +646,11 @@ export function authoredStepFailure(
|
|
|
632
646
|
severity: 'failure',
|
|
633
647
|
// A predicate gate that judged false is a run failure with its own
|
|
634
648
|
// name, so the report says which kind of check the body did not pass.
|
|
635
|
-
|
|
649
|
+
// A refused out-of-order replay names itself the same way.
|
|
650
|
+
kind: error.code === 'gate_failed' || error.code === 'serial_replay_order_mismatch' ? error.code : 'step_failed',
|
|
636
651
|
// The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
|
|
637
652
|
// redundant once the diagnostic is labelled `[step_failed]`.
|
|
638
|
-
message: error.message.replace(/^(?:step_failed|gate_failed): /, ''),
|
|
653
|
+
message: error.message.replace(/^(?:step_failed|gate_failed|serial_replay_order_mismatch): /, ''),
|
|
639
654
|
}],
|
|
640
655
|
},
|
|
641
656
|
};
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { resolveNamedFlow, type NamedFlowSource } from './cloud-flow-source.js';
|
|
3
|
+
import { isFlowName } from './bundle-transport.js';
|
|
4
|
+
import { CloudFlowError } from './cloud-http.js';
|
|
2
5
|
import { unavailableValidation, validationAnnotation, type DaemonValidationMode } from './daemon-spec-validation.js';
|
|
3
6
|
import { foldUnprovableEffects, type FoldOptions } from './cli/diagnostic-fold.js';
|
|
4
7
|
import { startDetachedRun, emitDetachedHandle } from './cli/detached-run.js';
|
|
@@ -134,6 +137,7 @@ const USAGE = [
|
|
|
134
137
|
'flows plugin update [--json] [--yes] [--to <ref>] [<name>]',
|
|
135
138
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
136
139
|
'flows build --verify <bundle-dir>',
|
|
140
|
+
'flows deploy <flow.ts> [--name <name>] [--json]',
|
|
137
141
|
'flows deploy <flow.ts> --repo <owner/name|gitlab:group/project> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--plugin <ref>] [--no-connect] [--json]',
|
|
138
142
|
'flows deploy <flow.ts> --flow <name|listener-id> [--plugin <ref>] [--no-connect] [--json]',
|
|
139
143
|
'flows deployments [--json]',
|
|
@@ -152,6 +156,7 @@ const USAGE = [
|
|
|
152
156
|
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] [--budget-minutes <n>] <flow.ts> --input <inline-json-or-file>',
|
|
153
157
|
'flows sync [--json] [--dry-run] [--dir <path>] <run-id>',
|
|
154
158
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--detach] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <flow.ts> --input <inline-json-or-file>',
|
|
159
|
+
'flows run <name> [--input <inline-json-or-file>]',
|
|
155
160
|
'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>',
|
|
156
161
|
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--detach] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <run-id>',
|
|
157
162
|
'flows answer --cloud [--json] [--note <text>] [--source <path>] <run-id> <yes|no>',
|
|
@@ -259,7 +264,7 @@ export async function runCli(
|
|
|
259
264
|
const link = resolveObserverLinkEnv(process.env);
|
|
260
265
|
if (!link.suppressed && link.workspaceKey === undefined) {
|
|
261
266
|
io.stderr('Observer unavailable: no workspace key configured; set RELAYCAST_WORKSPACE_KEY or configure '
|
|
262
|
-
+ '~/.agentworkforce/relay/workspaces.json for an Observer link, or
|
|
267
|
+
+ '~/.agentworkforce/relay/workspaces.json for an Observer link, or sign in with agent-relay cloud login for the Cloud dashboard');
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
|
|
@@ -379,6 +384,27 @@ export async function runCli(
|
|
|
379
384
|
}, io) as CliExitCode);
|
|
380
385
|
}
|
|
381
386
|
|
|
387
|
+
let named: NamedFlowSource | undefined;
|
|
388
|
+
if (parsed.command === 'run') {
|
|
389
|
+
try {
|
|
390
|
+
named = await resolveNamedFlow(parsed.value);
|
|
391
|
+
if (!named && parsed.input !== undefined && !isAuthoredFlowPath(parsed.value)) {
|
|
392
|
+
throw new CloudFlowError('invalid_input', '--input requires an authored .flow.ts or a saved flow name.');
|
|
393
|
+
}
|
|
394
|
+
if (named) {
|
|
395
|
+
if (parsed.reuseFromRunId !== undefined) throw new CloudFlowError('invalid_input', '--reuse-from is not supported for authored named flows.');
|
|
396
|
+
parsed.value = named.path;
|
|
397
|
+
parsed.input ??= '{}';
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const code = error instanceof CloudFlowError ? error.refusal?.code ?? error.code : 'flow_resolution_failed';
|
|
401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
402
|
+
if (parsed.json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
403
|
+
else io.stderr(`${code}: ${message}`);
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
382
408
|
// Attach-or-spawn runs inside `runFlow`/`resumeFlow`/`runDirectFlow`, at the
|
|
383
409
|
// single `connect()` seam immediately before journal-client.ts is used --
|
|
384
410
|
// not here. Hoisting it above the dispatch would start a daemon as a side
|
|
@@ -388,21 +414,13 @@ export async function runCli(
|
|
|
388
414
|
onObserverUrl: url => options.detachedReceipt?.observerUrl(url),
|
|
389
415
|
});
|
|
390
416
|
const runnerLog: string[] = [];
|
|
391
|
-
|
|
392
|
-
// way to watch a local run: it is free, it needs only a workspace key, and
|
|
393
|
-
// it carries a step projection. The dashboard is the richer, hosted view —
|
|
394
|
-
// it stores the flow source, every step's transcript and this invocation's
|
|
395
|
-
// own output — so a local run joins it because someone asked, never because
|
|
396
|
-
// a login happened to be lying around.
|
|
397
|
-
const mirror = !(parsed.cloudMirror || cloudMirrorRequested(process.env))
|
|
398
|
-
? undefined
|
|
399
|
-
: createCloudMirrorSession({
|
|
417
|
+
const mirror = createCloudMirrorSession({
|
|
400
418
|
source: parsed.command === 'run'
|
|
401
|
-
? mirrorSourceFromPath(parsed.value, parsed.input, parsed.dataDir)
|
|
419
|
+
? mirrorSourceFromPath(parsed.value, parsed.input, parsed.dataDir, named)
|
|
402
420
|
: mirrorSourceFromJournal(parsed.dataDir),
|
|
403
421
|
dataDir: parsed.dataDir,
|
|
404
422
|
log: () => runnerLog,
|
|
405
|
-
requested: parsed.cloudMirror ? 'flag' : 'env',
|
|
423
|
+
requested: parsed.cloudMirror ? 'flag' : cloudMirrorRequested(process.env) ? 'env' : 'login',
|
|
406
424
|
}, io);
|
|
407
425
|
// Everything this invocation prints, kept in order, so the mirror can upload
|
|
408
426
|
// it as the run's `runner.log` — the object the dashboard's log pane reads,
|
|
@@ -888,7 +906,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
888
906
|
if (wait || syncCode || noConnect || budgetMinutes !== undefined) return undefined;
|
|
889
907
|
if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
890
908
|
|
|
891
|
-
if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
909
|
+
if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!) && !isFlowName(positionals[0]!)) return undefined;
|
|
892
910
|
return command === 'check'
|
|
893
911
|
? { command, json, watch, explainWarnings, value: positionals[0]!,
|
|
894
912
|
daemonValidation: daemonValidation ?? (process.env['FLOWS_NO_DAEMON_CHECK'] === '1' ? 'off'
|