agent-relay 3.2.15 → 3.2.17

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 (140) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +3865 -17179
  6. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  7. package/dist/src/cli/commands/setup.js +2 -0
  8. package/dist/src/cli/commands/setup.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/package.json +2 -2
  15. package/packages/policy/package.json +2 -2
  16. package/packages/sdk/dist/broker-path.d.ts +19 -0
  17. package/packages/sdk/dist/broker-path.d.ts.map +1 -0
  18. package/packages/sdk/dist/broker-path.js +71 -0
  19. package/packages/sdk/dist/broker-path.js.map +1 -0
  20. package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
  21. package/packages/sdk/dist/cli-registry.js +4 -0
  22. package/packages/sdk/dist/cli-registry.js.map +1 -1
  23. package/packages/sdk/dist/client.d.ts +6 -1
  24. package/packages/sdk/dist/client.d.ts.map +1 -1
  25. package/packages/sdk/dist/client.js +18 -0
  26. package/packages/sdk/dist/client.js.map +1 -1
  27. package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
  28. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
  29. package/packages/sdk/dist/communicate/adapters/index.js +0 -5
  30. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
  31. package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
  32. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
  33. package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
  34. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
  35. package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
  36. package/packages/sdk/dist/communicate/core.js +2 -3
  37. package/packages/sdk/dist/communicate/core.js.map +1 -1
  38. package/packages/sdk/dist/communicate/index.d.ts +17 -1
  39. package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
  40. package/packages/sdk/dist/communicate/index.js +40 -1
  41. package/packages/sdk/dist/communicate/index.js.map +1 -1
  42. package/packages/sdk/dist/communicate/transport.d.ts +0 -1
  43. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
  44. package/packages/sdk/dist/communicate/transport.js +42 -134
  45. package/packages/sdk/dist/communicate/transport.js.map +1 -1
  46. package/packages/sdk/dist/http.d.ts +38 -0
  47. package/packages/sdk/dist/http.d.ts.map +1 -0
  48. package/packages/sdk/dist/http.js +60 -0
  49. package/packages/sdk/dist/http.js.map +1 -0
  50. package/packages/sdk/dist/protocol.d.ts +25 -0
  51. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  52. package/packages/sdk/dist/relay.d.ts +26 -3
  53. package/packages/sdk/dist/relay.d.ts.map +1 -1
  54. package/packages/sdk/dist/relay.js +62 -4
  55. package/packages/sdk/dist/relay.js.map +1 -1
  56. package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
  57. package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
  58. package/packages/sdk/dist/workflows/api-executor.js +94 -0
  59. package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
  60. package/packages/sdk/dist/workflows/builder.d.ts +14 -0
  61. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  62. package/packages/sdk/dist/workflows/builder.js +26 -0
  63. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  64. package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
  65. package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
  66. package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
  67. package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
  68. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  69. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  70. package/packages/sdk/dist/workflows/index.js +1 -0
  71. package/packages/sdk/dist/workflows/index.js.map +1 -1
  72. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  73. package/packages/sdk/dist/workflows/run.js +4 -0
  74. package/packages/sdk/dist/workflows/run.js.map +1 -1
  75. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  76. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  77. package/packages/sdk/dist/workflows/runner.js +169 -28
  78. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  79. package/packages/sdk/dist/workflows/types.d.ts +13 -3
  80. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  81. package/packages/sdk/dist/workflows/types.js +5 -1
  82. package/packages/sdk/dist/workflows/types.js.map +1 -1
  83. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  84. package/packages/sdk/dist/workflows/validator.js +12 -0
  85. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  86. package/packages/sdk/package.json +13 -3
  87. package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
  88. package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
  89. package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
  90. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
  91. package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
  92. package/packages/sdk/src/broker-path.ts +74 -0
  93. package/packages/sdk/src/cli-registry.ts +4 -0
  94. package/packages/sdk/src/client.ts +28 -0
  95. package/packages/sdk/src/communicate/adapters/index.ts +0 -5
  96. package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
  97. package/packages/sdk/src/communicate/core.ts +6 -10
  98. package/packages/sdk/src/communicate/index.ts +57 -1
  99. package/packages/sdk/src/communicate/transport.ts +46 -177
  100. package/packages/sdk/src/http.ts +96 -0
  101. package/packages/sdk/src/protocol.ts +24 -0
  102. package/packages/sdk/src/relay.ts +93 -8
  103. package/packages/sdk/src/workflows/README.md +5 -2
  104. package/packages/sdk/src/workflows/api-executor.ts +108 -0
  105. package/packages/sdk/src/workflows/builder.ts +40 -0
  106. package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
  107. package/packages/sdk/src/workflows/index.ts +2 -0
  108. package/packages/sdk/src/workflows/run.ts +5 -0
  109. package/packages/sdk/src/workflows/runner.ts +197 -30
  110. package/packages/sdk/src/workflows/types.ts +19 -4
  111. package/packages/sdk/src/workflows/validator.ts +15 -0
  112. package/packages/sdk-py/README.md +7 -0
  113. package/packages/sdk-py/pyproject.toml +1 -1
  114. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  115. package/packages/sdk-py/src/agent_relay/builder.py +64 -7
  116. package/packages/sdk-py/src/agent_relay/client.py +4 -0
  117. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
  118. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
  119. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
  120. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
  121. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
  122. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
  123. package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
  124. package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
  125. package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
  126. package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
  127. package/packages/sdk-py/src/agent_relay/relay.py +9 -1
  128. package/packages/sdk-py/src/agent_relay/types.py +1 -0
  129. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
  130. package/packages/sdk-py/tests/communicate/conftest.py +86 -233
  131. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
  132. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
  133. package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
  134. package/packages/sdk-py/tests/test_builder.py +58 -0
  135. package/packages/sdk-py/tests/test_dry_run.py +215 -0
  136. package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
  137. package/packages/telemetry/package.json +1 -1
  138. package/packages/trajectory/package.json +2 -2
  139. package/packages/user-directory/package.json +2 -2
  140. package/packages/utils/package.json +2 -2
@@ -36,6 +36,7 @@ import {
36
36
  CustomStepResolutionError,
37
37
  } from './custom-steps.js';
38
38
  import { collectCliSession, type CliSessionReport } from './cli-session-collector.js';
39
+ import { executeApiStep } from './api-executor.js';
39
40
  import { InMemoryWorkflowDb } from './memory-db.js';
40
41
  import { formatRunSummaryTable } from './run-summary-table.js';
41
42
  import type {
@@ -180,6 +181,7 @@ export interface WorkflowRunnerOptions {
180
181
  cwd?: string;
181
182
  summaryDir?: string;
182
183
  executor?: StepExecutor;
184
+ envSecrets?: Record<string, string>;
183
185
  }
184
186
 
185
187
  // ── Step executor interface ──────────────────────────────────────────────────
@@ -202,6 +204,12 @@ export interface StepExecutor {
202
204
  resolvedCommand: string,
203
205
  cwd: string
204
206
  ): Promise<{ output: string; exitCode: number }>;
207
+
208
+ executeIntegrationStep?(
209
+ step: WorkflowStep,
210
+ resolvedParams: Record<string, string>,
211
+ context: { workspaceId?: string }
212
+ ): Promise<{ output: string; success: boolean }>;
205
213
  }
206
214
 
207
215
  // ── Variable context for template resolution ────────────────────────────────
@@ -297,6 +305,7 @@ export class WorkflowRunner {
297
305
  private readonly cwd: string;
298
306
  private readonly summaryDir: string;
299
307
  private readonly executor?: StepExecutor;
308
+ private readonly envSecrets?: Record<string, string>;
300
309
 
301
310
  /** @internal exposed for CLI signal-handler shutdown only */
302
311
  relay?: AgentRelay;
@@ -364,6 +373,7 @@ export class WorkflowRunner {
364
373
  this.summaryDir = options.summaryDir ?? path.join(this.cwd, '.relay', 'summaries');
365
374
  this.workersPath = path.join(this.cwd, '.agent-relay', 'team', 'workers.json');
366
375
  this.executor = options.executor;
376
+ this.envSecrets = options.envSecrets;
367
377
  }
368
378
 
369
379
  // ── Path resolution ─────────────────────────────────────────────────────
@@ -1502,7 +1512,7 @@ export class WorkflowRunner {
1502
1512
  // 6. Resource estimation
1503
1513
  const peakConcurrency = Math.max(...waves.map((w) => w.steps.length), 0);
1504
1514
  const totalAgentSteps = resolvedSteps.filter(
1505
- (s) => s.type !== 'deterministic' && s.type !== 'worktree'
1515
+ (s) => s.type !== 'deterministic' && s.type !== 'worktree' && s.type !== 'integration'
1506
1516
  ).length;
1507
1517
 
1508
1518
  // 7. Check maxConcurrency against wave widths
@@ -1559,6 +1569,14 @@ export class WorkflowRunner {
1559
1569
  if (typeof s.command !== 'string') {
1560
1570
  throw new Error(`${source}: deterministic step "${s.name}" must have a "command" field`);
1561
1571
  }
1572
+ } else if (s.type === 'integration') {
1573
+ // Integration steps require integration and action
1574
+ if (typeof s.integration !== 'string') {
1575
+ throw new Error(`${source}: integration step "${s.name}" must have an "integration" string field`);
1576
+ }
1577
+ if (typeof s.action !== 'string') {
1578
+ throw new Error(`${source}: integration step "${s.name}" must have an "action" string field`);
1579
+ }
1562
1580
  } else {
1563
1581
  // Agent steps (type undefined or 'agent') require agent and task
1564
1582
  if (typeof s.agent !== 'string' || typeof s.task !== 'string') {
@@ -1583,7 +1601,7 @@ export class WorkflowRunner {
1583
1601
 
1584
1602
  // Warn if non-interactive agent task is excessively large before interpolation
1585
1603
  for (const step of w.steps as WorkflowStep[]) {
1586
- if (step.type === 'deterministic' || step.type === 'worktree') continue;
1604
+ if (step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration') continue;
1587
1605
  const agentDef = agents.find((a) => a.name === step.agent);
1588
1606
  const isNonInteractive =
1589
1607
  agentDef?.interactive === false || ['worker', 'reviewer', 'analyst'].includes(agentDef?.preset ?? '');
@@ -1642,7 +1660,7 @@ export class WorkflowRunner {
1642
1660
 
1643
1661
  for (const step of steps) {
1644
1662
  // Only check interactive agent steps (leads)
1645
- if (step.type === 'deterministic' || step.type === 'worktree') continue;
1663
+ if (step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration') continue;
1646
1664
  const agentDef = agents.find((a) => a.name === step.agent);
1647
1665
  // Skip non-interactive agents — they can't wait for channel signals
1648
1666
  if (
@@ -1697,6 +1715,15 @@ export class WorkflowRunner {
1697
1715
  if (step.command) {
1698
1716
  step.command = this.interpolate(step.command, vars);
1699
1717
  }
1718
+ // Resolve variables in integration step params
1719
+ if (step.params && typeof step.params === 'object') {
1720
+ for (const key of Object.keys(step.params)) {
1721
+ const val = (step.params as Record<string, unknown>)[key];
1722
+ if (typeof val === 'string') {
1723
+ (step.params as Record<string, string>)[key] = this.interpolate(val, vars);
1724
+ }
1725
+ }
1726
+ }
1700
1727
  }
1701
1728
  }
1702
1729
  }
@@ -1841,22 +1868,24 @@ export class WorkflowRunner {
1841
1868
  // Build step rows
1842
1869
  const stepStates = new Map<string, StepState>();
1843
1870
  for (const step of resolvedWorkflow.steps) {
1844
- // Handle agent, deterministic, and worktree steps
1845
- const isNonAgent = step.type === 'deterministic' || step.type === 'worktree';
1871
+ // Handle agent, deterministic, worktree, and integration steps
1872
+ const isNonAgent = step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
1846
1873
 
1847
1874
  const stepRow: WorkflowStepRow = {
1848
1875
  id: this.generateId(),
1849
1876
  runId,
1850
1877
  stepName: step.name,
1851
1878
  agentName: isNonAgent ? null : (step.agent ?? null),
1852
- stepType: isNonAgent ? (step.type as 'deterministic' | 'worktree') : 'agent',
1879
+ stepType: isNonAgent ? (step.type as 'deterministic' | 'worktree' | 'integration') : 'agent',
1853
1880
  status: 'pending',
1854
1881
  task:
1855
1882
  step.type === 'deterministic'
1856
1883
  ? (step.command ?? '')
1857
1884
  : step.type === 'worktree'
1858
1885
  ? (step.branch ?? '')
1859
- : (step.task ?? ''),
1886
+ : step.type === 'integration'
1887
+ ? (`${step.integration}.${step.action}`)
1888
+ : (step.task ?? ''),
1860
1889
  dependsOn: step.dependsOn ?? [],
1861
1890
  retryCount: 0,
1862
1891
  createdAt: now,
@@ -2041,7 +2070,7 @@ export class WorkflowRunner {
2041
2070
  this.relayOptions.env?.AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST === '1';
2042
2071
  const requiresBroker =
2043
2072
  !this.executor &&
2044
- workflow.steps.some((step) => step.type !== 'deterministic' && step.type !== 'worktree');
2073
+ workflow.steps.some((step) => step.type !== 'deterministic' && step.type !== 'worktree' && step.type !== 'integration');
2045
2074
  // Skip broker/relay init when an external executor handles agent spawning
2046
2075
  if (requiresBroker) {
2047
2076
  if (!relaycastDisabled) {
@@ -2236,8 +2265,14 @@ export class WorkflowRunner {
2236
2265
  this.relaycast = undefined;
2237
2266
  this.relaycastAgent = undefined;
2238
2267
 
2239
- // Wire broker stderr to console for observability
2268
+ // Wire broker stderr to console for observability — skip empty and
2269
+ // JSON event lines (already surfaced via the broker:event emitter).
2240
2270
  this.unsubBrokerStderr = this.relay.onBrokerStderr((line: string) => {
2271
+ const trimmed = line.trim();
2272
+ if (!trimmed) return;
2273
+ // JSON event lines from the Rust EventEmitter are already parsed
2274
+ // and emitted as broker:event — no need to double-log them.
2275
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) return;
2241
2276
  console.log(`${chalk.dim.yellow('[broker]')} ${line}`);
2242
2277
  });
2243
2278
 
@@ -2684,6 +2719,11 @@ export class WorkflowRunner {
2684
2719
  return step.type === 'worktree';
2685
2720
  }
2686
2721
 
2722
+ /** Check if a step is an integration (external service) step. */
2723
+ private isIntegrationStep(step: WorkflowStep): boolean {
2724
+ return step.type === 'integration';
2725
+ }
2726
+
2687
2727
  private async executeStep(
2688
2728
  step: WorkflowStep,
2689
2729
  stepStates: Map<string, StepState>,
@@ -2701,6 +2741,11 @@ export class WorkflowRunner {
2701
2741
  return this.executeWorktreeStep(step, stepStates, runId);
2702
2742
  }
2703
2743
 
2744
+ // Branch: integration steps interact with external services
2745
+ if (this.isIntegrationStep(step)) {
2746
+ return this.executeIntegrationStep(step, stepStates, runId);
2747
+ }
2748
+
2704
2749
  // Agent step execution
2705
2750
  return this.executeAgentStep(step, stepStates, agentMap, errorHandling, runId);
2706
2751
  }
@@ -3194,6 +3239,76 @@ export class WorkflowRunner {
3194
3239
  }
3195
3240
  }
3196
3241
 
3242
+ /**
3243
+ * Execute an integration step (external service interaction via executor).
3244
+ */
3245
+ private async executeIntegrationStep(
3246
+ step: WorkflowStep,
3247
+ stepStates: Map<string, StepState>,
3248
+ runId: string
3249
+ ): Promise<void> {
3250
+ const state = stepStates.get(step.name);
3251
+ if (!state) throw new Error(`Step state not found: ${step.name}`);
3252
+
3253
+ this.checkAborted();
3254
+
3255
+ // Mark step as running
3256
+ state.row.status = 'running';
3257
+ state.row.error = undefined;
3258
+ state.row.completionReason = undefined;
3259
+ state.row.startedAt = new Date().toISOString();
3260
+ await this.db.updateStep(state.row.id, {
3261
+ status: 'running',
3262
+ error: undefined,
3263
+ completionReason: undefined,
3264
+ startedAt: state.row.startedAt,
3265
+ updatedAt: new Date().toISOString(),
3266
+ });
3267
+ this.emit({ type: 'step:started', runId, stepName: step.name });
3268
+ this.postToChannel(`**[${step.name}]** Started (integration: ${step.integration}.${step.action})`);
3269
+
3270
+ // Resolve {{steps.X.output}} in params
3271
+ const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
3272
+ const resolvedParams: Record<string, string> = {};
3273
+ for (const [key, value] of Object.entries(step.params ?? {})) {
3274
+ resolvedParams[key] = this.interpolateStepTask(value, stepOutputContext);
3275
+ }
3276
+
3277
+ try {
3278
+ if (!this.executor?.executeIntegrationStep) {
3279
+ throw new Error(
3280
+ `Integration steps require a cloud executor. Step "${step.name}" cannot run locally. ` +
3281
+ `Use "cloud run" to execute workflows with integration steps.`
3282
+ );
3283
+ }
3284
+
3285
+ const result = await this.executor.executeIntegrationStep(step, resolvedParams, { workspaceId: this.workspaceId });
3286
+
3287
+ if (!result.success) {
3288
+ throw new Error(`Integration step "${step.name}" failed: ${result.output}`);
3289
+ }
3290
+
3291
+ // Mark completed
3292
+ state.row.status = 'completed';
3293
+ state.row.output = result.output;
3294
+ state.row.completedAt = new Date().toISOString();
3295
+ await this.db.updateStep(state.row.id, {
3296
+ status: 'completed',
3297
+ output: result.output,
3298
+ completedAt: state.row.completedAt,
3299
+ updatedAt: new Date().toISOString(),
3300
+ });
3301
+ await this.persistStepOutput(runId, step.name, result.output);
3302
+ this.emit({ type: 'step:completed', runId, stepName: step.name, output: result.output });
3303
+ this.postToChannel(`**[${step.name}]** Completed (integration: ${step.integration}.${step.action})`);
3304
+ } catch (err) {
3305
+ const errorMsg = err instanceof Error ? err.message : String(err);
3306
+ this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
3307
+ await this.markStepFailed(state, errorMsg, runId);
3308
+ throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
3309
+ }
3310
+ }
3311
+
3197
3312
  /**
3198
3313
  * Execute an agent step (LLM-powered).
3199
3314
  */
@@ -3216,6 +3331,58 @@ export class WorkflowRunner {
3216
3331
  throw new Error(`Agent "${agentName}" not found in config`);
3217
3332
  }
3218
3333
  const specialistDef = WorkflowRunner.resolveAgentDef(rawAgentDef);
3334
+
3335
+ // API-mode agents: execute via direct API call instead of spawning a PTY/subprocess.
3336
+ if (specialistDef.cli === 'api') {
3337
+ const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
3338
+ const resolvedTask = this.interpolateStepTask(step.task ?? '', stepOutputContext);
3339
+
3340
+ state.row.status = 'running';
3341
+ state.row.startedAt = new Date().toISOString();
3342
+ await this.db.updateStep(state.row.id, {
3343
+ status: 'running',
3344
+ startedAt: state.row.startedAt,
3345
+ updatedAt: new Date().toISOString(),
3346
+ });
3347
+ this.emit({ type: 'step:started', runId, stepName: step.name });
3348
+ this.postToChannel(`**[${step.name}]** Started (api)`);
3349
+
3350
+ try {
3351
+ const output = await executeApiStep(
3352
+ specialistDef.constraints?.model ?? 'claude-sonnet-4-20250514',
3353
+ resolvedTask,
3354
+ { envSecrets: this.envSecrets, skills: specialistDef.skills, defaultMaxTokens: specialistDef.constraints?.maxTokens },
3355
+ );
3356
+
3357
+ state.row.status = 'completed';
3358
+ state.row.output = output;
3359
+ state.row.completedAt = new Date().toISOString();
3360
+ await this.db.updateStep(state.row.id, {
3361
+ status: 'completed',
3362
+ output,
3363
+ completedAt: state.row.completedAt,
3364
+ updatedAt: new Date().toISOString(),
3365
+ });
3366
+ await this.persistStepOutput(runId, step.name, output);
3367
+ this.emit({ type: 'step:completed', runId, stepName: step.name, output });
3368
+ } catch (apiError) {
3369
+ const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
3370
+ state.row.status = 'failed';
3371
+ state.row.error = errorMessage;
3372
+ state.row.completedAt = new Date().toISOString();
3373
+ await this.db.updateStep(state.row.id, {
3374
+ status: 'failed',
3375
+ error: errorMessage,
3376
+ completedAt: state.row.completedAt,
3377
+ updatedAt: new Date().toISOString(),
3378
+ });
3379
+ this.emit({ type: 'step:failed', runId, stepName: step.name, error: errorMessage });
3380
+ this.postToChannel(`**[${step.name}]** Failed (api): ${errorMessage}`);
3381
+ throw apiError;
3382
+ }
3383
+ return;
3384
+ }
3385
+
3219
3386
  const usesOwnerFlow = specialistDef.interactive !== false;
3220
3387
  const currentPattern = this.currentConfig?.swarm?.pattern ?? '';
3221
3388
  const isHubPattern = WorkflowRunner.HUB_PATTERNS.has(currentPattern);
@@ -4734,10 +4901,13 @@ export class WorkflowRunner {
4734
4901
  task: string,
4735
4902
  extraArgs: string[] = []
4736
4903
  ): { cmd: string; args: string[] } {
4904
+ if (cli === 'api') {
4905
+ throw new Error('cli "api" uses direct API calls, not a subprocess command');
4906
+ }
4737
4907
  const resolvedCli: AgentCli = cli === 'cursor' ? resolveCursorCli() : cli;
4738
4908
  const def = getCliDefinition(resolvedCli);
4739
- if (!def) {
4740
- throw new Error(`Unknown CLI: ${resolvedCli}`);
4909
+ if (!def || def.binaries.length === 0) {
4910
+ throw new Error(`Unknown or non-executable CLI: ${resolvedCli}`);
4741
4911
  }
4742
4912
  return {
4743
4913
  cmd: def.binaries[0],
@@ -6053,26 +6223,23 @@ export class WorkflowRunner {
6053
6223
  );
6054
6224
  console.log(chalk.dim('━'.repeat(70)));
6055
6225
 
6056
- if (this.agentReports.size > 0) {
6057
- console.log(formatRunSummaryTable(outcomes, this.agentReports));
6058
- } else {
6059
- for (const outcome of outcomes) {
6060
- const icon =
6061
- outcome.status === 'completed' ? chalk.green('✓') : outcome.status === 'failed' ? chalk.red('✗') : chalk.dim('⊘');
6062
- const retryNote = outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : '';
6063
- console.log(` ${icon} ${outcome.name} [${outcome.agent}]${retryNote}`);
6064
-
6065
- if (outcome.error) {
6066
- console.log(` Error: ${outcome.error}`);
6067
- }
6226
+ // Always show the summary table — with agent reports when available,
6227
+ // with just step/status/duration when not (non-interactive agents).
6228
+ console.log(formatRunSummaryTable(outcomes, this.agentReports));
6068
6229
 
6069
- // Extract last meaningful lines from raw PTY output
6070
- if (outcome.output) {
6071
- const excerpt = this.extractOutputExcerpt(outcome.output);
6072
- if (excerpt) {
6073
- for (const line of excerpt.split('\n')) {
6074
- console.log(` ${line}`);
6075
- }
6230
+ // Show errors and output excerpts for failed steps below the table
6231
+ for (const outcome of outcomes) {
6232
+ if (outcome.status !== 'failed') continue;
6233
+
6234
+ if (outcome.error) {
6235
+ console.log(chalk.red(` ${outcome.name}: ${outcome.error}`));
6236
+ }
6237
+
6238
+ if (outcome.output) {
6239
+ const excerpt = this.extractOutputExcerpt(outcome.output);
6240
+ if (excerpt) {
6241
+ for (const line of excerpt.split('\n')) {
6242
+ console.log(` ${line}`);
6076
6243
  }
6077
6244
  }
6078
6245
  }
@@ -145,9 +145,11 @@ export interface AgentDefinition {
145
145
  * analyst → interactive: false, reads code/files, writes findings, no sub-agents
146
146
  */
147
147
  preset?: AgentPreset;
148
+ /** System prompt / skills for API-mode agents (cli: 'api'). */
149
+ skills?: string;
148
150
  }
149
151
 
150
- export type AgentCli = 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'opencode' | 'droid' | 'cursor' | 'cursor-agent' | 'agent';
152
+ export type AgentCli = 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'opencode' | 'droid' | 'cursor' | 'cursor-agent' | 'agent' | 'api';
151
153
 
152
154
  /** Resource and behavioral constraints for an agent. */
153
155
  export interface AgentConstraints {
@@ -183,8 +185,8 @@ export interface WorkflowDefinition {
183
185
  onError?: 'fail' | 'skip' | 'retry';
184
186
  }
185
187
 
186
- /** Step type: agent (LLM-powered), deterministic (shell command), or worktree (git worktree setup). */
187
- export type WorkflowStepType = 'agent' | 'deterministic' | 'worktree';
188
+ /** Step type: agent (LLM-powered), deterministic (shell command), worktree (git worktree setup), or integration (external service). */
189
+ export type WorkflowStepType = 'agent' | 'deterministic' | 'worktree' | 'integration';
188
190
 
189
191
  // ── Custom step definitions ─────────────────────────────────────────────────
190
192
 
@@ -277,6 +279,14 @@ export interface WorkflowStep {
277
279
  /** Capture stdout as step output for downstream steps. Default: true. */
278
280
  captureOutput?: boolean;
279
281
 
282
+ // ── Integration step fields ────────────────────────────────────────────────
283
+ /** Integration name: 'github', 'linear', 'slack' (required for integration steps). */
284
+ integration?: string;
285
+ /** Action within the integration, e.g. 'create-pr', 'create-branch' (required for integration steps). */
286
+ action?: string;
287
+ /** Action parameters, supports {{steps.X.output}} interpolation. */
288
+ params?: Record<string, string>;
289
+
280
290
  // ── Worktree step fields ──────────────────────────────────────────────────
281
291
  /** Branch name for the worktree (required for worktree steps). */
282
292
  branch?: string;
@@ -298,6 +308,11 @@ export function isWorktreeStep(step: WorkflowStep): boolean {
298
308
  return step.type === 'worktree';
299
309
  }
300
310
 
311
+ /** Type guard: Check if a step is an integration (external service) step. */
312
+ export function isIntegrationStep(step: WorkflowStep): boolean {
313
+ return step.type === 'integration';
314
+ }
315
+
301
316
  /** Type guard: Check if a step uses a custom step definition. */
302
317
  export function isCustomStep(step: WorkflowStep): boolean {
303
318
  return step.use !== undefined;
@@ -305,7 +320,7 @@ export function isCustomStep(step: WorkflowStep): boolean {
305
320
 
306
321
  /** Type guard: Check if a step is an agent (LLM-powered) step. */
307
322
  export function isAgentStep(step: WorkflowStep): boolean {
308
- return step.type !== 'deterministic' && step.type !== 'worktree';
323
+ return step.type !== 'deterministic' && step.type !== 'worktree' && step.type !== 'integration';
309
324
  }
310
325
 
311
326
  // Legacy type aliases for backward compatibility
@@ -8,8 +8,23 @@ export interface ValidationIssue {
8
8
  location?: string; // e.g. "step:analyze" or "agent:analyst"
9
9
  }
10
10
 
11
+ const CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
12
+
11
13
  export function validateWorkflow(config: RelayYamlConfig): ValidationIssue[] {
12
14
  const issues: ValidationIssue[] = [];
15
+
16
+ // Validate channel name format (must be lowercase alphanumeric + hyphens)
17
+ const channel = (config as any).swarm?.channel ?? (config as any).channel;
18
+ if (channel && !CHANNEL_NAME_RE.test(channel)) {
19
+ issues.push({
20
+ severity: 'error',
21
+ code: 'INVALID_CHANNEL_NAME',
22
+ message: `Channel name "${channel}" is invalid. Must be lowercase alphanumeric and hyphens, starting with a letter or number.`,
23
+ fix: `Use .toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') on the channel name.`,
24
+ location: 'swarm:channel',
25
+ });
26
+ }
27
+
13
28
  const agentMap = new Map(config.agents.map((a) => [a.name, a]));
14
29
  const hasReviewerAgent = config.agents.some((a) => {
15
30
  const role = a.role?.toLowerCase() ?? '';
@@ -127,6 +127,13 @@ relay = Relay("Researcher")
127
127
  await relay.send("Lead", "Status update")
128
128
  await relay.post("docs", "Wave 5.1 complete")
129
129
  messages = await relay.inbox()
130
+
131
+ human = relay.system()
132
+ await human.send_message(
133
+ to="Agent1",
134
+ text="Please start the analysis",
135
+ mode="wait", # or "steer"
136
+ )
130
137
  ```
131
138
 
132
139
  ### `on_relay()`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.2.15"
7
+ version = "3.2.17"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -17,6 +17,7 @@ from .protocol import (
17
17
  AgentRuntime,
18
18
  AgentSpec,
19
19
  BrokerEvent,
20
+ MessageInjectionMode,
20
21
  ProtocolEnvelope,
21
22
  RestartPolicy as ProtocolRestartPolicy,
22
23
  )
@@ -92,6 +93,7 @@ __all__ = [
92
93
  "AgentRuntime",
93
94
  "AgentSpec",
94
95
  "BrokerEvent",
96
+ "MessageInjectionMode",
95
97
  "ProtocolEnvelope",
96
98
  "ProtocolRestartPolicy",
97
99
  # Workflow builder (backward compat)
@@ -18,9 +18,11 @@ Example::
18
18
  from __future__ import annotations
19
19
 
20
20
  import copy
21
+ import os
21
22
  import re
22
23
  import shutil
23
24
  import subprocess
25
+ import sys
24
26
  import tempfile
25
27
  from pathlib import Path
26
28
  from typing import Any
@@ -388,9 +390,36 @@ class WorkflowBuilder:
388
390
  """Serialize the config to a YAML string."""
389
391
  return yaml.dump(self.to_config(), default_flow_style=False, sort_keys=False)
390
392
 
393
+ def dry_run(self, options: RunOptions | None = None) -> WorkflowResult:
394
+ """Validate the workflow and show execution plan without running."""
395
+ opts = RunOptions(
396
+ workflow=(options.workflow if options else None),
397
+ cwd=(options.cwd if options else None),
398
+ vars=(options.vars if options else None),
399
+ trajectories=(options.trajectories if options else None),
400
+ on_event=(options.on_event if options else None),
401
+ dry_run=True,
402
+ )
403
+ return self.run(opts)
404
+
391
405
  def run(self, options: RunOptions | None = None) -> WorkflowResult:
392
- """Build the config and execute it via ``agent-relay run <tempfile>``."""
393
- opts = options or RunOptions()
406
+ """Build the config and execute it via ``agent-relay run <tempfile>``.
407
+
408
+ Dry-run is enabled when:
409
+ - ``options.dry_run`` is ``True``, or
410
+ - the ``DRY_RUN`` environment variable is set to ``"true"``
411
+ (set automatically by ``agent-relay run script.py --dry-run``).
412
+ """
413
+ opts = RunOptions(
414
+ workflow=(options.workflow if options else None),
415
+ cwd=(options.cwd if options else None),
416
+ vars=(options.vars if options else None),
417
+ trajectories=(options.trajectories if options else None),
418
+ on_event=(options.on_event if options else None),
419
+ dry_run=(options.dry_run if options else None),
420
+ )
421
+ if opts.dry_run is None and os.environ.get("DRY_RUN") == "true":
422
+ opts.dry_run = True
394
423
  config = _apply_runtime_overrides(self.to_config(), opts)
395
424
  return _run_config(config, opts)
396
425
 
@@ -402,7 +431,16 @@ def workflow(name: str) -> WorkflowBuilder:
402
431
 
403
432
  def run_yaml(yaml_path: str, options: RunOptions | None = None) -> WorkflowResult:
404
433
  """Run an existing relay YAML workflow file."""
405
- opts = options or RunOptions()
434
+ opts = RunOptions(
435
+ workflow=(options.workflow if options else None),
436
+ cwd=(options.cwd if options else None),
437
+ vars=(options.vars if options else None),
438
+ trajectories=(options.trajectories if options else None),
439
+ on_event=(options.on_event if options else None),
440
+ dry_run=(options.dry_run if options else None),
441
+ )
442
+ if opts.dry_run is None and os.environ.get("DRY_RUN") == "true":
443
+ opts.dry_run = True
406
444
 
407
445
  if opts.trajectories is None and not opts.vars:
408
446
  return _run_yaml_path(yaml_path, opts)
@@ -436,6 +474,8 @@ def _run_yaml_path(yaml_path: str, options: RunOptions) -> WorkflowResult:
436
474
  )
437
475
 
438
476
  cmd = [*cmd_prefix, "run", yaml_path]
477
+ if options.dry_run:
478
+ cmd.append("--dry-run")
439
479
  if options.workflow:
440
480
  cmd.extend(["--workflow", options.workflow])
441
481
 
@@ -462,7 +502,26 @@ def _execute_cli(
462
502
  cwd: str | None,
463
503
  on_event: WorkflowEventCallback | None,
464
504
  ) -> WorkflowResult:
465
- """Execute CLI command and parse emitted workflow events."""
505
+ """Execute CLI command and parse emitted workflow events.
506
+
507
+ When no event callback is registered, stdio is passed straight through so
508
+ the TypeScript runner's listr progress and summary table render directly
509
+ to the terminal — identical output to YAML and TypeScript workflows.
510
+ """
511
+ # Passthrough mode: no callback → let the TS runner render directly
512
+ if on_event is None:
513
+ process = subprocess.Popen(cmd, cwd=cwd)
514
+ process.wait()
515
+
516
+ return WorkflowResult(
517
+ status="completed" if process.returncode == 0 else "failed",
518
+ run_id="",
519
+ error=None if process.returncode == 0 else "Workflow failed",
520
+ steps=[],
521
+ events=[],
522
+ )
523
+
524
+ # Capture mode: callback registered → parse events line by line
466
525
  process = subprocess.Popen(
467
526
  cmd,
468
527
  cwd=cwd,
@@ -487,9 +546,7 @@ def _execute_cli(
487
546
 
488
547
  events.append(event)
489
548
  _sync_step_result(steps, event)
490
-
491
- if on_event is not None:
492
- on_event(event)
549
+ on_event(event)
493
550
 
494
551
  return_code = process.wait()
495
552
  output = "\n".join(lines).strip()
@@ -25,6 +25,7 @@ from .protocol import (
25
25
  AgentSpec,
26
26
  BrokerEvent,
27
27
  HeadlessProvider,
28
+ MessageInjectionMode,
28
29
  ProtocolEnvelope,
29
30
  )
30
31
 
@@ -715,6 +716,7 @@ class AgentRelayClient:
715
716
  thread_id: Optional[str] = None,
716
717
  priority: Optional[int] = None,
717
718
  data: Optional[dict[str, Any]] = None,
719
+ mode: Optional[MessageInjectionMode] = None,
718
720
  ) -> dict[str, Any]:
719
721
  await self.start_client()
720
722
  payload: dict[str, Any] = {"to": to, "text": text}
@@ -726,6 +728,8 @@ class AgentRelayClient:
726
728
  payload["priority"] = priority
727
729
  if data is not None:
728
730
  payload["data"] = data
731
+ if mode is not None:
732
+ payload["mode"] = mode
729
733
  try:
730
734
  return await self._request_ok("send_message", payload)
731
735
  except AgentRelayProtocolError as e:
@@ -1,10 +1 @@
1
1
  """Framework-specific adapters for on_relay()."""
2
-
3
- _has_pi = False
4
- try:
5
- from .pi import on_relay as on_pi_relay
6
- _has_pi = True
7
- except ImportError:
8
- pass
9
-
10
- __all__ = [*(["on_pi_relay"] if _has_pi else [])]