@relayflows/sdk 2.0.55 → 2.0.57
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 +2 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/devin.d.ts +4 -0
- package/dist/adapters/devin.d.ts.map +1 -0
- package/dist/adapters/devin.js +20 -0
- package/dist/adapters/devin.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -1
- package/dist/adapters/index.js.map +1 -1
- 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 +18 -3
- 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/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +72 -11
- package/dist/cli/cli-probe.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/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +1 -0
- package/dist/communication/environment.js.map +1 -1
- package/dist/communication/pty.d.ts +29 -0
- package/dist/communication/pty.d.ts.map +1 -0
- package/dist/communication/pty.js +44 -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 +18 -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/devin-runtime.d.ts +10 -0
- package/dist/devin-runtime.d.ts.map +1 -0
- package/dist/devin-runtime.js +37 -0
- package/dist/devin-runtime.js.map +1 -0
- 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 +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +14 -4
- package/dist/preflight.js.map +1 -1
- package/dist/step-secrets.d.ts.map +1 -1
- package/dist/step-secrets.js +1 -0
- package/dist/step-secrets.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +15 -3
- package/dist/worker-cli.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/adapters/base.ts +2 -0
- package/src/adapters/devin.ts +22 -0
- package/src/adapters/index.ts +5 -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 +16 -2
- 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/cli-probe.ts +65 -11
- 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/environment.ts +1 -0
- package/src/communication/pty.ts +51 -0
- package/src/communication/relay.ts +12 -1
- package/src/communication/skills.ts +17 -0
- package/src/communication/worker.ts +9 -26
- package/src/devin-runtime.ts +33 -0
- package/src/failure-kinds.ts +2 -0
- package/src/plugin-manifest.ts +1 -1
- package/src/preflight.ts +15 -4
- package/src/step-secrets.ts +1 -0
- package/src/worker-cli.ts +13 -5
- package/src/worker.ts +11 -0
|
@@ -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,16 +35,29 @@ 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
|
+
}
|
|
41
48
|
const resolution = resolved.get(step.id);
|
|
49
|
+
const cli = resolution?.cli ?? step.cli ?? (step.type === 'agent' && step.agent !== undefined
|
|
50
|
+
? flow.agents?.[step.agent]?.cli : undefined);
|
|
51
|
+
if (cli !== undefined && resolveAdapterKind(cli) === 'devin') {
|
|
52
|
+
warnings.push({ severity: 'warning', kind: 'budget_unmetered', stepId: step.id,
|
|
53
|
+
message: `Step "${step.id}" uses Devin print mode, which reports no token or dollar usage. Token and dollar ceilings cannot bound its spend; timeout bounds its duration.` });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!dollarBudget) continue;
|
|
42
57
|
const model = resolution?.model
|
|
43
58
|
?? step.model ?? (step.type === 'agent' && step.agent !== undefined
|
|
44
59
|
? flow.agents?.[step.agent]?.model : undefined);
|
|
45
60
|
if (hasPricing(model)) continue;
|
|
46
|
-
const cli = resolution?.cli ?? step.cli;
|
|
47
61
|
const reason = model !== undefined
|
|
48
62
|
? `model "${model}" has no frozen price`
|
|
49
63
|
: cli !== undefined && resolveAdapterKind(cli) === 'codex'
|
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/cli-probe.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { accessSync, constants, realpathSync } from 'node:fs';
|
|
2
2
|
import { basename, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { execFile, spawnSync } from 'node:child_process';
|
|
4
|
+
import { redactText } from '../agent-transcript.js';
|
|
4
5
|
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
5
6
|
import { adapterIdentification, authenticationProbe, classifyModelProbeFailure, cliAdapterKind,
|
|
6
7
|
displayInvocation, modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
|
|
7
8
|
import { MODEL_ENV } from '../worker-cli.js';
|
|
8
9
|
import { CliProbeError, type CliProbeResult } from '../preflight.js';
|
|
9
10
|
import { pinCliAlias, pinCliAliasSync } from './pinned-cli-alias.js';
|
|
11
|
+
import { prepareDevinRuntime } from '../devin-runtime.js';
|
|
10
12
|
import { agentProbeEnvironment } from '../step-secrets.js';
|
|
11
13
|
|
|
12
14
|
interface ProbeRequest {
|
|
@@ -40,15 +42,18 @@ function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
|
40
42
|
while (!next.done) {
|
|
41
43
|
const request = next.value;
|
|
42
44
|
const pinned = pinCliAliasSync(request.executable, request.argv0 ?? basename(request.executable));
|
|
45
|
+
let prepared: ReturnType<typeof prepareProbe> | undefined;
|
|
43
46
|
const result = (() => {
|
|
44
47
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
prepared = prepareProbe(request);
|
|
49
|
+
return spawnSync(pinned.executable, prepared.args, {
|
|
50
|
+
...probeOptions({ ...request, environment: prepared.env }),
|
|
47
51
|
// Preserve the old synchronous probe contract: provider CLIs must not
|
|
48
52
|
// inherit a readable stdin that can block auth/identify probes.
|
|
49
53
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
54
|
});
|
|
51
55
|
} finally {
|
|
56
|
+
prepared?.close();
|
|
52
57
|
pinned.release();
|
|
53
58
|
}
|
|
54
59
|
})();
|
|
@@ -69,9 +74,12 @@ function probeOptions({ argv0, directory, invocation, environment }: ProbeReques
|
|
|
69
74
|
|
|
70
75
|
async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
71
76
|
const pinned = await pinCliAlias(request.executable, request.argv0 ?? basename(request.executable));
|
|
77
|
+
let prepared: ReturnType<typeof prepareProbe> | undefined;
|
|
72
78
|
try {
|
|
79
|
+
prepared = prepareProbe(request);
|
|
80
|
+
const current = prepared;
|
|
73
81
|
return await new Promise((resolve, reject) => {
|
|
74
|
-
const child = execFile(pinned.executable,
|
|
82
|
+
const child = execFile(pinned.executable, current.args, probeOptions({ ...request, environment: current.env }),
|
|
75
83
|
(error, stdout, stderr) => {
|
|
76
84
|
// Numeric exit codes are probe results; launch, timeout, signal and buffer
|
|
77
85
|
// errors are failures to collect a fact, just as in the synchronous driver.
|
|
@@ -86,6 +94,7 @@ async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
|
86
94
|
child.stdin?.end();
|
|
87
95
|
});
|
|
88
96
|
} finally {
|
|
97
|
+
prepared?.close();
|
|
89
98
|
await pinned.release();
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -133,12 +142,16 @@ function* probeSequence(
|
|
|
133
142
|
const auth = authenticationProbe(kind);
|
|
134
143
|
const authCommand = displayInvocation(cli, auth);
|
|
135
144
|
if (model === undefined) {
|
|
145
|
+
const authProbe = yield probe(auth);
|
|
146
|
+
const authenticated = authProbe.status === 0;
|
|
136
147
|
return {
|
|
137
148
|
exists: true,
|
|
138
149
|
supported: true,
|
|
139
|
-
authenticated
|
|
150
|
+
authenticated,
|
|
140
151
|
executable,
|
|
141
152
|
authCommand,
|
|
153
|
+
...(authenticated ? {} : { authExitCode: authProbe.status,
|
|
154
|
+
authFailureDetail: redactProbeOutput(`${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment).trim().slice(0, 500) }),
|
|
142
155
|
};
|
|
143
156
|
}
|
|
144
157
|
|
|
@@ -168,7 +181,7 @@ function* probeSequence(
|
|
|
168
181
|
// because Claude Code prints the provider's API error there and its own
|
|
169
182
|
// catalog warnings on stderr.
|
|
170
183
|
const failure = classifyModelProbeFailure(kind, `${scopedProbe.stdout}\n${scopedProbe.stderr}`);
|
|
171
|
-
const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim()).slice(0, 500);
|
|
184
|
+
const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim(), sourceEnvironment).slice(0, 500);
|
|
172
185
|
return {
|
|
173
186
|
exists: true,
|
|
174
187
|
supported: true,
|
|
@@ -187,7 +200,7 @@ function* probeSequence(
|
|
|
187
200
|
: {
|
|
188
201
|
authExitCode: authProbe.status,
|
|
189
202
|
authFailureDetail: redactProbeOutput(
|
|
190
|
-
`${authProbe.stderr}${authProbe.stdout}`,
|
|
203
|
+
`${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment,
|
|
191
204
|
).trim().slice(0, 500),
|
|
192
205
|
}),
|
|
193
206
|
};
|
|
@@ -201,11 +214,42 @@ function* probeSequence(
|
|
|
201
214
|
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
202
215
|
* refusal message that gets pasted into issues does not.
|
|
203
216
|
*/
|
|
204
|
-
function redactProbeOutput(text: string): string {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
function redactProbeOutput(text: string, environment: NodeJS.ProcessEnv): string {
|
|
218
|
+
const key = environment.DEVIN_API_KEY?.trim();
|
|
219
|
+
const normalized = { ...environment, ...(key === undefined ? {} : { DEVIN_API_KEY: key }) };
|
|
220
|
+
// Preserve the shared redactor's known-value, header and cookie handling.
|
|
221
|
+
const redacted = redactText(text, normalized);
|
|
222
|
+
// One byte per diagnostic character bounds auxiliary memory even when a
|
|
223
|
+
// one-character credential matches the entire probe buffer. No span lists.
|
|
224
|
+
const covered = new Uint8Array(redacted.length);
|
|
225
|
+
const replacements = ['', '<redacted-email>', '<redacted-token>', '<redacted-hex>', '[redacted:DEVIN_API_KEY]'];
|
|
226
|
+
for (const [pattern, tag] of [
|
|
227
|
+
[/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, 1],
|
|
228
|
+
[/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, 2],
|
|
229
|
+
[/\b[A-Fa-f0-9]{32,}\b/g, 3],
|
|
230
|
+
] as const) {
|
|
231
|
+
for (const match of redacted.matchAll(pattern)) covered.fill(tag, match.index, match.index + match[0].length);
|
|
232
|
+
}
|
|
233
|
+
// Mark short credentials AFTER the shapes, on the same text: replacing
|
|
234
|
+
// either first can split the other and leak a suffix. Connected marks are
|
|
235
|
+
// redacted together; a known credential's marker takes precedence.
|
|
236
|
+
if (key && key.length < 8) {
|
|
237
|
+
for (let start = redacted.indexOf(key); start !== -1; start = redacted.indexOf(key, start + 1)) {
|
|
238
|
+
covered.fill(4, start, start + key.length);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let result = ''; let cursor = 0;
|
|
242
|
+
for (let start = 0; start < covered.length;) {
|
|
243
|
+
if (covered[start] === 0) { start++; continue; }
|
|
244
|
+
let tag = covered[start]!; let end = start + 1;
|
|
245
|
+
while (end < covered.length && covered[end] !== 0) {
|
|
246
|
+
if (covered[end] === 4) tag = 4;
|
|
247
|
+
end++;
|
|
248
|
+
}
|
|
249
|
+
result += redacted.slice(cursor, start) + replacements[tag];
|
|
250
|
+
cursor = end; start = end;
|
|
251
|
+
}
|
|
252
|
+
return result + redacted.slice(cursor);
|
|
209
253
|
}
|
|
210
254
|
|
|
211
255
|
function* executableSequence(command: string, directory: string, environment: NodeJS.ProcessEnv): Generator<ProbeRequest, string | undefined, ProbeOutput> {
|
|
@@ -245,3 +289,13 @@ function classifySpawnFailure(
|
|
|
245
289
|
}
|
|
246
290
|
return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
|
|
247
291
|
}
|
|
292
|
+
|
|
293
|
+
function prepareProbe(request: ProbeRequest) {
|
|
294
|
+
if (cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
|
|
295
|
+
&& request.environment.DEVIN_API_KEY !== undefined && !request.environment.DEVIN_API_KEY.trim()) {
|
|
296
|
+
throw new CliProbeError('devin_api_key_empty');
|
|
297
|
+
}
|
|
298
|
+
return cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
|
|
299
|
+
? prepareDevinRuntime(request.environment, request.invocation)
|
|
300
|
+
: { args: request.invocation.args, env: request.environment, close() {} };
|
|
301
|
+
}
|
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
|
};
|