@relayflows/sdk 2.0.0
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-flow-error.d.ts +9 -0
- package/dist/authored-flow-error.d.ts.map +1 -0
- package/dist/authored-flow-error.js +13 -0
- package/dist/authored-flow-error.js.map +1 -0
- package/dist/authored-flow-executor.d.ts +28 -0
- package/dist/authored-flow-executor.d.ts.map +1 -0
- package/dist/authored-flow-executor.js +203 -0
- package/dist/authored-flow-executor.js.map +1 -0
- package/dist/authored-flow-lifecycle.d.ts +61 -0
- package/dist/authored-flow-lifecycle.d.ts.map +1 -0
- package/dist/authored-flow-lifecycle.js +284 -0
- package/dist/authored-flow-lifecycle.js.map +1 -0
- package/dist/authored-flow-loader.d.ts +7 -0
- package/dist/authored-flow-loader.d.ts.map +1 -0
- package/dist/authored-flow-loader.js +39 -0
- package/dist/authored-flow-loader.js.map +1 -0
- package/dist/authored-flow-operation.d.ts +43 -0
- package/dist/authored-flow-operation.d.ts.map +1 -0
- package/dist/authored-flow-operation.js +176 -0
- package/dist/authored-flow-operation.js.map +1 -0
- package/dist/authored-flow.d.ts +12 -0
- package/dist/authored-flow.d.ts.map +1 -0
- package/dist/authored-flow.js +13 -0
- package/dist/authored-flow.js.map +1 -0
- package/dist/authored-promise-graph.d.ts +67 -0
- package/dist/authored-promise-graph.d.ts.map +1 -0
- package/dist/authored-promise-graph.js +206 -0
- package/dist/authored-promise-graph.js.map +1 -0
- package/dist/backlog-picker.d.ts +55 -0
- package/dist/backlog-picker.d.ts.map +1 -0
- package/dist/backlog-picker.js +117 -0
- package/dist/backlog-picker.js.map +1 -0
- package/dist/canonical.d.ts +18 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +55 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cli/check.d.ts +30 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +324 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/direct-run.d.ts +3 -0
- package/dist/cli/direct-run.d.ts.map +1 -0
- package/dist/cli/direct-run.js +69 -0
- package/dist/cli/direct-run.js.map +1 -0
- package/dist/cli/hn-monitor.d.ts +107 -0
- package/dist/cli/hn-monitor.d.ts.map +1 -0
- package/dist/cli/hn-monitor.js +220 -0
- package/dist/cli/hn-monitor.js.map +1 -0
- package/dist/cli/interruptible-sleep.d.ts +13 -0
- package/dist/cli/interruptible-sleep.d.ts.map +1 -0
- package/dist/cli/interruptible-sleep.js +31 -0
- package/dist/cli/interruptible-sleep.js.map +1 -0
- package/dist/cli/run.d.ts +54 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +316 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/tick-runner.d.ts +151 -0
- package/dist/cli/tick-runner.d.ts.map +1 -0
- package/dist/cli/tick-runner.js +258 -0
- package/dist/cli/tick-runner.js.map +1 -0
- package/dist/cli-adapter.d.ts +29 -0
- package/dist/cli-adapter.d.ts.map +1 -0
- package/dist/cli-adapter.js +92 -0
- package/dist/cli-adapter.js.map +1 -0
- package/dist/cli-executable.d.ts +3 -0
- package/dist/cli-executable.d.ts.map +1 -0
- package/dist/cli-executable.js +10 -0
- package/dist/cli-executable.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +348 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +42 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +515 -0
- package/dist/compile.js.map +1 -0
- package/dist/demo-hn-monitor.d.ts +2 -0
- package/dist/demo-hn-monitor.d.ts.map +1 -0
- package/dist/demo-hn-monitor.js +118 -0
- package/dist/demo-hn-monitor.js.map +1 -0
- package/dist/dir-watcher-poller.d.ts +73 -0
- package/dist/dir-watcher-poller.d.ts.map +1 -0
- package/dist/dir-watcher-poller.js +80 -0
- package/dist/dir-watcher-poller.js.map +1 -0
- package/dist/direct-input.d.ts +10 -0
- package/dist/direct-input.d.ts.map +1 -0
- package/dist/direct-input.js +61 -0
- package/dist/direct-input.js.map +1 -0
- package/dist/failure-kinds.d.ts +25 -0
- package/dist/failure-kinds.d.ts.map +1 -0
- package/dist/failure-kinds.js +64 -0
- package/dist/failure-kinds.js.map +1 -0
- package/dist/gate-contract.d.ts +24 -0
- package/dist/gate-contract.d.ts.map +1 -0
- package/dist/gate-contract.js +50 -0
- package/dist/gate-contract.js.map +1 -0
- package/dist/hn-poller.d.ts +50 -0
- package/dist/hn-poller.d.ts.map +1 -0
- package/dist/hn-poller.js +79 -0
- package/dist/hn-poller.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/journal-client.d.ts +122 -0
- package/dist/journal-client.d.ts.map +1 -0
- package/dist/journal-client.js +277 -0
- package/dist/journal-client.js.map +1 -0
- package/dist/json-schema-bound.d.ts +7 -0
- package/dist/json-schema-bound.d.ts.map +1 -0
- package/dist/json-schema-bound.js +353 -0
- package/dist/json-schema-bound.js.map +1 -0
- package/dist/json-schema.d.ts +5 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +72 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/json-value.d.ts +6 -0
- package/dist/json-value.d.ts.map +1 -0
- package/dist/json-value.js +94 -0
- package/dist/json-value.js.map +1 -0
- package/dist/model-name.d.ts +7 -0
- package/dist/model-name.d.ts.map +1 -0
- package/dist/model-name.js +21 -0
- package/dist/model-name.js.map +1 -0
- package/dist/output-schema.d.ts +8 -0
- package/dist/output-schema.d.ts.map +1 -0
- package/dist/output-schema.js +28 -0
- package/dist/output-schema.js.map +1 -0
- package/dist/preflight.d.ts +80 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +361 -0
- package/dist/preflight.js.map +1 -0
- package/dist/protocol.d.ts +359 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/dist/spec.d.ts +294 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +13 -0
- package/dist/spec.js.map +1 -0
- package/dist/step-dependencies.d.ts +2 -0
- package/dist/step-dependencies.d.ts.map +1 -0
- package/dist/step-dependencies.js +88 -0
- package/dist/step-dependencies.js.map +1 -0
- package/dist/step-fields.d.ts +17 -0
- package/dist/step-fields.d.ts.map +1 -0
- package/dist/step-fields.js +32 -0
- package/dist/step-fields.js.map +1 -0
- package/dist/tick-source.d.ts +205 -0
- package/dist/tick-source.d.ts.map +1 -0
- package/dist/tick-source.js +234 -0
- package/dist/tick-source.js.map +1 -0
- package/dist/unknown-keys.d.ts +7 -0
- package/dist/unknown-keys.d.ts.map +1 -0
- package/dist/unknown-keys.js +47 -0
- package/dist/unknown-keys.js.map +1 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +458 -0
- package/dist/validate.js.map +1 -0
- package/dist/work-package-consumer.d.ts +33 -0
- package/dist/work-package-consumer.d.ts.map +1 -0
- package/dist/work-package-consumer.js +44 -0
- package/dist/work-package-consumer.js.map +1 -0
- package/dist/work-package-validator.d.ts +15 -0
- package/dist/work-package-validator.d.ts.map +1 -0
- package/dist/work-package-validator.js +72 -0
- package/dist/work-package-validator.js.map +1 -0
- package/dist/worker-cli.d.ts +16 -0
- package/dist/worker-cli.d.ts.map +1 -0
- package/dist/worker-cli.js +77 -0
- package/dist/worker-cli.js.map +1 -0
- package/dist/worker.d.ts +57 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +129 -0
- package/dist/worker.js.map +1 -0
- package/dist/wrapper-runtime.d.ts +10 -0
- package/dist/wrapper-runtime.d.ts.map +1 -0
- package/dist/wrapper-runtime.js +83 -0
- package/dist/wrapper-runtime.js.map +1 -0
- package/dist/wrapper-session.d.ts +16 -0
- package/dist/wrapper-session.d.ts.map +1 -0
- package/dist/wrapper-session.js +244 -0
- package/dist/wrapper-session.js.map +1 -0
- package/package.json +50 -0
- package/src/authored-flow-error.ts +31 -0
- package/src/authored-flow-executor.ts +365 -0
- package/src/authored-flow-lifecycle.ts +344 -0
- package/src/authored-flow-loader.ts +44 -0
- package/src/authored-flow-operation.ts +252 -0
- package/src/authored-flow.ts +21 -0
- package/src/authored-promise-graph.ts +196 -0
- package/src/backlog-picker.ts +153 -0
- package/src/canonical.ts +57 -0
- package/src/cli/check.ts +391 -0
- package/src/cli/direct-run.ts +86 -0
- package/src/cli/hn-monitor.ts +286 -0
- package/src/cli/interruptible-sleep.ts +27 -0
- package/src/cli/run.ts +439 -0
- package/src/cli/tick-runner.ts +378 -0
- package/src/cli-adapter.ts +115 -0
- package/src/cli-executable.ts +12 -0
- package/src/cli.ts +364 -0
- package/src/compile.ts +573 -0
- package/src/demo-hn-monitor.ts +132 -0
- package/src/dir-watcher-poller.ts +122 -0
- package/src/direct-input.ts +69 -0
- package/src/failure-kinds.ts +77 -0
- package/src/gate-contract.ts +72 -0
- package/src/hn-poller.ts +104 -0
- package/src/index.ts +195 -0
- package/src/journal-client.ts +379 -0
- package/src/json-schema-bound.ts +357 -0
- package/src/json-schema.ts +65 -0
- package/src/json-value.ts +110 -0
- package/src/model-name.ts +20 -0
- package/src/output-schema.ts +33 -0
- package/src/preflight.ts +482 -0
- package/src/protocol.ts +364 -0
- package/src/spec.ts +343 -0
- package/src/step-dependencies.ts +100 -0
- package/src/step-fields.ts +36 -0
- package/src/tick-source.ts +334 -0
- package/src/unknown-keys.ts +52 -0
- package/src/validate.ts +503 -0
- package/src/work-package-consumer.ts +73 -0
- package/src/work-package-validator.ts +90 -0
- package/src/worker-cli.ts +110 -0
- package/src/worker.ts +150 -0
- package/src/wrapper-runtime.ts +93 -0
- package/src/wrapper-session.ts +287 -0
package/src/canonical.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Canonical JSON for specs — sorted keys, no whitespace, deterministic.
|
|
2
|
+
//
|
|
3
|
+
// The kernel hashes `serde_json::to_vec(to_value(spec))`; serde_json objects
|
|
4
|
+
// are BTreeMaps, so that is exactly this canonical form. Given the same spec
|
|
5
|
+
// value (a kernel-dialect spec from `toKernelSpec`), sha256 here equals the
|
|
6
|
+
// kernel's `spec_hash`. This is pinned by `tests/spec-parity.test.ts` and the
|
|
7
|
+
// kernel's `tests/spec_parity.rs` over the shared `testdata/` fixture — not
|
|
8
|
+
// assumed.
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { snapshotJsonValue, type JsonValue } from './json-value.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serialize a value as canonical JSON: object keys sorted recursively,
|
|
15
|
+
* arrays in order, no whitespace. Numbers are kept as-is (tokens are
|
|
16
|
+
* integers; money is a decimal string, never a float — so no float drift).
|
|
17
|
+
*
|
|
18
|
+
* This is an exported boundary, so runtime input is snapshotted into frozen,
|
|
19
|
+
* behavior-free data before a single byte is serialized — the same guard
|
|
20
|
+
* `validateSpec` and `kernelToAuthoring` use. Without it a Proxy or a plain
|
|
21
|
+
* getter could return one value while the identity is computed and another
|
|
22
|
+
* afterwards, and `specHash` would stamp a spec nobody ever declared.
|
|
23
|
+
*/
|
|
24
|
+
export function canonicalize(value: unknown): string {
|
|
25
|
+
return serialize(snapshotJsonValue(value, 'value'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* sha256 of the canonical JSON of a spec. Pass a kernel-dialect spec
|
|
30
|
+
* (`toKernelSpec`) to get the identity the kernel stamps as `spec_hash`.
|
|
31
|
+
*/
|
|
32
|
+
export function specHash(spec: unknown): string {
|
|
33
|
+
return createHash('sha256').update(serialize(snapshotJsonValue(spec, 'spec'))).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Serialize an already-snapshotted value. Each key is read EXACTLY ONCE: the
|
|
38
|
+
* previous `keys.filter(k => obj[k] !== undefined).map(k => … obj[k])` read
|
|
39
|
+
* every property twice, which on a live getter is a TOCTOU — the filter and
|
|
40
|
+
* the serialize could disagree, and two `canonicalize` calls on one object
|
|
41
|
+
* could return different strings.
|
|
42
|
+
*/
|
|
43
|
+
function serialize(value: JsonValue): string {
|
|
44
|
+
if (value === null || typeof value !== 'object') {
|
|
45
|
+
return JSON.stringify(value);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return '[' + value.map(serialize).join(',') + ']';
|
|
49
|
+
}
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
for (const key of Object.keys(value).sort()) {
|
|
52
|
+
const child = value[key];
|
|
53
|
+
if (child === undefined) continue;
|
|
54
|
+
parts.push(JSON.stringify(key) + ':' + serialize(child));
|
|
55
|
+
}
|
|
56
|
+
return '{' + parts.join(',') + '}';
|
|
57
|
+
}
|
package/src/cli/check.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { CompileError, compileSpec, kernelToAuthoring } from '../compile.js';
|
|
6
|
+
import {
|
|
7
|
+
adapterIdentification,
|
|
8
|
+
authenticationProbe,
|
|
9
|
+
cliAdapterKind,
|
|
10
|
+
displayInvocation,
|
|
11
|
+
modelReadinessProbe,
|
|
12
|
+
type CliInvocation,
|
|
13
|
+
} from '../cli-adapter.js';
|
|
14
|
+
import { MODEL_ENV } from '../worker-cli.js';
|
|
15
|
+
import { modelNameError } from '../model-name.js';
|
|
16
|
+
import type { FlowSpec } from '../spec.js';
|
|
17
|
+
import type { StepGateInspection } from '../gate-contract.js';
|
|
18
|
+
import type { CheckFailureKind } from '../failure-kinds.js';
|
|
19
|
+
import {
|
|
20
|
+
preflight,
|
|
21
|
+
CliProbeError,
|
|
22
|
+
type CliResolution,
|
|
23
|
+
type CliProbeResult,
|
|
24
|
+
type PreflightDiagnostic,
|
|
25
|
+
type PreflightProbes,
|
|
26
|
+
} from '../preflight.js';
|
|
27
|
+
|
|
28
|
+
interface ProjectConfig {
|
|
29
|
+
cli?: string;
|
|
30
|
+
executors: string[];
|
|
31
|
+
models: string[];
|
|
32
|
+
directory: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CheckReport {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
path?: string;
|
|
39
|
+
projectConfigPath?: string;
|
|
40
|
+
gates: StepGateInspection[];
|
|
41
|
+
resolutions: CliResolution[];
|
|
42
|
+
diagnostics: Array<PreflightDiagnostic | CheckInputDiagnostic>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CheckInputDiagnostic {
|
|
46
|
+
severity: 'refusal';
|
|
47
|
+
kind: CheckFailureKind;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CheckExecution {
|
|
52
|
+
report: CheckReport;
|
|
53
|
+
flow?: FlowSpec;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class CheckFailure extends Error {
|
|
57
|
+
constructor(readonly kind: CheckFailureKind, message: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Validate and preflight one working-tree spec without starting a run. */
|
|
63
|
+
export function checkFlow(path: string): CheckExecution {
|
|
64
|
+
const absolutePath = resolve(path);
|
|
65
|
+
try {
|
|
66
|
+
const flow = readFlow(absolutePath);
|
|
67
|
+
return checkAuthoredFlow(flow, path);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const failure = error instanceof CheckFailure
|
|
70
|
+
? error
|
|
71
|
+
: new CheckFailure('invalid_spec', `Flow "${path}" could not be checked as a Relayflow spec.`);
|
|
72
|
+
return { report: inputFailureReport(failure, path) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Preflight a validated authored flow through the same path as YAML/JSON. */
|
|
77
|
+
export function checkAuthoredFlow(authoring: FlowSpec, path: string): CheckExecution {
|
|
78
|
+
const absolutePath = resolve(path);
|
|
79
|
+
try {
|
|
80
|
+
const config = readProjectConfig(dirname(absolutePath));
|
|
81
|
+
const probes = systemProbes(dirname(absolutePath), config);
|
|
82
|
+
const result = preflight(authoring, {
|
|
83
|
+
projectCli: config.cli,
|
|
84
|
+
projectConfigPath: config.path,
|
|
85
|
+
projectSearchStart: dirname(absolutePath),
|
|
86
|
+
models: config.models,
|
|
87
|
+
...(config.path !== undefined ? { modelRegistryPath: config.path } : {}),
|
|
88
|
+
probes,
|
|
89
|
+
});
|
|
90
|
+
const flow = result.ok
|
|
91
|
+
? bindResolvedCliPaths(
|
|
92
|
+
compileSpec(authoring),
|
|
93
|
+
result.resolutions,
|
|
94
|
+
dirname(absolutePath),
|
|
95
|
+
config.directory,
|
|
96
|
+
)
|
|
97
|
+
: undefined;
|
|
98
|
+
return {
|
|
99
|
+
report: {
|
|
100
|
+
ok: result.ok,
|
|
101
|
+
path,
|
|
102
|
+
...(config.path !== undefined ? { projectConfigPath: config.path } : {}),
|
|
103
|
+
gates: result.gates,
|
|
104
|
+
resolutions: result.resolutions,
|
|
105
|
+
diagnostics: result.diagnostics,
|
|
106
|
+
},
|
|
107
|
+
...(flow !== undefined ? { flow } : {}),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const failure = error instanceof CheckFailure
|
|
111
|
+
? error
|
|
112
|
+
: new CheckFailure('invalid_spec', `Flow "${path}" could not be checked as a Relayflow spec.`);
|
|
113
|
+
return { report: inputFailureReport(failure, path) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function inputFailureReport(
|
|
118
|
+
failure: { kind: CheckFailureKind; message: string },
|
|
119
|
+
path?: string,
|
|
120
|
+
): CheckReport {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
...(path !== undefined ? { path } : {}),
|
|
124
|
+
gates: [],
|
|
125
|
+
resolutions: [],
|
|
126
|
+
diagnostics: [{ severity: 'refusal', kind: failure.kind, message: failure.message }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readFlow(path: string): FlowSpec {
|
|
131
|
+
let source: string;
|
|
132
|
+
try {
|
|
133
|
+
source = readFileSync(path, 'utf8');
|
|
134
|
+
} catch {
|
|
135
|
+
throw new CheckFailure('input_unreadable', `Flow "${path}" is not readable.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let parsed: unknown;
|
|
139
|
+
try {
|
|
140
|
+
parsed = parseYaml(source);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new CheckFailure('invalid_spec', `Flow "${path}" contains invalid YAML or JSON.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const marker = kernelDialectMarker(parsed);
|
|
147
|
+
const authoring = marker === undefined ? parsed : kernelToAuthoring(parsed);
|
|
148
|
+
// Public preflight owns the first validation pass. Returning raw authoring
|
|
149
|
+
// here preserves named-agent provenance until every declaration is checked.
|
|
150
|
+
return authoring as FlowSpec;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof CheckFailure) throw error;
|
|
153
|
+
if (error instanceof CompileError) {
|
|
154
|
+
const marker = kernelDialectMarker(parsed);
|
|
155
|
+
const dialect = marker === undefined
|
|
156
|
+
? ''
|
|
157
|
+
: `Flow "${path}" was read as a compiled kernel spec because ${marker} is present. `;
|
|
158
|
+
throw new CheckFailure('invalid_spec', `${dialect}${error.errors.join('; ')}`);
|
|
159
|
+
}
|
|
160
|
+
throw new CheckFailure('invalid_spec', `Flow "${path}" is not a valid Relayflow spec.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readProjectConfig(start: string): ProjectConfig {
|
|
165
|
+
const configPath = findConfig(start);
|
|
166
|
+
if (configPath === undefined) return { executors: [], models: [], directory: start };
|
|
167
|
+
let value: unknown;
|
|
168
|
+
try {
|
|
169
|
+
value = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
170
|
+
} catch {
|
|
171
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" is not valid JSON.`);
|
|
172
|
+
}
|
|
173
|
+
if (!isObject(value) || Object.keys(value).some((key) => !['cli', 'executors', 'models'].includes(key))) {
|
|
174
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" expects only cli, executors, and models.`);
|
|
175
|
+
}
|
|
176
|
+
if (value['cli'] !== undefined && !isNonEmptyString(value['cli'])) {
|
|
177
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" has an invalid cli.`);
|
|
178
|
+
}
|
|
179
|
+
if (value['executors'] !== undefined && (!Array.isArray(value['executors']) || !value['executors'].every(isNonEmptyString))) {
|
|
180
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" has invalid executors.`);
|
|
181
|
+
}
|
|
182
|
+
if (value['models'] !== undefined) {
|
|
183
|
+
if (!Array.isArray(value['models'])) {
|
|
184
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" has invalid models; expected an exact string allowlist.`);
|
|
185
|
+
}
|
|
186
|
+
for (const [index, model] of value['models'].entries()) {
|
|
187
|
+
const problem = modelNameError(model);
|
|
188
|
+
if (problem !== undefined) {
|
|
189
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" models[${index}]: ${problem}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (new Set(value['models']).size !== value['models'].length) {
|
|
193
|
+
throw new CheckFailure('config_invalid', `Project config "${configPath}" has duplicate models.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
...(value['cli'] !== undefined ? { cli: value['cli'] as string } : {}),
|
|
198
|
+
executors: (value['executors'] as string[] | undefined) ?? [],
|
|
199
|
+
models: (value['models'] as string[] | undefined) ?? [],
|
|
200
|
+
directory: dirname(configPath),
|
|
201
|
+
path: configPath,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function findConfig(start: string): string | undefined {
|
|
206
|
+
let directory = start;
|
|
207
|
+
while (true) {
|
|
208
|
+
const candidate = join(directory, 'flows.json');
|
|
209
|
+
try {
|
|
210
|
+
accessSync(candidate, constants.R_OK);
|
|
211
|
+
return candidate;
|
|
212
|
+
} catch {
|
|
213
|
+
// Continue toward the filesystem root.
|
|
214
|
+
}
|
|
215
|
+
const parent = dirname(directory);
|
|
216
|
+
if (parent === directory || directory === parsePath(directory).root) return undefined;
|
|
217
|
+
directory = parent;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes {
|
|
222
|
+
return {
|
|
223
|
+
cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model),
|
|
224
|
+
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
225
|
+
command: (binary) => executableExists(binary, flowDirectory),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function bindResolvedCliPaths(
|
|
230
|
+
flow: FlowSpec,
|
|
231
|
+
resolutions: readonly CliResolution[],
|
|
232
|
+
flowDirectory: string,
|
|
233
|
+
configDirectory: string,
|
|
234
|
+
): FlowSpec {
|
|
235
|
+
const byStep = new Map(resolutions.map((resolution) => [resolution.stepId, resolution]));
|
|
236
|
+
return {
|
|
237
|
+
...flow,
|
|
238
|
+
steps: flow.steps.map((step) => {
|
|
239
|
+
if (step.type === 'deterministic') return step;
|
|
240
|
+
const resolution = byStep.get(step.id);
|
|
241
|
+
if (resolution === undefined) return step;
|
|
242
|
+
const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
|
|
243
|
+
return { ...step, cli: canonicalCli(resolution.cli, directory) };
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function canonicalCli(cli: string, directory: string): string {
|
|
249
|
+
if (isAbsolute(cli) || (!cli.includes('/') && !cli.includes('\\'))) return cli;
|
|
250
|
+
return resolve(directory, cli);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function probeCli(
|
|
254
|
+
cli: string,
|
|
255
|
+
directory: string,
|
|
256
|
+
model?: string,
|
|
257
|
+
): CliProbeResult {
|
|
258
|
+
const executable = resolveExecutable(cli, directory);
|
|
259
|
+
if (executable === undefined) return { exists: false, authenticated: false };
|
|
260
|
+
const kind = cliAdapterKind(executable);
|
|
261
|
+
const identification = adapterIdentification(kind);
|
|
262
|
+
const identified = runProbe(executable, directory, identification.invocation);
|
|
263
|
+
if (
|
|
264
|
+
identified.status !== 0
|
|
265
|
+
|| (identification.expectedStdout !== undefined
|
|
266
|
+
&& identified.stdout.trim() !== identification.expectedStdout)
|
|
267
|
+
) {
|
|
268
|
+
return { exists: true, supported: false, authenticated: false };
|
|
269
|
+
}
|
|
270
|
+
const auth = authenticationProbe(kind);
|
|
271
|
+
const authCommand = displayInvocation(cli, auth);
|
|
272
|
+
if (model === undefined) {
|
|
273
|
+
return {
|
|
274
|
+
exists: true,
|
|
275
|
+
supported: true,
|
|
276
|
+
authenticated: runProbe(executable, directory, auth).status === 0,
|
|
277
|
+
authCommand,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const scoped = modelReadinessProbe(kind, model);
|
|
282
|
+
const modelCommand = displayInvocation(cli, scoped);
|
|
283
|
+
// A successful real provider round trip (or identified wrapper probe)
|
|
284
|
+
// proves both auth and exact-model access. On failure, run the adapter's
|
|
285
|
+
// actual auth command solely to classify auth vs model access truthfully.
|
|
286
|
+
if (runProbe(executable, directory, scoped).status === 0) {
|
|
287
|
+
return {
|
|
288
|
+
exists: true,
|
|
289
|
+
supported: true,
|
|
290
|
+
authenticated: true,
|
|
291
|
+
modelAvailable: true,
|
|
292
|
+
authCommand,
|
|
293
|
+
modelCommand,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const authStatus = runProbe(executable, directory, auth).status;
|
|
297
|
+
return {
|
|
298
|
+
exists: true,
|
|
299
|
+
supported: true,
|
|
300
|
+
authenticated: authStatus === 0,
|
|
301
|
+
modelAvailable: false,
|
|
302
|
+
authCommand,
|
|
303
|
+
modelCommand,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function runProbe(
|
|
308
|
+
executable: string,
|
|
309
|
+
directory: string,
|
|
310
|
+
invocation: CliInvocation,
|
|
311
|
+
): { status: number | null; stdout: string } {
|
|
312
|
+
const env = { ...process.env };
|
|
313
|
+
delete env[MODEL_ENV];
|
|
314
|
+
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
315
|
+
const result = spawnSync(executable, invocation.args, {
|
|
316
|
+
cwd: directory,
|
|
317
|
+
encoding: 'utf8',
|
|
318
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
319
|
+
timeout: invocation.timeoutMs,
|
|
320
|
+
env,
|
|
321
|
+
});
|
|
322
|
+
const failure = classifySpawnFailure(result.error, result.signal, invocation.timeoutMs);
|
|
323
|
+
if (failure !== undefined) throw failure;
|
|
324
|
+
return { status: result.status, stdout: result.stdout };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function resolveExecutable(command: string, directory: string): string | undefined {
|
|
328
|
+
if (command.includes('/') || isAbsolute(command)) {
|
|
329
|
+
const path = isAbsolute(command) ? command : resolve(directory, command);
|
|
330
|
+
try {
|
|
331
|
+
accessSync(path, constants.X_OK);
|
|
332
|
+
return path;
|
|
333
|
+
} catch {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const result = spawnSync('which', [command], { encoding: 'utf8', timeout: 5_000 });
|
|
338
|
+
const failure = classifySpawnFailure(result.error, result.signal, 5_000);
|
|
339
|
+
if (failure !== undefined) throw failure;
|
|
340
|
+
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function classifySpawnFailure(
|
|
344
|
+
error: Error | undefined,
|
|
345
|
+
signal: NodeJS.Signals | null,
|
|
346
|
+
timeoutMs: number,
|
|
347
|
+
): CliProbeError | undefined {
|
|
348
|
+
if (error !== undefined) {
|
|
349
|
+
const detail = (error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
|
|
350
|
+
? `timeout:${timeoutMs}ms` as const
|
|
351
|
+
: 'spawn_failed' as const;
|
|
352
|
+
return new CliProbeError(detail);
|
|
353
|
+
}
|
|
354
|
+
return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function executableExists(command: string, directory: string): boolean {
|
|
358
|
+
return resolveExecutable(command, directory) !== undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function kernelDialectMarker(value: unknown): string | undefined {
|
|
362
|
+
if (!isObject(value) || !Array.isArray(value['steps'])) return undefined;
|
|
363
|
+
const kernelStepKeys = ['depends_on', 'max_iterations', 'retry', 'timeout_ms', 'recovery_mode'];
|
|
364
|
+
const kernelBudgetKeys = ['max_tokens_in', 'max_tokens_out', 'max_dollars'];
|
|
365
|
+
const kernelVerificationKeys = ['output_contains', 'json_schema'];
|
|
366
|
+
const kernelPermissionKeys = ['file_globs', 'network_allowlist', 'access_preset'];
|
|
367
|
+
const budget = firstPresentKey(value['budget'], kernelBudgetKeys);
|
|
368
|
+
if (budget !== undefined) return `spec.budget.${budget}`;
|
|
369
|
+
for (const [index, step] of value['steps'].entries()) {
|
|
370
|
+
if (!isObject(step)) continue;
|
|
371
|
+
const stepKey = firstPresentKey(step, kernelStepKeys);
|
|
372
|
+
if (stepKey !== undefined) return `spec.steps[${index}].${stepKey}`;
|
|
373
|
+
const verification = firstPresentKey(step['verification'], kernelVerificationKeys);
|
|
374
|
+
if (verification !== undefined) return `spec.steps[${index}].verification.${verification}`;
|
|
375
|
+
const permission = firstPresentKey(step['permissions'], kernelPermissionKeys);
|
|
376
|
+
if (permission !== undefined) return `spec.steps[${index}].permissions.${permission}`;
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function firstPresentKey(value: unknown, keys: readonly string[]): string | undefined {
|
|
382
|
+
return isObject(value) ? keys.find((key) => key in value) : undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
386
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
390
|
+
return typeof value === 'string' && value.length > 0;
|
|
391
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthoredFlowExecutionError,
|
|
3
|
+
executeAuthoredFlow,
|
|
4
|
+
} from '../authored-flow-executor.js';
|
|
5
|
+
import { AuthoredFlowLoadError, loadAuthoredFlow } from '../authored-flow-loader.js';
|
|
6
|
+
import { DirectInputError, parseDirectInput } from '../direct-input.js';
|
|
7
|
+
import { JournalClient } from '../journal-client.js';
|
|
8
|
+
import { inputFailureReport } from './check.js';
|
|
9
|
+
import {
|
|
10
|
+
connect,
|
|
11
|
+
emptyReport,
|
|
12
|
+
fromCheckReport,
|
|
13
|
+
protocolFailure,
|
|
14
|
+
socketFor,
|
|
15
|
+
type RunExecution,
|
|
16
|
+
type RunLifecycleOptions,
|
|
17
|
+
type RunReport,
|
|
18
|
+
} from './run.js';
|
|
19
|
+
|
|
20
|
+
export async function runDirectFlow(
|
|
21
|
+
path: string,
|
|
22
|
+
inputArgument: string | undefined,
|
|
23
|
+
dataDir: string,
|
|
24
|
+
_options: RunLifecycleOptions = {},
|
|
25
|
+
): Promise<RunExecution> {
|
|
26
|
+
let input: unknown;
|
|
27
|
+
try {
|
|
28
|
+
input = parseDirectInput(inputArgument);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const failure = error instanceof DirectInputError ? error : {
|
|
31
|
+
kind: 'input_invalid' as const,
|
|
32
|
+
message: 'Direct input could not be parsed.',
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
exitCode: 2,
|
|
36
|
+
report: fromCheckReport('run', inputFailureReport(failure, path)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const socketPath = socketFor(dataDir);
|
|
41
|
+
const base: RunReport = { ...emptyReport('run'), path };
|
|
42
|
+
const client = new JournalClient(socketPath);
|
|
43
|
+
const connected = await connect(client, 'run', dataDir, base);
|
|
44
|
+
if (connected !== undefined) return connected;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const handle = await loadAuthoredFlow(path);
|
|
48
|
+
const result = await executeAuthoredFlow(handle, client, input);
|
|
49
|
+
const terminal = result.journalSteps.at(-1);
|
|
50
|
+
if (terminal === undefined) {
|
|
51
|
+
return protocolFailure('run', base, socketPath, new Error(
|
|
52
|
+
`authored flow "${result.name}" completed without a journal step`,
|
|
53
|
+
));
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
exitCode: 0,
|
|
57
|
+
report: {
|
|
58
|
+
...base,
|
|
59
|
+
ok: true,
|
|
60
|
+
runId: terminal.runId,
|
|
61
|
+
socketPath,
|
|
62
|
+
status: 'completed',
|
|
63
|
+
completionReason: result.completionReason,
|
|
64
|
+
completedSteps: result.journalSteps.length,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof AuthoredFlowLoadError
|
|
69
|
+
|| (error instanceof AuthoredFlowExecutionError && error.code === 'unsupported_header')) {
|
|
70
|
+
return {
|
|
71
|
+
exitCode: 2,
|
|
72
|
+
report: {
|
|
73
|
+
...fromCheckReport('run', inputFailureReport({
|
|
74
|
+
kind: 'invalid_spec',
|
|
75
|
+
message: error.message,
|
|
76
|
+
}, path)),
|
|
77
|
+
socketPath,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
|
|
82
|
+
return protocolFailure('run', base, socketPath, error, runId);
|
|
83
|
+
} finally {
|
|
84
|
+
client.close();
|
|
85
|
+
}
|
|
86
|
+
}
|