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.
- 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 +3865 -17179
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +2 -0
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/package.json +8 -8
- package/packages/acp-bridge/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/dist/broker-path.d.ts +19 -0
- package/packages/sdk/dist/broker-path.d.ts.map +1 -0
- package/packages/sdk/dist/broker-path.js +71 -0
- package/packages/sdk/dist/broker-path.js.map +1 -0
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
- package/packages/sdk/dist/cli-registry.js +4 -0
- package/packages/sdk/dist/cli-registry.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +6 -1
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +18 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.d.ts +0 -5
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/index.js +0 -5
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +0 -1
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/adapters/pi.js +0 -4
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -1
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/core.js +2 -3
- package/packages/sdk/dist/communicate/core.js.map +1 -1
- package/packages/sdk/dist/communicate/index.d.ts +17 -1
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/index.js +40 -1
- package/packages/sdk/dist/communicate/index.js.map +1 -1
- package/packages/sdk/dist/communicate/transport.d.ts +0 -1
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -1
- package/packages/sdk/dist/communicate/transport.js +42 -134
- package/packages/sdk/dist/communicate/transport.js.map +1 -1
- package/packages/sdk/dist/http.d.ts +38 -0
- package/packages/sdk/dist/http.d.ts.map +1 -0
- package/packages/sdk/dist/http.js +60 -0
- package/packages/sdk/dist/http.js.map +1 -0
- package/packages/sdk/dist/protocol.d.ts +25 -0
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -3
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +62 -4
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/api-executor.d.ts +16 -0
- package/packages/sdk/dist/workflows/api-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/api-executor.js +94 -0
- package/packages/sdk/dist/workflows/api-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +14 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +26 -0
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cloud-runner.d.ts +15 -0
- package/packages/sdk/dist/workflows/cloud-runner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/cloud-runner.js +41 -0
- package/packages/sdk/dist/workflows/cloud-runner.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +2 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +1 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js +4 -0
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +14 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +169 -28
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +13 -3
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js +5 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +12 -0
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +13 -3
- package/packages/sdk/src/__tests__/channel-management.test.ts +131 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +36 -88
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +41 -80
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +120 -0
- package/packages/sdk/src/__tests__/relay-channel-ops.test.ts +121 -0
- package/packages/sdk/src/broker-path.ts +74 -0
- package/packages/sdk/src/cli-registry.ts +4 -0
- package/packages/sdk/src/client.ts +28 -0
- package/packages/sdk/src/communicate/adapters/index.ts +0 -5
- package/packages/sdk/src/communicate/adapters/pi.ts +1 -5
- package/packages/sdk/src/communicate/core.ts +6 -10
- package/packages/sdk/src/communicate/index.ts +57 -1
- package/packages/sdk/src/communicate/transport.ts +46 -177
- package/packages/sdk/src/http.ts +96 -0
- package/packages/sdk/src/protocol.ts +24 -0
- package/packages/sdk/src/relay.ts +93 -8
- package/packages/sdk/src/workflows/README.md +5 -2
- package/packages/sdk/src/workflows/api-executor.ts +108 -0
- package/packages/sdk/src/workflows/builder.ts +40 -0
- package/packages/sdk/src/workflows/cloud-runner.ts +56 -0
- package/packages/sdk/src/workflows/index.ts +2 -0
- package/packages/sdk/src/workflows/run.ts +5 -0
- package/packages/sdk/src/workflows/runner.ts +197 -30
- package/packages/sdk/src/workflows/types.ts +19 -4
- package/packages/sdk/src/workflows/validator.ts +15 -0
- package/packages/sdk-py/README.md +7 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
- package/packages/sdk-py/src/agent_relay/builder.py +64 -7
- package/packages/sdk-py/src/agent_relay/client.py +4 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +0 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +5 -7
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +3 -13
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +5 -2
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +5 -9
- package/packages/sdk-py/src/agent_relay/communicate/core.py +7 -24
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +35 -212
- package/packages/sdk-py/src/agent_relay/communicate/types.py +1 -1
- package/packages/sdk-py/src/agent_relay/protocol.py +1 -0
- package/packages/sdk-py/src/agent_relay/relay.py +9 -1
- package/packages/sdk-py/src/agent_relay/types.py +1 -0
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +6 -6
- package/packages/sdk-py/tests/communicate/conftest.py +86 -233
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +2 -2
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +14 -24
- package/packages/sdk-py/tests/communicate/test_transport.py +65 -54
- package/packages/sdk-py/tests/test_builder.py +58 -0
- package/packages/sdk-py/tests/test_dry_run.py +215 -0
- package/packages/sdk-py/tests/test_send_message_mode.py +91 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- 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
|
|
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
|
-
:
|
|
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
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
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
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
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),
|
|
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()`
|
|
@@ -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
|
-
|
|
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 =
|
|
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:
|