agent-relay 3.1.15 → 3.1.16

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.
Files changed (45) hide show
  1. package/dist/index.cjs +125 -10
  2. package/dist/src/cli/commands/core.d.ts +2 -0
  3. package/dist/src/cli/commands/core.d.ts.map +1 -1
  4. package/dist/src/cli/commands/core.js +1 -0
  5. package/dist/src/cli/commands/core.js.map +1 -1
  6. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  7. package/dist/src/cli/lib/broker-lifecycle.js +19 -2
  8. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  9. package/package.json +8 -8
  10. package/packages/acp-bridge/package.json +2 -2
  11. package/packages/config/package.json +1 -1
  12. package/packages/hooks/package.json +4 -4
  13. package/packages/memory/package.json +2 -2
  14. package/packages/openclaw/README.md +4 -7
  15. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  16. package/packages/openclaw/dist/setup.js +10 -3
  17. package/packages/openclaw/dist/setup.js.map +1 -1
  18. package/packages/openclaw/package.json +2 -2
  19. package/packages/openclaw/skill/SKILL.md +27 -5
  20. package/packages/openclaw/src/setup.ts +10 -3
  21. package/packages/policy/package.json +2 -2
  22. package/packages/sdk/dist/client.d.ts +1 -0
  23. package/packages/sdk/dist/client.d.ts.map +1 -1
  24. package/packages/sdk/dist/client.js +3 -0
  25. package/packages/sdk/dist/client.js.map +1 -1
  26. package/packages/sdk/dist/workflows/runner.d.ts +20 -0
  27. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  28. package/packages/sdk/dist/workflows/runner.js +130 -11
  29. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  30. package/packages/sdk/dist/workflows/types.d.ts +26 -0
  31. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  32. package/packages/sdk/dist/workflows/types.js.map +1 -1
  33. package/packages/sdk/package.json +2 -2
  34. package/packages/sdk/src/client.ts +4 -0
  35. package/packages/sdk/src/workflows/runner.ts +158 -11
  36. package/packages/sdk/src/workflows/schema.json +219 -40
  37. package/packages/sdk/src/workflows/types.ts +29 -0
  38. package/packages/sdk-py/pyproject.toml +1 -1
  39. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  40. package/packages/sdk-py/src/agent_relay/types.py +33 -0
  41. package/packages/telemetry/package.json +1 -1
  42. package/packages/trajectory/package.json +2 -2
  43. package/packages/user-directory/package.json +2 -2
  44. package/packages/utils/package.json +2 -2
  45. package/bin/agent-relay-broker-linux-arm64 +0 -0
@@ -86,6 +86,8 @@ export class WorkflowRunner {
86
86
  lastIdleLog = new Map();
87
87
  /** Tracks last logged activity type per agent to avoid duplicate status lines. */
88
88
  lastActivity = new Map();
89
+ /** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
90
+ resolvedPaths = new Map();
89
91
  constructor(options = {}) {
90
92
  this.db = options.db ?? new InMemoryWorkflowDb();
91
93
  this.workspaceId = options.workspaceId ?? 'local';
@@ -95,6 +97,76 @@ export class WorkflowRunner {
95
97
  this.workersPath = path.join(this.cwd, '.agent-relay', 'team', 'workers.json');
96
98
  this.executor = options.executor;
97
99
  }
100
+ // ── Path resolution ─────────────────────────────────────────────────────
101
+ /** Expand environment variables like $HOME or $VAR in a path string. */
102
+ static resolveEnvVars(p) {
103
+ return p.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
104
+ return process.env[varName] ?? _match;
105
+ });
106
+ }
107
+ /**
108
+ * Resolve and validate the top-level `paths` definitions from the config.
109
+ * Returns a map of name → absolute directory path.
110
+ * Throws if a required path does not exist.
111
+ */
112
+ resolvePathDefinitions(pathDefs, baseCwd) {
113
+ const resolved = new Map();
114
+ const errors = [];
115
+ const warnings = [];
116
+ if (!pathDefs || pathDefs.length === 0)
117
+ return { resolved, errors, warnings };
118
+ const seenNames = new Set();
119
+ for (const pd of pathDefs) {
120
+ if (seenNames.has(pd.name)) {
121
+ errors.push(`Duplicate path name "${pd.name}"`);
122
+ continue;
123
+ }
124
+ seenNames.add(pd.name);
125
+ const expanded = WorkflowRunner.resolveEnvVars(pd.path);
126
+ const abs = path.resolve(baseCwd, expanded);
127
+ resolved.set(pd.name, abs);
128
+ const isRequired = pd.required !== false; // default true
129
+ if (!existsSync(abs)) {
130
+ if (isRequired) {
131
+ errors.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (required)`);
132
+ }
133
+ else {
134
+ warnings.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (optional)`);
135
+ }
136
+ }
137
+ }
138
+ return { resolved, errors, warnings };
139
+ }
140
+ /**
141
+ * Resolve an agent's effective working directory, considering `workdir` (named path reference)
142
+ * and `cwd` (explicit path). `workdir` takes precedence when both are set.
143
+ */
144
+ resolveAgentCwd(agent) {
145
+ if (agent.workdir) {
146
+ const resolved = this.resolvedPaths.get(agent.workdir);
147
+ if (!resolved) {
148
+ throw new Error(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
149
+ }
150
+ return resolved;
151
+ }
152
+ if (agent.cwd) {
153
+ return path.resolve(this.cwd, agent.cwd);
154
+ }
155
+ return this.cwd;
156
+ }
157
+ /**
158
+ * Resolve a step's working directory from its `workdir` field (named path reference).
159
+ * Returns undefined if no workdir is set.
160
+ */
161
+ resolveStepWorkdir(step) {
162
+ if (!step.workdir)
163
+ return undefined;
164
+ const resolved = this.resolvedPaths.get(step.workdir);
165
+ if (!resolved) {
166
+ throw new Error(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
167
+ }
168
+ return resolved;
169
+ }
98
170
  // ── Progress logging ────────────────────────────────────────────────────
99
171
  /** Log a progress message with elapsed time since run start. */
100
172
  log(msg) {
@@ -346,6 +418,17 @@ export class WorkflowRunner {
346
418
  estimatedWaves: 0,
347
419
  };
348
420
  }
421
+ // 1b. Resolve and validate named paths
422
+ const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
423
+ errors.push(...pathResult.errors);
424
+ warnings.push(...pathResult.warnings);
425
+ const dryRunPaths = pathResult.resolved;
426
+ // Validate workdir references on agents
427
+ for (const agent of resolved.agents) {
428
+ if (agent.workdir && !dryRunPaths.has(agent.workdir)) {
429
+ errors.push(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
430
+ }
431
+ }
349
432
  // 2. Find target workflow
350
433
  const workflows = resolved.workflows ?? [];
351
434
  const workflow = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
@@ -411,6 +494,12 @@ export class WorkflowRunner {
411
494
  stepAgentCounts.set(step.agent, (stepAgentCounts.get(step.agent) ?? 0) + 1);
412
495
  }
413
496
  }
497
+ // Validate workdir references on steps
498
+ for (const step of resolvedSteps) {
499
+ if (step.workdir && !dryRunPaths.has(step.workdir)) {
500
+ errors.push(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
501
+ }
502
+ }
414
503
  // Validate cwd paths
415
504
  for (const agent of resolved.agents) {
416
505
  if (agent.cwd) {
@@ -499,7 +588,7 @@ export class WorkflowRunner {
499
588
  name: a.name,
500
589
  cli: a.cli,
501
590
  role: a.role,
502
- cwd: a.cwd,
591
+ cwd: a.workdir ? dryRunPaths.get(a.workdir) : a.cwd,
503
592
  stepCount: stepAgentCounts.get(a.name) ?? 0,
504
593
  }));
505
594
  // 5. Simulate execution waves
@@ -775,6 +864,17 @@ export class WorkflowRunner {
775
864
  /** Execute a named workflow from a validated config. */
776
865
  async execute(config, workflowName, vars) {
777
866
  const resolved = vars ? this.resolveVariables(config, vars) : config;
867
+ // Resolve and validate named paths from the top-level `paths` config
868
+ const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
869
+ if (pathResult.errors.length > 0) {
870
+ throw new Error(`Path validation failed:\n ${pathResult.errors.join('\n ')}`);
871
+ }
872
+ this.resolvedPaths = pathResult.resolved;
873
+ if (this.resolvedPaths.size > 0) {
874
+ for (const [name, abs] of this.resolvedPaths) {
875
+ console.log(`[workflow] path "${name}" → ${abs}`);
876
+ }
877
+ }
778
878
  const workflows = resolved.workflows ?? [];
779
879
  const workflow = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
780
880
  if (!workflow) {
@@ -841,6 +941,12 @@ export class WorkflowRunner {
841
941
  throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
842
942
  }
843
943
  const config = vars ? this.resolveVariables(run.config, vars) : run.config;
944
+ // Resolve path definitions (same as execute()) so workdir lookups work on resume
945
+ const pathResult = this.resolvePathDefinitions(config.paths, this.cwd);
946
+ if (pathResult.errors.length > 0) {
947
+ throw new Error(`Path validation failed:\n ${pathResult.errors.join('\n ')}`);
948
+ }
949
+ this.resolvedPaths = pathResult.resolved;
844
950
  const workflows = config.workflows ?? [];
845
951
  const workflow = workflows.find((w) => w.name === run.workflowName);
846
952
  if (!workflow) {
@@ -1386,10 +1492,12 @@ export class WorkflowRunner {
1386
1492
  const value = this.resolveDotPath(key, stepOutputContext);
1387
1493
  return value !== undefined ? String(value) : _match;
1388
1494
  });
1495
+ // Resolve step workdir (named path reference) for deterministic steps
1496
+ const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
1389
1497
  try {
1390
1498
  // Delegate to executor if present
1391
1499
  if (this.executor?.executeDeterministicStep) {
1392
- const result = await this.executor.executeDeterministicStep(step, resolvedCommand, this.cwd);
1500
+ const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
1393
1501
  const failOnError = step.failOnError !== false;
1394
1502
  if (failOnError && result.exitCode !== 0) {
1395
1503
  throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
@@ -1412,7 +1520,7 @@ export class WorkflowRunner {
1412
1520
  const output = await new Promise((resolve, reject) => {
1413
1521
  const child = cpSpawn('sh', ['-c', resolvedCommand], {
1414
1522
  stdio: 'pipe',
1415
- cwd: this.cwd,
1523
+ cwd: stepCwd,
1416
1524
  env: { ...process.env },
1417
1525
  });
1418
1526
  const stdoutChunks = [];
@@ -1527,6 +1635,8 @@ export class WorkflowRunner {
1527
1635
  ? this.interpolateStepTask(step.path, stepOutputContext)
1528
1636
  : path.join('.worktrees', step.name);
1529
1637
  const createBranch = step.createBranch !== false;
1638
+ // Resolve workdir for worktree steps (same as deterministic/agent steps)
1639
+ const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
1530
1640
  if (!branch) {
1531
1641
  const errorMsg = 'Worktree step missing required "branch" field';
1532
1642
  await this.markStepFailed(state, errorMsg, runId);
@@ -1535,14 +1645,14 @@ export class WorkflowRunner {
1535
1645
  try {
1536
1646
  // Build the git worktree command
1537
1647
  // If createBranch is true and branch doesn't exist, use -b flag
1538
- const absoluteWorktreePath = path.resolve(this.cwd, worktreePath);
1648
+ const absoluteWorktreePath = path.resolve(stepCwd, worktreePath);
1539
1649
  // First, check if the branch already exists
1540
1650
  const checkBranchCmd = `git rev-parse --verify --quiet ${branch} 2>/dev/null`;
1541
1651
  let branchExists = false;
1542
1652
  await new Promise((resolve) => {
1543
1653
  const checkChild = cpSpawn('sh', ['-c', checkBranchCmd], {
1544
1654
  stdio: 'pipe',
1545
- cwd: this.cwd,
1655
+ cwd: stepCwd,
1546
1656
  env: { ...process.env },
1547
1657
  });
1548
1658
  checkChild.on('close', (code) => {
@@ -1570,7 +1680,7 @@ export class WorkflowRunner {
1570
1680
  const output = await new Promise((resolve, reject) => {
1571
1681
  const child = cpSpawn('sh', ['-c', worktreeCmd], {
1572
1682
  stdio: 'pipe',
1573
- cwd: this.cwd,
1683
+ cwd: stepCwd,
1574
1684
  env: { ...process.env },
1575
1685
  });
1576
1686
  const stdoutChunks = [];
@@ -1710,12 +1820,20 @@ export class WorkflowRunner {
1710
1820
  resolvedTask += nonInteractiveInfo;
1711
1821
  }
1712
1822
  }
1823
+ // Apply step-level workdir override to agent definition if present
1824
+ let effectiveAgentDef = agentDef;
1825
+ if (step.workdir) {
1826
+ const stepWorkdir = this.resolveStepWorkdir(step);
1827
+ if (stepWorkdir) {
1828
+ effectiveAgentDef = { ...agentDef, cwd: stepWorkdir, workdir: undefined };
1829
+ }
1830
+ }
1713
1831
  // Spawn agent via AgentRelay
1714
- this.log(`[${step.name}] Spawning agent "${agentDef.name}" (cli: ${agentDef.cli})`);
1832
+ this.log(`[${step.name}] Spawning agent "${effectiveAgentDef.name}" (cli: ${effectiveAgentDef.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ''}`);
1715
1833
  const resolvedStep = { ...step, task: resolvedTask };
1716
1834
  const output = this.executor
1717
- ? await this.executor.executeAgentStep(resolvedStep, agentDef, resolvedTask, timeoutMs)
1718
- : await this.spawnAndWait(agentDef, resolvedStep, timeoutMs);
1835
+ ? await this.executor.executeAgentStep(resolvedStep, effectiveAgentDef, resolvedTask, timeoutMs)
1836
+ : await this.spawnAndWait(effectiveAgentDef, resolvedStep, timeoutMs);
1719
1837
  this.log(`[${step.name}] Agent "${agentDef.name}" exited`);
1720
1838
  // Run verification if configured
1721
1839
  if (step.verification) {
@@ -1882,7 +2000,7 @@ export class WorkflowRunner {
1882
2000
  const output = await new Promise((resolve, reject) => {
1883
2001
  const child = cpSpawn(cmd, args, {
1884
2002
  stdio: ['ignore', 'pipe', 'pipe'],
1885
- cwd: agentDef.cwd ? path.resolve(this.cwd, agentDef.cwd) : this.cwd,
2003
+ cwd: this.resolveAgentCwd(agentDef),
1886
2004
  env: this.getRelayEnv() ?? { ...process.env },
1887
2005
  });
1888
2006
  // Update workers.json with PID now that we have it
@@ -2028,6 +2146,7 @@ export class WorkflowRunner {
2028
2146
  let stopHeartbeat;
2029
2147
  let ptyChunks = [];
2030
2148
  try {
2149
+ const agentCwd = this.resolveAgentCwd(agentDef);
2031
2150
  agent = await this.relay.spawnPty({
2032
2151
  name: agentName,
2033
2152
  cli: agentDef.cli,
@@ -2036,7 +2155,7 @@ export class WorkflowRunner {
2036
2155
  channels: agentChannels,
2037
2156
  task: taskWithExit,
2038
2157
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
2039
- cwd: agentDef.cwd ? path.resolve(this.cwd, agentDef.cwd) : undefined,
2158
+ cwd: agentCwd !== this.cwd ? agentCwd : undefined,
2040
2159
  });
2041
2160
  // Re-key PTY maps if broker assigned a different name than requested
2042
2161
  if (agent.name !== agentName) {