agent-relay 3.2.15 → 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 +3853 -17163
- 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 +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/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 +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/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
|
@@ -33,7 +33,14 @@ import {
|
|
|
33
33
|
type SendMessageInput,
|
|
34
34
|
type SpawnPtyInput,
|
|
35
35
|
} from './client.js';
|
|
36
|
-
import type {
|
|
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: {
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
}
|