@relayflows/sdk 2.0.38 → 2.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-transcript.d.ts +9 -0
- package/dist/agent-transcript.d.ts.map +1 -1
- package/dist/agent-transcript.js +12 -0
- package/dist/agent-transcript.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +22 -4
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +5 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +52 -3
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +9 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +24 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-versions.d.ts +22 -0
- package/dist/cli/cloud-versions.d.ts.map +1 -0
- package/dist/cli/cloud-versions.js +96 -0
- package/dist/cli/cloud-versions.js.map +1 -0
- package/dist/cli/step-evidence.d.ts +5 -0
- package/dist/cli/step-evidence.d.ts.map +1 -1
- package/dist/cli/step-evidence.js +45 -7
- package/dist/cli/step-evidence.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +13 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +27 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +18 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-transport-evidence.d.ts +30 -0
- package/dist/cli-transport-evidence.d.ts.map +1 -0
- package/dist/cli-transport-evidence.js +31 -0
- package/dist/cli-transport-evidence.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +17 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +8 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-versions-wire.d.ts +25 -0
- package/dist/cloud-versions-wire.d.ts.map +1 -0
- package/dist/cloud-versions-wire.js +39 -0
- package/dist/cloud-versions-wire.js.map +1 -0
- package/dist/cloud-versions.d.ts +47 -0
- package/dist/cloud-versions.d.ts.map +1 -0
- package/dist/cloud-versions.js +131 -0
- package/dist/cloud-versions.js.map +1 -0
- package/dist/compile.d.ts +3 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +37 -13
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +13 -0
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/pty-sidechannel.d.ts +17 -0
- package/dist/pty-sidechannel.d.ts.map +1 -1
- package/dist/pty-sidechannel.js +41 -2
- package/dist/pty-sidechannel.js.map +1 -1
- package/dist/spec.d.ts +6 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +2 -2
- package/dist/step-fields.d.ts.map +1 -1
- package/dist/step-fields.js +2 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/validate.d.ts +2 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +18 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +10 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +116 -44
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +4 -1
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +15 -4
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +2 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +29 -10
- package/dist/wrapper-session.js.map +1 -1
- package/dist/yaml-helpers.js +1 -1
- package/dist/yaml-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-transcript.ts +12 -0
- package/src/authored-node-runner.ts +23 -5
- package/src/authored-root.ts +5 -6
- package/src/authored-worker-step.ts +58 -3
- package/src/cli/cloud-deploy.ts +21 -7
- package/src/cli/cloud-versions.ts +103 -0
- package/src/cli/step-evidence.ts +51 -7
- package/src/cli/step-failure.ts +13 -0
- package/src/cli-commands.ts +18 -0
- package/src/cli-transport-evidence.ts +70 -0
- package/src/cli.ts +15 -0
- package/src/cloud-deploy.ts +24 -3
- package/src/cloud-versions-wire.ts +58 -0
- package/src/cloud-versions.ts +179 -0
- package/src/compile.ts +39 -13
- package/src/failure-kinds.ts +13 -0
- package/src/pty-sidechannel.ts +38 -2
- package/src/spec.ts +6 -1
- package/src/step-fields.ts +2 -1
- package/src/validate.ts +18 -0
- package/src/worker-cli.ts +140 -42
- package/src/worker-spend.ts +4 -1
- package/src/worker.ts +21 -4
- package/src/wrapper-session.ts +32 -10
- package/src/yaml-helpers.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
|
44
44
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
45
45
|
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
46
46
|
import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
|
|
47
|
+
import { runCloudRollbackCli, runCloudVersionsCli } from './cli/cloud-versions.js';
|
|
47
48
|
import { parseCloudScheduleArgs, runCloudScheduleCli, runCloudSchedulesCli, runCloudUnscheduleCli, type CloudScheduleArgs } from './cli/cloud-schedule.js';
|
|
48
49
|
import { isAuthoredFlowPath } from './direct-input.js';
|
|
49
50
|
import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
|
|
@@ -97,6 +98,8 @@ export type ParsedArgs =
|
|
|
97
98
|
| CloudDeployArgs
|
|
98
99
|
| { command: 'deployments'; json: boolean }
|
|
99
100
|
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
101
|
+
| { command: 'versions'; flow: string; json: boolean }
|
|
102
|
+
| { command: 'rollback'; flow: string; version: string; json: boolean }
|
|
100
103
|
| CloudScheduleArgs
|
|
101
104
|
| { command: 'schedules'; json: boolean }
|
|
102
105
|
| { command: 'unschedule'; scheduleId: string; json: boolean }
|
|
@@ -123,7 +126,10 @@ const USAGE = [
|
|
|
123
126
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
124
127
|
'flows build --verify <bundle-dir>',
|
|
125
128
|
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--plugin <ref>] [--no-connect] [--json]',
|
|
129
|
+
'flows deploy <flow.ts> --flow <name|listener-id> [--plugin <ref>] [--no-connect] [--json]',
|
|
126
130
|
'flows deployments [--json]',
|
|
131
|
+
'flows versions [--json] <name|listener-id>',
|
|
132
|
+
'flows rollback [--json] <name|listener-id> <version>',
|
|
127
133
|
'flows undeploy [--json] <deployment-id>',
|
|
128
134
|
'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--no-connect] [--json]',
|
|
129
135
|
'flows schedules [--json]',
|
|
@@ -244,6 +250,8 @@ export async function runCli(
|
|
|
244
250
|
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
245
251
|
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
246
252
|
if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
|
|
253
|
+
if (parsed.command === 'versions') return runCloudVersionsCli(parsed, io);
|
|
254
|
+
if (parsed.command === 'rollback') return runCloudRollbackCli(parsed, io);
|
|
247
255
|
if (parsed.command === 'schedule') return runCloudScheduleCli(parsed, io);
|
|
248
256
|
if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
|
|
249
257
|
if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
|
|
@@ -631,6 +639,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
631
639
|
if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
|
|
632
640
|
return { command: 'undeploy', agentId: rest[0]!, json: json === 1 };
|
|
633
641
|
}
|
|
642
|
+
if (command === 'versions' || command === 'rollback') {
|
|
643
|
+
const rest = args.slice(1).filter(a => a !== '--json');
|
|
644
|
+
const json = args.length - 1 - rest.length;
|
|
645
|
+
if (json > 1 || rest.some(a => a.startsWith('-'))) return undefined;
|
|
646
|
+
if (command === 'versions') return rest.length === 1 ? { command, flow: rest[0]!, json: json === 1 } : undefined;
|
|
647
|
+
return rest.length === 2 ? { command, flow: rest[0]!, version: rest[1]!, json: json === 1 } : undefined;
|
|
648
|
+
}
|
|
634
649
|
if (command === 'deployments') {
|
|
635
650
|
const rest = args.slice(1);
|
|
636
651
|
if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
|
package/src/cloud-deploy.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './flow-requirements.js';
|
|
12
12
|
import { readProjectConfig } from './cli/check.js';
|
|
13
13
|
import { assertNoUseDependencies, collectExtensionSubmissions } from './flow-extension-submit.js';
|
|
14
|
+
import { parseVersionChange, type CloudFlowVersionChange } from './cloud-versions-wire.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Hosted listener deployment: the CLI form of the agentrelay.com onboarding's
|
|
@@ -104,6 +105,8 @@ export interface CloudDeployment {
|
|
|
104
105
|
requirements: FlowRequirements;
|
|
105
106
|
/** Integrations connected through the prompt during this deploy. */
|
|
106
107
|
connected: string[];
|
|
108
|
+
/** The listener's active version after this deploy; absent from a Cloud without versions. */
|
|
109
|
+
version?: CloudFlowVersionChange;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export function parseRepository(value: string): { owner: string; name: string } {
|
|
@@ -165,9 +168,18 @@ export function parseTriggerSource(value: string): FlowTriggerSource {
|
|
|
165
168
|
return { provider: provider as FlowTriggerProvider, settings };
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
/** One authored source, read and loaded the way every hosted deploy sends it. */
|
|
172
|
+
export interface DeploySource {
|
|
173
|
+
bytes: Buffer;
|
|
174
|
+
source: string;
|
|
175
|
+
definition: ReturnType<Awaited<ReturnType<typeof loadAuthoredFlow>>['getDefinition']>;
|
|
176
|
+
extensions: Awaited<ReturnType<typeof collectExtensionSubmissions>>;
|
|
177
|
+
projectCli: string | undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function loadDeploySource(
|
|
181
|
+
input: { path: string; plugins?: readonly string[] },
|
|
182
|
+
): Promise<DeploySource> {
|
|
171
183
|
if (!/\.flow\.ts$/iu.test(input.path)) {
|
|
172
184
|
throw new CloudFlowError('unsupported_source', 'flows deploy takes one authored .flow.ts source.');
|
|
173
185
|
}
|
|
@@ -204,6 +216,13 @@ export async function deployToCloud(
|
|
|
204
216
|
} catch {
|
|
205
217
|
projectCli = undefined;
|
|
206
218
|
}
|
|
219
|
+
return { bytes, source, definition, extensions, projectCli };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function deployToCloud(
|
|
223
|
+
input: DeployToCloudInput, options: CloudConnectionOptions = {},
|
|
224
|
+
): Promise<CloudDeployment> {
|
|
225
|
+
const { bytes, source, definition, extensions, projectCli } = await loadDeploySource(input);
|
|
207
226
|
if (input.sources.length === 0 || input.sources.length > 10) {
|
|
208
227
|
throw new CloudFlowError('invalid_input', 'Give between one and ten --on trigger sources.');
|
|
209
228
|
}
|
|
@@ -274,11 +293,13 @@ export async function deployToCloud(
|
|
|
274
293
|
if (!isCloudRecord(result) || typeof result.agentId !== 'string' || typeof result.status !== 'string') {
|
|
275
294
|
throw new CloudFlowError('invalid_response', 'Cloud did not return a deployment.');
|
|
276
295
|
}
|
|
296
|
+
const version = parseVersionChange(result.version);
|
|
277
297
|
return {
|
|
278
298
|
agentId: result.agentId, name, status: result.status,
|
|
279
299
|
repository: input.repository, sources,
|
|
280
300
|
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
281
301
|
requirements, connected,
|
|
302
|
+
...(version === undefined ? {} : { version }),
|
|
282
303
|
};
|
|
283
304
|
}
|
|
284
305
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isCloudRecord } from './cloud-http.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a Cloud write did to a listener's active version (cloud#4115).
|
|
5
|
+
* `reactivated`: the bytes match an earlier version, so the pointer moved to
|
|
6
|
+
* it without a new row, and `version` can be LOWER than `previousVersion`.
|
|
7
|
+
*/
|
|
8
|
+
export interface CloudFlowVersionChange {
|
|
9
|
+
version: number;
|
|
10
|
+
previousVersion: number | null;
|
|
11
|
+
change: 'created' | 'reactivated' | 'unchanged';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CloudFlowVersion {
|
|
15
|
+
version: number;
|
|
16
|
+
sourceSha256: string;
|
|
17
|
+
origin: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CHANGES = ['created', 'reactivated', 'unchanged'] as const;
|
|
22
|
+
|
|
23
|
+
function isVersionNumber(value: unknown): value is number {
|
|
24
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Undefined for a Cloud that predates versions or a malformed field. */
|
|
28
|
+
export function parseVersionChange(value: unknown): CloudFlowVersionChange | undefined {
|
|
29
|
+
if (!isCloudRecord(value) || !isVersionNumber(value.version)
|
|
30
|
+
|| !(value.previousVersion === null || isVersionNumber(value.previousVersion))
|
|
31
|
+
|| !(CHANGES as readonly unknown[]).includes(value.change)) return undefined;
|
|
32
|
+
return {
|
|
33
|
+
version: value.version,
|
|
34
|
+
previousVersion: value.previousVersion as number | null,
|
|
35
|
+
change: value.change as CloudFlowVersionChange['change'],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseVersion(value: unknown): CloudFlowVersion | undefined {
|
|
40
|
+
if (!isCloudRecord(value) || !isVersionNumber(value.version) || typeof value.sourceSha256 !== 'string'
|
|
41
|
+
|| typeof value.origin !== 'string' || typeof value.createdAt !== 'string') return undefined;
|
|
42
|
+
return { version: value.version, sourceSha256: value.sourceSha256, origin: value.origin, createdAt: value.createdAt };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `version 4 (was 3)`; a rollback by content says the number went down, so
|
|
47
|
+
* nobody reads v2-after-v3 as a failed deploy.
|
|
48
|
+
*/
|
|
49
|
+
export function describeVersionChange(change: CloudFlowVersionChange): string {
|
|
50
|
+
if (change.change === 'unchanged') return `version ${change.version} (unchanged)`;
|
|
51
|
+
const was = change.previousVersion === null ? '' : ` (was ${change.previousVersion}`;
|
|
52
|
+
if (change.change === 'reactivated') {
|
|
53
|
+
const down = change.previousVersion !== null && change.version < change.previousVersion
|
|
54
|
+
? '; active version went down' : '';
|
|
55
|
+
return `re-activated version ${change.version}${was}${down}${was ? ')' : ''}`;
|
|
56
|
+
}
|
|
57
|
+
return `version ${change.version}${was}${was ? ')' : ''}`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ensureIntegrationsConnected, type ConnectPrompt } from './cloud-connect.js';
|
|
3
|
+
import {
|
|
4
|
+
CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions,
|
|
5
|
+
} from './cloud-http.js';
|
|
6
|
+
import {
|
|
7
|
+
FLOW_AGENT_HARNESSES, listCloudDeployments, loadDeploySource, type FlowTriggerSource,
|
|
8
|
+
} from './cloud-deploy.js';
|
|
9
|
+
import {
|
|
10
|
+
parseVersion, parseVersionChange, type CloudFlowVersion, type CloudFlowVersionChange,
|
|
11
|
+
} from './cloud-versions-wire.js';
|
|
12
|
+
import { flowRequirements, mergeFlowExtensionRequirements, type FlowRequirements } from './flow-requirements.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Versions of a hosted listener (AgentWorkforce/cloud#4115). A deployed
|
|
16
|
+
* flow's source is immutable: `flows deploy <file> --flow <name-or-id>` makes
|
|
17
|
+
* the next version and moves the listener's pointer to it, leaving the
|
|
18
|
+
* listener's repository, triggers, approver, agents and run budget untouched;
|
|
19
|
+
* `flows rollback` moves the pointer to any recorded version.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const LISTENER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
23
|
+
|
|
24
|
+
export interface CloudListener {
|
|
25
|
+
agentId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
status: string;
|
|
28
|
+
repository: { owner: string; name: string; host?: 'gitlab' };
|
|
29
|
+
sources: FlowTriggerSource[];
|
|
30
|
+
activeVersion: CloudFlowVersion | null;
|
|
31
|
+
versions: CloudFlowVersion[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A listener id as given, else the one deployment in the workspace with that name. */
|
|
35
|
+
export async function resolveCloudFlow(flow: string, options: CloudConnectionOptions = {}): Promise<string> {
|
|
36
|
+
const wanted = flow.trim();
|
|
37
|
+
if (!wanted) throw new CloudFlowError('invalid_input', '--flow needs a flow name or listener id.');
|
|
38
|
+
if (LISTENER_UUID.test(wanted)) return wanted.toLowerCase();
|
|
39
|
+
const deployments = await listCloudDeployments(options);
|
|
40
|
+
// Any listener id the workspace lists, before names: ids are not all uuids.
|
|
41
|
+
const byId = deployments.find(deployment => deployment.agentId === wanted);
|
|
42
|
+
if (byId) return byId.agentId;
|
|
43
|
+
const matches = deployments.filter(deployment => deployment.name.toLowerCase() === wanted.toLowerCase());
|
|
44
|
+
if (matches.length === 1) return matches[0]!.agentId;
|
|
45
|
+
if (matches.length === 0) {
|
|
46
|
+
throw new CloudFlowError('invalid_input', `No flow named "${wanted}" in this workspace; list them with: flows deployments`);
|
|
47
|
+
}
|
|
48
|
+
throw new CloudFlowError('invalid_input',
|
|
49
|
+
`More than one flow is named "${wanted}" (${matches.map(match => match.agentId).join(', ')}); pass the listener id.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getCloudListener(agentId: string, options: CloudConnectionOptions = {}): Promise<CloudListener> {
|
|
53
|
+
const payload = await cloudRequest(`/api/v1/flows/listeners/${encodeURIComponent(agentId)}`, options);
|
|
54
|
+
const listener = isCloudRecord(payload) && isCloudRecord(payload.listener) ? payload.listener : undefined;
|
|
55
|
+
if (!listener || typeof listener.name !== 'string' || typeof listener.status !== 'string'
|
|
56
|
+
|| !isCloudRecord(listener.repository) || typeof listener.repository.owner !== 'string'
|
|
57
|
+
|| typeof listener.repository.name !== 'string' || !Array.isArray(listener.sources)) {
|
|
58
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return the flow listener.');
|
|
59
|
+
}
|
|
60
|
+
const versions = isCloudRecord(payload) && Array.isArray(payload.versions)
|
|
61
|
+
? payload.versions.flatMap(entry => parseVersion(entry) ?? []) : [];
|
|
62
|
+
return {
|
|
63
|
+
agentId,
|
|
64
|
+
name: listener.name,
|
|
65
|
+
status: listener.status,
|
|
66
|
+
repository: {
|
|
67
|
+
owner: listener.repository.owner, name: listener.repository.name,
|
|
68
|
+
...(listener.repository.host === 'gitlab' ? { host: 'gitlab' as const } : {}),
|
|
69
|
+
},
|
|
70
|
+
sources: listener.sources.flatMap((entry): FlowTriggerSource[] => isCloudRecord(entry) && typeof entry.provider === 'string'
|
|
71
|
+
? [{ provider: entry.provider as FlowTriggerSource['provider'],
|
|
72
|
+
settings: Object.fromEntries(Object.entries(isCloudRecord(entry.settings) ? entry.settings : {})
|
|
73
|
+
.flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : typeof value === 'boolean' ? [[key, String(value)]] : [])) }]
|
|
74
|
+
: []),
|
|
75
|
+
activeVersion: isCloudRecord(payload) ? parseVersion(payload.activeVersion) ?? null : null,
|
|
76
|
+
versions,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DeployVersionInput {
|
|
81
|
+
path: string;
|
|
82
|
+
/** The flow's deployed name or its listener id. */
|
|
83
|
+
flow: string;
|
|
84
|
+
plugins?: readonly string[];
|
|
85
|
+
connect?: ConnectPrompt;
|
|
86
|
+
checkConnections?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CloudVersionDeployment {
|
|
90
|
+
agentId: string;
|
|
91
|
+
name: string;
|
|
92
|
+
status: string;
|
|
93
|
+
sourceSha256: string;
|
|
94
|
+
requirements: FlowRequirements;
|
|
95
|
+
connected: string[];
|
|
96
|
+
version: CloudFlowVersionChange;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `POST /api/v1/flows/listeners/<id>/versions`: the source alone becomes the next version. */
|
|
100
|
+
export async function deployVersionToCloud(
|
|
101
|
+
input: DeployVersionInput, options: CloudConnectionOptions = {},
|
|
102
|
+
): Promise<CloudVersionDeployment> {
|
|
103
|
+
const { bytes, source, definition, extensions, projectCli } = await loadDeploySource(input);
|
|
104
|
+
const agentId = await resolveCloudFlow(input.flow, options);
|
|
105
|
+
const listener = await getCloudListener(agentId, options);
|
|
106
|
+
options.signal?.throwIfAborted();
|
|
107
|
+
// Requirements derive from the source, read against the listener's own
|
|
108
|
+
// repository and triggers, exactly as the create form reads its flags.
|
|
109
|
+
const requirements = mergeFlowExtensionRequirements(
|
|
110
|
+
flowRequirements(definition, {
|
|
111
|
+
sources: listener.sources,
|
|
112
|
+
// A GitHub repository means every run needs GitHub; a GitLab project does not.
|
|
113
|
+
...(listener.repository.host === 'gitlab' ? {} : { repository: listener.repository }),
|
|
114
|
+
...(projectCli === undefined ? {} : { projectCli }),
|
|
115
|
+
}),
|
|
116
|
+
extensions.map(extension => ({ name: extension.name, permissions: extension.manifest.permissions })),
|
|
117
|
+
);
|
|
118
|
+
// As on create: a harness Cloud cannot run is refused here, not deployed.
|
|
119
|
+
// There is no --agents override beside --flow; the listener's agents stay.
|
|
120
|
+
const unsupported = requirements.harnessUses
|
|
121
|
+
.filter(use => !(FLOW_AGENT_HARNESSES as readonly string[]).includes(use.harness));
|
|
122
|
+
if (unsupported.length > 0) {
|
|
123
|
+
throw new CloudFlowError('unsupported_source',
|
|
124
|
+
`This flow declares ${unsupported.map(use => `${use.harness} (${use.detail})`).join(', ')}, which Cloud deployments cannot run yet; `
|
|
125
|
+
+ `Cloud runs ${FLOW_AGENT_HARNESSES.join(' and ')}. Change the declaration before deploying a new version.`);
|
|
126
|
+
}
|
|
127
|
+
const whoami = await cloudRequest('/api/v1/auth/whoami', options);
|
|
128
|
+
const workspace = isCloudRecord(whoami) && isCloudRecord(whoami.currentWorkspace) ? whoami.currentWorkspace : undefined;
|
|
129
|
+
if (workspace === undefined || typeof workspace.id !== 'string' || !workspace.id) {
|
|
130
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not report a current workspace for this credential.');
|
|
131
|
+
}
|
|
132
|
+
// A draft activates nothing, so Cloud checks nothing; match it here.
|
|
133
|
+
const connected = listener.status !== 'listening' || input.checkConnections === false
|
|
134
|
+
? []
|
|
135
|
+
: (await ensureIntegrationsConnected(requirements, {
|
|
136
|
+
...options, workspaceId: workspace.id, ...(input.connect === undefined ? {} : { prompt: input.connect }),
|
|
137
|
+
})).connected;
|
|
138
|
+
options.signal?.throwIfAborted();
|
|
139
|
+
const result = await cloudFetch(`/api/v1/flows/listeners/${encodeURIComponent(agentId)}/versions`, options, {
|
|
140
|
+
method: 'POST', detail: true, body: JSON.stringify({
|
|
141
|
+
workspaceId: workspace.id,
|
|
142
|
+
source,
|
|
143
|
+
requirements: {
|
|
144
|
+
integrations: requirements.integrations.map(integration => integration.provider),
|
|
145
|
+
harnesses: requirements.harnesses,
|
|
146
|
+
mcp: requirements.mcp,
|
|
147
|
+
},
|
|
148
|
+
...(extensions.length === 0 ? {} : { extensions }),
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const version = isCloudRecord(result) ? parseVersionChange(result.version) : undefined;
|
|
152
|
+
if (!isCloudRecord(result) || typeof result.status !== 'string' || version === undefined) {
|
|
153
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return the new flow version.');
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
agentId, name: listener.name, status: result.status,
|
|
157
|
+
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
158
|
+
requirements, connected, version,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `POST /api/v1/flows/listeners/<id>/versions/<n>/activate`: history is kept; only the pointer moves. */
|
|
163
|
+
export async function activateCloudFlowVersion(
|
|
164
|
+
flow: string, version: number, options: CloudConnectionOptions = {},
|
|
165
|
+
): Promise<{ agentId: string; status: string; version: CloudFlowVersionChange }> {
|
|
166
|
+
if (!Number.isSafeInteger(version) || version < 1) {
|
|
167
|
+
throw new CloudFlowError('invalid_input', `A version is a positive whole number, got "${version}".`);
|
|
168
|
+
}
|
|
169
|
+
const agentId = await resolveCloudFlow(flow, options);
|
|
170
|
+
const result = await cloudFetch(
|
|
171
|
+
`/api/v1/flows/listeners/${encodeURIComponent(agentId)}/versions/${version}/activate`,
|
|
172
|
+
options, { method: 'POST', detail: true },
|
|
173
|
+
);
|
|
174
|
+
const change = isCloudRecord(result) ? parseVersionChange(result.version) : undefined;
|
|
175
|
+
if (!isCloudRecord(result) || typeof result.status !== 'string' || change === undefined) {
|
|
176
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not confirm the active version.');
|
|
177
|
+
}
|
|
178
|
+
return { agentId, status: result.status, version: change };
|
|
179
|
+
}
|
package/src/compile.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type {
|
|
|
38
38
|
import { SPEC_SCHEMA_VERSION } from './spec.js';
|
|
39
39
|
import { canonicalize, specHash } from './canonical.js';
|
|
40
40
|
import { validateOutputDeclaration } from './output-schema.js';
|
|
41
|
-
import { validateSpec, type ValidationResult } from './validate.js';
|
|
41
|
+
import { isTransportRetries, validateSpec, type ValidationResult } from './validate.js';
|
|
42
42
|
import { snapshotJsonValue } from './json-value.js';
|
|
43
43
|
import { expandYamlHelpers } from './yaml-helpers.js';
|
|
44
44
|
import { expandCommunication, validateCommunicationTopology } from './communication/spec.js';
|
|
@@ -74,7 +74,12 @@ export class CompileError extends Error {
|
|
|
74
74
|
* survives float-precision (`1.1 * 1000 = 1100.0000000000002`) rather than
|
|
75
75
|
* being rejected by `isSafeInteger`.
|
|
76
76
|
*/
|
|
77
|
-
export
|
|
77
|
+
export const AGENT_STEP_TIMEOUT_MAX_MS = 60 * 60_000;
|
|
78
|
+
export function parseAgentStepTimeout(timeout: unknown): number {
|
|
79
|
+
return parseStepTimeout(timeout, true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseStepTimeout(timeout: unknown, agent = false): number {
|
|
78
83
|
const unitToMs: Record<'ms' | 's' | 'm', number> = { ms: 1, s: 1000, m: 60_000 };
|
|
79
84
|
let milliseconds: number;
|
|
80
85
|
if (typeof timeout === 'number') {
|
|
@@ -96,10 +101,10 @@ export function parseStepTimeout(timeout: unknown): number {
|
|
|
96
101
|
milliseconds = NaN;
|
|
97
102
|
}
|
|
98
103
|
if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {
|
|
99
|
-
throw new CompileError(['f.run timeout must be a positive whole number of milliseconds or a duration such as "10s" or "5m"
|
|
104
|
+
throw new CompileError([`${agent ? 'f.agent' : 'f.run'} timeout must be a positive whole number of milliseconds or a duration such as "10s" or "5m".`], 'timeout_invalid');
|
|
100
105
|
}
|
|
101
|
-
if (milliseconds > 15 * 60_000) {
|
|
102
|
-
throw new CompileError(['f.run timeout exceeds the maximum of 15 minutes declared in SURFACE.md
|
|
106
|
+
if (milliseconds > (agent ? AGENT_STEP_TIMEOUT_MAX_MS : 15 * 60_000)) {
|
|
107
|
+
throw new CompileError([`${agent ? 'f.agent' : 'f.run'} timeout exceeds the maximum of ${agent ? 60 : 15} minutes declared in SURFACE.md.`], 'lease_exceeded');
|
|
103
108
|
}
|
|
104
109
|
return milliseconds;
|
|
105
110
|
}
|
|
@@ -190,6 +195,8 @@ function compileStep(step: StepSpec): StepSpec {
|
|
|
190
195
|
? { dependsOn: step.input === undefined ? step.dependsOn : [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])] } : {}),
|
|
191
196
|
...(step.input !== undefined ? { input: step.input } : {}),
|
|
192
197
|
maxIterations,
|
|
198
|
+
...(step.transportRetries !== undefined && step.transportRetries !== DEFAULT_TRANSPORT_RETRIES
|
|
199
|
+
? { transportRetries: step.transportRetries } : {}),
|
|
193
200
|
...(step.memory !== undefined ? { memory: step.memory } : {}),
|
|
194
201
|
...(step.requirements !== undefined ? { requirements: step.requirements } : {}),
|
|
195
202
|
};
|
|
@@ -209,7 +216,7 @@ function compileStep(step: StepSpec): StepSpec {
|
|
|
209
216
|
type: 'deterministic',
|
|
210
217
|
command: s.command,
|
|
211
218
|
verification: verification ?? { type: 'exit_code' as const },
|
|
212
|
-
//
|
|
219
|
+
// Timeout fields stay per-verb — worker-backed verbs own
|
|
213
220
|
// their dispatch timeout. It must be spread HERE and nowhere in `base`.
|
|
214
221
|
...(s.timeoutMs !== undefined ? { timeoutMs: s.timeoutMs } : {}),
|
|
215
222
|
...(s.lease_ms !== undefined ? { lease_ms: parseStepTimeout(s.lease_ms) } : {}),
|
|
@@ -240,6 +247,7 @@ function compileStep(step: StepSpec): StepSpec {
|
|
|
240
247
|
...base,
|
|
241
248
|
type: 'agent',
|
|
242
249
|
instruction: s.instruction,
|
|
250
|
+
...(s.timeoutMs === undefined ? {} : { timeoutMs: s.timeoutMs }),
|
|
243
251
|
...(verification !== undefined ? { verification: outputGate(verification, s.id) } : {}),
|
|
244
252
|
...(s.agent !== undefined ? { agent: s.agent } : {}),
|
|
245
253
|
...(s.cli !== undefined ? { cli: s.cli } : {}),
|
|
@@ -320,6 +328,10 @@ const KERNEL_RETRY_DEFAULTS = {
|
|
|
320
328
|
jitter_percent: 20,
|
|
321
329
|
} as const;
|
|
322
330
|
|
|
331
|
+
// spec.rs `default_max_transport_retries`. The kernel serializes this value as
|
|
332
|
+
// absent, so the SDK must too, or an explicit default changes the spec hash.
|
|
333
|
+
const DEFAULT_TRANSPORT_RETRIES = 1;
|
|
334
|
+
|
|
323
335
|
/**
|
|
324
336
|
* Map an authoring `FlowSpec` to the kernel spec dialect — the single shape at
|
|
325
337
|
* the SDK↔kernel boundary (`kernel/relayflowd-core/src/spec.rs`). Authoring
|
|
@@ -452,7 +464,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
452
464
|
const unionKeys = [
|
|
453
465
|
'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
|
|
454
466
|
'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
|
|
455
|
-
'cwd', 'recovery_mode', 'surfaces', 'permissions', 'cli_identity',
|
|
467
|
+
'cwd', 'recovery_mode', 'surfaces', 'permissions', 'transport', 'cli_identity',
|
|
456
468
|
] as const;
|
|
457
469
|
const step = requireKernelObject(value, unionKeys, at);
|
|
458
470
|
if (step['cli_identity'] !== undefined) {
|
|
@@ -467,10 +479,11 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
467
479
|
: type === 'llm'
|
|
468
480
|
? ['prompt', 'model', 'cli', 'cli_identity'] as const
|
|
469
481
|
: type === 'agent'
|
|
470
|
-
? ['
|
|
482
|
+
? ['timeout_ms', 'instruction', 'cli', 'model', 'recovery_mode', 'surfaces', 'permissions', 'cwd', 'transport', 'cli_identity'] as const
|
|
471
483
|
: [];
|
|
472
484
|
assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
|
|
473
|
-
|
|
485
|
+
const transportRetries = step['retry'] === undefined
|
|
486
|
+
? undefined : validateAuthoringRetryDefaults(step['retry'], `${at}.retry`);
|
|
474
487
|
const dependsOn = step['depends_on'];
|
|
475
488
|
const common = {
|
|
476
489
|
id: step['id'],
|
|
@@ -481,6 +494,7 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
481
494
|
? { dependsOn }
|
|
482
495
|
: {}),
|
|
483
496
|
...(step['max_iterations'] !== undefined ? { maxIterations: step['max_iterations'] } : {}),
|
|
497
|
+
...(transportRetries === undefined ? {} : { transportRetries }),
|
|
484
498
|
...kernelVerificationToAuthoring(type, step['verification'], `${at}.verification`),
|
|
485
499
|
...(step['memory'] !== undefined ? { memory: kernelMemoryToAuthoring(step['memory'], `${at}.memory`) } : {}),
|
|
486
500
|
};
|
|
@@ -500,8 +514,9 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
500
514
|
return {
|
|
501
515
|
...common,
|
|
502
516
|
instruction: step['instruction'],
|
|
517
|
+
...(step['timeout_ms'] === undefined ? {} : { timeoutMs: step['timeout_ms'] }),
|
|
503
518
|
...(step['recovery_mode'] !== undefined ? { recoveryMode: step['recovery_mode'] } : {}),
|
|
504
|
-
...copyDefined(step, ['cli', 'model', 'cwd', '
|
|
519
|
+
...copyDefined(step, ['cli', 'model', 'surfaces', 'cwd', 'transport']),
|
|
505
520
|
...(step['permissions'] !== undefined
|
|
506
521
|
? { permissions: kernelPermissionsToAuthoring(step['permissions'], `${at}.permissions`) }
|
|
507
522
|
: {}),
|
|
@@ -510,9 +525,9 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
510
525
|
return common;
|
|
511
526
|
}
|
|
512
527
|
|
|
513
|
-
function validateAuthoringRetryDefaults(value: unknown, at: string):
|
|
528
|
+
function validateAuthoringRetryDefaults(value: unknown, at: string): number | undefined {
|
|
514
529
|
const retry = requireKernelObject(value, [
|
|
515
|
-
'initial_backoff_ms', 'max_backoff_ms', 'multiplier', 'jitter_percent',
|
|
530
|
+
'initial_backoff_ms', 'max_backoff_ms', 'multiplier', 'jitter_percent', 'max_transport_retries',
|
|
516
531
|
], at);
|
|
517
532
|
for (const [field, expected] of Object.entries(KERNEL_RETRY_DEFAULTS)) {
|
|
518
533
|
if (retry[field] !== expected) {
|
|
@@ -521,6 +536,12 @@ function validateAuthoringRetryDefaults(value: unknown, at: string): void {
|
|
|
521
536
|
]);
|
|
522
537
|
}
|
|
523
538
|
}
|
|
539
|
+
const transportRetries = retry['max_transport_retries'];
|
|
540
|
+
if (transportRetries === undefined) return undefined;
|
|
541
|
+
if (!isTransportRetries(transportRetries)) {
|
|
542
|
+
throw new CompileError([`${at}.max_transport_retries must be a non-negative u32 integer`]);
|
|
543
|
+
}
|
|
544
|
+
return transportRetries === DEFAULT_TRANSPORT_RETRIES ? undefined : transportRetries;
|
|
524
545
|
}
|
|
525
546
|
|
|
526
547
|
function kernelVerificationToAuthoring(
|
|
@@ -617,7 +638,11 @@ function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepS
|
|
|
617
638
|
: [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])],
|
|
618
639
|
...(step.input !== undefined ? { input: step.input } : {}),
|
|
619
640
|
max_iterations: step.maxIterations ?? 1,
|
|
620
|
-
retry: {
|
|
641
|
+
retry: {
|
|
642
|
+
...KERNEL_RETRY_DEFAULTS,
|
|
643
|
+
...(step.transportRetries !== undefined && step.transportRetries !== DEFAULT_TRANSPORT_RETRIES
|
|
644
|
+
? { max_transport_retries: step.transportRetries } : {}),
|
|
645
|
+
},
|
|
621
646
|
verification: toKernelVerification(step),
|
|
622
647
|
...(cliIdentity !== undefined ? { cli_identity: cliIdentity } : {}),
|
|
623
648
|
...(step.requirements !== undefined ? { requirements: {
|
|
@@ -662,6 +687,7 @@ function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepS
|
|
|
662
687
|
...common,
|
|
663
688
|
type: 'agent',
|
|
664
689
|
instruction: step.instruction,
|
|
690
|
+
...(step.timeoutMs === undefined ? {} : { timeout_ms: step.timeoutMs }),
|
|
665
691
|
...(step.cli !== undefined ? { cli: step.cli } : {}),
|
|
666
692
|
...(step.model !== undefined ? { model: step.model } : {}),
|
|
667
693
|
...(step.cwd !== undefined ? { cwd: step.cwd } : {}),
|
package/src/failure-kinds.ts
CHANGED
|
@@ -207,6 +207,12 @@ export interface StepAttemptFailure {
|
|
|
207
207
|
/** `retry`, `step_done` or `park`: what the kernel did next, not why it failed. */
|
|
208
208
|
disposition?: string;
|
|
209
209
|
exitCode?: number;
|
|
210
|
+
/** This attempt's own transport evidence; see `StepFailedDetails`. */
|
|
211
|
+
transportPhase?: string;
|
|
212
|
+
transportCause?: string;
|
|
213
|
+
signal?: string;
|
|
214
|
+
errorCode?: string;
|
|
215
|
+
retryableTransport?: boolean;
|
|
210
216
|
/** Redacted, terminal-safe UTF-8 excerpt, at most 256 bytes. */
|
|
211
217
|
stdoutTail?: string;
|
|
212
218
|
/** Redacted, terminal-safe UTF-8 excerpt, at most 256 bytes. */
|
|
@@ -277,6 +283,8 @@ export interface StepFailedDetails {
|
|
|
277
283
|
* `retries_exhausted`.
|
|
278
284
|
*/
|
|
279
285
|
maxIterations?: number;
|
|
286
|
+
/** Additional classified infrastructure retries declared for the step. */
|
|
287
|
+
transportRetries?: number;
|
|
280
288
|
/**
|
|
281
289
|
* Attempts before this one that died without a result -- the kernel
|
|
282
290
|
* journaled them `crashed` or `lease_expired` itself -- and so, by design,
|
|
@@ -286,6 +294,11 @@ export interface StepFailedDetails {
|
|
|
286
294
|
*/
|
|
287
295
|
unchargedAttempts?: number;
|
|
288
296
|
exitCode?: number;
|
|
297
|
+
transportPhase?: string;
|
|
298
|
+
transportCause?: string;
|
|
299
|
+
signal?: string;
|
|
300
|
+
errorCode?: string;
|
|
301
|
+
retryableTransport?: boolean;
|
|
289
302
|
/**
|
|
290
303
|
* Terminal-safe UTF-8 excerpt of the captured stdout, at most
|
|
291
304
|
* `EXCERPT_BYTES` (cli/step-excerpt.ts). The field name is kept for
|
package/src/pty-sidechannel.ts
CHANGED
|
@@ -31,6 +31,8 @@ export async function openSidechannel(
|
|
|
31
31
|
canDrive: () => boolean = () => true,
|
|
32
32
|
) {
|
|
33
33
|
const peers = new Map<Socket, boolean>();
|
|
34
|
+
const drivePeers = new Set<Socket>();
|
|
35
|
+
const driveIdle: Array<() => void> = [];
|
|
34
36
|
let closed = false;
|
|
35
37
|
const server = createServer(socket => {
|
|
36
38
|
if (peers.size >= 16) { socket.destroy(); return; }
|
|
@@ -39,7 +41,12 @@ export async function openSidechannel(
|
|
|
39
41
|
let mode: string | undefined;
|
|
40
42
|
socket.setTimeout(2_000, () => socket.destroy());
|
|
41
43
|
socket.on('error', () => socket.destroy());
|
|
42
|
-
socket.on('close', () =>
|
|
44
|
+
socket.on('close', () => {
|
|
45
|
+
peers.delete(socket);
|
|
46
|
+
if (drivePeers.delete(socket) && drivePeers.size === 0) {
|
|
47
|
+
for (const listener of driveIdle.splice(0)) listener();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
43
50
|
socket.on('data', (bytes: Buffer) => {
|
|
44
51
|
if (mode === undefined) {
|
|
45
52
|
hello = Buffer.concat([hello, bytes]);
|
|
@@ -52,7 +59,10 @@ export async function openSidechannel(
|
|
|
52
59
|
socket.setTimeout(0);
|
|
53
60
|
peers.set(socket, true);
|
|
54
61
|
// Passthrough is a passive raw-byte view in this initial slice.
|
|
55
|
-
if (mode === 'drive')
|
|
62
|
+
if (mode === 'drive') {
|
|
63
|
+
drivePeers.add(socket);
|
|
64
|
+
context.onDrive();
|
|
65
|
+
}
|
|
56
66
|
bytes = hello.subarray(end + 1);
|
|
57
67
|
hello = Buffer.alloc(0);
|
|
58
68
|
}
|
|
@@ -89,6 +99,32 @@ export async function openSidechannel(
|
|
|
89
99
|
if (ready && !peer.write(bytes)) peer.destroy();
|
|
90
100
|
}
|
|
91
101
|
},
|
|
102
|
+
/**
|
|
103
|
+
* Wait for a drive peer before the CLI is spawned.
|
|
104
|
+
*
|
|
105
|
+
* A direct headless CLI must not be handed a pipe merely because a
|
|
106
|
+
* sidechannel exists: Codex treats any non-TTY stdin as an additional
|
|
107
|
+
* prompt source and waits for its lifecycle. The caller uses this bounded
|
|
108
|
+
* enrollment window to choose `pipe` only when a driver actually joined;
|
|
109
|
+
* view and passthrough peers never change the child's stdin contract.
|
|
110
|
+
*/
|
|
111
|
+
async waitForDrive(timeoutMs: number): Promise<boolean> {
|
|
112
|
+
// Enrollment is a live state, not a sticky historical event. Hold the
|
|
113
|
+
// whole bounded window so a peer that greets and then disconnects before
|
|
114
|
+
// spawn cannot leave the child with piped stdin and nobody to close it.
|
|
115
|
+
if (timeoutMs > 0) await new Promise<void>(resolve => setTimeout(resolve, timeoutMs));
|
|
116
|
+
return drivePeers.size > 0;
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* Run `listener` once no drive peer is live: now, or when the last one
|
|
120
|
+
* disconnects. `waitForDrive` is a snapshot, and a peer can leave the
|
|
121
|
+
* moment after it is taken; the caller that chose `pipe` on it closes the
|
|
122
|
+
* child's stdin here, so the child never waits on a pipe nobody holds.
|
|
123
|
+
*/
|
|
124
|
+
whenDriveIdle(listener: () => void): void {
|
|
125
|
+
if (drivePeers.size === 0) listener();
|
|
126
|
+
else driveIdle.push(listener);
|
|
127
|
+
},
|
|
92
128
|
close() {
|
|
93
129
|
if (closed) return;
|
|
94
130
|
closed = true;
|
package/src/spec.ts
CHANGED
|
@@ -228,6 +228,8 @@ export interface BaseStepSpec {
|
|
|
228
228
|
dependsOn?: string[];
|
|
229
229
|
/** Semantic retry bound (kernel DESIGN.md §1.2 `max_iterations`). Default 1. */
|
|
230
230
|
maxIterations?: number;
|
|
231
|
+
/** Additional attempts after classified infrastructure loss. Default 1. */
|
|
232
|
+
transportRetries?: number;
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
export interface OutputBinding {
|
|
@@ -288,6 +290,7 @@ export interface LlmStepSpec extends BaseStepSpec {
|
|
|
288
290
|
*/
|
|
289
291
|
export interface AgentStepSpec extends BaseStepSpec {
|
|
290
292
|
type: 'agent';
|
|
293
|
+
timeoutMs?: number;
|
|
291
294
|
instruction: string;
|
|
292
295
|
verification?: OutputVerificationSpec;
|
|
293
296
|
/** Named authoring declaration selected from `FlowSpec.agents`. Compiled away. */
|
|
@@ -352,7 +355,7 @@ export interface YamlHelperParams {
|
|
|
352
355
|
type OneKey<T> = { [K in keyof T]: Pick<T, K> & Partial<Record<Exclude<keyof T, K>, never>> }[keyof T];
|
|
353
356
|
|
|
354
357
|
/** Helper sugar is removed before validation of the three kernel step types. */
|
|
355
|
-
export type YamlHelperStepSpec = Pick<BaseStepSpec, 'id' | 'dependsOn' | 'maxIterations'> & {
|
|
358
|
+
export type YamlHelperStepSpec = Pick<BaseStepSpec, 'id' | 'dependsOn' | 'maxIterations' | 'transportRetries'> & {
|
|
356
359
|
verification?: OutputVerificationSpec;
|
|
357
360
|
output?: JsonOutputSchema;
|
|
358
361
|
} & OneKey<{ [P in keyof YamlHelperParams]: OneKey<YamlHelperParams[P]> }>;
|
|
@@ -453,6 +456,7 @@ export interface KernelRetryPolicy {
|
|
|
453
456
|
max_backoff_ms: number;
|
|
454
457
|
multiplier: number;
|
|
455
458
|
jitter_percent: number;
|
|
459
|
+
max_transport_retries?: number;
|
|
456
460
|
}
|
|
457
461
|
|
|
458
462
|
/**
|
|
@@ -510,6 +514,7 @@ export interface KernelPermissionsSpec {
|
|
|
510
514
|
|
|
511
515
|
export interface KernelAgentStep extends KernelStepCommon {
|
|
512
516
|
type: 'agent';
|
|
517
|
+
timeout_ms?: number;
|
|
513
518
|
instruction: string;
|
|
514
519
|
cli?: string;
|
|
515
520
|
model?: string;
|