agent-relay 3.2.14 → 3.2.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.
- 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 +3859 -17164
- 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 +10 -1
- 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 +154 -10
- 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/__tests__/workflow-runner.test.ts +2 -2
- package/packages/sdk/src/broker-path.ts +74 -0
- package/packages/sdk/src/cli-registry.ts +10 -1
- 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 +181 -11
- 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/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/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_send_message_mode.py +91 -0
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +323 -0
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserverTypes.swift +143 -0
- package/packages/sdk-swift/Tests/AgentRelaySDKTests/RelayObserverTests.swift +526 -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],
|
|
@@ -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)
|
|
@@ -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:
|
|
@@ -9,10 +9,10 @@ if TYPE_CHECKING:
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
|
|
12
|
-
content = "
|
|
12
|
+
content = "New messages from other agents:\n"
|
|
13
13
|
for message in messages:
|
|
14
14
|
content += f" {message.sender}: {message.text}\n"
|
|
15
|
-
return f"{
|
|
15
|
+
return f"{base_instructions}\n\n{content}" if base_instructions else content
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
@@ -45,11 +45,8 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
45
45
|
|
|
46
46
|
agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
|
|
47
47
|
|
|
48
|
-
# 2. Wrap instructions
|
|
48
|
+
# 2. Wrap instructions
|
|
49
49
|
orig_instructions = agent.instructions
|
|
50
|
-
pending_messages: list[Any] = []
|
|
51
|
-
|
|
52
|
-
relay.on_message(lambda msg: pending_messages.append(msg))
|
|
53
50
|
|
|
54
51
|
async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
|
|
55
52
|
if callable(orig_instructions):
|
|
@@ -63,11 +60,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
63
60
|
base = orig_instructions
|
|
64
61
|
|
|
65
62
|
base = base or ""
|
|
66
|
-
|
|
63
|
+
messages = await relay.inbox()
|
|
64
|
+
if not messages:
|
|
67
65
|
return base
|
|
68
66
|
|
|
69
|
-
messages = list(pending_messages)
|
|
70
|
-
pending_messages.clear()
|
|
71
67
|
return _format_instructions_with_inbox(messages, base)
|
|
72
68
|
|
|
73
69
|
agent.instructions = instructions_wrapper
|
|
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from ..core import Relay
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def on_relay(
|
|
12
|
+
def on_relay(options: Any, relay: "Relay | None" = None, *, name: str | None = None) -> Any:
|
|
13
13
|
"""Wrap Claude Agent SDK query options to connect them to the relay."""
|
|
14
14
|
try:
|
|
15
15
|
from claude_agent_sdk.types import HookResult
|
|
@@ -24,10 +24,8 @@ def on_relay(name: str, options: Any, relay: "Relay | None" = None) -> Any:
|
|
|
24
24
|
from ..core import Relay
|
|
25
25
|
relay = Relay(agent_name)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
hooks = SimpleNamespace(post_tool_use=None, stop=None)
|
|
30
|
-
options.hooks = hooks
|
|
27
|
+
if getattr(options, "hooks", None) is None:
|
|
28
|
+
options.hooks = SimpleNamespace(post_tool_use=None, stop=None)
|
|
31
29
|
|
|
32
30
|
# 1. Inject Relaycast MCP server
|
|
33
31
|
mcp_config = {"name": "relaycast", "command": "agent-relay", "args": ["mcp"]}
|
|
@@ -46,8 +44,8 @@ def on_relay(name: str, options: Any, relay: "Relay | None" = None) -> Any:
|
|
|
46
44
|
return content
|
|
47
45
|
|
|
48
46
|
# 3. Hook wrappers
|
|
49
|
-
orig_post =
|
|
50
|
-
orig_stop =
|
|
47
|
+
orig_post = options.hooks.post_tool_use if hasattr(options.hooks, "post_tool_use") else None
|
|
48
|
+
orig_stop = options.hooks.stop if hasattr(options.hooks, "stop") else None
|
|
51
49
|
|
|
52
50
|
async def post_tool_use_hook(*args, **kwargs):
|
|
53
51
|
res = None
|
|
@@ -47,18 +47,9 @@ class _RelayBackstory:
|
|
|
47
47
|
def _resolve_sync(self) -> str:
|
|
48
48
|
messages = self._drain_buffer()
|
|
49
49
|
try:
|
|
50
|
-
loop = asyncio.get_running_loop()
|
|
51
|
-
except RuntimeError:
|
|
52
|
-
loop = None
|
|
53
|
-
|
|
54
|
-
if loop is None:
|
|
55
50
|
messages.extend(self._relay.inbox_sync())
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
import concurrent.futures
|
|
59
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
60
|
-
polled = pool.submit(asyncio.run, self._relay.inbox()).result()
|
|
61
|
-
messages.extend(polled)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass # Buffer messages are still available
|
|
62
53
|
return _format_backstory(self._dedupe(messages), self._base_backstory)
|
|
63
54
|
|
|
64
55
|
async def _resolve_async(self) -> str:
|
|
@@ -136,8 +127,7 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
136
127
|
def _buffer_message(message: Any) -> None:
|
|
137
128
|
backstory_buffer.append(message)
|
|
138
129
|
|
|
139
|
-
|
|
130
|
+
relay.on_message(_buffer_message)
|
|
140
131
|
agent.backstory = _RelayBackstory(relay, agent.backstory or "", backstory_buffer)
|
|
141
|
-
agent._relay_unsubscribe = unsubscribe # type: ignore[attr-defined]
|
|
142
132
|
|
|
143
133
|
return agent
|
|
@@ -47,7 +47,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
47
47
|
if inspect.isawaitable(orig_result):
|
|
48
48
|
orig_result = await orig_result
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# If original callback short-circuited (returned Content), respect that
|
|
51
|
+
if orig_result is not None:
|
|
52
|
+
return orig_result
|
|
53
|
+
|
|
51
54
|
messages = await relay.inbox()
|
|
52
55
|
if messages:
|
|
53
56
|
from google.genai.types import Content, Part
|
|
@@ -63,7 +66,7 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
63
66
|
)
|
|
64
67
|
)
|
|
65
68
|
|
|
66
|
-
return
|
|
69
|
+
return None
|
|
67
70
|
|
|
68
71
|
agent.before_model_callback = relay_callback
|
|
69
72
|
return agent
|
|
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
|
|
13
|
-
content = "
|
|
13
|
+
content = "New messages from other agents:\n"
|
|
14
14
|
for message in messages:
|
|
15
15
|
content += f" {message.sender}: {message.text}\n"
|
|
16
|
-
return f"{
|
|
16
|
+
return f"{base_instructions}\n\n{content}" if base_instructions else content
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
@@ -57,11 +57,8 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
57
57
|
function_tool(relay_agents)
|
|
58
58
|
])
|
|
59
59
|
|
|
60
|
-
# 2. Wrap instructions
|
|
60
|
+
# 2. Wrap instructions
|
|
61
61
|
orig_instructions = agent.instructions
|
|
62
|
-
pending_messages: list[Any] = []
|
|
63
|
-
|
|
64
|
-
relay.on_message(lambda msg: pending_messages.append(msg))
|
|
65
62
|
|
|
66
63
|
async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
|
|
67
64
|
if callable(orig_instructions):
|
|
@@ -75,11 +72,10 @@ def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
|
75
72
|
base = orig_instructions
|
|
76
73
|
|
|
77
74
|
base = base or ""
|
|
78
|
-
|
|
75
|
+
messages = await relay.inbox()
|
|
76
|
+
if not messages:
|
|
79
77
|
return base
|
|
80
78
|
|
|
81
|
-
messages = list(pending_messages)
|
|
82
|
-
pending_messages.clear()
|
|
83
79
|
return _format_instructions_with_inbox(messages, base)
|
|
84
80
|
|
|
85
81
|
agent.instructions = instructions_wrapper
|