@relayflows/sdk 2.0.39 → 2.0.41
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 +1 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +6 -3
- package/dist/cli/cloud-deploy.js.map +1 -1
- 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 +2 -2
- package/dist/cli-commands.js +1 -1
- 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.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +14 -20
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +49 -16
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-versions.d.ts +2 -6
- package/dist/cloud-versions.d.ts.map +1 -1
- 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/flow-requirements.d.ts +2 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +4 -2
- package/dist/flow-requirements.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.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 +4 -3
- 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 +8 -4
- package/src/cli/step-evidence.ts +51 -7
- package/src/cli/step-failure.ts +13 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli-transport-evidence.ts +70 -0
- package/src/cli.ts +1 -1
- package/src/cloud-deploy.ts +62 -20
- package/src/cloud-versions.ts +2 -2
- package/src/compile.ts +39 -13
- package/src/failure-kinds.ts +13 -0
- package/src/flow-requirements.ts +6 -3
- package/src/index.ts +1 -1
- 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/step-failure.ts
CHANGED
|
@@ -60,6 +60,7 @@ export async function stepFailureDetails(
|
|
|
60
60
|
// enforcing (relayflowd-core/src/entry.rs `AttemptStartedPayload`). They are
|
|
61
61
|
// collected on the same walk and reported only when the journal held them.
|
|
62
62
|
const budgets = new Map<string, number>();
|
|
63
|
+
const transportBudgets = new Map<string, number>();
|
|
63
64
|
// Keyed by step, so interleaved steps never pool their attempts and a page
|
|
64
65
|
// boundary never splits one step's history.
|
|
65
66
|
const histories = new Map<string, AttemptHistory>();
|
|
@@ -82,6 +83,10 @@ export async function stepFailureDetails(
|
|
|
82
83
|
if (typeof maxIterations === 'number' && Number.isSafeInteger(maxIterations) && maxIterations > 0) {
|
|
83
84
|
budgets.set(stepId, maxIterations);
|
|
84
85
|
}
|
|
86
|
+
const transportRetries = record(entry['payload'])?.['max_transport_retries'];
|
|
87
|
+
if (typeof transportRetries === 'number' && Number.isSafeInteger(transportRetries) && transportRetries >= 0) {
|
|
88
|
+
transportBudgets.set(stepId, transportRetries);
|
|
89
|
+
}
|
|
85
90
|
continue;
|
|
86
91
|
}
|
|
87
92
|
if (entry['entry_type'] !== 'step.completed') continue;
|
|
@@ -117,12 +122,14 @@ export async function stepFailureDetails(
|
|
|
117
122
|
const stepType = snapshot.steps[stepId]?.type;
|
|
118
123
|
const attempt = entry['attempt'];
|
|
119
124
|
const maxIterations = budgets.get(stepId);
|
|
125
|
+
const transportRetries = transportBudgets.get(stepId);
|
|
120
126
|
failures.set(stepId, {
|
|
121
127
|
stepId,
|
|
122
128
|
completionReason,
|
|
123
129
|
...(stepType === undefined ? {} : { stepType }),
|
|
124
130
|
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
125
131
|
...(maxIterations === undefined ? {} : { maxIterations }),
|
|
132
|
+
...(transportRetries === undefined ? {} : { transportRetries }),
|
|
126
133
|
...((uncharged.get(stepId) ?? 0) > 0 ? { unchargedAttempts: uncharged.get(stepId) } : {}),
|
|
127
134
|
...terminalEvidence(payload),
|
|
128
135
|
// A single failed attempt is already fully described by the scalars
|
|
@@ -166,7 +173,13 @@ export function renderStepEvidence(details: StepFailedDetails): string {
|
|
|
166
173
|
+ (details.stepType === undefined ? '' : ` (${details.stepType})`)
|
|
167
174
|
+ ` completionReason: ${details.completionReason}`
|
|
168
175
|
+ renderAttempt(details)
|
|
176
|
+
+ (details.transportRetries === undefined ? '' : ` transportRetries=${details.transportRetries}`)
|
|
169
177
|
+ (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
|
|
178
|
+
+ (details.signal === undefined ? '' : ` signal=${details.signal}`)
|
|
179
|
+
+ (details.transportCause === undefined ? '' : ` transport=${details.transportCause}`)
|
|
180
|
+
+ (details.transportPhase === undefined ? '' : ` phase=${details.transportPhase}`)
|
|
181
|
+
+ (details.errorCode === undefined ? '' : ` error_code=${details.errorCode}`)
|
|
182
|
+
+ (details.retryableTransport === undefined ? '' : ` retryable=${details.retryableTransport}`)
|
|
170
183
|
+ '.'
|
|
171
184
|
+ renderAttemptHistory(details)
|
|
172
185
|
+ (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
|
package/src/cli-commands.ts
CHANGED
|
@@ -156,7 +156,7 @@ export const CLI_VERBS = [
|
|
|
156
156
|
args: [{ name: 'flow', description: 'flow.ts for a hosted listener, or <flow>@sha256:<digest> for a bundle', required: true }],
|
|
157
157
|
options: [
|
|
158
158
|
{ flags: '--to <file-bucket-uri>', description: 'Destination file bucket for a sealed bundle' },
|
|
159
|
-
{ flags: '--repo <owner/name>', description: '
|
|
159
|
+
{ flags: '--repo <owner/name|gitlab:group/project>', description: 'Target repository: owner/name or github:owner/name for GitHub; gitlab:group/project (subgroups allowed) or a github.com/gitlab.com HTTP(S) URL' },
|
|
160
160
|
{ flags: '--on <provider>', description: 'Trigger source, as <provider>[:key=value,...]; repeatable' },
|
|
161
161
|
{ flags: '--approver <handle>', description: 'Handle delivered to every launched run as input.approver' },
|
|
162
162
|
{ flags: '--agents <list>', description: 'Agent harnesses to allow, as claude[,codex]' },
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { boundedTail, redactText } from './agent-transcript.js';
|
|
2
|
+
|
|
3
|
+
export type CliTransportPhase = 'spawn' | 'close' | 'abort' | 'timeout' | 'result_exit_grace';
|
|
4
|
+
export type CliTransportCause =
|
|
5
|
+
| 'exited'
|
|
6
|
+
| 'nonzero_exit'
|
|
7
|
+
| 'signal'
|
|
8
|
+
| 'close_without_status'
|
|
9
|
+
| 'spawn_error'
|
|
10
|
+
| 'process_error'
|
|
11
|
+
| 'lease_lost'
|
|
12
|
+
| 'timeout'
|
|
13
|
+
| 'result_exit_timeout'
|
|
14
|
+
| 'codex_stdin_lifecycle';
|
|
15
|
+
|
|
16
|
+
export interface CliTransportEvidence {
|
|
17
|
+
phase: CliTransportPhase;
|
|
18
|
+
cause: CliTransportCause;
|
|
19
|
+
exit_code: number | null;
|
|
20
|
+
signal: NodeJS.Signals | null;
|
|
21
|
+
/** OS/libuv error code, never an unbounded error message. */
|
|
22
|
+
error_code?: string;
|
|
23
|
+
/** True only for a closed set of infrastructure failures. */
|
|
24
|
+
retryable: boolean;
|
|
25
|
+
/** Redacted UTF-8 tail with its truncation made explicit. */
|
|
26
|
+
stderr_tail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Infrastructure classification is deliberately narrower than "nonzero". */
|
|
30
|
+
export function agentCompletionReason(
|
|
31
|
+
result: { exit_code: number | null; transport?: CliTransportEvidence },
|
|
32
|
+
): 'success' | 'crashed' | 'timeout' | 'worker_error' {
|
|
33
|
+
if (result.exit_code === 0) return 'success';
|
|
34
|
+
if (result.transport?.retryable === true) return 'crashed';
|
|
35
|
+
if (result.transport?.cause === 'timeout') return 'timeout';
|
|
36
|
+
return 'worker_error';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TRANSPORT_STDERR_MAX_BYTES = 2 * 1024;
|
|
40
|
+
const RETRYABLE_SPAWN_ERROR_CODES = new Set(['EAGAIN', 'EMFILE', 'ENFILE', 'ENOMEM', 'ETXTBSY']);
|
|
41
|
+
|
|
42
|
+
export function isRetryableSpawnError(code: string | undefined): boolean {
|
|
43
|
+
return code !== undefined && RETRYABLE_SPAWN_ERROR_CODES.has(code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function transportEvidence(
|
|
47
|
+
value: {
|
|
48
|
+
phase: CliTransportPhase;
|
|
49
|
+
cause: CliTransportCause;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
signal: NodeJS.Signals | null;
|
|
52
|
+
stderr: string;
|
|
53
|
+
errorCode?: string;
|
|
54
|
+
retryable: boolean;
|
|
55
|
+
},
|
|
56
|
+
env: NodeJS.ProcessEnv,
|
|
57
|
+
): CliTransportEvidence {
|
|
58
|
+
const bounded = boundedTail(redactText(value.stderr, env), TRANSPORT_STDERR_MAX_BYTES, 'transport stderr: ');
|
|
59
|
+
const errorCode = value.errorCode !== undefined && /^[A-Z0-9_-]{1,64}$/.test(value.errorCode)
|
|
60
|
+
? value.errorCode : undefined;
|
|
61
|
+
return {
|
|
62
|
+
phase: value.phase,
|
|
63
|
+
cause: value.cause,
|
|
64
|
+
exit_code: value.exitCode,
|
|
65
|
+
signal: value.signal,
|
|
66
|
+
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
67
|
+
retryable: value.retryable,
|
|
68
|
+
stderr_tail: bounded.text,
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -125,7 +125,7 @@ const USAGE = [
|
|
|
125
125
|
'flows plugin update [--json] [--yes] [--to <ref>] [<name>]',
|
|
126
126
|
'flows build [--out <dir>] <flow.yaml|flow.ts>',
|
|
127
127
|
'flows build --verify <bundle-dir>',
|
|
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]',
|
|
128
|
+
'flows deploy <flow.ts> --repo <owner/name|gitlab:group/project> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--plugin <ref>] [--no-connect] [--json]',
|
|
129
129
|
'flows deploy <flow.ts> --flow <name|listener-id> [--plugin <ref>] [--no-connect] [--json]',
|
|
130
130
|
'flows deployments [--json]',
|
|
131
131
|
'flows versions [--json] <name|listener-id>',
|
package/src/cloud-deploy.ts
CHANGED
|
@@ -18,10 +18,11 @@ import { parseVersionChange, type CloudFlowVersionChange } from './cloud-version
|
|
|
18
18
|
* deploy wizard. `POST /api/v1/flows/deploy` stores one self-contained
|
|
19
19
|
* authored source and creates a proactive listener whose watch rules match
|
|
20
20
|
* the chosen ticket sources on the workspace's relayfile projections. There
|
|
21
|
-
* is no webhook to register: the GitHub App installation (or
|
|
22
|
-
* Jira
|
|
23
|
-
* a run of the stored source with
|
|
24
|
-
* inside a fresh branch of the
|
|
21
|
+
* is no webhook to register: the GitHub App installation (or the GitLab,
|
|
22
|
+
* Slack, Linear, Jira or Shortcut connection) is the ingress, and each
|
|
23
|
+
* matching ticket launches a run of the stored source with
|
|
24
|
+
* `{ approver, issue, event }` as its input, inside a fresh branch of the
|
|
25
|
+
* deployment's repository.
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
export const FLOW_TRIGGER_PROVIDERS = ['github', 'gitlab', 'linear', 'jira', 'shortcut', 'slack'] as const;
|
|
@@ -45,6 +46,25 @@ const MAX_SOURCE_BYTES = 256_000;
|
|
|
45
46
|
const MAX_SETTING_LENGTH = 500;
|
|
46
47
|
const REPO_OWNER = /^[A-Za-z0-9-]{1,39}$/u;
|
|
47
48
|
const REPO_NAME = /^[A-Za-z0-9_.-]{1,100}$/u;
|
|
49
|
+
// GitLab coordinates mirror Cloud's isValidFlowRepositoryCoordinates
|
|
50
|
+
// (AgentWorkforce/cloud packages/web/lib/flows/flow-repository.ts): the owner
|
|
51
|
+
// is the namespace path, at most 255 characters and 20 segments (the root
|
|
52
|
+
// group counts; the project name does not), and every owner segment and the
|
|
53
|
+
// project name match GITLAB_SEGMENT without a trailing ., .git or .atom.
|
|
54
|
+
const GITLAB_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/u;
|
|
55
|
+
const GITLAB_OWNER_SEGMENTS = 20;
|
|
56
|
+
const GITLAB_OWNER_LENGTH = 255;
|
|
57
|
+
|
|
58
|
+
function validGitlabSegment(segment: string): boolean {
|
|
59
|
+
return GITLAB_SEGMENT.test(segment)
|
|
60
|
+
&& !segment.endsWith('.') && !segment.endsWith('.git') && !segment.endsWith('.atom');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DeployRepository {
|
|
64
|
+
owner: string;
|
|
65
|
+
name: string;
|
|
66
|
+
host?: 'gitlab';
|
|
67
|
+
}
|
|
48
68
|
|
|
49
69
|
export interface FlowTriggerSource {
|
|
50
70
|
provider: FlowTriggerProvider;
|
|
@@ -53,7 +73,7 @@ export interface FlowTriggerSource {
|
|
|
53
73
|
|
|
54
74
|
export interface DeployToCloudInput {
|
|
55
75
|
path: string;
|
|
56
|
-
repository:
|
|
76
|
+
repository: DeployRepository;
|
|
57
77
|
sources: FlowTriggerSource[];
|
|
58
78
|
/** The `f.human` approver handle every launched run receives as `input.approver`. */
|
|
59
79
|
approver: string;
|
|
@@ -98,7 +118,7 @@ export interface CloudDeployment {
|
|
|
98
118
|
agentId: string;
|
|
99
119
|
name: string;
|
|
100
120
|
status: string;
|
|
101
|
-
repository:
|
|
121
|
+
repository: DeployRepository;
|
|
102
122
|
sources: FlowTriggerSource[];
|
|
103
123
|
sourceSha256: string;
|
|
104
124
|
/** What the source declared it needs; the harnesses became `inputs.agents` unless `agents` was given. */
|
|
@@ -109,12 +129,24 @@ export interface CloudDeployment {
|
|
|
109
129
|
version?: CloudFlowVersionChange;
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
export function parseRepository(value: string):
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
export function parseRepository(value: string): DeployRepository {
|
|
133
|
+
const prefix = /^(github|gitlab):/iu.exec(value);
|
|
134
|
+
const path = prefix ? value.slice(prefix[0].length) : value;
|
|
135
|
+
const url = /^https?:\/\/(github|gitlab)\.com\//iu.exec(path);
|
|
136
|
+
const host = (prefix?.[1] ?? url?.[1] ?? 'github').toLowerCase();
|
|
137
|
+
const parts = (url ? path.slice(url[0].length) : path).replace(/\.git$/iu, '').split('/');
|
|
138
|
+
const name = parts.at(-1)!;
|
|
139
|
+
const owner = parts.slice(0, -1).join('/');
|
|
140
|
+
const valid = host === 'gitlab'
|
|
141
|
+
? parts.length >= 2 && parts.length - 1 <= GITLAB_OWNER_SEGMENTS
|
|
142
|
+
&& owner.length <= GITLAB_OWNER_LENGTH && parts.every(validGitlabSegment)
|
|
143
|
+
: parts.length === 2 && REPO_OWNER.test(owner) && REPO_NAME.test(name);
|
|
144
|
+
if (!valid || (url && url[1]!.toLowerCase() !== host)) {
|
|
145
|
+
throw new CloudFlowError('invalid_input',
|
|
146
|
+
`Expected --repo <owner/name> (GitHub) or gitlab:<group/project> (up to ${GITLAB_OWNER_SEGMENTS} namespace segments), `
|
|
147
|
+
+ `or a github.com/gitlab.com HTTP(S) project URL, got "${value}".`);
|
|
116
148
|
}
|
|
117
|
-
return { owner, name };
|
|
149
|
+
return { owner, name, ...(host === 'gitlab' ? { host: 'gitlab' as const } : {}) };
|
|
118
150
|
}
|
|
119
151
|
|
|
120
152
|
/** `github`, `github:labels=agent,contains=urgent`, `slack:channel=#eng`. */
|
|
@@ -233,11 +265,21 @@ export async function deployToCloud(
|
|
|
233
265
|
if (!approver) throw new CloudFlowError('invalid_input', '--approver must name who approves f.human questions.');
|
|
234
266
|
const name = (input.name ?? definition.name).trim();
|
|
235
267
|
if (!name || name.length > 100) throw new CloudFlowError('invalid_input', 'Deployment name must be 1-100 characters.');
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
const sources = input.sources.map(s =>
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
// Scope same-host sources to the target rather than waking on every project.
|
|
269
|
+
const target = `${input.repository.owner}/${input.repository.name}`;
|
|
270
|
+
const sources = input.sources.map(s => {
|
|
271
|
+
if (s.provider === 'github' && s.settings['repository'] === undefined) {
|
|
272
|
+
if (input.repository.host === 'gitlab') {
|
|
273
|
+
throw new CloudFlowError('invalid_input',
|
|
274
|
+
'A GitLab target needs an explicit --on github:repository=owner/name for a GitHub source.');
|
|
275
|
+
}
|
|
276
|
+
return { ...s, settings: { ...s.settings, repository: target } };
|
|
277
|
+
}
|
|
278
|
+
if (s.provider === 'gitlab' && s.settings['project'] === undefined && input.repository.host === 'gitlab') {
|
|
279
|
+
return { ...s, settings: { ...s.settings, project: target } };
|
|
280
|
+
}
|
|
281
|
+
return s;
|
|
282
|
+
});
|
|
241
283
|
options.signal?.throwIfAborted();
|
|
242
284
|
|
|
243
285
|
const whoami = await cloudRequest('/api/v1/auth/whoami', options);
|
|
@@ -245,8 +287,7 @@ export async function deployToCloud(
|
|
|
245
287
|
if (workspace === undefined || typeof workspace.id !== 'string' || !workspace.id) {
|
|
246
288
|
throw new CloudFlowError('invalid_response', 'Cloud did not report a current workspace for this credential.');
|
|
247
289
|
}
|
|
248
|
-
// Every launched run
|
|
249
|
-
// required even when no GitHub source wakes it.
|
|
290
|
+
// Every launched run needs the target's host, even when another provider wakes it.
|
|
250
291
|
const requirements = mergeFlowExtensionRequirements(
|
|
251
292
|
flowRequirements(definition, {
|
|
252
293
|
sources, repository: input.repository, ...(projectCli === undefined ? {} : { projectCli }),
|
|
@@ -307,7 +348,7 @@ export interface CloudDeploymentSummary {
|
|
|
307
348
|
agentId: string;
|
|
308
349
|
name: string;
|
|
309
350
|
status: string;
|
|
310
|
-
repository?:
|
|
351
|
+
repository?: DeployRepository;
|
|
311
352
|
sources: FlowTriggerSource[];
|
|
312
353
|
createdAt?: string;
|
|
313
354
|
updatedAt?: string;
|
|
@@ -323,7 +364,8 @@ export async function listCloudDeployments(options: CloudConnectionOptions = {})
|
|
|
323
364
|
throw new CloudFlowError('invalid_response', 'Cloud returned a malformed deployment row.');
|
|
324
365
|
}
|
|
325
366
|
const repository = isCloudRecord(row.repository) && typeof row.repository.owner === 'string'
|
|
326
|
-
&& typeof row.repository.name === 'string' ? { owner: row.repository.owner, name: row.repository.name
|
|
367
|
+
&& typeof row.repository.name === 'string' ? { owner: row.repository.owner, name: row.repository.name,
|
|
368
|
+
...(row.repository.host === 'gitlab' ? { host: 'gitlab' as const } : {}) } : undefined;
|
|
327
369
|
const sources = Array.isArray(row.sources) ? row.sources.flatMap((s): FlowTriggerSource[] =>
|
|
328
370
|
isCloudRecord(s) && typeof s.provider === 'string' && (FLOW_TRIGGER_PROVIDERS as readonly string[]).includes(s.provider)
|
|
329
371
|
? [{ provider: s.provider as FlowTriggerProvider,
|
package/src/cloud-versions.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions,
|
|
5
5
|
} from './cloud-http.js';
|
|
6
6
|
import {
|
|
7
|
-
FLOW_AGENT_HARNESSES, listCloudDeployments, loadDeploySource, type FlowTriggerSource,
|
|
7
|
+
FLOW_AGENT_HARNESSES, listCloudDeployments, loadDeploySource, type DeployRepository, type FlowTriggerSource,
|
|
8
8
|
} from './cloud-deploy.js';
|
|
9
9
|
import {
|
|
10
10
|
parseVersion, parseVersionChange, type CloudFlowVersion, type CloudFlowVersionChange,
|
|
@@ -25,7 +25,7 @@ export interface CloudListener {
|
|
|
25
25
|
agentId: string;
|
|
26
26
|
name: string;
|
|
27
27
|
status: string;
|
|
28
|
-
repository:
|
|
28
|
+
repository: DeployRepository;
|
|
29
29
|
sources: FlowTriggerSource[];
|
|
30
30
|
activeVersion: CloudFlowVersion | null;
|
|
31
31
|
versions: CloudFlowVersion[];
|
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/flow-requirements.ts
CHANGED
|
@@ -51,8 +51,8 @@ export interface FlowRequirements {
|
|
|
51
51
|
export interface FlowRequirementsContext {
|
|
52
52
|
/** Trigger sources the deployment listens on (`--on`, or the wizard's chosen sources). */
|
|
53
53
|
sources?: readonly { provider: string }[];
|
|
54
|
-
/** Set when the deployment targets a repository: every launched run needs GitHub. */
|
|
55
|
-
repository?: boolean | { owner: string; name: string };
|
|
54
|
+
/** Set when the deployment targets a repository: every launched run needs its host (true means GitHub). */
|
|
55
|
+
repository?: boolean | { owner: string; name: string; host?: 'gitlab' };
|
|
56
56
|
/** The nearest `flows.json` `cli`, when one applies. */
|
|
57
57
|
projectCli?: string;
|
|
58
58
|
}
|
|
@@ -180,7 +180,10 @@ export function flowRequirements(
|
|
|
180
180
|
for (const source of context.sources ?? []) {
|
|
181
181
|
declare({ provider: source.provider, from: 'source', detail: `--on ${source.provider}` });
|
|
182
182
|
}
|
|
183
|
-
if (context.repository)
|
|
183
|
+
if (context.repository) {
|
|
184
|
+
const provider = typeof context.repository === 'object' && context.repository.host === 'gitlab' ? 'gitlab' : 'github';
|
|
185
|
+
declare({ provider, from: 'source', detail: 'deploy target' });
|
|
186
|
+
}
|
|
184
187
|
|
|
185
188
|
const uses = [...harnessUses.values()];
|
|
186
189
|
return {
|
package/src/index.ts
CHANGED
|
@@ -107,7 +107,7 @@ export {
|
|
|
107
107
|
export { prepareCloudSubmission, cloudSubmissionBody, type CloudSubmission } from './cloud-run.js';
|
|
108
108
|
export {
|
|
109
109
|
deployToCloud, listCloudDeployments, undeployFromCloud, parseRepository, parseTriggerSource, FLOW_TRIGGER_PROVIDERS,
|
|
110
|
-
type DeployToCloudInput, type CloudDeployment, type CloudDeploymentSummary, type FlowTriggerSource, type FlowTriggerProvider,
|
|
110
|
+
type DeployRepository, type DeployToCloudInput, type CloudDeployment, type CloudDeploymentSummary, type FlowTriggerSource, type FlowTriggerProvider,
|
|
111
111
|
} from './cloud-deploy.js';
|
|
112
112
|
export {
|
|
113
113
|
ensureIntegrationsConnected, integrationConnected, providerLabel,
|
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;
|