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,140 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
const piAdapterModulePath = '../../../communicate/adapters/pi.js';
|
|
5
|
+
|
|
6
|
+
async function loadPiAdapterModule(): Promise<any> {
|
|
7
|
+
return import(piAdapterModulePath);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class FakeRelay {
|
|
11
|
+
private callbacks: Array<(message: any) => void | Promise<void>> = [];
|
|
12
|
+
|
|
13
|
+
async send(_to: string, _text: string): Promise<void> {}
|
|
14
|
+
|
|
15
|
+
async post(_channel: string, _text: string): Promise<void> {}
|
|
16
|
+
|
|
17
|
+
async inbox(): Promise<any[]> {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async agents(): Promise<string[]> {
|
|
22
|
+
return ['Lead', 'Impl-TS'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onMessage(callback: (message: any) => void | Promise<void>): () => void {
|
|
26
|
+
this.callbacks.push(callback);
|
|
27
|
+
return () => {
|
|
28
|
+
this.callbacks = this.callbacks.filter((entry) => entry !== callback);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async emit(message: any): Promise<void> {
|
|
33
|
+
for (const callback of [...this.callbacks]) {
|
|
34
|
+
await callback(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createSession(isStreaming: boolean) {
|
|
40
|
+
return {
|
|
41
|
+
isStreaming,
|
|
42
|
+
steerCalls: [] as string[],
|
|
43
|
+
followUpCalls: [] as string[],
|
|
44
|
+
async steer(text: string) {
|
|
45
|
+
this.steerCalls.push(text);
|
|
46
|
+
},
|
|
47
|
+
async followUp(text: string) {
|
|
48
|
+
this.followUpCalls.push(text);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('Pi onRelay appends relay tools to customTools', async () => {
|
|
54
|
+
const { onRelay } = await loadPiAdapterModule();
|
|
55
|
+
const relay = new FakeRelay();
|
|
56
|
+
|
|
57
|
+
const config = onRelay(
|
|
58
|
+
'PiTester',
|
|
59
|
+
{
|
|
60
|
+
customTools: [
|
|
61
|
+
{
|
|
62
|
+
name: 'existing-tool',
|
|
63
|
+
label: 'Existing tool',
|
|
64
|
+
description: 'Existing tool description',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
relay
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const toolNames = (config.customTools ?? []).map((tool: any) => tool.name);
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(toolNames, [
|
|
74
|
+
'existing-tool',
|
|
75
|
+
'relay_send',
|
|
76
|
+
'relay_inbox',
|
|
77
|
+
'relay_post',
|
|
78
|
+
'relay_agents',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
for (const toolName of ['relay_send', 'relay_inbox', 'relay_post', 'relay_agents']) {
|
|
82
|
+
const tool = config.customTools.find((entry: any) => entry.name === toolName);
|
|
83
|
+
assert.ok(tool, `Expected ${toolName} to be registered`);
|
|
84
|
+
assert.equal(typeof tool.execute, 'function');
|
|
85
|
+
assert.ok(tool.parameters);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Pi onSessionCreated captures the session and steers live messages while streaming', async () => {
|
|
90
|
+
const { onRelay } = await loadPiAdapterModule();
|
|
91
|
+
const relay = new FakeRelay();
|
|
92
|
+
const existingHookCalls: any[] = [];
|
|
93
|
+
|
|
94
|
+
const config = onRelay(
|
|
95
|
+
'PiTester',
|
|
96
|
+
{
|
|
97
|
+
onSessionCreated(session: any) {
|
|
98
|
+
existingHookCalls.push(session);
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
relay
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
assert.equal(typeof config.onSessionCreated, 'function');
|
|
105
|
+
|
|
106
|
+
const session = createSession(true);
|
|
107
|
+
await config.onSessionCreated(session);
|
|
108
|
+
await relay.emit({
|
|
109
|
+
sender: 'Lead',
|
|
110
|
+
text: 'Need status',
|
|
111
|
+
messageId: 'message-1',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.deepEqual(existingHookCalls, [session]);
|
|
115
|
+
assert.equal(session.steerCalls.length, 1);
|
|
116
|
+
assert.equal(session.followUpCalls.length, 0);
|
|
117
|
+
assert.match(session.steerCalls[0], /Lead/);
|
|
118
|
+
assert.match(session.steerCalls[0], /Need status/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('Pi onSessionCreated routes live messages via followUp when the agent is idle', async () => {
|
|
122
|
+
const { onRelay } = await loadPiAdapterModule();
|
|
123
|
+
const relay = new FakeRelay();
|
|
124
|
+
|
|
125
|
+
const config = onRelay('PiTester', {}, relay);
|
|
126
|
+
assert.equal(typeof config.onSessionCreated, 'function');
|
|
127
|
+
|
|
128
|
+
const session = createSession(false);
|
|
129
|
+
await config.onSessionCreated(session);
|
|
130
|
+
await relay.emit({
|
|
131
|
+
sender: 'Review-Adapters',
|
|
132
|
+
text: 'Waiting on Gate 2.3',
|
|
133
|
+
messageId: 'message-2',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.equal(session.steerCalls.length, 0);
|
|
137
|
+
assert.equal(session.followUpCalls.length, 1);
|
|
138
|
+
assert.match(session.followUpCalls[0], /Review-Adapters/);
|
|
139
|
+
assert.match(session.followUpCalls[0], /Waiting on Gate 2.3/);
|
|
140
|
+
});
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
+
|
|
9
|
+
const coreModulePath = '../../communicate/core.js';
|
|
10
|
+
|
|
11
|
+
async function loadCoreModule(): Promise<any> {
|
|
12
|
+
return import(coreModulePath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function waitFor(predicate: () => boolean, timeoutMs = 1_000): Promise<void> {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
18
|
+
if (predicate()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await sleep(10);
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Timed out waiting for async condition.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarizeMessage(message: any) {
|
|
27
|
+
return {
|
|
28
|
+
sender: message.sender,
|
|
29
|
+
text: message.text,
|
|
30
|
+
channel: message.channel,
|
|
31
|
+
threadId: message.threadId,
|
|
32
|
+
messageId: message.messageId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readJson(request: IncomingMessage): Promise<any> {
|
|
37
|
+
const chunks: Buffer[] = [];
|
|
38
|
+
for await (const chunk of request) {
|
|
39
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
40
|
+
}
|
|
41
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
42
|
+
return raw.length > 0 ? JSON.parse(raw) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sendJson(response: ServerResponse, statusCode: number, payload: unknown): void {
|
|
46
|
+
response.statusCode = statusCode;
|
|
47
|
+
response.setHeader('content-type', 'application/json');
|
|
48
|
+
response.end(JSON.stringify(payload));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class MockRelayServer {
|
|
52
|
+
readonly apiKey = 'test-key';
|
|
53
|
+
readonly workspace = 'test-workspace';
|
|
54
|
+
readonly inboxes = new Map<string, any[]>();
|
|
55
|
+
readonly registeredAgents = new Map<string, { name: string; token: string }>();
|
|
56
|
+
|
|
57
|
+
private readonly extraAgents = new Set<string>();
|
|
58
|
+
private readonly requestLog = new Map<string, Array<{ json?: any }>>();
|
|
59
|
+
private readonly websocketCounts = new Map<string, number>();
|
|
60
|
+
private readonly websockets = new Map<string, WebSocket>();
|
|
61
|
+
|
|
62
|
+
private server = createServer(this.handleRequest.bind(this));
|
|
63
|
+
private wsServer = new WebSocketServer({ noServer: true });
|
|
64
|
+
private nextAgentId = 1;
|
|
65
|
+
private nextMessageId = 1;
|
|
66
|
+
|
|
67
|
+
baseUrl = '';
|
|
68
|
+
|
|
69
|
+
/** Track agent tokens from registration: token -> agentId */
|
|
70
|
+
private tokenToAgentId = new Map<string, string>();
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
this.server.on('upgrade', this.handleUpgrade.bind(this));
|
|
74
|
+
this.wsServer.on('connection', (socket, request) => {
|
|
75
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
76
|
+
const token = url.searchParams.get('token');
|
|
77
|
+
const agentId = token ? this.tokenToAgentId.get(token) : undefined;
|
|
78
|
+
if (!agentId) {
|
|
79
|
+
socket.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const current = this.websocketCounts.get(agentId) ?? 0;
|
|
84
|
+
this.websocketCounts.set(agentId, current + 1);
|
|
85
|
+
this.websockets.set(agentId, socket);
|
|
86
|
+
socket.on('close', () => {
|
|
87
|
+
if (this.websockets.get(agentId) === socket) {
|
|
88
|
+
this.websockets.delete(agentId);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async start(): Promise<void> {
|
|
95
|
+
this.server.listen(0, '127.0.0.1');
|
|
96
|
+
await once(this.server, 'listening');
|
|
97
|
+
const address = this.server.address();
|
|
98
|
+
if (!address || typeof address === 'string') {
|
|
99
|
+
throw new Error('Failed to start mock Relaycast server.');
|
|
100
|
+
}
|
|
101
|
+
this.baseUrl = `http://127.0.0.1:${address.port}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async stop(): Promise<void> {
|
|
105
|
+
for (const socket of this.websockets.values()) {
|
|
106
|
+
socket.close();
|
|
107
|
+
}
|
|
108
|
+
this.wsServer.close();
|
|
109
|
+
this.server.close();
|
|
110
|
+
await once(this.server, 'close');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
makeConfig() {
|
|
114
|
+
return {
|
|
115
|
+
workspace: this.workspace,
|
|
116
|
+
apiKey: this.apiKey,
|
|
117
|
+
baseUrl: this.baseUrl,
|
|
118
|
+
autoCleanup: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
addAgent(name: string): void {
|
|
123
|
+
this.extraAgents.add(name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
findAgentId(name: string): string | undefined {
|
|
127
|
+
for (const [agentId, registration] of this.registeredAgents.entries()) {
|
|
128
|
+
if (registration.name === name) {
|
|
129
|
+
return agentId;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
requestCount(operation: string): number {
|
|
136
|
+
return this.requestLog.get(operation)?.length ?? 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lastJson(operation: string): any {
|
|
140
|
+
return this.requestLog.get(operation)?.at(-1)?.json;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
websocketConnectionCountForName(name: string): number {
|
|
144
|
+
const agentId = this.findAgentId(name);
|
|
145
|
+
if (!agentId) return 0;
|
|
146
|
+
return this.websocketCounts.get(agentId) ?? 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
websocketConnected(agentId: string): boolean {
|
|
150
|
+
const socket = this.websockets.get(agentId);
|
|
151
|
+
return socket !== undefined && socket.readyState === WebSocket.OPEN;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async waitForAgentConnection(name: string): Promise<string> {
|
|
155
|
+
let agentId: string | undefined;
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
agentId = this.findAgentId(name);
|
|
158
|
+
return agentId !== undefined && this.websocketConnected(agentId);
|
|
159
|
+
});
|
|
160
|
+
return agentId!;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async pushWsMessage(
|
|
164
|
+
agentId: string,
|
|
165
|
+
message: {
|
|
166
|
+
sender: string;
|
|
167
|
+
text: string;
|
|
168
|
+
channel?: string;
|
|
169
|
+
thread_id?: string;
|
|
170
|
+
message_id?: string;
|
|
171
|
+
timestamp?: number;
|
|
172
|
+
}
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const socket = this.websockets.get(agentId);
|
|
175
|
+
assert.ok(socket && socket.readyState === WebSocket.OPEN, `No active websocket for ${agentId}`);
|
|
176
|
+
|
|
177
|
+
socket.send(
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
type: 'message',
|
|
180
|
+
...message,
|
|
181
|
+
message_id: message.message_id ?? `message-${this.nextMessageId++}`,
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
await sleep(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private record(operation: string, json?: any): void {
|
|
188
|
+
const entries = this.requestLog.get(operation) ?? [];
|
|
189
|
+
entries.push({ json });
|
|
190
|
+
this.requestLog.set(operation, entries);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private authorizeWorkspaceKey(request: IncomingMessage, response: ServerResponse): boolean {
|
|
194
|
+
if (request.headers.authorization !== `Bearer ${this.apiKey}`) {
|
|
195
|
+
sendJson(response, 401, { message: 'Unauthorized' });
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private resolveAgentFromToken(request: IncomingMessage): string | undefined {
|
|
202
|
+
const auth = request.headers.authorization ?? '';
|
|
203
|
+
if (auth.startsWith('Bearer ')) {
|
|
204
|
+
const token = auth.slice(7);
|
|
205
|
+
return this.tokenToAgentId.get(token);
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private authorizeAgentToken(request: IncomingMessage, response: ServerResponse): string | undefined {
|
|
211
|
+
const agentId = this.resolveAgentFromToken(request);
|
|
212
|
+
if (!agentId) {
|
|
213
|
+
sendJson(response, 401, { ok: false, error: { message: 'Unauthorized' } });
|
|
214
|
+
}
|
|
215
|
+
return agentId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
219
|
+
const url = new URL(request.url ?? '/', this.baseUrl || 'http://127.0.0.1');
|
|
220
|
+
const pathname = url.pathname;
|
|
221
|
+
|
|
222
|
+
// POST /v1/agents — register (workspace key auth)
|
|
223
|
+
if (request.method === 'POST' && pathname === '/v1/agents') {
|
|
224
|
+
const json = await readJson(request);
|
|
225
|
+
this.record('register_agent', json);
|
|
226
|
+
if (!this.authorizeWorkspaceKey(request, response)) return;
|
|
227
|
+
|
|
228
|
+
const agentId = `agent-${this.nextAgentId++}`;
|
|
229
|
+
const token = `token-${agentId}`;
|
|
230
|
+
this.registeredAgents.set(agentId, { name: json.name, token });
|
|
231
|
+
this.tokenToAgentId.set(token, agentId);
|
|
232
|
+
sendJson(response, 200, { ok: true, data: { id: agentId, name: json.name, token, status: 'online' } });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// POST /v1/agents/disconnect — unregister (agent token auth)
|
|
237
|
+
if (request.method === 'POST' && pathname === '/v1/agents/disconnect') {
|
|
238
|
+
const agentId = this.authorizeAgentToken(request, response);
|
|
239
|
+
if (!agentId) return;
|
|
240
|
+
this.record('unregister_agent', { agent_id: agentId });
|
|
241
|
+
|
|
242
|
+
// Remove token mapping
|
|
243
|
+
const auth = request.headers.authorization ?? '';
|
|
244
|
+
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
245
|
+
this.tokenToAgentId.delete(token);
|
|
246
|
+
this.registeredAgents.delete(agentId);
|
|
247
|
+
this.inboxes.delete(agentId);
|
|
248
|
+
this.websockets.get(agentId)?.close();
|
|
249
|
+
sendJson(response, 200, { ok: true });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// POST /v1/dm — send DM (agent token auth)
|
|
254
|
+
if (request.method === 'POST' && pathname === '/v1/dm') {
|
|
255
|
+
const json = await readJson(request);
|
|
256
|
+
const agentId = this.authorizeAgentToken(request, response);
|
|
257
|
+
if (!agentId) return;
|
|
258
|
+
const senderName = this.registeredAgents.get(agentId)?.name ?? 'unknown';
|
|
259
|
+
this.record('send_dm', { ...json, from: senderName });
|
|
260
|
+
|
|
261
|
+
const msgId = `message-${this.nextMessageId++}`;
|
|
262
|
+
sendJson(response, 201, { ok: true, data: { id: msgId, text: json.text } });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// POST /v1/channels/{channel}/messages — channel post (agent token auth)
|
|
267
|
+
const channelMatch = pathname.match(/^\/v1\/channels\/([^/]+)\/messages$/);
|
|
268
|
+
if (request.method === 'POST' && channelMatch) {
|
|
269
|
+
const json = await readJson(request);
|
|
270
|
+
const channel = decodeURIComponent(channelMatch[1]);
|
|
271
|
+
const agentId = this.authorizeAgentToken(request, response);
|
|
272
|
+
if (!agentId) return;
|
|
273
|
+
const senderName = this.registeredAgents.get(agentId)?.name ?? 'unknown';
|
|
274
|
+
this.record('post_message', { ...json, channel, from: senderName });
|
|
275
|
+
|
|
276
|
+
const msgId = `message-${this.nextMessageId++}`;
|
|
277
|
+
sendJson(response, 201, { ok: true, data: { id: msgId, channel_name: channel, text: json.text } });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// POST /v1/messages/{id}/replies — reply (agent token auth)
|
|
282
|
+
const replyMatch = pathname.match(/^\/v1\/messages\/([^/]+)\/replies$/);
|
|
283
|
+
if (request.method === 'POST' && replyMatch) {
|
|
284
|
+
const json = await readJson(request);
|
|
285
|
+
const parentId = decodeURIComponent(replyMatch[1]);
|
|
286
|
+
const agentId = this.authorizeAgentToken(request, response);
|
|
287
|
+
if (!agentId) return;
|
|
288
|
+
const senderName = this.registeredAgents.get(agentId)?.name ?? 'unknown';
|
|
289
|
+
this.record('reply', { ...json, message_id: parentId, from: senderName });
|
|
290
|
+
|
|
291
|
+
const msgId = `message-${this.nextMessageId++}`;
|
|
292
|
+
sendJson(response, 201, { ok: true, data: { id: msgId, text: json.text } });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// GET /v1/inbox — inbox (agent token auth)
|
|
297
|
+
if (request.method === 'GET' && pathname === '/v1/inbox') {
|
|
298
|
+
const agentId = this.authorizeAgentToken(request, response);
|
|
299
|
+
if (!agentId) return;
|
|
300
|
+
this.record('check_inbox', { agent_id: agentId });
|
|
301
|
+
|
|
302
|
+
const messages = this.inboxes.get(agentId) ?? [];
|
|
303
|
+
this.inboxes.set(agentId, []);
|
|
304
|
+
sendJson(response, 200, { ok: true, data: { unread_channels: [], mentions: [], unread_dms: messages, recent_reactions: [] } });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// GET /v1/agents — list agents (workspace key auth)
|
|
309
|
+
if (request.method === 'GET' && pathname === '/v1/agents') {
|
|
310
|
+
this.record('list_agents');
|
|
311
|
+
if (!this.authorizeWorkspaceKey(request, response)) return;
|
|
312
|
+
|
|
313
|
+
const agentList: Array<{ name: string; id: string; status: string }> = [];
|
|
314
|
+
for (const [agentId, registration] of this.registeredAgents.entries()) {
|
|
315
|
+
agentList.push({ name: registration.name, id: agentId, status: 'online' });
|
|
316
|
+
}
|
|
317
|
+
for (const name of this.extraAgents) {
|
|
318
|
+
agentList.push({ name, id: `extra-${name}`, status: 'online' });
|
|
319
|
+
}
|
|
320
|
+
sendJson(response, 200, { ok: true, data: agentList });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
sendJson(response, 404, { message: 'Not found' });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private handleUpgrade(request: IncomingMessage, socket: any, head: Buffer): void {
|
|
328
|
+
const url = new URL(request.url ?? '/', this.baseUrl || 'http://127.0.0.1');
|
|
329
|
+
const pathname = url.pathname;
|
|
330
|
+
if (pathname !== '/v1/ws') {
|
|
331
|
+
socket.destroy();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const token = url.searchParams.get('token');
|
|
336
|
+
if (!token) {
|
|
337
|
+
socket.destroy();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const agentId = this.tokenToAgentId.get(token);
|
|
341
|
+
if (!agentId || !this.registeredAgents.has(agentId)) {
|
|
342
|
+
socket.destroy();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
347
|
+
this.wsServer.emit('connection', ws, request);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function withServer(run: (server: MockRelayServer) => Promise<void>): Promise<void> {
|
|
353
|
+
const server = new MockRelayServer();
|
|
354
|
+
await server.start();
|
|
355
|
+
try {
|
|
356
|
+
await run(server);
|
|
357
|
+
} finally {
|
|
358
|
+
await server.stop();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function waitForInbox(relay: any, timeoutMs = 1_000): Promise<any[]> {
|
|
363
|
+
const startedAt = Date.now();
|
|
364
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
365
|
+
const messages = await relay.inbox();
|
|
366
|
+
if (messages.length > 0) {
|
|
367
|
+
return messages;
|
|
368
|
+
}
|
|
369
|
+
await sleep(10);
|
|
370
|
+
}
|
|
371
|
+
throw new Error('Timed out waiting for inbox messages.');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test('Relay lazily connects on first send and delegates DMs', async () => {
|
|
375
|
+
await withServer(async (server) => {
|
|
376
|
+
const { Relay } = await loadCoreModule();
|
|
377
|
+
const relay = new Relay('CoreTester', server.makeConfig());
|
|
378
|
+
|
|
379
|
+
assert.equal(server.requestCount('register_agent'), 0);
|
|
380
|
+
assert.equal(server.websocketConnectionCountForName('CoreTester'), 0);
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const result = await relay.send('Impl-Core', 'hello');
|
|
384
|
+
|
|
385
|
+
assert.equal(result, undefined);
|
|
386
|
+
assert.equal(server.requestCount('register_agent'), 1);
|
|
387
|
+
await waitFor(() => server.websocketConnectionCountForName('CoreTester') === 1);
|
|
388
|
+
assert.deepEqual(server.lastJson('send_dm'), {
|
|
389
|
+
to: 'Impl-Core',
|
|
390
|
+
text: 'hello',
|
|
391
|
+
from: 'CoreTester',
|
|
392
|
+
});
|
|
393
|
+
} finally {
|
|
394
|
+
await relay.close();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('Relay delegates channel posts', async () => {
|
|
400
|
+
await withServer(async (server) => {
|
|
401
|
+
const { Relay } = await loadCoreModule();
|
|
402
|
+
const relay = new Relay('CorePoster', server.makeConfig());
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const result = await relay.post('ts-track', 'status update');
|
|
406
|
+
|
|
407
|
+
assert.equal(result, undefined);
|
|
408
|
+
assert.deepEqual(server.lastJson('post_message'), {
|
|
409
|
+
channel: 'ts-track',
|
|
410
|
+
text: 'status update',
|
|
411
|
+
from: 'CorePoster',
|
|
412
|
+
});
|
|
413
|
+
} finally {
|
|
414
|
+
await relay.close();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('Relay delegates thread replies', async () => {
|
|
420
|
+
await withServer(async (server) => {
|
|
421
|
+
const { Relay } = await loadCoreModule();
|
|
422
|
+
const relay = new Relay('CoreReplier', server.makeConfig());
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const result = await relay.reply('message-123', 'thread response');
|
|
426
|
+
|
|
427
|
+
assert.equal(result, undefined);
|
|
428
|
+
assert.deepEqual(server.lastJson('reply'), {
|
|
429
|
+
message_id: 'message-123',
|
|
430
|
+
text: 'thread response',
|
|
431
|
+
from: 'CoreReplier',
|
|
432
|
+
});
|
|
433
|
+
} finally {
|
|
434
|
+
await relay.close();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('Relay inbox drains buffered websocket messages and clears the buffer', async () => {
|
|
440
|
+
await withServer(async (server) => {
|
|
441
|
+
const { Relay } = await loadCoreModule();
|
|
442
|
+
const relay = new Relay('CoreInbox', server.makeConfig());
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
await relay.agents();
|
|
446
|
+
const agentId = await server.waitForAgentConnection('CoreInbox');
|
|
447
|
+
|
|
448
|
+
await server.pushWsMessage(agentId, {
|
|
449
|
+
sender: 'Review-Core',
|
|
450
|
+
text: 'one',
|
|
451
|
+
message_id: 'message-1',
|
|
452
|
+
});
|
|
453
|
+
await server.pushWsMessage(agentId, {
|
|
454
|
+
sender: 'Impl-Core',
|
|
455
|
+
text: 'two',
|
|
456
|
+
channel: 'ts-track',
|
|
457
|
+
thread_id: 'thread-1',
|
|
458
|
+
message_id: 'message-2',
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const first = await waitForInbox(relay);
|
|
462
|
+
const second = await relay.inbox();
|
|
463
|
+
|
|
464
|
+
assert.deepEqual(first.map(summarizeMessage), [
|
|
465
|
+
{
|
|
466
|
+
sender: 'Review-Core',
|
|
467
|
+
text: 'one',
|
|
468
|
+
channel: undefined,
|
|
469
|
+
threadId: undefined,
|
|
470
|
+
messageId: 'message-1',
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
sender: 'Impl-Core',
|
|
474
|
+
text: 'two',
|
|
475
|
+
channel: 'ts-track',
|
|
476
|
+
threadId: 'thread-1',
|
|
477
|
+
messageId: 'message-2',
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
assert.deepEqual(second, []);
|
|
481
|
+
assert.equal(server.requestCount('check_inbox'), 0);
|
|
482
|
+
} finally {
|
|
483
|
+
await relay.close();
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('Relay onMessage callbacks receive live messages and unsubscribe restores buffering', async () => {
|
|
489
|
+
await withServer(async (server) => {
|
|
490
|
+
const { Relay } = await loadCoreModule();
|
|
491
|
+
const relay = new Relay('CoreCallback', server.makeConfig());
|
|
492
|
+
const received: any[] = [];
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const unsubscribe = relay.onMessage((message: any) => {
|
|
496
|
+
received.push(message);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const agentId = await server.waitForAgentConnection('CoreCallback');
|
|
500
|
+
await server.pushWsMessage(agentId, {
|
|
501
|
+
sender: 'Lead',
|
|
502
|
+
text: 'callback',
|
|
503
|
+
message_id: 'message-callback',
|
|
504
|
+
});
|
|
505
|
+
await waitFor(() => received.length === 1);
|
|
506
|
+
|
|
507
|
+
unsubscribe();
|
|
508
|
+
|
|
509
|
+
await server.pushWsMessage(agentId, {
|
|
510
|
+
sender: 'Impl-Core',
|
|
511
|
+
text: 'buffered',
|
|
512
|
+
message_id: 'message-buffered',
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const inboxMessages = await waitForInbox(relay);
|
|
516
|
+
|
|
517
|
+
assert.deepEqual(received.map(summarizeMessage), [
|
|
518
|
+
{
|
|
519
|
+
sender: 'Lead',
|
|
520
|
+
text: 'callback',
|
|
521
|
+
channel: undefined,
|
|
522
|
+
threadId: undefined,
|
|
523
|
+
messageId: 'message-callback',
|
|
524
|
+
},
|
|
525
|
+
]);
|
|
526
|
+
assert.deepEqual(inboxMessages.map(summarizeMessage), [
|
|
527
|
+
{
|
|
528
|
+
sender: 'Impl-Core',
|
|
529
|
+
text: 'buffered',
|
|
530
|
+
channel: undefined,
|
|
531
|
+
threadId: undefined,
|
|
532
|
+
messageId: 'message-buffered',
|
|
533
|
+
},
|
|
534
|
+
]);
|
|
535
|
+
} finally {
|
|
536
|
+
await relay.close();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('Relay agents() returns online agent names', async () => {
|
|
542
|
+
await withServer(async (server) => {
|
|
543
|
+
const { Relay } = await loadCoreModule();
|
|
544
|
+
server.addAgent('Review-Core');
|
|
545
|
+
server.addAgent('Impl-Core');
|
|
546
|
+
const relay = new Relay('CoreRoster', server.makeConfig());
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const agents = await relay.agents();
|
|
550
|
+
|
|
551
|
+
assert.deepEqual([...agents].sort(), ['CoreRoster', 'Impl-Core', 'Review-Core']);
|
|
552
|
+
assert.equal(server.requestCount('list_agents'), 1);
|
|
553
|
+
} finally {
|
|
554
|
+
await relay.close();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test('Relay close() unregisters the agent and closes the websocket', async () => {
|
|
560
|
+
await withServer(async (server) => {
|
|
561
|
+
const { Relay } = await loadCoreModule();
|
|
562
|
+
const relay = new Relay('CoreCloser', server.makeConfig());
|
|
563
|
+
|
|
564
|
+
await relay.send('Impl-Core', 'hello');
|
|
565
|
+
const agentId = await server.waitForAgentConnection('CoreCloser');
|
|
566
|
+
assert.ok(server.registeredAgents.has(agentId));
|
|
567
|
+
|
|
568
|
+
await relay.close();
|
|
569
|
+
|
|
570
|
+
assert.equal(server.requestCount('unregister_agent'), 1);
|
|
571
|
+
assert.equal(server.registeredAgents.has(agentId), false);
|
|
572
|
+
assert.equal(server.websocketConnected(agentId), false);
|
|
573
|
+
});
|
|
574
|
+
});
|