agent-relay 3.2.3 → 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/dist/index.cjs +265 -108
- 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/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/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
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge that connects an external A2A agent into a Relay workspace.
|
|
3
|
+
*
|
|
4
|
+
* - Registers a proxy agent on the Relay workspace
|
|
5
|
+
* - When Relay messages arrive for the proxy, forwards them as A2A JSON-RPC
|
|
6
|
+
* message/send calls to the external agent
|
|
7
|
+
* - When A2A responses come back, forwards them as Relay DMs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
import { Relay } from './core.js';
|
|
13
|
+
import {
|
|
14
|
+
type A2AAgentCard,
|
|
15
|
+
a2aAgentCardFromDict,
|
|
16
|
+
} from './a2a-types.js';
|
|
17
|
+
import type { Message, RelayConfig } from './types.js';
|
|
18
|
+
|
|
19
|
+
export class A2ABridge {
|
|
20
|
+
readonly relay: Relay;
|
|
21
|
+
readonly a2aAgentUrl: string;
|
|
22
|
+
readonly proxyName: string;
|
|
23
|
+
|
|
24
|
+
private _agentCard?: A2AAgentCard;
|
|
25
|
+
private _started = false;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
relayConfig: RelayConfig,
|
|
29
|
+
a2aAgentUrl: string,
|
|
30
|
+
proxyName: string,
|
|
31
|
+
) {
|
|
32
|
+
this.relay = new Relay(proxyName, relayConfig);
|
|
33
|
+
this.a2aAgentUrl = a2aAgentUrl.replace(/\/+$/, '');
|
|
34
|
+
this.proxyName = proxyName;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start(): Promise<void> {
|
|
38
|
+
this.relay.onMessage((msg) => this._handleRelayMessage(msg));
|
|
39
|
+
this._started = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async stop(): Promise<void> {
|
|
43
|
+
this._started = false;
|
|
44
|
+
await this.relay.close();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async discoverAgent(): Promise<A2AAgentCard> {
|
|
48
|
+
const url = `${this.a2aAgentUrl}/.well-known/agent.json`;
|
|
49
|
+
const response = await fetch(url);
|
|
50
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
51
|
+
this._agentCard = a2aAgentCardFromDict(data);
|
|
52
|
+
return this._agentCard;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async sendA2AMessage(text: string): Promise<string | null> {
|
|
56
|
+
const a2aMsg = {
|
|
57
|
+
role: 'user' as const,
|
|
58
|
+
parts: [{ text }],
|
|
59
|
+
messageId: randomUUID(),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const jsonrpcRequest = {
|
|
63
|
+
jsonrpc: '2.0' as const,
|
|
64
|
+
method: 'message/send',
|
|
65
|
+
params: { message: a2aMsg },
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const targetUrl = this._agentCard?.url ?? this.a2aAgentUrl;
|
|
70
|
+
|
|
71
|
+
const response = await fetch(targetUrl, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify(jsonrpcRequest),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
78
|
+
const result = (data.result ?? {}) as Record<string, unknown>;
|
|
79
|
+
|
|
80
|
+
// Extract response text from task status message
|
|
81
|
+
const status = (result.status ?? {}) as Record<string, unknown>;
|
|
82
|
+
const statusMsg = (status.message ?? {}) as Record<string, unknown>;
|
|
83
|
+
const statusParts = (statusMsg.parts ?? []) as Record<string, unknown>[];
|
|
84
|
+
if (statusParts.length > 0) {
|
|
85
|
+
const t = statusParts[0].text as string | undefined;
|
|
86
|
+
if (t) return t;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Try from messages list
|
|
90
|
+
const messages = (result.messages ?? []) as Record<string, unknown>[];
|
|
91
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
92
|
+
const msg = messages[i];
|
|
93
|
+
if (msg.role === 'agent') {
|
|
94
|
+
const msgParts = (msg.parts ?? []) as Record<string, unknown>[];
|
|
95
|
+
if (msgParts.length > 0) {
|
|
96
|
+
const t = msgParts[0].text as string | undefined;
|
|
97
|
+
if (t) return t;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async _handleRelayMessage(msg: Message): Promise<void> {
|
|
106
|
+
const responseText = await this.sendA2AMessage(msg.text);
|
|
107
|
+
if (responseText) {
|
|
108
|
+
await this.relay.send(msg.sender, responseText);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|