agent-relay 3.2.21 → 4.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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +5065 -1422
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +30 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1071 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +241 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +2 -2
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/package.json +14 -11
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/client.d.ts +108 -196
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +336 -824
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +24 -5
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/cli.js +46 -2
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/file-db.d.ts +2 -0
- package/packages/sdk/dist/workflows/file-db.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/file-db.js +20 -3
- package/packages/sdk/dist/workflows/file-db.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +157 -11
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/resume-fallback.test.ts +415 -0
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/client.ts +422 -1072
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +8 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +260 -57
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/cli.ts +53 -2
- package/packages/sdk/src/workflows/file-db.ts +22 -3
- package/packages/sdk/src/workflows/runner.ts +178 -11
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +121 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input/output types for the broker SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentRuntime, HeadlessProvider, MessageInjectionMode, RestartPolicy } from './protocol.js';
|
|
6
|
+
|
|
7
|
+
export interface SpawnPtyInput {
|
|
8
|
+
name: string;
|
|
9
|
+
cli: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
channels?: string[];
|
|
12
|
+
task?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
team?: string;
|
|
16
|
+
shadowOf?: string;
|
|
17
|
+
shadowMode?: string;
|
|
18
|
+
idleThresholdSecs?: number;
|
|
19
|
+
restartPolicy?: RestartPolicy;
|
|
20
|
+
continueFrom?: string;
|
|
21
|
+
skipRelayPrompt?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SpawnHeadlessInput {
|
|
25
|
+
name: string;
|
|
26
|
+
provider: HeadlessProvider;
|
|
27
|
+
args?: string[];
|
|
28
|
+
channels?: string[];
|
|
29
|
+
task?: string;
|
|
30
|
+
skipRelayPrompt?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AgentTransport = 'pty' | 'headless';
|
|
34
|
+
|
|
35
|
+
export interface SpawnProviderInput {
|
|
36
|
+
name: string;
|
|
37
|
+
provider: string;
|
|
38
|
+
transport?: AgentTransport;
|
|
39
|
+
args?: string[];
|
|
40
|
+
channels?: string[];
|
|
41
|
+
task?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
team?: string;
|
|
45
|
+
shadowOf?: string;
|
|
46
|
+
shadowMode?: string;
|
|
47
|
+
idleThresholdSecs?: number;
|
|
48
|
+
restartPolicy?: RestartPolicy;
|
|
49
|
+
continueFrom?: string;
|
|
50
|
+
skipRelayPrompt?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SendMessageInput {
|
|
54
|
+
to: string;
|
|
55
|
+
text: string;
|
|
56
|
+
from?: string;
|
|
57
|
+
threadId?: string;
|
|
58
|
+
workspaceId?: string;
|
|
59
|
+
workspaceAlias?: string;
|
|
60
|
+
priority?: number;
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
mode?: MessageInjectionMode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ListAgent {
|
|
66
|
+
name: string;
|
|
67
|
+
runtime: AgentRuntime;
|
|
68
|
+
provider?: HeadlessProvider;
|
|
69
|
+
cli?: string;
|
|
70
|
+
model?: string;
|
|
71
|
+
team?: string;
|
|
72
|
+
channels: string[];
|
|
73
|
+
parent?: string;
|
|
74
|
+
pid?: number;
|
|
75
|
+
}
|
|
@@ -52,6 +52,21 @@ type ExecuteOptions = {
|
|
|
52
52
|
previousRunId?: string;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/** Flags that consume the next argument as their value. Single source of truth for CLI parsing. */
|
|
56
|
+
const FLAGS_WITH_VALUES = new Set(['--resume', '--workflow', '--start-from', '--previous-run-id']);
|
|
57
|
+
|
|
58
|
+
function getYamlPathArg(args: string[]): string | undefined {
|
|
59
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
60
|
+
const arg = args[i];
|
|
61
|
+
if (arg.startsWith('--')) {
|
|
62
|
+
if (FLAGS_WITH_VALUES.has(arg)) i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
return arg;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
interface RenderableTask {
|
|
56
71
|
output?: string;
|
|
57
72
|
title: string;
|
|
@@ -302,6 +317,7 @@ async function runWithListr(
|
|
|
302
317
|
|
|
303
318
|
async function main(): Promise<void> {
|
|
304
319
|
const args = process.argv.slice(2);
|
|
320
|
+
const yamlPath = getYamlPathArg(args);
|
|
305
321
|
|
|
306
322
|
if (args.length === 0 || args.includes('--help')) {
|
|
307
323
|
printUsage();
|
|
@@ -358,7 +374,37 @@ async function main(): Promise<void> {
|
|
|
358
374
|
break;
|
|
359
375
|
}
|
|
360
376
|
});
|
|
361
|
-
|
|
377
|
+
let result: RunnerResult;
|
|
378
|
+
try {
|
|
379
|
+
const resumeConfig = yamlPath ? await runner.parseYamlFile(yamlPath) : undefined;
|
|
380
|
+
if (resumeConfig) {
|
|
381
|
+
console.warn(
|
|
382
|
+
chalk.yellow(
|
|
383
|
+
'[workflow] warning: resuming with current config from disk — ' +
|
|
384
|
+
'if the workflow YAML changed since the original run, behaviour may differ'
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
result = await runner.resume(runId, undefined, resumeConfig);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
391
|
+
const isRunNotFound = message.startsWith(`Run "${runId}" not found`);
|
|
392
|
+
if (isRunNotFound) {
|
|
393
|
+
if (fileDb.hasStepOutputs(runId)) {
|
|
394
|
+
console.error(
|
|
395
|
+
chalk.red(
|
|
396
|
+
`Error: ${message}. Step outputs exist for this run, but persisted run state is missing from ${dbPath}. ` +
|
|
397
|
+
`Use --start-from with --previous-run-id ${runId} to recover from the cached step outputs instead.`
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
405
|
+
}
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
362
408
|
|
|
363
409
|
if (result.status === 'completed') {
|
|
364
410
|
console.log(chalk.green('\nWorkflow completed successfully.'));
|
|
@@ -371,7 +417,6 @@ async function main(): Promise<void> {
|
|
|
371
417
|
}
|
|
372
418
|
|
|
373
419
|
// ── Normal / validate / dry-run mode ──────────────────────────────────────
|
|
374
|
-
const yamlPath = args[0];
|
|
375
420
|
let workflowName: string | undefined;
|
|
376
421
|
|
|
377
422
|
const workflowIdx = args.indexOf('--workflow');
|
|
@@ -391,6 +436,12 @@ async function main(): Promise<void> {
|
|
|
391
436
|
previousRunId = args[prevRunIdx + 1];
|
|
392
437
|
}
|
|
393
438
|
|
|
439
|
+
if (!yamlPath) {
|
|
440
|
+
console.error(chalk.red('Error: workflow YAML path is required'));
|
|
441
|
+
printUsage();
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
394
445
|
const isValidate = args.includes('--validate');
|
|
395
446
|
const isDryRun = !!process.env.DRY_RUN;
|
|
396
447
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import type { WorkflowRunRow, WorkflowStepRow } from './types.js';
|
|
@@ -24,6 +24,7 @@ export class JsonFileWorkflowDb implements WorkflowDb {
|
|
|
24
24
|
|
|
25
25
|
/** Whether the storage directory is writable. False = silent no-op mode. */
|
|
26
26
|
private readonly writable: boolean;
|
|
27
|
+
private appendFailedOnce = false;
|
|
27
28
|
|
|
28
29
|
constructor(filePath: string) {
|
|
29
30
|
this.filePath = filePath;
|
|
@@ -43,14 +44,32 @@ export class JsonFileWorkflowDb implements WorkflowDb {
|
|
|
43
44
|
return this.writable;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
hasStepOutputs(runId: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
const dir = path.join(path.dirname(this.filePath), 'step-outputs', runId);
|
|
50
|
+
return existsSync(dir) && readdirSync(dir).length > 0;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
// ── Private helpers ─────────────────────────────────────────────────────
|
|
47
57
|
|
|
48
58
|
private append(entry: DbEntry): void {
|
|
49
59
|
if (!this.writable) return;
|
|
50
60
|
try {
|
|
51
61
|
appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf8');
|
|
52
|
-
} catch {
|
|
53
|
-
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (!this.appendFailedOnce) {
|
|
64
|
+
this.appendFailedOnce = true;
|
|
65
|
+
console.warn(
|
|
66
|
+
'[workflow] warning: failed to write run state to ' +
|
|
67
|
+
this.filePath +
|
|
68
|
+
' — --resume will not be available for this run. Use --start-from instead. ' +
|
|
69
|
+
'Error: ' +
|
|
70
|
+
(err instanceof Error ? err.message : String(err))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
|
|
@@ -1952,14 +1952,25 @@ export class WorkflowRunner {
|
|
|
1952
1952
|
}
|
|
1953
1953
|
|
|
1954
1954
|
/** Resume a previously paused or partially completed run. */
|
|
1955
|
-
async resume(runId: string, vars?: VariableContext): Promise<WorkflowRunRow> {
|
|
1955
|
+
async resume(runId: string, vars?: VariableContext, config?: RelayYamlConfig): Promise<WorkflowRunRow> {
|
|
1956
1956
|
// Set up abort controller early so callers can abort() even during setup
|
|
1957
1957
|
this.abortController = new AbortController();
|
|
1958
1958
|
this.paused = false;
|
|
1959
1959
|
|
|
1960
|
-
|
|
1960
|
+
let run = await this.db.getRun(runId);
|
|
1961
|
+
let stepStates = new Map<string, StepState>();
|
|
1961
1962
|
if (!run) {
|
|
1962
|
-
|
|
1963
|
+
const reconstructed = this.reconstructRunFromCache(runId, config);
|
|
1964
|
+
if (!reconstructed) {
|
|
1965
|
+
throw new Error(`Run "${runId}" not found (no database entry or cached step outputs)`);
|
|
1966
|
+
}
|
|
1967
|
+
this.log('[resume] Reconstructing run from cached step outputs (workflow-runs.jsonl missing)');
|
|
1968
|
+
run = reconstructed.run;
|
|
1969
|
+
stepStates = reconstructed.stepStates;
|
|
1970
|
+
await this.db.insertRun(run);
|
|
1971
|
+
for (const [, state] of stepStates) {
|
|
1972
|
+
await this.db.insertStep(state.row);
|
|
1973
|
+
}
|
|
1963
1974
|
}
|
|
1964
1975
|
this.persistRunIdHint(runId);
|
|
1965
1976
|
|
|
@@ -1967,25 +1978,26 @@ export class WorkflowRunner {
|
|
|
1967
1978
|
throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
|
|
1968
1979
|
}
|
|
1969
1980
|
|
|
1970
|
-
const
|
|
1981
|
+
const resolvedConfig = vars ? this.resolveVariables(run.config, vars) : run.config;
|
|
1971
1982
|
|
|
1972
1983
|
// Resolve path definitions (same as execute()) so workdir lookups work on resume
|
|
1973
|
-
const pathResult = this.resolvePathDefinitions(
|
|
1984
|
+
const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd);
|
|
1974
1985
|
if (pathResult.errors.length > 0) {
|
|
1975
1986
|
throw new Error(`Path validation failed:\n ${pathResult.errors.join('\n ')}`);
|
|
1976
1987
|
}
|
|
1977
1988
|
this.resolvedPaths = pathResult.resolved;
|
|
1978
1989
|
|
|
1979
|
-
const workflows =
|
|
1990
|
+
const workflows = resolvedConfig.workflows ?? [];
|
|
1980
1991
|
const workflow = workflows.find((w) => w.name === run.workflowName);
|
|
1981
1992
|
if (!workflow) {
|
|
1982
1993
|
throw new Error(`Workflow "${run.workflowName}" not found in stored config`);
|
|
1983
1994
|
}
|
|
1984
1995
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1996
|
+
if (stepStates.size === 0) {
|
|
1997
|
+
const existingSteps = await this.db.getStepsByRunId(runId);
|
|
1998
|
+
for (const stepRow of existingSteps) {
|
|
1999
|
+
stepStates.set(stepRow.stepName, { row: stepRow });
|
|
2000
|
+
}
|
|
1989
2001
|
}
|
|
1990
2002
|
|
|
1991
2003
|
// Reset failed steps to pending for retry
|
|
@@ -2006,7 +2018,7 @@ export class WorkflowRunner {
|
|
|
2006
2018
|
return this.runWorkflowCore({
|
|
2007
2019
|
run,
|
|
2008
2020
|
workflow,
|
|
2009
|
-
config,
|
|
2021
|
+
config: resolvedConfig,
|
|
2010
2022
|
stepStates,
|
|
2011
2023
|
isResume: true,
|
|
2012
2024
|
});
|
|
@@ -6547,8 +6559,16 @@ export class WorkflowRunner {
|
|
|
6547
6559
|
.slice(0, 32);
|
|
6548
6560
|
}
|
|
6549
6561
|
|
|
6562
|
+
/** Validate that a runId is safe for use in file paths (no traversal). */
|
|
6563
|
+
private validateRunId(runId: string): void {
|
|
6564
|
+
if (/[/\\]|^\.\.?$/.test(runId) || runId.includes('..')) {
|
|
6565
|
+
throw new Error(`Invalid runId: "${runId}" contains path traversal characters`);
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6550
6569
|
/** Directory for persisted step outputs: .agent-relay/step-outputs/{runId}/ */
|
|
6551
6570
|
private getStepOutputDir(runId: string): string {
|
|
6571
|
+
this.validateRunId(runId);
|
|
6552
6572
|
return path.join(this.cwd, '.agent-relay', 'step-outputs', runId);
|
|
6553
6573
|
}
|
|
6554
6574
|
|
|
@@ -6638,6 +6658,153 @@ export class WorkflowRunner {
|
|
|
6638
6658
|
}
|
|
6639
6659
|
}
|
|
6640
6660
|
|
|
6661
|
+
/** Match the best workflow from config given a set of cached step names. */
|
|
6662
|
+
private matchWorkflowFromCache(
|
|
6663
|
+
workflows: WorkflowDefinition[],
|
|
6664
|
+
cachedStepNames: Set<string>
|
|
6665
|
+
): WorkflowDefinition | null {
|
|
6666
|
+
if (workflows.length === 1) return workflows[0];
|
|
6667
|
+
|
|
6668
|
+
if (cachedStepNames.size === 0) {
|
|
6669
|
+
// No cached steps to disambiguate — ambiguous when multiple workflows exist
|
|
6670
|
+
this.log('[resume] Multiple workflows in config with empty cache — cannot disambiguate');
|
|
6671
|
+
return null;
|
|
6672
|
+
}
|
|
6673
|
+
|
|
6674
|
+
// Score each workflow by how many cached steps match, excluding those with unknown steps
|
|
6675
|
+
const scored = workflows
|
|
6676
|
+
.map((candidate) => ({
|
|
6677
|
+
workflow: candidate,
|
|
6678
|
+
matchedSteps: candidate.steps.filter((step) => cachedStepNames.has(step.name)).length,
|
|
6679
|
+
unknownSteps: [...cachedStepNames].filter(
|
|
6680
|
+
(name) => !candidate.steps.some((step) => step.name === name)
|
|
6681
|
+
).length,
|
|
6682
|
+
}))
|
|
6683
|
+
.filter((candidate) => candidate.unknownSteps === 0)
|
|
6684
|
+
.sort((a, b) => b.matchedSteps - a.matchedSteps);
|
|
6685
|
+
|
|
6686
|
+
return scored[0]?.workflow ?? null;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
private reconstructRunFromCache(
|
|
6690
|
+
runId: string,
|
|
6691
|
+
config?: RelayYamlConfig
|
|
6692
|
+
): { run: WorkflowRunRow; stepStates: Map<string, StepState> } | null {
|
|
6693
|
+
const stepOutputDir = this.getStepOutputDir(runId);
|
|
6694
|
+
if (!existsSync(stepOutputDir)) return null;
|
|
6695
|
+
|
|
6696
|
+
let resumeConfig = config ?? this.currentConfig;
|
|
6697
|
+
if (!resumeConfig) {
|
|
6698
|
+
// Attempt to load config from relay.yaml on disk (resume() may call before runWorkflowCore sets currentConfig)
|
|
6699
|
+
const yamlPath = path.join(this.cwd, 'relay.yaml');
|
|
6700
|
+
if (existsSync(yamlPath)) {
|
|
6701
|
+
try {
|
|
6702
|
+
const raw = readFileSync(yamlPath, 'utf-8');
|
|
6703
|
+
resumeConfig = this.parseYamlString(raw, yamlPath);
|
|
6704
|
+
} catch {
|
|
6705
|
+
return null;
|
|
6706
|
+
}
|
|
6707
|
+
} else {
|
|
6708
|
+
return null;
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
let entries: Dirent[];
|
|
6713
|
+
try {
|
|
6714
|
+
entries = readdirSync(stepOutputDir, { withFileTypes: true });
|
|
6715
|
+
} catch {
|
|
6716
|
+
return null;
|
|
6717
|
+
}
|
|
6718
|
+
|
|
6719
|
+
const cachedStepNames = new Set(
|
|
6720
|
+
entries
|
|
6721
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
6722
|
+
.map((entry) => entry.name.slice(0, -3))
|
|
6723
|
+
.filter(Boolean)
|
|
6724
|
+
);
|
|
6725
|
+
const workflows = resumeConfig.workflows ?? [];
|
|
6726
|
+
if (workflows.length === 0) return null;
|
|
6727
|
+
|
|
6728
|
+
// Empty cache directory is valid — all steps will be re-run
|
|
6729
|
+
const workflow = this.matchWorkflowFromCache(workflows, cachedStepNames);
|
|
6730
|
+
if (!workflow) return null;
|
|
6731
|
+
|
|
6732
|
+
// Use actual file modification times from cached outputs instead of synthetic timestamps
|
|
6733
|
+
const stepMtimes = new Map<string, string>();
|
|
6734
|
+
let earliestMtime = Date.now();
|
|
6735
|
+
for (const stepName of cachedStepNames) {
|
|
6736
|
+
try {
|
|
6737
|
+
const mdPath = path.join(stepOutputDir, `${stepName}.md`);
|
|
6738
|
+
const reportPath = path.join(stepOutputDir, `${stepName}.report.json`);
|
|
6739
|
+
const mdStat = existsSync(mdPath) ? statSync(mdPath) : null;
|
|
6740
|
+
const reportStat = existsSync(reportPath) ? statSync(reportPath) : null;
|
|
6741
|
+
// Use the latest mtime between .md and .report.json
|
|
6742
|
+
const mtime = Math.max(mdStat?.mtimeMs ?? 0, reportStat?.mtimeMs ?? 0);
|
|
6743
|
+
if (mtime > 0) {
|
|
6744
|
+
stepMtimes.set(stepName, new Date(mtime).toISOString());
|
|
6745
|
+
if (mtime < earliestMtime) earliestMtime = mtime;
|
|
6746
|
+
}
|
|
6747
|
+
} catch {
|
|
6748
|
+
// Fall back to current time if stat fails
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
const fallbackTime = new Date().toISOString();
|
|
6752
|
+
|
|
6753
|
+
const completedSteps = new Set(workflow.steps.filter((step) => cachedStepNames.has(step.name)).map((step) => step.name));
|
|
6754
|
+
// Heuristic: mark the first eligible non-completed step as failed (the likely failure point)
|
|
6755
|
+
const failedStepName = workflow.steps.find(
|
|
6756
|
+
(step) => !completedSteps.has(step.name) && (step.dependsOn ?? []).every((dep) => completedSteps.has(dep))
|
|
6757
|
+
)?.name;
|
|
6758
|
+
|
|
6759
|
+
const runStartedAt = new Date(earliestMtime).toISOString();
|
|
6760
|
+
const run: WorkflowRunRow = {
|
|
6761
|
+
id: runId,
|
|
6762
|
+
workspaceId: this.workspaceId,
|
|
6763
|
+
workflowName: workflow.name,
|
|
6764
|
+
pattern: resumeConfig.swarm.pattern,
|
|
6765
|
+
status: 'failed',
|
|
6766
|
+
config: resumeConfig,
|
|
6767
|
+
startedAt: runStartedAt,
|
|
6768
|
+
createdAt: runStartedAt,
|
|
6769
|
+
updatedAt: fallbackTime,
|
|
6770
|
+
};
|
|
6771
|
+
|
|
6772
|
+
const stepStates = new Map<string, StepState>();
|
|
6773
|
+
for (const step of workflow.steps) {
|
|
6774
|
+
const isNonAgent = step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
|
|
6775
|
+
const cachedOutput = completedSteps.has(step.name) ? this.loadStepOutput(runId, step.name) : undefined;
|
|
6776
|
+
const status: WorkflowStepStatus =
|
|
6777
|
+
completedSteps.has(step.name) ? 'completed' : step.name === failedStepName ? 'failed' : 'pending';
|
|
6778
|
+
|
|
6779
|
+
const stepRow: WorkflowStepRow = {
|
|
6780
|
+
id: this.generateId(),
|
|
6781
|
+
runId,
|
|
6782
|
+
stepName: step.name,
|
|
6783
|
+
agentName: isNonAgent ? null : (step.agent ?? null),
|
|
6784
|
+
stepType: isNonAgent ? (step.type as 'deterministic' | 'worktree' | 'integration') : 'agent',
|
|
6785
|
+
status,
|
|
6786
|
+
task:
|
|
6787
|
+
step.type === 'deterministic'
|
|
6788
|
+
? (step.command ?? '')
|
|
6789
|
+
: step.type === 'worktree'
|
|
6790
|
+
? (step.branch ?? '')
|
|
6791
|
+
: step.type === 'integration'
|
|
6792
|
+
? (`${step.integration}.${step.action}`)
|
|
6793
|
+
: (step.task ?? ''),
|
|
6794
|
+
dependsOn: step.dependsOn ?? [],
|
|
6795
|
+
output: cachedOutput,
|
|
6796
|
+
error: status === 'failed' ? 'Recovered from cached step outputs' : undefined,
|
|
6797
|
+
completedAt: status === 'completed' ? (stepMtimes.get(step.name) ?? fallbackTime) : undefined,
|
|
6798
|
+
retryCount: 0,
|
|
6799
|
+
createdAt: stepMtimes.get(step.name) ?? fallbackTime,
|
|
6800
|
+
updatedAt: stepMtimes.get(step.name) ?? fallbackTime,
|
|
6801
|
+
};
|
|
6802
|
+
stepStates.set(step.name, { row: stepRow });
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6805
|
+
return { run, stepStates };
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6641
6808
|
/** Get or create the worker logs directory (.agent-relay/team/worker-logs) */
|
|
6642
6809
|
private getWorkerLogsDir(): string {
|
|
6643
6810
|
const logsDir = path.join(this.cwd, '.agent-relay', 'team', 'worker-logs');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RelayYamlConfig, AgentDefinition, WorkflowStep } from './types.js';
|
|
2
|
+
import { CodexModels } from '@agent-relay/config';
|
|
2
3
|
|
|
3
4
|
export interface ValidationIssue {
|
|
4
5
|
severity: 'error' | 'warning' | 'info';
|
|
@@ -117,7 +118,24 @@ export function validateWorkflow(config: RelayYamlConfig): ValidationIssue[] {
|
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
// Check 4:
|
|
121
|
+
// Check 4: codex-spark cannot be used in non-interactive mode
|
|
122
|
+
const CODEX_SPARK_MODELS: string[] = [CodexModels.GPT_5_3_CODEX_SPARK];
|
|
123
|
+
if (
|
|
124
|
+
def.interactive === false &&
|
|
125
|
+
def.cli === 'codex' &&
|
|
126
|
+
def.constraints?.model &&
|
|
127
|
+
(CODEX_SPARK_MODELS.includes(def.constraints.model) || /codex-spark/i.test(def.constraints.model))
|
|
128
|
+
) {
|
|
129
|
+
issues.push({
|
|
130
|
+
severity: 'error',
|
|
131
|
+
code: 'CODEX_SPARK_NON_INTERACTIVE',
|
|
132
|
+
message: `Agent "${step.agent}" uses codex-spark model in non-interactive mode. Codex Spark does not support non-interactive (headless) execution.`,
|
|
133
|
+
fix: `Switch to a different model (e.g. gpt-5.3-codex) or set the agent to interactive mode.`,
|
|
134
|
+
location: `step:${step.name}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check 5: non-interactive agent that references relay messaging tools in task
|
|
121
139
|
if (
|
|
122
140
|
def.interactive === false &&
|
|
123
141
|
(task.includes('mcp__relaycast__message_dm_send') || task.includes('mcp__relaycast__message_post') || task.includes('mcp__relaycast__message_inbox_check'))
|
|
@@ -132,7 +150,7 @@ export function validateWorkflow(config: RelayYamlConfig): ValidationIssue[] {
|
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
// Check
|
|
153
|
+
// Check 6: maxConcurrency vs interactive agent count
|
|
136
154
|
const interactiveSteps = (workflow.steps ?? []).filter((s) => {
|
|
137
155
|
if (s.type === 'deterministic') return false;
|
|
138
156
|
const def = agentMap.get(s.agent ?? '');
|
|
@@ -13,13 +13,9 @@ except ImportError:
|
|
|
13
13
|
from .models import Models
|
|
14
14
|
from .client import AgentRelayClient, AgentRelayProtocolError, AgentRelayProcessError
|
|
15
15
|
from .protocol import (
|
|
16
|
-
PROTOCOL_VERSION,
|
|
17
16
|
AgentRuntime,
|
|
18
|
-
AgentSpec,
|
|
19
17
|
BrokerEvent,
|
|
20
18
|
MessageInjectionMode,
|
|
21
|
-
ProtocolEnvelope,
|
|
22
|
-
RestartPolicy as ProtocolRestartPolicy,
|
|
23
19
|
)
|
|
24
20
|
|
|
25
21
|
# ── Secondary API: Workflow builder (backward compatibility) ──────────────────
|
|
@@ -89,13 +85,9 @@ __all__ = [
|
|
|
89
85
|
"AgentRelayClient",
|
|
90
86
|
"AgentRelayProtocolError",
|
|
91
87
|
"AgentRelayProcessError",
|
|
92
|
-
"PROTOCOL_VERSION",
|
|
93
88
|
"AgentRuntime",
|
|
94
|
-
"AgentSpec",
|
|
95
89
|
"BrokerEvent",
|
|
96
90
|
"MessageInjectionMode",
|
|
97
|
-
"ProtocolEnvelope",
|
|
98
|
-
"ProtocolRestartPolicy",
|
|
99
91
|
# Workflow builder (backward compat)
|
|
100
92
|
"workflow",
|
|
101
93
|
"WorkflowBuilder",
|