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
@@ -33,7 +33,14 @@ import {
33
33
  type SendMessageInput,
34
34
  type SpawnPtyInput,
35
35
  } from './client.js';
36
- import type { AgentRuntime, BrokerEvent, BrokerStatus, HeadlessProvider, RestartPolicy } from './protocol.js';
36
+ import type {
37
+ AgentRuntime,
38
+ BrokerEvent,
39
+ BrokerStatus,
40
+ HeadlessProvider,
41
+ MessageInjectionMode,
42
+ RestartPolicy,
43
+ } from './protocol.js';
37
44
  import {
38
45
  followLogs as followLogsFromFile,
39
46
  getLogs as getLogsFromFile,
@@ -49,7 +56,13 @@ function isUnsupportedOperation(error: unknown): error is AgentRelayProtocolErro
49
56
 
50
57
  function buildUnsupportedOperationMessage(
51
58
  from: string,
52
- input: { to: string; text: string; threadId?: string; data?: Record<string, unknown> }
59
+ input: {
60
+ to: string;
61
+ text: string;
62
+ threadId?: string;
63
+ data?: Record<string, unknown>;
64
+ mode?: MessageInjectionMode;
65
+ }
53
66
  ): Message {
54
67
  return {
55
68
  eventId: 'unsupported_operation',
@@ -58,6 +71,7 @@ function buildUnsupportedOperationMessage(
58
71
  text: input.text,
59
72
  threadId: input.threadId,
60
73
  data: input.data,
74
+ mode: input.mode,
61
75
  };
62
76
  }
63
77
 
@@ -70,6 +84,7 @@ export interface Message {
70
84
  text: string;
71
85
  threadId?: string;
72
86
  data?: Record<string, unknown>;
87
+ mode?: MessageInjectionMode;
73
88
  }
74
89
 
75
90
  export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited';
@@ -178,9 +193,15 @@ export interface Agent {
178
193
  threadId?: string;
179
194
  priority?: number;
180
195
  data?: Record<string, unknown>;
196
+ mode?: MessageInjectionMode;
181
197
  }): Promise<Message>;
182
- /** Register a callback for PTY output from this agent. Returns an unsubscribe function. */
183
- onOutput(callback: AgentOutputCallback): () => void;
198
+ subscribe(channels: string[]): Promise<void>;
199
+ unsubscribe(channels: string[]): Promise<void>;
200
+ /** Register a callback for PTY output from this agent. Returns an unsubscribe function.
201
+ * @param options.stream — if provided, only invoke callback when the event stream matches (e.g. 'stdout', 'stderr')
202
+ * @param options.mode — 'chunk' for raw string callbacks, 'structured' for { stream, chunk } callbacks. Auto-detected if omitted.
203
+ */
204
+ onOutput(callback: AgentOutputCallback, options?: { stream?: string; mode?: 'chunk' | 'structured' }): () => void;
184
205
  }
185
206
 
186
207
  export interface HumanHandle {
@@ -191,6 +212,7 @@ export interface HumanHandle {
191
212
  threadId?: string;
192
213
  priority?: number;
193
214
  data?: Record<string, unknown>;
215
+ mode?: MessageInjectionMode;
194
216
  }): Promise<Message>;
195
217
  }
196
218
 
@@ -237,6 +259,11 @@ export interface AgentRelayOptions {
237
259
  type OutputListener = {
238
260
  callback: AgentOutputCallback;
239
261
  mode: 'chunk' | 'structured';
262
+ stream?: string;
263
+ };
264
+
265
+ type InternalAgent = Agent & {
266
+ _setChannels: (channels: string[]) => void;
240
267
  };
241
268
 
242
269
  // ── AgentRelay facade ───────────────────────────────────────────────────────
@@ -253,6 +280,8 @@ export class AgentRelay {
253
280
  onDeliveryUpdate: EventHook<BrokerEvent> = null;
254
281
  onAgentExitRequested: EventHook<{ name: string; reason: string }> = null;
255
282
  onAgentIdle: EventHook<{ name: string; idleSecs: number }> = null;
283
+ onChannelSubscribed: ((agent: string, channels: string[]) => void) | null = null;
284
+ onChannelUnsubscribed: ((agent: string, channels: string[]) => void) | null = null;
256
285
 
257
286
  // ── Public accessors ────────────────────────────────────────────────────
258
287
 
@@ -451,6 +480,7 @@ export class AgentRelay {
451
480
  threadId: input.threadId,
452
481
  priority: input.priority,
453
482
  data: input.data,
483
+ mode: input.mode,
454
484
  });
455
485
  } catch (error) {
456
486
  if (isUnsupportedOperation(error)) {
@@ -470,6 +500,7 @@ export class AgentRelay {
470
500
  text: input.text,
471
501
  threadId: input.threadId,
472
502
  data: input.data,
503
+ mode: input.mode,
473
504
  };
474
505
  this.onMessageSent?.(msg);
475
506
  return msg;
@@ -579,6 +610,18 @@ export class AgentRelay {
579
610
  return client.getStatus();
580
611
  }
581
612
 
613
+ async subscribe(opts: { agent: string; channels: string[] }): Promise<void> {
614
+ const client = await this.ensureStarted();
615
+ await client.subscribeChannels(opts.agent, opts.channels);
616
+ this.addAgentChannels(opts.agent, opts.channels);
617
+ }
618
+
619
+ async unsubscribe(opts: { agent: string; channels: string[] }): Promise<void> {
620
+ const client = await this.ensureStarted();
621
+ await client.unsubscribeChannels(opts.agent, opts.channels);
622
+ this.removeAgentChannels(opts.agent, opts.channels);
623
+ }
624
+
582
625
  getDeliveryState(eventId: string): DeliveryState | undefined {
583
626
  return this.deliveryStates.get(eventId);
584
627
  }
@@ -841,6 +884,20 @@ export class AgentRelay {
841
884
  return typeof candidate === 'number' ? candidate : Date.now();
842
885
  }
843
886
 
887
+ private addAgentChannels(name: string, channels: string[]): void {
888
+ const agent = this.knownAgents.get(name) as InternalAgent | undefined;
889
+ if (!agent || channels.length === 0) return;
890
+ const next = [...new Set([...agent.channels, ...channels])];
891
+ agent._setChannels(next);
892
+ }
893
+
894
+ private removeAgentChannels(name: string, channels: string[]): void {
895
+ const agent = this.knownAgents.get(name) as InternalAgent | undefined;
896
+ if (!agent || channels.length === 0) return;
897
+ const removed = new Set(channels);
898
+ agent._setChannels(agent.channels.filter((channel) => !removed.has(channel)));
899
+ }
900
+
844
901
  /** Resolve a target to a channel name. If `to` is `#channel`, use that
845
902
  * channel. If it's a known agent name, use the agent's first channel.
846
903
  * Otherwise fall back to the relay's default channel. */
@@ -863,6 +920,7 @@ export class AgentRelay {
863
920
  const listeners = this.outputListeners.get(name);
864
921
  if (!listeners) return;
865
922
  for (const listener of listeners) {
923
+ if (listener.stream !== undefined && listener.stream !== stream) continue;
866
924
  if (listener.mode === 'structured') {
867
925
  (listener.callback as (data: AgentOutputPayload) => void)({ stream, chunk });
868
926
  } else {
@@ -960,6 +1018,7 @@ export class AgentRelay {
960
1018
  to: event.target,
961
1019
  text: event.body,
962
1020
  threadId: event.thread_id,
1021
+ mode: event.injection_mode ?? event.mode,
963
1022
  };
964
1023
  this.onMessageReceived?.(msg);
965
1024
  break;
@@ -1020,6 +1079,16 @@ export class AgentRelay {
1020
1079
  this.onAgentReady?.(agent);
1021
1080
  break;
1022
1081
  }
1082
+ case 'channel_subscribed': {
1083
+ this.addAgentChannels(event.name, event.channels);
1084
+ this.onChannelSubscribed?.(event.name, event.channels);
1085
+ break;
1086
+ }
1087
+ case 'channel_unsubscribed': {
1088
+ this.removeAgentChannels(event.name, event.channels);
1089
+ this.onChannelUnsubscribed?.(event.name, event.channels);
1090
+ break;
1091
+ }
1023
1092
  case 'delivery_queued': {
1024
1093
  this.updateDeliveryState(
1025
1094
  event.event_id,
@@ -1083,10 +1152,13 @@ export class AgentRelay {
1083
1152
  private makeAgent(name: string, runtime: AgentRuntime, channels: string[]): Agent {
1084
1153
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1085
1154
  const relay = this;
1086
- return {
1155
+ let agentChannels = [...channels];
1156
+ const agent: InternalAgent = {
1087
1157
  name,
1088
1158
  runtime,
1089
- channels,
1159
+ get channels() {
1160
+ return [...agentChannels];
1161
+ },
1090
1162
  get status(): AgentStatus {
1091
1163
  if (relay.exitedAgents.has(name)) return 'exited';
1092
1164
  if (relay.idleAgents.has(name)) return 'idle';
@@ -1230,6 +1302,7 @@ export class AgentRelay {
1230
1302
  threadId: input.threadId,
1231
1303
  priority: input.priority,
1232
1304
  data: input.data,
1305
+ mode: input.mode,
1233
1306
  });
1234
1307
  } catch (error) {
1235
1308
  if (isUnsupportedOperation(error)) {
@@ -1248,11 +1321,18 @@ export class AgentRelay {
1248
1321
  text: input.text,
1249
1322
  threadId: input.threadId,
1250
1323
  data: input.data,
1324
+ mode: input.mode,
1251
1325
  };
1252
1326
  relay.onMessageSent?.(msg);
1253
1327
  return msg;
1254
1328
  },
1255
- onOutput(callback: AgentOutputCallback): () => void {
1329
+ async subscribe(channelsToAdd: string[]) {
1330
+ await relay.subscribe({ agent: name, channels: channelsToAdd });
1331
+ },
1332
+ async unsubscribe(channelsToRemove: string[]) {
1333
+ await relay.unsubscribe({ agent: name, channels: channelsToRemove });
1334
+ },
1335
+ onOutput(callback: AgentOutputCallback, options?: { stream?: string; mode?: 'chunk' | 'structured' }): () => void {
1256
1336
  let listeners = relay.outputListeners.get(name);
1257
1337
  if (!listeners) {
1258
1338
  listeners = new Set();
@@ -1260,7 +1340,8 @@ export class AgentRelay {
1260
1340
  }
1261
1341
  const listener: OutputListener = {
1262
1342
  callback,
1263
- mode: relay.inferOutputMode(callback),
1343
+ mode: options?.mode ?? relay.inferOutputMode(callback),
1344
+ stream: options?.stream,
1264
1345
  };
1265
1346
  listeners.add(listener);
1266
1347
  return () => {
@@ -1270,7 +1351,11 @@ export class AgentRelay {
1270
1351
  }
1271
1352
  };
1272
1353
  },
1354
+ _setChannels(nextChannels: string[]) {
1355
+ agentChannels = [...nextChannels];
1356
+ },
1273
1357
  };
1358
+ return agent;
1274
1359
  }
1275
1360
 
1276
1361
  private createSpawner(cli: string, defaultName: string, runtime: AgentRuntime): AgentSpawner {
@@ -515,8 +515,9 @@ const runner = new WorkflowRunner({
515
515
  relay: { port: 3000 }, // AgentRelay options (optional)
516
516
  });
517
517
 
518
- // Listen to events
518
+ // Listen to events (broker:event fires frequently — filter it out for cleaner output)
519
519
  runner.on((event) => {
520
+ if (event.type === 'broker:event') return;
520
521
  console.log(event.type, event);
521
522
  });
522
523
 
@@ -543,7 +544,9 @@ import { runWorkflow } from "@agent-relay/sdk/workflows";
543
544
  const result = await runWorkflow("workflow.yaml", {
544
545
  workflow: "deploy",
545
546
  vars: { environment: "staging" },
546
- onEvent: (event) => console.log(event.type),
547
+ onEvent: (event) => {
548
+ if (event.type !== 'broker:event') console.log(event.type);
549
+ },
547
550
  });
548
551
  ```
549
552
 
@@ -0,0 +1,108 @@
1
+ /**
2
+ * API Executor — calls LLM provider APIs directly via fetch().
3
+ * Used when agent cli is 'api'. No sandbox, no CLI, no PTY.
4
+ */
5
+
6
+ type Provider = 'anthropic' | 'openai' | 'google';
7
+
8
+ function detectProvider(model: string): Provider {
9
+ if (model.startsWith('claude')) return 'anthropic';
10
+ if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'openai';
11
+ if (model.startsWith('gemini')) return 'google';
12
+ return 'anthropic';
13
+ }
14
+
15
+ function getApiKey(provider: Provider, envSecrets?: Record<string, string>): string {
16
+ const envMap: Record<Provider, string[]> = {
17
+ anthropic: ['ANTHROPIC_API_KEY'],
18
+ openai: ['OPENAI_API_KEY'],
19
+ google: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
20
+ };
21
+ for (const key of envMap[provider]) {
22
+ const value = envSecrets?.[key] ?? process.env[key];
23
+ if (value) return value;
24
+ }
25
+ throw new Error(`No API key for "${provider}". Set ${envMap[provider].join(' or ')}.`);
26
+ }
27
+
28
+ interface ApiResponse {
29
+ content: string;
30
+ model: string;
31
+ usage?: { inputTokens: number; outputTokens: number };
32
+ }
33
+
34
+ async function callAnthropic(apiKey: string, model: string, task: string, maxTokens: number, systemPrompt?: string): Promise<ApiResponse> {
35
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
36
+ method: 'POST',
37
+ headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
38
+ body: JSON.stringify({
39
+ model, max_tokens: maxTokens,
40
+ ...(systemPrompt ? { system: systemPrompt } : {}),
41
+ messages: [{ role: 'user', content: task }],
42
+ }),
43
+ });
44
+ if (!res.ok) throw new Error(`Anthropic API error (${res.status}): ${await res.text()}`);
45
+ const data = await res.json() as { content: Array<{ type: string; text?: string }>; model: string; usage?: { input_tokens: number; output_tokens: number } };
46
+ return {
47
+ content: data.content.filter(c => c.type === 'text').map(c => c.text ?? '').join(''),
48
+ model: data.model,
49
+ usage: data.usage ? { inputTokens: data.usage.input_tokens, outputTokens: data.usage.output_tokens } : undefined,
50
+ };
51
+ }
52
+
53
+ async function callOpenAI(apiKey: string, model: string, task: string, maxTokens: number, systemPrompt?: string): Promise<ApiResponse> {
54
+ const messages: Array<{ role: string; content: string }> = [];
55
+ if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
56
+ messages.push({ role: 'user', content: task });
57
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
58
+ method: 'POST',
59
+ headers: { 'content-type': 'application/json', 'authorization': `Bearer ${apiKey}` },
60
+ body: JSON.stringify({ model, max_tokens: maxTokens, messages }),
61
+ });
62
+ if (!res.ok) throw new Error(`OpenAI API error (${res.status}): ${await res.text()}`);
63
+ const data = await res.json() as { choices: Array<{ message: { content: string } }>; model: string; usage?: { prompt_tokens: number; completion_tokens: number } };
64
+ return {
65
+ content: data.choices[0]?.message?.content ?? '',
66
+ model: data.model,
67
+ usage: data.usage ? { inputTokens: data.usage.prompt_tokens, outputTokens: data.usage.completion_tokens } : undefined,
68
+ };
69
+ }
70
+
71
+ async function callGoogle(apiKey: string, model: string, task: string, maxTokens: number, systemPrompt?: string): Promise<ApiResponse> {
72
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
73
+ method: 'POST',
74
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
75
+ body: JSON.stringify({
76
+ ...(systemPrompt ? { systemInstruction: { parts: [{ text: systemPrompt }] } } : {}),
77
+ contents: [{ parts: [{ text: task }] }],
78
+ generationConfig: { maxOutputTokens: maxTokens },
79
+ }),
80
+ });
81
+ if (!res.ok) throw new Error(`Google API error (${res.status}): ${await res.text()}`);
82
+ const data = await res.json() as { candidates: Array<{ content: { parts: Array<{ text: string }> } }>; usageMetadata?: { promptTokenCount: number; candidatesTokenCount: number } };
83
+ return {
84
+ content: data.candidates[0]?.content?.parts?.map(p => p.text).join('') ?? '',
85
+ model,
86
+ usage: data.usageMetadata ? { inputTokens: data.usageMetadata.promptTokenCount, outputTokens: data.usageMetadata.candidatesTokenCount } : undefined,
87
+ };
88
+ }
89
+
90
+ const PROVIDER_CALLERS = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle } as const;
91
+
92
+ export interface ApiExecutorOptions {
93
+ envSecrets?: Record<string, string>;
94
+ defaultModel?: string;
95
+ defaultMaxTokens?: number;
96
+ skills?: string;
97
+ }
98
+
99
+ export async function executeApiStep(model: string, task: string, options: ApiExecutorOptions = {}): Promise<string> {
100
+ const resolvedModel = model || options.defaultModel || 'claude-sonnet-4-20250514';
101
+ const maxTokens = options.defaultMaxTokens ?? 4096;
102
+ const provider = detectProvider(resolvedModel);
103
+ const apiKey = getApiKey(provider, options.envSecrets);
104
+ const response = await PROVIDER_CALLERS[provider](apiKey, resolvedModel, task, maxTokens, options.skills);
105
+ return response.content;
106
+ }
107
+
108
+ export { detectProvider, getApiKey };
@@ -23,6 +23,7 @@ import type {
23
23
  import { WorkflowRunner, type WorkflowEventListener, type VariableContext, type StepExecutor } from './runner.js';
24
24
  import { formatDryRunReport } from './dry-run-format.js';
25
25
  import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
26
+ import { runInCloud, type CloudRunOptions } from './cloud-runner.js';
26
27
 
27
28
  // ── Option types for the builder API ────────────────────────────────────────
28
29
 
@@ -42,6 +43,8 @@ export interface AgentOptions {
42
43
  interactive?: boolean;
43
44
  /** Agent preset: 'lead' (interactive PTY), 'worker' | 'reviewer' | 'analyst' (non-interactive subprocess). */
44
45
  preset?: AgentPreset;
46
+ /** Skills to make available to the agent (for API-mode agents). */
47
+ skills?: string;
45
48
  }
46
49
 
47
50
  /** Options for agent steps (default). */
@@ -111,6 +114,18 @@ export interface WorkflowRunOptions {
111
114
  logLevel?: LogLevel;
112
115
  /** Renderer: "listr" for listr2 UI, "default" for console logger, false to disable. */
113
116
  renderer?: 'listr' | 'default' | false;
117
+ /** Run the workflow in the cloud instead of locally. */
118
+ cloud?: boolean;
119
+ /** Cloud API base URL (or set CLOUD_API_URL env var). */
120
+ cloudApiUrl?: string;
121
+ /** Cloud API authentication token (or set CLOUD_API_TOKEN env var). */
122
+ cloudApiToken?: string;
123
+ /** Environment secrets to forward to cloud agents. */
124
+ envSecrets?: Record<string, string>;
125
+ /** Polling interval in ms for cloud run status checks. */
126
+ cloudPollIntervalMs?: number;
127
+ /** Callback invoked when the cloud run status changes. */
128
+ onCloudStatusChange?: (status: string, runId: string) => void;
114
129
  }
115
130
 
116
131
  // ── WorkflowBuilder ─────────────────────────────────────────────────────────
@@ -177,6 +192,13 @@ export class WorkflowBuilder {
177
192
 
178
193
  /** Set the relay channel for agent communication. */
179
194
  channel(ch: string): this {
195
+ const CHANNEL_RE = /^[a-z0-9][a-z0-9-]*$/;
196
+ if (!CHANNEL_RE.test(ch)) {
197
+ throw new Error(
198
+ `Invalid channel name "${ch}". Channel names must be lowercase alphanumeric and hyphens, starting with a letter or number. ` +
199
+ `Fix: use .toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')`
200
+ );
201
+ }
180
202
  this._channel = ch;
181
203
  return this;
182
204
  }
@@ -229,6 +251,7 @@ export class WorkflowBuilder {
229
251
  if (options.channels !== undefined) def.channels = options.channels;
230
252
  if (options.preset !== undefined) def.preset = options.preset;
231
253
  if (options.interactive !== undefined) def.interactive = options.interactive;
254
+ if (options.skills !== undefined) def.skills = options.skills;
232
255
 
233
256
  if (
234
257
  options.model !== undefined ||
@@ -365,6 +388,7 @@ export class WorkflowBuilder {
365
388
  cwd: options.cwd,
366
389
  relay: options.relay,
367
390
  executor: options.executor,
391
+ envSecrets: options.envSecrets,
368
392
  });
369
393
 
370
394
  // Auto-detect DRY_RUN env var so existing scripts get dry-run for free
@@ -376,6 +400,22 @@ export class WorkflowBuilder {
376
400
  return report;
377
401
  }
378
402
 
403
+ // Cloud execution path — submit to remote API and poll for completion
404
+ if (options.cloud) {
405
+ const cloudApiUrl = options.cloudApiUrl ?? process.env.CLOUD_API_URL;
406
+ const cloudApiToken = options.cloudApiToken ?? process.env.CLOUD_API_TOKEN;
407
+ if (!cloudApiUrl) throw new Error('cloud: true requires cloudApiUrl or CLOUD_API_URL env var');
408
+ if (!cloudApiToken) throw new Error('cloud: true requires cloudApiToken or CLOUD_API_TOKEN env var');
409
+ return runInCloud(config, {
410
+ cloudApiUrl,
411
+ cloudApiToken,
412
+ envSecrets: options.envSecrets,
413
+ pollIntervalMs: options.cloudPollIntervalMs,
414
+ timeoutMs: this._timeoutMs,
415
+ onStatusChange: options.onCloudStatusChange,
416
+ });
417
+ }
418
+
379
419
  // Wire up default console logger unless explicitly disabled
380
420
  // renderer: "listr" owns the terminal — skip console logger to avoid garbled output
381
421
  // renderer: false implies no output at all
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cloud workflow runner — submits workflows to AgentWorkforce cloud API
3
+ * and polls for completion.
4
+ */
5
+ import type { RelayYamlConfig, WorkflowRunRow, WorkflowRunStatus } from './types.js';
6
+
7
+ export interface CloudRunOptions {
8
+ cloudApiUrl: string;
9
+ cloudApiToken: string;
10
+ envSecrets?: Record<string, string>;
11
+ pollIntervalMs?: number;
12
+ timeoutMs?: number;
13
+ onStatusChange?: (status: WorkflowRunStatus, runId: string) => void;
14
+ }
15
+
16
+ export async function runInCloud(config: RelayYamlConfig, options: CloudRunOptions): Promise<WorkflowRunRow> {
17
+ const { cloudApiUrl, cloudApiToken, envSecrets, pollIntervalMs = 3000, timeoutMs = 1800000 } = options;
18
+ const baseUrl = cloudApiUrl.replace(/\/$/, '');
19
+
20
+ const { stringify: stringifyYaml } = await import('yaml');
21
+ const yamlStr = stringifyYaml(config);
22
+
23
+ const submitRes = await fetch(`${baseUrl}/api/v1/workflows/run`, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cloudApiToken}` },
26
+ body: JSON.stringify({ workflow: yamlStr, fileType: 'yaml' as const, ...(envSecrets ? { envSecrets } : {}) }),
27
+ });
28
+ if (!submitRes.ok) throw new Error(`Cloud submit failed (${submitRes.status}): ${await submitRes.text()}`);
29
+
30
+ const { runId } = (await submitRes.json()) as { runId: string; sandboxId: string; status: string };
31
+ const deadline = Date.now() + timeoutMs;
32
+ let lastStatus: WorkflowRunStatus = 'pending';
33
+
34
+ while (Date.now() < deadline) {
35
+ await new Promise(r => setTimeout(r, pollIntervalMs));
36
+ const statusRes = await fetch(`${baseUrl}/api/v1/workflows/runs/${runId}`, {
37
+ headers: { 'Authorization': `Bearer ${cloudApiToken}` },
38
+ });
39
+ if (!statusRes.ok) continue;
40
+
41
+ const data = (await statusRes.json()) as { runId: string; status: WorkflowRunStatus; error?: string; createdAt?: string; updatedAt?: string };
42
+ if (data.status !== lastStatus) { lastStatus = data.status; options.onStatusChange?.(lastStatus, runId); }
43
+
44
+ if (data.status === 'completed' || data.status === 'failed') {
45
+ return {
46
+ id: runId, workspaceId: '', workflowName: config.name ?? 'cloud-workflow',
47
+ pattern: (config.swarm?.pattern as any) ?? 'dag', status: data.status, config,
48
+ startedAt: data.createdAt ?? new Date().toISOString(),
49
+ completedAt: data.updatedAt ?? new Date().toISOString(),
50
+ error: data.error, createdAt: data.createdAt ?? new Date().toISOString(),
51
+ updatedAt: data.updatedAt ?? new Date().toISOString(),
52
+ };
53
+ }
54
+ }
55
+ throw new Error(`Cloud workflow timed out after ${timeoutMs}ms (runId: ${runId})`);
56
+ }
@@ -26,3 +26,5 @@ export { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
26
26
  export { formatDryRunReport } from './dry-run-format.js';
27
27
  export { createWorkflowRenderer, type WorkflowRenderer } from './listr-renderer.js';
28
28
  export { createDefaultEventLogger } from './default-logger.js';
29
+ export { executeApiStep, type ApiExecutorOptions } from './api-executor.js';
30
+ export type { CloudRunOptions } from './cloud-runner.js';
@@ -1,6 +1,7 @@
1
1
  import type { AgentRelayOptions } from '../relay.js';
2
2
  import type { DryRunReport, TrajectoryConfig, WorkflowRunRow } from './types.js';
3
3
  import { WorkflowRunner, type WorkflowEventListener, type VariableContext } from './runner.js';
4
+ import { createDefaultEventLogger } from './default-logger.js';
4
5
  import { formatDryRunReport } from './dry-run-format.js';
5
6
 
6
7
  /**
@@ -70,6 +71,10 @@ export async function runWorkflow(
70
71
  return report;
71
72
  }
72
73
 
74
+ // Attach default console logger so callers get progress output without
75
+ // needing to wire up their own handler.
76
+ runner.on(createDefaultEventLogger('normal'));
77
+
73
78
  if (options.onEvent) {
74
79
  runner.on(options.onEvent);
75
80
  }