agent-relay 3.2.2 → 3.2.4
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 +1358 -941
- package/dist/src/cli/commands/agent-management.d.ts +2 -2
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +41 -240
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +14 -5
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/lib/agent-management-listing.d.ts +4 -1
- package/dist/src/cli/lib/agent-management-listing.d.ts.map +1 -1
- package/dist/src/cli/lib/agent-management-listing.js +27 -2
- package/dist/src/cli/lib/agent-management-listing.js.map +1 -1
- package/package.json +11 -10
- 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/ADAPTER_REVIEW.md +109 -0
- package/packages/sdk/dist/client.d.ts +66 -0
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +230 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.js +220 -0
- package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
- package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.js +209 -0
- package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.js +7 -0
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
- package/packages/sdk/dist/communicate/core.d.ts +58 -0
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/core.js +128 -0
- package/packages/sdk/dist/communicate/core.js.map +1 -0
- package/packages/sdk/dist/communicate/index.d.ts +4 -0
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/index.js +4 -0
- package/packages/sdk/dist/communicate/index.js.map +1 -0
- package/packages/sdk/dist/communicate/transport.d.ts +36 -0
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/transport.js +371 -0
- package/packages/sdk/dist/communicate/transport.js.map +1 -0
- package/packages/sdk/dist/communicate/types.d.ts +58 -0
- package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/types.js +66 -0
- package/packages/sdk/dist/communicate/types.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +35 -5
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +81 -7
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cli.js +14 -1
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +10 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +95 -1
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +11 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
- package/packages/sdk/examples/communicate/pi_example.ts +8 -0
- package/packages/sdk/package.json +48 -2
- package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
- package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
- package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
- package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
- package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
- package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
- package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
- package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
- package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
- package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
- package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
- package/packages/sdk/src/client.ts +301 -0
- package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
- package/packages/sdk/src/communicate/a2a-server.ts +277 -0
- package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
- package/packages/sdk/src/communicate/a2a-types.ts +338 -0
- package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
- package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
- package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
- package/packages/sdk/src/communicate/adapters/index.ts +6 -0
- package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
- package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
- package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
- package/packages/sdk/src/communicate/core.ts +157 -0
- package/packages/sdk/src/communicate/index.ts +3 -0
- package/packages/sdk/src/communicate/transport.ts +489 -0
- package/packages/sdk/src/communicate/types.ts +106 -0
- package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
- package/packages/sdk/src/workflows/builder.ts +97 -9
- package/packages/sdk/src/workflows/cli.ts +16 -1
- package/packages/sdk/src/workflows/runner.ts +110 -1
- package/packages/sdk/src/workflows/types.ts +14 -0
- package/packages/sdk/tsconfig.build.json +1 -7
- package/packages/sdk/tsconfig.json +1 -7
- package/packages/sdk-py/README.md +67 -25
- package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
- package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
- package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
- package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
- package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
- package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
- package/packages/sdk-py/pyproject.toml +12 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
- package/packages/sdk-py/src/agent_relay/builder.py +65 -26
- package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
- package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
- package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
- package/packages/sdk-py/src/agent_relay/types.py +2 -1
- package/packages/sdk-py/tests/communicate/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
- package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
- package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
- package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
- package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
- package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
- package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
- package/packages/sdk-py/tests/communicate/conftest.py +555 -0
- package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
- package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
- package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
- package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
- package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
- package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
- package/packages/sdk-py/tests/communicate/test_core.py +331 -0
- package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
- package/packages/sdk-py/tests/communicate/test_types.py +285 -0
- package/packages/sdk-py/tests/test_builder_deterministic.py +118 -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
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.js +0 -305
- package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.js +0 -205
- package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.js +0 -20
- package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
- package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.js +0 -357
- package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
- package/packages/sdk-py/agent_relay/__init__.py +0 -21
- package/packages/sdk-py/agent_relay/models.py +0 -398
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the startFrom workflow execution feature.
|
|
3
|
+
*
|
|
4
|
+
* Validates that callers can start a workflow from a specific step,
|
|
5
|
+
* skipping all predecessor steps and loading cached outputs when available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import type { WorkflowDb } from '../workflows/runner.js';
|
|
13
|
+
import type { RelayYamlConfig, WorkflowRunRow, WorkflowStepRow } from '../workflows/types.js';
|
|
14
|
+
|
|
15
|
+
// ── Mock fetch ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
18
|
+
ok: true,
|
|
19
|
+
json: () => Promise.resolve({ data: { api_key: 'rk_live_test', workspace_id: 'ws-test' } }),
|
|
20
|
+
text: () => Promise.resolve(''),
|
|
21
|
+
});
|
|
22
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
23
|
+
|
|
24
|
+
// ── Mock RelayCast SDK ───────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const mockRelaycastAgent = {
|
|
27
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
heartbeat: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
channels: {
|
|
30
|
+
create: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
join: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
invite: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mockRelaycast = {
|
|
37
|
+
agents: {
|
|
38
|
+
register: vi.fn().mockResolvedValue({ token: 'token-1' }),
|
|
39
|
+
},
|
|
40
|
+
as: vi.fn().mockReturnValue(mockRelaycastAgent),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class MockRelayError extends Error {
|
|
44
|
+
code: string;
|
|
45
|
+
constructor(code: string, message: string, status = 400) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.name = 'RelayError';
|
|
49
|
+
(this as any).status = status;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
vi.mock('@relaycast/sdk', () => ({
|
|
54
|
+
RelayCast: vi.fn().mockImplementation(() => mockRelaycast),
|
|
55
|
+
RelayError: MockRelayError,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// ── Mock AgentRelay ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
let waitForExitFn: (ms?: number) => Promise<'exited' | 'timeout' | 'released'>;
|
|
61
|
+
|
|
62
|
+
const mockAgent = {
|
|
63
|
+
name: 'test-agent-abc',
|
|
64
|
+
get waitForExit() { return waitForExitFn; },
|
|
65
|
+
get waitForIdle() { return vi.fn().mockImplementation(() => new Promise(() => {})); },
|
|
66
|
+
release: vi.fn().mockResolvedValue(undefined),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const mockHuman = {
|
|
70
|
+
name: 'WorkflowRunner',
|
|
71
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mockRelayInstance = {
|
|
75
|
+
spawnPty: vi.fn().mockImplementation(async ({ name, task }: { name: string; task?: string }) => {
|
|
76
|
+
const stepComplete = task?.match(/STEP_COMPLETE:([^\n]+)/)?.[1]?.trim();
|
|
77
|
+
const isReview = task?.includes('REVIEW_DECISION: APPROVE or REJECT');
|
|
78
|
+
const output = isReview
|
|
79
|
+
? 'REVIEW_DECISION: APPROVE\nREVIEW_REASON: looks good\n'
|
|
80
|
+
: stepComplete
|
|
81
|
+
? `STEP_COMPLETE:${stepComplete}\n`
|
|
82
|
+
: 'STEP_COMPLETE:unknown\n';
|
|
83
|
+
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
if (typeof mockRelayInstance.onWorkerOutput === 'function') {
|
|
86
|
+
mockRelayInstance.onWorkerOutput({ name, chunk: output });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { ...mockAgent, name };
|
|
91
|
+
}),
|
|
92
|
+
human: vi.fn().mockReturnValue(mockHuman),
|
|
93
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
94
|
+
onBrokerStderr: vi.fn().mockReturnValue(() => {}),
|
|
95
|
+
onWorkerOutput: null as ((frame: { name: string; chunk: string }) => void) | null,
|
|
96
|
+
onMessageReceived: null as any,
|
|
97
|
+
onAgentSpawned: null as any,
|
|
98
|
+
onAgentReleased: null as any,
|
|
99
|
+
onAgentExited: null as any,
|
|
100
|
+
onAgentIdle: null as any,
|
|
101
|
+
onDeliveryUpdate: null as any,
|
|
102
|
+
listAgentsRaw: vi.fn().mockResolvedValue([]),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
vi.mock('../relay.js', () => ({
|
|
106
|
+
AgentRelay: vi.fn().mockImplementation(() => mockRelayInstance),
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// Import after mocking
|
|
110
|
+
const { WorkflowRunner } = await import('../workflows/runner.js');
|
|
111
|
+
const { workflow } = await import('../workflows/builder.js');
|
|
112
|
+
|
|
113
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function makeDb(): WorkflowDb {
|
|
116
|
+
const runs = new Map<string, WorkflowRunRow>();
|
|
117
|
+
const steps = new Map<string, WorkflowStepRow>();
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
insertRun: vi.fn(async (run: WorkflowRunRow) => {
|
|
121
|
+
runs.set(run.id, { ...run });
|
|
122
|
+
}),
|
|
123
|
+
updateRun: vi.fn(async (id: string, patch: Partial<WorkflowRunRow>) => {
|
|
124
|
+
const existing = runs.get(id);
|
|
125
|
+
if (existing) runs.set(id, { ...existing, ...patch });
|
|
126
|
+
}),
|
|
127
|
+
getRun: vi.fn(async (id: string) => {
|
|
128
|
+
const run = runs.get(id);
|
|
129
|
+
return run ? { ...run } : null;
|
|
130
|
+
}),
|
|
131
|
+
insertStep: vi.fn(async (step: WorkflowStepRow) => {
|
|
132
|
+
steps.set(step.id, { ...step });
|
|
133
|
+
}),
|
|
134
|
+
updateStep: vi.fn(async (id: string, patch: Partial<WorkflowStepRow>) => {
|
|
135
|
+
const existing = steps.get(id);
|
|
136
|
+
if (existing) steps.set(id, { ...existing, ...patch });
|
|
137
|
+
}),
|
|
138
|
+
getStepsByRunId: vi.fn(async (runId: string) => {
|
|
139
|
+
return [...steps.values()].filter((s) => s.runId === runId);
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeLinearConfig(): RelayYamlConfig {
|
|
145
|
+
return {
|
|
146
|
+
version: '1',
|
|
147
|
+
name: 'test-start-from',
|
|
148
|
+
swarm: { pattern: 'dag' },
|
|
149
|
+
agents: [
|
|
150
|
+
{ name: 'agent-a', cli: 'claude' },
|
|
151
|
+
],
|
|
152
|
+
workflows: [
|
|
153
|
+
{
|
|
154
|
+
name: 'default',
|
|
155
|
+
steps: [
|
|
156
|
+
{ name: 'step-1', agent: 'agent-a', task: 'Do step 1' },
|
|
157
|
+
{ name: 'step-2', agent: 'agent-a', task: 'Do step 2', dependsOn: ['step-1'] },
|
|
158
|
+
{ name: 'step-3', agent: 'agent-a', task: 'Do step 3', dependsOn: ['step-2'] },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
trajectories: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function makeDiamondConfig(): RelayYamlConfig {
|
|
167
|
+
return {
|
|
168
|
+
version: '1',
|
|
169
|
+
name: 'test-diamond',
|
|
170
|
+
swarm: { pattern: 'dag' },
|
|
171
|
+
agents: [
|
|
172
|
+
{ name: 'agent-a', cli: 'claude' },
|
|
173
|
+
],
|
|
174
|
+
workflows: [
|
|
175
|
+
{
|
|
176
|
+
name: 'default',
|
|
177
|
+
steps: [
|
|
178
|
+
{ name: 'root', agent: 'agent-a', task: 'Root step' },
|
|
179
|
+
{ name: 'left', agent: 'agent-a', task: 'Left branch', dependsOn: ['root'] },
|
|
180
|
+
{ name: 'right', agent: 'agent-a', task: 'Right branch', dependsOn: ['root'] },
|
|
181
|
+
{ name: 'merge', agent: 'agent-a', task: 'Merge', dependsOn: ['left', 'right'] },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
trajectories: false,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe('startFrom', () => {
|
|
192
|
+
let db: WorkflowDb;
|
|
193
|
+
let runner: InstanceType<typeof WorkflowRunner>;
|
|
194
|
+
let tmpDir: string;
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
vi.clearAllMocks();
|
|
198
|
+
waitForExitFn = vi.fn().mockResolvedValue('exited');
|
|
199
|
+
mockRelayInstance.onWorkerOutput = null;
|
|
200
|
+
tmpDir = mkdtempSync(path.join(os.tmpdir(), 'start-from-'));
|
|
201
|
+
db = makeDb();
|
|
202
|
+
runner = new WorkflowRunner({ db, workspaceId: 'ws-test', cwd: tmpDir });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should throw when startFrom step does not exist', async () => {
|
|
206
|
+
const config = makeLinearConfig();
|
|
207
|
+
await expect(
|
|
208
|
+
runner.execute(config, 'default', undefined, { startFrom: 'nonexistent' })
|
|
209
|
+
).rejects.toThrow('startFrom step "nonexistent" not found in workflow');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should skip predecessor steps in a linear chain', async () => {
|
|
213
|
+
const config = makeLinearConfig();
|
|
214
|
+
const events: Array<{ type: string; stepName?: string }> = [];
|
|
215
|
+
runner.on((event) => {
|
|
216
|
+
if ('stepName' in event) {
|
|
217
|
+
events.push({ type: event.type, stepName: event.stepName });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const run = await runner.execute(config, 'default', undefined, { startFrom: 'step-3' });
|
|
222
|
+
expect(run.status, run.error).toBe('completed');
|
|
223
|
+
|
|
224
|
+
// step-1 and step-2 should NOT have step:started events (they were pre-completed)
|
|
225
|
+
const startedSteps = events.filter((e) => e.type === 'step:started').map((e) => e.stepName);
|
|
226
|
+
expect(startedSteps).not.toContain('step-1');
|
|
227
|
+
expect(startedSteps).not.toContain('step-2');
|
|
228
|
+
expect(startedSteps).toContain('step-3');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should skip all transitive deps in a diamond DAG', async () => {
|
|
232
|
+
const config = makeDiamondConfig();
|
|
233
|
+
const events: Array<{ type: string; stepName?: string }> = [];
|
|
234
|
+
runner.on((event) => {
|
|
235
|
+
if ('stepName' in event) {
|
|
236
|
+
events.push({ type: event.type, stepName: event.stepName });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const run = await runner.execute(config, 'default', undefined, { startFrom: 'merge' });
|
|
241
|
+
expect(run.status, run.error).toBe('completed');
|
|
242
|
+
|
|
243
|
+
const startedSteps = events.filter((e) => e.type === 'step:started').map((e) => e.stepName);
|
|
244
|
+
expect(startedSteps).not.toContain('root');
|
|
245
|
+
expect(startedSteps).not.toContain('left');
|
|
246
|
+
expect(startedSteps).not.toContain('right');
|
|
247
|
+
expect(startedSteps).toContain('merge');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should load cached output from disk for skipped steps', async () => {
|
|
251
|
+
const config = makeLinearConfig();
|
|
252
|
+
|
|
253
|
+
// Pre-create cached output for step-1 (simulating a prior run)
|
|
254
|
+
// We need to intercept the runId to write to the correct path.
|
|
255
|
+
// Instead, we'll verify updateStep was called with expected output.
|
|
256
|
+
const run = await runner.execute(config, 'default', undefined, { startFrom: 'step-2' });
|
|
257
|
+
expect(run.status, run.error).toBe('completed');
|
|
258
|
+
|
|
259
|
+
// step-1 should have been marked completed with empty string (no cached output)
|
|
260
|
+
const updateCalls = (db.updateStep as any).mock.calls as Array<[string, Partial<WorkflowStepRow>]>;
|
|
261
|
+
const step1Completion = updateCalls.find(
|
|
262
|
+
([_, patch]) => patch.status === 'completed' && patch.output === ''
|
|
263
|
+
);
|
|
264
|
+
expect(step1Completion).toBeDefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should load cached output when available on disk via previousRunId', async () => {
|
|
268
|
+
const config = makeLinearConfig();
|
|
269
|
+
|
|
270
|
+
// Write cached output for step-1 under a known previous run ID
|
|
271
|
+
const prevRunId = 'prev-run-abc123';
|
|
272
|
+
const outputDir = path.join(tmpDir, '.agent-relay', 'step-outputs', prevRunId);
|
|
273
|
+
mkdirSync(outputDir, { recursive: true });
|
|
274
|
+
writeFileSync(path.join(outputDir, 'step-1.md'), 'cached-output-from-step-1');
|
|
275
|
+
|
|
276
|
+
const run = await runner.execute(config, 'default', undefined, {
|
|
277
|
+
startFrom: 'step-2',
|
|
278
|
+
previousRunId: prevRunId,
|
|
279
|
+
});
|
|
280
|
+
expect(run.status, run.error).toBe('completed');
|
|
281
|
+
|
|
282
|
+
// Verify step-1 was marked completed with the cached output
|
|
283
|
+
const updateCalls = (db.updateStep as any).mock.calls as Array<[string, Partial<WorkflowStepRow>]>;
|
|
284
|
+
const step1WithCachedOutput = updateCalls.find(
|
|
285
|
+
([_, patch]) => patch.status === 'completed' && patch.output === 'cached-output-from-step-1'
|
|
286
|
+
);
|
|
287
|
+
expect(step1WithCachedOutput).toBeDefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should work when startFrom targets the first step (no deps to skip)', async () => {
|
|
291
|
+
const config = makeLinearConfig();
|
|
292
|
+
const events: Array<{ type: string; stepName?: string }> = [];
|
|
293
|
+
runner.on((event) => {
|
|
294
|
+
if ('stepName' in event) {
|
|
295
|
+
events.push({ type: event.type, stepName: event.stepName });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const run = await runner.execute(config, 'default', undefined, { startFrom: 'step-1' });
|
|
300
|
+
expect(run.status, run.error).toBe('completed');
|
|
301
|
+
|
|
302
|
+
// All 3 steps should start since step-1 has no deps
|
|
303
|
+
const startedSteps = events.filter((e) => e.type === 'step:started').map((e) => e.stepName);
|
|
304
|
+
expect(startedSteps).toContain('step-1');
|
|
305
|
+
expect(startedSteps).toContain('step-2');
|
|
306
|
+
expect(startedSteps).toContain('step-3');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should work with builder .startFrom() chainable method', () => {
|
|
310
|
+
const config = workflow('test')
|
|
311
|
+
.agent('worker', { cli: 'claude' })
|
|
312
|
+
.step('build', { agent: 'worker', task: 'Build' })
|
|
313
|
+
.step('test', { agent: 'worker', task: 'Test', dependsOn: ['build'] })
|
|
314
|
+
.step('deploy', { agent: 'worker', task: 'Deploy', dependsOn: ['test'] })
|
|
315
|
+
.startFrom('deploy')
|
|
316
|
+
.toConfig();
|
|
317
|
+
|
|
318
|
+
// toConfig() should still produce valid config — startFrom is a runtime option
|
|
319
|
+
expect(config.workflows![0].steps).toHaveLength(3);
|
|
320
|
+
expect(config.agents).toHaveLength(1);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should pass startFrom from WorkflowRunOptions', async () => {
|
|
324
|
+
const config = makeLinearConfig();
|
|
325
|
+
const events: Array<{ type: string; stepName?: string }> = [];
|
|
326
|
+
|
|
327
|
+
// Test via runner.execute directly with options
|
|
328
|
+
runner.on((event) => {
|
|
329
|
+
if ('stepName' in event) {
|
|
330
|
+
events.push({ type: event.type, stepName: event.stepName });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const run = await runner.execute(config, 'default', undefined, { startFrom: 'step-2' });
|
|
335
|
+
expect(run.status, run.error).toBe('completed');
|
|
336
|
+
|
|
337
|
+
const startedSteps = events.filter((e) => e.type === 'step:started').map((e) => e.stepName);
|
|
338
|
+
expect(startedSteps).not.toContain('step-1');
|
|
339
|
+
expect(startedSteps).toContain('step-2');
|
|
340
|
+
expect(startedSteps).toContain('step-3');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
afterEach(() => {
|
|
344
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -6,6 +6,8 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
|
+
import { getProjectPaths } from '@agent-relay/config';
|
|
10
|
+
|
|
9
11
|
import {
|
|
10
12
|
PROTOCOL_VERSION,
|
|
11
13
|
type AgentRuntime,
|
|
@@ -886,3 +888,302 @@ function resolveDefaultBinaryPath(): string {
|
|
|
886
888
|
// 4. Auto-install from GitHub releases
|
|
887
889
|
return installBrokerBinary();
|
|
888
890
|
}
|
|
891
|
+
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// HTTP transport client — connects to an already-running broker's HTTP API
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
export interface HttpAgentRelayClientOptions {
|
|
897
|
+
port: number;
|
|
898
|
+
apiKey?: string;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export interface DiscoverAndConnectOptions {
|
|
902
|
+
cwd?: string;
|
|
903
|
+
apiKey?: string;
|
|
904
|
+
/** Auto-start the broker if not running (default: false). */
|
|
905
|
+
autoStart?: boolean;
|
|
906
|
+
/**
|
|
907
|
+
* Path to the broker binary for auto-start.
|
|
908
|
+
* If not provided, the SDK resolves it automatically via standard install locations
|
|
909
|
+
* (~/.agent-relay/bin, bundled platform binary, or Cargo release build).
|
|
910
|
+
* Only used when `autoStart: true`.
|
|
911
|
+
*/
|
|
912
|
+
brokerBinaryPath?: string;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const DEFAULT_DASHBOARD_PORT = (() => {
|
|
916
|
+
const envPort = typeof process !== 'undefined' ? process.env.AGENT_RELAY_DASHBOARD_PORT : undefined;
|
|
917
|
+
if (envPort) {
|
|
918
|
+
const parsed = Number.parseInt(envPort, 10);
|
|
919
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
920
|
+
}
|
|
921
|
+
return 3888;
|
|
922
|
+
})();
|
|
923
|
+
const HTTP_MAX_PORT_SCAN = 25;
|
|
924
|
+
const HTTP_AUTOSTART_TIMEOUT_MS = 10_000;
|
|
925
|
+
const HTTP_AUTOSTART_POLL_MS = 250;
|
|
926
|
+
|
|
927
|
+
function sanitizeBrokerName(name: string): string {
|
|
928
|
+
return name.replace(/[^\p{L}\p{N}-]/gu, '-');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function brokerPidFilename(projectRoot: string): string {
|
|
932
|
+
const brokerName = path.basename(projectRoot) || 'project';
|
|
933
|
+
return `broker-${sanitizeBrokerName(brokerName)}.pid`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export class HttpAgentRelayClient {
|
|
937
|
+
private readonly port: number;
|
|
938
|
+
private readonly apiKey?: string;
|
|
939
|
+
|
|
940
|
+
constructor(options: HttpAgentRelayClientOptions) {
|
|
941
|
+
this.port = options.port;
|
|
942
|
+
this.apiKey = options.apiKey;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Connect to an already-running broker on the given port.
|
|
947
|
+
*/
|
|
948
|
+
static async connectHttp(
|
|
949
|
+
port: number,
|
|
950
|
+
options?: { apiKey?: string }
|
|
951
|
+
): Promise<HttpAgentRelayClient> {
|
|
952
|
+
const client = new HttpAgentRelayClient({ port, apiKey: options?.apiKey });
|
|
953
|
+
// Verify connectivity
|
|
954
|
+
await client.healthCheck();
|
|
955
|
+
return client;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Discover a running broker for the current project and connect to it.
|
|
960
|
+
* Reads the broker PID file, verifies the process is alive, scans ports
|
|
961
|
+
* for the HTTP API, and returns a connected client.
|
|
962
|
+
*/
|
|
963
|
+
static async discoverAndConnect(
|
|
964
|
+
options?: DiscoverAndConnectOptions
|
|
965
|
+
): Promise<HttpAgentRelayClient> {
|
|
966
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
967
|
+
const apiKey = options?.apiKey ?? process.env.RELAY_BROKER_API_KEY?.trim();
|
|
968
|
+
const autoStart = options?.autoStart ?? false;
|
|
969
|
+
const paths = getProjectPaths(cwd);
|
|
970
|
+
const preferredApiPort = DEFAULT_DASHBOARD_PORT + 1;
|
|
971
|
+
|
|
972
|
+
// Try to find a running broker via PID file
|
|
973
|
+
const pidFilePath = path.join(paths.dataDir, brokerPidFilename(paths.projectRoot));
|
|
974
|
+
const legacyPidPath = path.join(paths.dataDir, 'broker.pid');
|
|
975
|
+
let brokerRunning = false;
|
|
976
|
+
|
|
977
|
+
for (const pidPath of [pidFilePath, legacyPidPath]) {
|
|
978
|
+
if (fs.existsSync(pidPath)) {
|
|
979
|
+
const pidStr = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
980
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
981
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
982
|
+
try {
|
|
983
|
+
process.kill(pid, 0);
|
|
984
|
+
brokerRunning = true;
|
|
985
|
+
break;
|
|
986
|
+
} catch {
|
|
987
|
+
// Process not running
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (brokerRunning) {
|
|
994
|
+
const port = await HttpAgentRelayClient.scanForBrokerPort(preferredApiPort);
|
|
995
|
+
if (port !== null) {
|
|
996
|
+
return new HttpAgentRelayClient({ port, apiKey });
|
|
997
|
+
}
|
|
998
|
+
throw new AgentRelayProcessError(
|
|
999
|
+
'broker is running for this project, but its local API is unavailable'
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (!autoStart) {
|
|
1004
|
+
throw new AgentRelayProcessError('broker is not running for this project');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Auto-start the broker using the resolved binary path (not process.argv[1],
|
|
1008
|
+
// which only works from CLI context — breaks when SDK is imported by user apps).
|
|
1009
|
+
// The broker binary requires the `init` subcommand with `--api-port` and
|
|
1010
|
+
// `--persist` so it writes PID files for subsequent discovery.
|
|
1011
|
+
const brokerBinary = options?.brokerBinaryPath ?? resolveDefaultBinaryPath();
|
|
1012
|
+
|
|
1013
|
+
const child = spawn(
|
|
1014
|
+
brokerBinary,
|
|
1015
|
+
['init', '--persist', '--api-port', String(preferredApiPort)],
|
|
1016
|
+
{
|
|
1017
|
+
cwd: paths.projectRoot,
|
|
1018
|
+
env: process.env,
|
|
1019
|
+
detached: true,
|
|
1020
|
+
stdio: 'ignore',
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
child.unref();
|
|
1024
|
+
|
|
1025
|
+
const startedAt = Date.now();
|
|
1026
|
+
while (Date.now() - startedAt < HTTP_AUTOSTART_TIMEOUT_MS) {
|
|
1027
|
+
const port = await HttpAgentRelayClient.scanForBrokerPort(preferredApiPort);
|
|
1028
|
+
if (port !== null) {
|
|
1029
|
+
return new HttpAgentRelayClient({ port, apiKey });
|
|
1030
|
+
}
|
|
1031
|
+
await new Promise((resolve) => setTimeout(resolve, HTTP_AUTOSTART_POLL_MS));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
throw new AgentRelayProcessError(
|
|
1035
|
+
`broker did not become ready within ${HTTP_AUTOSTART_TIMEOUT_MS}ms`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private static async scanForBrokerPort(startPort: number): Promise<number | null> {
|
|
1040
|
+
for (let i = 0; i < HTTP_MAX_PORT_SCAN; i++) {
|
|
1041
|
+
const port = startPort + i;
|
|
1042
|
+
try {
|
|
1043
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
1044
|
+
if (!res.ok) continue;
|
|
1045
|
+
const payload = (await res.json().catch(() => null)) as { service?: string } | null;
|
|
1046
|
+
if (payload?.service === 'agent-relay-listen') {
|
|
1047
|
+
return port;
|
|
1048
|
+
}
|
|
1049
|
+
} catch {
|
|
1050
|
+
// Keep scanning
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private async request<T = unknown>(pathname: string, init?: RequestInit): Promise<T> {
|
|
1057
|
+
const headers = new Headers(init?.headers);
|
|
1058
|
+
if (this.apiKey && !headers.has('x-api-key') && !headers.has('authorization')) {
|
|
1059
|
+
headers.set('x-api-key', this.apiKey);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const response = await fetch(`http://127.0.0.1:${this.port}${pathname}`, {
|
|
1063
|
+
...init,
|
|
1064
|
+
headers,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const text = await response.text();
|
|
1068
|
+
let payload: unknown;
|
|
1069
|
+
try {
|
|
1070
|
+
payload = text ? JSON.parse(text) : undefined;
|
|
1071
|
+
} catch {
|
|
1072
|
+
payload = text;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (!response.ok) {
|
|
1076
|
+
const msg = HttpAgentRelayClient.extractErrorMessage(response, payload);
|
|
1077
|
+
throw new AgentRelayProcessError(msg);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return payload as T;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
private static extractErrorMessage(response: Response, payload: unknown): string {
|
|
1084
|
+
if (typeof payload === 'string' && payload.trim()) return payload.trim();
|
|
1085
|
+
const p = payload as Record<string, unknown> | undefined;
|
|
1086
|
+
if (typeof p?.error === 'string') return p.error;
|
|
1087
|
+
if (typeof (p?.error as Record<string, unknown>)?.message === 'string')
|
|
1088
|
+
return (p!.error as Record<string, unknown>).message as string;
|
|
1089
|
+
if (typeof p?.message === 'string' && (p.message as string).trim())
|
|
1090
|
+
return (p.message as string).trim();
|
|
1091
|
+
return `${response.status} ${response.statusText}`.trim();
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async healthCheck(): Promise<{ service: string }> {
|
|
1095
|
+
return this.request<{ service: string }>('/health');
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** No-op — broker is already running. */
|
|
1099
|
+
async start(): Promise<void> {}
|
|
1100
|
+
|
|
1101
|
+
/** No-op — don't kill an externally-managed broker. */
|
|
1102
|
+
async shutdown(): Promise<void> {}
|
|
1103
|
+
|
|
1104
|
+
async spawnPty(input: SpawnPtyInput): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
1105
|
+
const payload = await this.request<{ name?: string }>('/api/spawn', {
|
|
1106
|
+
method: 'POST',
|
|
1107
|
+
headers: { 'content-type': 'application/json' },
|
|
1108
|
+
body: JSON.stringify({
|
|
1109
|
+
name: input.name,
|
|
1110
|
+
cli: input.cli,
|
|
1111
|
+
model: input.model,
|
|
1112
|
+
args: input.args ?? [],
|
|
1113
|
+
task: input.task,
|
|
1114
|
+
channels: input.channels ?? [],
|
|
1115
|
+
cwd: input.cwd,
|
|
1116
|
+
team: input.team,
|
|
1117
|
+
shadowOf: input.shadowOf,
|
|
1118
|
+
shadowMode: input.shadowMode,
|
|
1119
|
+
continueFrom: input.continueFrom,
|
|
1120
|
+
idleThresholdSecs: input.idleThresholdSecs,
|
|
1121
|
+
restartPolicy: input.restartPolicy,
|
|
1122
|
+
skipRelayPrompt: input.skipRelayPrompt,
|
|
1123
|
+
}),
|
|
1124
|
+
});
|
|
1125
|
+
return {
|
|
1126
|
+
name: typeof payload?.name === 'string' ? payload.name : input.name,
|
|
1127
|
+
runtime: 'pty',
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async sendMessage(input: SendMessageInput): Promise<{ event_id: string; targets: string[] }> {
|
|
1132
|
+
return this.request<{ event_id: string; targets: string[] }>('/api/send', {
|
|
1133
|
+
method: 'POST',
|
|
1134
|
+
headers: { 'content-type': 'application/json' },
|
|
1135
|
+
body: JSON.stringify({
|
|
1136
|
+
to: input.to,
|
|
1137
|
+
text: input.text,
|
|
1138
|
+
from: input.from,
|
|
1139
|
+
threadId: input.threadId,
|
|
1140
|
+
workspaceId: input.workspaceId,
|
|
1141
|
+
workspaceAlias: input.workspaceAlias,
|
|
1142
|
+
priority: input.priority,
|
|
1143
|
+
data: input.data,
|
|
1144
|
+
}),
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async listAgents(): Promise<ListAgent[]> {
|
|
1149
|
+
const payload = await this.request<{ agents?: ListAgent[] }>('/api/spawned', { method: 'GET' });
|
|
1150
|
+
return Array.isArray(payload?.agents) ? payload.agents : [];
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async release(name: string, reason?: string): Promise<{ name: string }> {
|
|
1154
|
+
const payload = await this.request<{ name?: string }>(
|
|
1155
|
+
`/api/spawned/${encodeURIComponent(name)}`,
|
|
1156
|
+
{
|
|
1157
|
+
method: 'DELETE',
|
|
1158
|
+
...(reason
|
|
1159
|
+
? { headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) }
|
|
1160
|
+
: {}),
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
return { name: typeof payload?.name === 'string' ? payload.name : name };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async setModel(
|
|
1167
|
+
name: string,
|
|
1168
|
+
model: string,
|
|
1169
|
+
opts?: { timeoutMs?: number }
|
|
1170
|
+
): Promise<{ name: string; model: string; success: boolean }> {
|
|
1171
|
+
const payload = await this.request<{ success?: boolean; model?: string }>(
|
|
1172
|
+
`/api/spawned/${encodeURIComponent(name)}/model`,
|
|
1173
|
+
{
|
|
1174
|
+
method: 'POST',
|
|
1175
|
+
headers: { 'content-type': 'application/json' },
|
|
1176
|
+
body: JSON.stringify({ model, timeoutMs: opts?.timeoutMs }),
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
return {
|
|
1180
|
+
name,
|
|
1181
|
+
model: typeof payload?.model === 'string' ? payload.model : model,
|
|
1182
|
+
success: payload?.success !== false,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async getConfig(): Promise<{ workspace_key?: string }> {
|
|
1187
|
+
return this.request<{ workspace_key?: string }>('/api/config');
|
|
1188
|
+
}
|
|
1189
|
+
}
|