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.
Files changed (225) hide show
  1. package/dist/index.cjs +265 -108
  2. package/package.json +11 -10
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/config/package.json +1 -1
  5. package/packages/hooks/package.json +4 -4
  6. package/packages/memory/package.json +2 -2
  7. package/packages/openclaw/package.json +2 -2
  8. package/packages/policy/package.json +2 -2
  9. package/packages/sdk/ADAPTER_REVIEW.md +109 -0
  10. package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
  11. package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
  12. package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
  13. package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
  14. package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
  15. package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
  16. package/packages/sdk/dist/communicate/a2a-server.js +220 -0
  17. package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
  18. package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
  19. package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
  20. package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
  21. package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
  22. package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
  23. package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
  24. package/packages/sdk/dist/communicate/a2a-types.js +209 -0
  25. package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
  26. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
  27. package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
  28. package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
  29. package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
  30. package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
  31. package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
  32. package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
  33. package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
  34. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
  35. package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
  36. package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
  37. package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
  38. package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
  39. package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
  40. package/packages/sdk/dist/communicate/adapters/index.js +7 -0
  41. package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
  42. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
  43. package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
  44. package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
  45. package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
  46. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
  47. package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
  48. package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
  49. package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
  50. package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
  51. package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
  52. package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
  53. package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
  54. package/packages/sdk/dist/communicate/core.d.ts +58 -0
  55. package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
  56. package/packages/sdk/dist/communicate/core.js +128 -0
  57. package/packages/sdk/dist/communicate/core.js.map +1 -0
  58. package/packages/sdk/dist/communicate/index.d.ts +4 -0
  59. package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
  60. package/packages/sdk/dist/communicate/index.js +4 -0
  61. package/packages/sdk/dist/communicate/index.js.map +1 -0
  62. package/packages/sdk/dist/communicate/transport.d.ts +36 -0
  63. package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
  64. package/packages/sdk/dist/communicate/transport.js +371 -0
  65. package/packages/sdk/dist/communicate/transport.js.map +1 -0
  66. package/packages/sdk/dist/communicate/types.d.ts +58 -0
  67. package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
  68. package/packages/sdk/dist/communicate/types.js +66 -0
  69. package/packages/sdk/dist/communicate/types.js.map +1 -0
  70. package/packages/sdk/dist/workflows/builder.d.ts +35 -5
  71. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  72. package/packages/sdk/dist/workflows/builder.js +81 -7
  73. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  74. package/packages/sdk/dist/workflows/cli.js +14 -1
  75. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  76. package/packages/sdk/dist/workflows/runner.d.ts +10 -2
  77. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  78. package/packages/sdk/dist/workflows/runner.js +95 -1
  79. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  80. package/packages/sdk/dist/workflows/types.d.ts +11 -0
  81. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  82. package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
  83. package/packages/sdk/examples/communicate/pi_example.ts +8 -0
  84. package/packages/sdk/package.json +48 -2
  85. package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
  86. package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
  87. package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
  88. package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
  89. package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
  90. package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
  91. package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
  92. package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
  93. package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
  94. package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
  95. package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
  96. package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
  97. package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
  98. package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
  99. package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
  100. package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
  101. package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
  102. package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
  103. package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
  104. package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
  105. package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
  106. package/packages/sdk/src/communicate/a2a-server.ts +277 -0
  107. package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
  108. package/packages/sdk/src/communicate/a2a-types.ts +338 -0
  109. package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
  110. package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
  111. package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
  112. package/packages/sdk/src/communicate/adapters/index.ts +6 -0
  113. package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
  114. package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
  115. package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
  116. package/packages/sdk/src/communicate/core.ts +157 -0
  117. package/packages/sdk/src/communicate/index.ts +3 -0
  118. package/packages/sdk/src/communicate/transport.ts +489 -0
  119. package/packages/sdk/src/communicate/types.ts +106 -0
  120. package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
  121. package/packages/sdk/src/workflows/builder.ts +97 -9
  122. package/packages/sdk/src/workflows/cli.ts +16 -1
  123. package/packages/sdk/src/workflows/runner.ts +110 -1
  124. package/packages/sdk/src/workflows/types.ts +14 -0
  125. package/packages/sdk/tsconfig.build.json +1 -7
  126. package/packages/sdk/tsconfig.json +1 -7
  127. package/packages/sdk-py/README.md +67 -25
  128. package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
  129. package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
  130. package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
  131. package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
  132. package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
  133. package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
  134. package/packages/sdk-py/pyproject.toml +12 -1
  135. package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
  136. package/packages/sdk-py/src/agent_relay/builder.py +65 -26
  137. package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
  138. package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
  139. package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
  140. package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
  141. package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
  142. package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
  143. package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
  144. package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
  145. package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
  146. package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
  147. package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
  148. package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
  149. package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
  150. package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
  151. package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
  152. package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
  153. package/packages/sdk-py/src/agent_relay/types.py +2 -1
  154. package/packages/sdk-py/tests/communicate/__init__.py +0 -0
  155. package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
  156. package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
  157. package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
  158. package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
  159. package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
  160. package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
  161. package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
  162. package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
  163. package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
  164. package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
  165. package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
  166. package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
  167. package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
  168. package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
  169. package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
  170. package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
  171. package/packages/sdk-py/tests/communicate/conftest.py +555 -0
  172. package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
  173. package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
  174. package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
  175. package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
  176. package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
  177. package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
  178. package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
  179. package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
  180. package/packages/sdk-py/tests/communicate/test_core.py +331 -0
  181. package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
  182. package/packages/sdk-py/tests/communicate/test_types.py +285 -0
  183. package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
  184. package/packages/telemetry/package.json +1 -1
  185. package/packages/trajectory/package.json +2 -2
  186. package/packages/user-directory/package.json +2 -2
  187. package/packages/utils/package.json +2 -2
  188. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
  189. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
  190. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
  191. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
  192. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
  193. package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
  194. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
  195. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
  196. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
  197. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
  198. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
  199. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
  200. package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
  201. package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
  202. package/packages/sdk/dist/__tests__/facade.test.js +0 -305
  203. package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
  204. package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
  205. package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
  206. package/packages/sdk/dist/__tests__/integration.test.js +0 -205
  207. package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
  208. package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
  209. package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
  210. package/packages/sdk/dist/__tests__/pty.test.js +0 -20
  211. package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
  212. package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
  213. package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
  214. package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
  215. package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
  216. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
  217. package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
  218. package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
  219. package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
  220. package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
  221. package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
  222. package/packages/sdk/dist/__tests__/unit.test.js +0 -357
  223. package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
  224. package/packages/sdk-py/agent_relay/__init__.py +0 -21
  225. package/packages/sdk-py/agent_relay/models.py +0 -398
@@ -0,0 +1,297 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import {
4
+ type A2AAgentCard,
5
+ type A2AMessage,
6
+ type A2APart,
7
+ type A2ASkill,
8
+ type A2ATask,
9
+ type A2ATaskStatus,
10
+ VALID_TASK_STATES,
11
+ a2aAgentCardFromDict,
12
+ a2aAgentCardToDict,
13
+ a2aMessageFromDict,
14
+ a2aMessageGetText,
15
+ a2aMessageToDict,
16
+ a2aPartFromDict,
17
+ a2aPartToDict,
18
+ a2aSkillFromDict,
19
+ a2aSkillToDict,
20
+ a2aTaskFromDict,
21
+ a2aTaskStatusFromDict,
22
+ a2aTaskStatusToDict,
23
+ a2aTaskToDict,
24
+ createA2AAgentCard,
25
+ createA2AMessage,
26
+ createA2APart,
27
+ createA2ATask,
28
+ createA2ATaskStatus,
29
+ makeJsonRpcError,
30
+ makeJsonRpcRequest,
31
+ makeJsonRpcResponse,
32
+ JSONRPC_PARSE_ERROR,
33
+ JSONRPC_METHOD_NOT_FOUND,
34
+ JSONRPC_INTERNAL_ERROR,
35
+ A2A_TASK_NOT_FOUND,
36
+ A2A_TASK_NOT_CANCELABLE,
37
+ } from '../../communicate/a2a-types.js';
38
+
39
+ describe('A2APart', () => {
40
+ it('creates a text part', () => {
41
+ const part = createA2APart('hello');
42
+ expect(part.text).toBe('hello');
43
+ });
44
+
45
+ it('serializes to dict with only defined fields', () => {
46
+ const part: A2APart = { text: 'hello' };
47
+ const d = a2aPartToDict(part);
48
+ expect(d).toEqual({ text: 'hello' });
49
+ expect(d).not.toHaveProperty('file');
50
+ expect(d).not.toHaveProperty('data');
51
+ });
52
+
53
+ it('deserializes from dict', () => {
54
+ const d = { text: 'world', file: { uri: 'test.txt' } };
55
+ const part = a2aPartFromDict(d);
56
+ expect(part.text).toBe('world');
57
+ expect(part.file).toEqual({ uri: 'test.txt' });
58
+ expect(part.data).toBeUndefined();
59
+ });
60
+ });
61
+
62
+ describe('A2AMessage', () => {
63
+ it('creates with auto-generated messageId', () => {
64
+ const msg = createA2AMessage('user', [{ text: 'hi' }]);
65
+ expect(msg.role).toBe('user');
66
+ expect(msg.messageId).toBeDefined();
67
+ expect(msg.messageId!.length).toBeGreaterThan(0);
68
+ expect(msg.parts).toHaveLength(1);
69
+ });
70
+
71
+ it('creates with explicit messageId', () => {
72
+ const msg = createA2AMessage('agent', [{ text: 'reply' }], {
73
+ messageId: 'msg-123',
74
+ contextId: 'ctx-1',
75
+ taskId: 'task-1',
76
+ });
77
+ expect(msg.messageId).toBe('msg-123');
78
+ expect(msg.contextId).toBe('ctx-1');
79
+ expect(msg.taskId).toBe('task-1');
80
+ });
81
+
82
+ it('serializes and deserializes roundtrip', () => {
83
+ const msg = createA2AMessage('user', [{ text: 'hello' }, { text: 'world' }], {
84
+ messageId: 'test-id',
85
+ contextId: 'ctx-1',
86
+ });
87
+ const dict = a2aMessageToDict(msg);
88
+ const restored = a2aMessageFromDict(dict);
89
+ expect(restored.role).toBe('user');
90
+ expect(restored.parts).toHaveLength(2);
91
+ expect(restored.messageId).toBe('test-id');
92
+ expect(restored.contextId).toBe('ctx-1');
93
+ });
94
+
95
+ it('extracts concatenated text', () => {
96
+ const msg: A2AMessage = {
97
+ role: 'agent',
98
+ parts: [{ text: 'hello' }, { text: 'world' }, { data: { key: 'val' } }],
99
+ };
100
+ expect(a2aMessageGetText(msg)).toBe('hello world');
101
+ });
102
+
103
+ it('extracts empty text from no text parts', () => {
104
+ const msg: A2AMessage = { role: 'agent', parts: [{ data: { x: 1 } }] };
105
+ expect(a2aMessageGetText(msg)).toBe('');
106
+ });
107
+ });
108
+
109
+ describe('A2ATaskStatus', () => {
110
+ it('creates with auto timestamp', () => {
111
+ const status = createA2ATaskStatus('submitted');
112
+ expect(status.state).toBe('submitted');
113
+ expect(status.timestamp).toBeDefined();
114
+ });
115
+
116
+ it('serializes and deserializes roundtrip', () => {
117
+ const msg = createA2AMessage('agent', [{ text: 'done' }], { messageId: 'r1' });
118
+ const status = createA2ATaskStatus('completed', msg);
119
+ const dict = a2aTaskStatusToDict(status);
120
+ expect(dict.state).toBe('completed');
121
+ expect(dict.message).toBeDefined();
122
+
123
+ const restored = a2aTaskStatusFromDict(dict);
124
+ expect(restored.state).toBe('completed');
125
+ expect(restored.message?.role).toBe('agent');
126
+ });
127
+
128
+ it('deserializes without message', () => {
129
+ const dict = { state: 'working', timestamp: '2024-01-01T00:00:00Z' };
130
+ const status = a2aTaskStatusFromDict(dict);
131
+ expect(status.state).toBe('working');
132
+ expect(status.message).toBeUndefined();
133
+ });
134
+ });
135
+
136
+ describe('VALID_TASK_STATES', () => {
137
+ it('contains all expected states', () => {
138
+ expect(VALID_TASK_STATES.has('submitted')).toBe(true);
139
+ expect(VALID_TASK_STATES.has('working')).toBe(true);
140
+ expect(VALID_TASK_STATES.has('completed')).toBe(true);
141
+ expect(VALID_TASK_STATES.has('failed')).toBe(true);
142
+ expect(VALID_TASK_STATES.has('canceled')).toBe(true);
143
+ expect(VALID_TASK_STATES.has('unknown')).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('A2ATask', () => {
148
+ it('creates with defaults', () => {
149
+ const task = createA2ATask('task-1', 'ctx-1');
150
+ expect(task.id).toBe('task-1');
151
+ expect(task.contextId).toBe('ctx-1');
152
+ expect(task.status.state).toBe('submitted');
153
+ expect(task.messages).toEqual([]);
154
+ expect(task.artifacts).toEqual([]);
155
+ });
156
+
157
+ it('serializes and deserializes roundtrip', () => {
158
+ const task: A2ATask = {
159
+ id: 'task-42',
160
+ contextId: 'ctx-7',
161
+ status: createA2ATaskStatus('completed'),
162
+ messages: [createA2AMessage('user', [{ text: 'do stuff' }], { messageId: 'm1' })],
163
+ artifacts: [{ type: 'text', data: 'result' }],
164
+ };
165
+
166
+ const dict = a2aTaskToDict(task);
167
+ expect(dict.id).toBe('task-42');
168
+
169
+ const restored = a2aTaskFromDict(dict);
170
+ expect(restored.id).toBe('task-42');
171
+ expect(restored.contextId).toBe('ctx-7');
172
+ expect(restored.status.state).toBe('completed');
173
+ expect(restored.messages).toHaveLength(1);
174
+ expect(restored.artifacts).toHaveLength(1);
175
+ });
176
+
177
+ it('deserializes with missing status defaults to submitted', () => {
178
+ const dict = { id: 't1', messages: [], artifacts: [] };
179
+ const task = a2aTaskFromDict(dict);
180
+ expect(task.status.state).toBe('submitted');
181
+ });
182
+ });
183
+
184
+ describe('A2ASkill', () => {
185
+ it('serializes and deserializes', () => {
186
+ const skill: A2ASkill = {
187
+ id: 'billing',
188
+ name: 'Billing',
189
+ description: 'Handles billing queries',
190
+ };
191
+ const dict = a2aSkillToDict(skill);
192
+ expect(dict).toEqual({
193
+ id: 'billing',
194
+ name: 'Billing',
195
+ description: 'Handles billing queries',
196
+ });
197
+
198
+ const restored = a2aSkillFromDict(dict);
199
+ expect(restored).toEqual(skill);
200
+ });
201
+ });
202
+
203
+ describe('A2AAgentCard', () => {
204
+ it('creates with defaults', () => {
205
+ const card = createA2AAgentCard('test-agent', 'A test agent', 'http://localhost:5000');
206
+ expect(card.name).toBe('test-agent');
207
+ expect(card.version).toBe('1.0.0');
208
+ expect(card.capabilities).toEqual({ streaming: true, pushNotifications: false });
209
+ expect(card.defaultInputModes).toEqual(['text']);
210
+ expect(card.defaultOutputModes).toEqual(['text']);
211
+ expect(card.skills).toEqual([]);
212
+ });
213
+
214
+ it('creates with skills', () => {
215
+ const skills: A2ASkill[] = [{ id: 's1', name: 'Search', description: 'Search things' }];
216
+ const card = createA2AAgentCard('agent', 'desc', 'http://localhost:5000', skills);
217
+ expect(card.skills).toHaveLength(1);
218
+ expect(card.skills[0].id).toBe('s1');
219
+ });
220
+
221
+ it('serializes and deserializes roundtrip', () => {
222
+ const card = createA2AAgentCard(
223
+ 'my-agent',
224
+ 'My agent',
225
+ 'http://example.com',
226
+ [{ id: 'sk1', name: 'Skill1', description: 'Does skill1' }],
227
+ );
228
+ const dict = a2aAgentCardToDict(card);
229
+ const restored = a2aAgentCardFromDict(dict);
230
+ expect(restored.name).toBe('my-agent');
231
+ expect(restored.description).toBe('My agent');
232
+ expect(restored.url).toBe('http://example.com');
233
+ expect(restored.version).toBe('1.0.0');
234
+ expect(restored.skills).toHaveLength(1);
235
+ expect(restored.skills[0].name).toBe('Skill1');
236
+ });
237
+
238
+ it('deserializes with defaults for missing fields', () => {
239
+ const dict = { name: 'agent', description: 'desc', url: 'http://localhost' };
240
+ const card = a2aAgentCardFromDict(dict);
241
+ expect(card.version).toBe('1.0.0');
242
+ expect(card.capabilities).toEqual({ streaming: true, pushNotifications: false });
243
+ expect(card.defaultInputModes).toEqual(['text']);
244
+ expect(card.defaultOutputModes).toEqual(['text']);
245
+ expect(card.skills).toEqual([]);
246
+ });
247
+ });
248
+
249
+ describe('JSON-RPC helpers', () => {
250
+ it('makeJsonRpcRequest creates valid request', () => {
251
+ const req = makeJsonRpcRequest('message/send', { message: { role: 'user', parts: [] } }, 'req-1');
252
+ expect(req.jsonrpc).toBe('2.0');
253
+ expect(req.method).toBe('message/send');
254
+ expect(req.id).toBe('req-1');
255
+ expect(req.params).toHaveProperty('message');
256
+ });
257
+
258
+ it('makeJsonRpcRequest auto-generates id', () => {
259
+ const req = makeJsonRpcRequest('test', {});
260
+ expect(req.id).toBeDefined();
261
+ expect(typeof req.id).toBe('string');
262
+ });
263
+
264
+ it('makeJsonRpcResponse creates valid response', () => {
265
+ const res = makeJsonRpcResponse({ ok: true }, 'req-1');
266
+ expect(res.jsonrpc).toBe('2.0');
267
+ expect(res.result).toEqual({ ok: true });
268
+ expect(res.id).toBe('req-1');
269
+ expect(res.error).toBeUndefined();
270
+ });
271
+
272
+ it('makeJsonRpcError creates valid error response', () => {
273
+ const res = makeJsonRpcError(-32600, 'Invalid request', 'req-1');
274
+ expect(res.jsonrpc).toBe('2.0');
275
+ expect(res.error).toEqual({ code: -32600, message: 'Invalid request' });
276
+ expect(res.id).toBe('req-1');
277
+ expect(res.result).toBeUndefined();
278
+ });
279
+
280
+ it('makeJsonRpcError handles null id', () => {
281
+ const res = makeJsonRpcError(-32700, 'Parse error', null);
282
+ expect(res.id).toBeNull();
283
+ });
284
+ });
285
+
286
+ describe('Error code constants', () => {
287
+ it('has correct standard JSON-RPC codes', () => {
288
+ expect(JSONRPC_PARSE_ERROR).toBe(-32700);
289
+ expect(JSONRPC_METHOD_NOT_FOUND).toBe(-32601);
290
+ expect(JSONRPC_INTERNAL_ERROR).toBe(-32603);
291
+ });
292
+
293
+ it('has correct A2A-specific codes', () => {
294
+ expect(A2A_TASK_NOT_FOUND).toBe(-32001);
295
+ expect(A2A_TASK_NOT_CANCELABLE).toBe(-32002);
296
+ });
297
+ });
@@ -0,0 +1,163 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ const claudeAdapterModulePath = '../../../communicate/adapters/claude-sdk.js';
5
+
6
+ async function loadClaudeAdapterModule(): Promise<any> {
7
+ return import(claudeAdapterModulePath);
8
+ }
9
+
10
+ class FakeRelay {
11
+ inboxCalls = 0;
12
+ private queuedMessages: any[] = [];
13
+
14
+ queue(...messages: any[]): void {
15
+ this.queuedMessages.push(...messages);
16
+ }
17
+
18
+ async inbox(): Promise<any[]> {
19
+ this.inboxCalls += 1;
20
+ const drained = [...this.queuedMessages];
21
+ this.queuedMessages = [];
22
+ return drained;
23
+ }
24
+ }
25
+
26
+ function getAddedHook(options: any, eventName: 'PostToolUse' | 'Stop') {
27
+ const matchers = options.hooks?.[eventName];
28
+ assert.ok(Array.isArray(matchers), `Expected ${eventName} matchers to be configured`);
29
+ assert.ok(matchers.length > 0, `Expected at least one ${eventName} matcher`);
30
+
31
+ const matcher = matchers.at(-1);
32
+ assert.ok(Array.isArray(matcher.hooks), `Expected ${eventName} matcher to contain hook callbacks`);
33
+ assert.equal(matcher.hooks.length, 1);
34
+
35
+ return matcher.hooks[0];
36
+ }
37
+
38
+ test('Claude onRelay injects the Relaycast MCP server into query options', async () => {
39
+ const { onRelay } = await loadClaudeAdapterModule();
40
+ const relay = new FakeRelay();
41
+
42
+ const options = onRelay(
43
+ 'ClaudeTester',
44
+ {
45
+ mcpServers: {
46
+ existing: {
47
+ command: 'node',
48
+ args: ['./other-mcp.js'],
49
+ },
50
+ },
51
+ },
52
+ relay
53
+ );
54
+
55
+ assert.ok(options.mcpServers);
56
+ assert.deepEqual(options.mcpServers.existing, {
57
+ command: 'node',
58
+ args: ['./other-mcp.js'],
59
+ });
60
+ assert.deepEqual(options.mcpServers.relaycast, {
61
+ command: 'agent-relay',
62
+ args: ['mcp'],
63
+ });
64
+ assert.ok(options.hooks?.PostToolUse?.length);
65
+ assert.ok(options.hooks?.Stop?.length);
66
+ });
67
+
68
+ test('Claude PostToolUse hook returns a systemMessage when relay inbox messages are pending', async () => {
69
+ const { onRelay } = await loadClaudeAdapterModule();
70
+ const relay = new FakeRelay();
71
+ relay.queue({
72
+ sender: 'Other',
73
+ text: 'Hello',
74
+ messageId: 'message-1',
75
+ });
76
+
77
+ const options = onRelay('ClaudeTester', {}, relay);
78
+ const postToolUseHook = getAddedHook(options, 'PostToolUse');
79
+
80
+ const result = await postToolUseHook(
81
+ {
82
+ hook_event_name: 'PostToolUse',
83
+ tool_name: 'Read',
84
+ tool_input: {},
85
+ tool_response: {},
86
+ tool_use_id: 'tool-1',
87
+ },
88
+ 'tool-1',
89
+ { signal: new AbortController().signal }
90
+ );
91
+
92
+ assert.equal(relay.inboxCalls, 1);
93
+ assert.ok(result);
94
+ assert.equal(typeof result.systemMessage, 'string');
95
+ assert.match(result.systemMessage, /New messages from other agents:/);
96
+ assert.match(result.systemMessage, /Relay message from Other: Hello/);
97
+ });
98
+
99
+ test('Claude Stop hook continues the agent when relay messages are pending', async () => {
100
+ const { onRelay } = await loadClaudeAdapterModule();
101
+ const relay = new FakeRelay();
102
+ relay.queue({
103
+ sender: 'Other',
104
+ text: 'Wait!',
105
+ messageId: 'message-2',
106
+ });
107
+
108
+ const options = onRelay('ClaudeTester', {}, relay);
109
+ const stopHook = getAddedHook(options, 'Stop');
110
+
111
+ const result = await stopHook(
112
+ {
113
+ hook_event_name: 'Stop',
114
+ stop_hook_active: true,
115
+ last_assistant_message: 'Stopping now',
116
+ },
117
+ undefined,
118
+ { signal: new AbortController().signal }
119
+ );
120
+
121
+ assert.equal(relay.inboxCalls, 1);
122
+ assert.ok(result);
123
+ assert.equal(result.continue, true);
124
+ assert.equal(typeof result.systemMessage, 'string');
125
+ assert.match(result.systemMessage, /Relay message from Other: Wait!/);
126
+ });
127
+
128
+ test('Claude onRelay preserves existing PostToolUse and Stop hook matchers', async () => {
129
+ const { onRelay } = await loadClaudeAdapterModule();
130
+ const relay = new FakeRelay();
131
+
132
+ const existingPostToolUse = async () => ({ systemMessage: 'Existing post hook' });
133
+ const existingStop = async () => ({ continue: false, systemMessage: 'Existing stop hook' });
134
+
135
+ const options = onRelay(
136
+ 'ClaudeTester',
137
+ {
138
+ hooks: {
139
+ PostToolUse: [
140
+ {
141
+ matcher: 'existing-post',
142
+ hooks: [existingPostToolUse],
143
+ },
144
+ ],
145
+ Stop: [
146
+ {
147
+ matcher: 'existing-stop',
148
+ hooks: [existingStop],
149
+ },
150
+ ],
151
+ },
152
+ },
153
+ relay
154
+ );
155
+
156
+ assert.equal(options.hooks.PostToolUse.length, 2);
157
+ assert.equal(options.hooks.PostToolUse[0].hooks[0], existingPostToolUse);
158
+ assert.equal(options.hooks.PostToolUse[0].matcher, 'existing-post');
159
+
160
+ assert.equal(options.hooks.Stop.length, 2);
161
+ assert.equal(options.hooks.Stop[0].hooks[0], existingStop);
162
+ assert.equal(options.hooks.Stop[0].matcher, 'existing-stop');
163
+ });
@@ -0,0 +1,219 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ const crewaiAdapterModulePath = '../../../communicate/adapters/crewai.js';
5
+
6
+ async function loadCrewAIAdapterModule(): Promise<any> {
7
+ return import(crewaiAdapterModulePath);
8
+ }
9
+
10
+ class FakeRelay {
11
+ private callbacks: Array<(message: any) => void | Promise<void>> = [];
12
+ sendCalls: Array<{ to: string; text: string }> = [];
13
+ postCalls: Array<{ channel: string; text: string }> = [];
14
+
15
+ async send(to: string, text: string): Promise<void> {
16
+ this.sendCalls.push({ to, text });
17
+ }
18
+
19
+ async post(channel: string, text: string): Promise<void> {
20
+ this.postCalls.push({ channel, text });
21
+ }
22
+
23
+ async inbox(): Promise<any[]> {
24
+ return [];
25
+ }
26
+
27
+ async agents(): Promise<string[]> {
28
+ return ['Lead', 'Worker-1'];
29
+ }
30
+
31
+ onMessage(callback: (message: any) => void | Promise<void>): () => void {
32
+ this.callbacks.push(callback);
33
+ return () => {
34
+ this.callbacks = this.callbacks.filter((entry) => entry !== callback);
35
+ };
36
+ }
37
+
38
+ async emit(message: any): Promise<void> {
39
+ for (const callback of [...this.callbacks]) {
40
+ await callback(message);
41
+ }
42
+ }
43
+ }
44
+
45
+ function createAgent(opts: { role?: string; tools?: any[]; step_callback?: any } = {}) {
46
+ return {
47
+ role: opts.role ?? 'researcher',
48
+ tools: opts.tools ?? [],
49
+ step_callback: opts.step_callback ?? null,
50
+ };
51
+ }
52
+
53
+ function createCrew(agents: any[]) {
54
+ return {
55
+ agents,
56
+ task_callback: null,
57
+ };
58
+ }
59
+
60
+ test('CrewAI onRelay appends relay tools to agent.tools', async () => {
61
+ const { onRelay } = await loadCrewAIAdapterModule();
62
+ const relay = new FakeRelay();
63
+ const agent = createAgent({ tools: [{ tool_name: 'existing_tool', description: 'Existing' }] });
64
+
65
+ onRelay(agent, relay);
66
+
67
+ const toolNames = agent.tools.map((t: any) => t.tool_name);
68
+ assert.deepEqual(toolNames, [
69
+ 'existing_tool',
70
+ 'relay_send',
71
+ 'relay_inbox',
72
+ 'relay_post',
73
+ 'relay_agents',
74
+ ]);
75
+
76
+ for (const toolName of ['relay_send', 'relay_inbox', 'relay_post', 'relay_agents']) {
77
+ const tool = agent.tools.find((t: any) => t.tool_name === toolName);
78
+ assert.ok(tool, `Expected ${toolName} to be registered`);
79
+ assert.equal(typeof tool.execute, 'function');
80
+ assert.ok(tool.description);
81
+ }
82
+ });
83
+
84
+ test('CrewAI relay_send tool calls relay.send', async () => {
85
+ const { onRelay } = await loadCrewAIAdapterModule();
86
+ const relay = new FakeRelay();
87
+ const agent = createAgent();
88
+
89
+ onRelay(agent, relay);
90
+
91
+ const sendTool = agent.tools.find((t: any) => t.tool_name === 'relay_send');
92
+ const result = await sendTool.execute({ to: 'Worker-1', text: 'hello' });
93
+
94
+ assert.equal(relay.sendCalls.length, 1);
95
+ assert.deepEqual(relay.sendCalls[0], { to: 'Worker-1', text: 'hello' });
96
+ assert.match(result, /Sent relay message to Worker-1/);
97
+ });
98
+
99
+ test('CrewAI relay_post tool calls relay.post', async () => {
100
+ const { onRelay } = await loadCrewAIAdapterModule();
101
+ const relay = new FakeRelay();
102
+ const agent = createAgent();
103
+
104
+ onRelay(agent, relay);
105
+
106
+ const postTool = agent.tools.find((t: any) => t.tool_name === 'relay_post');
107
+ const result = await postTool.execute({ channel: 'general', text: 'update' });
108
+
109
+ assert.equal(relay.postCalls.length, 1);
110
+ assert.deepEqual(relay.postCalls[0], { channel: 'general', text: 'update' });
111
+ assert.match(result, /Posted relay message to #general/);
112
+ });
113
+
114
+ test('CrewAI relay_inbox tool returns formatted inbox', async () => {
115
+ const { onRelay } = await loadCrewAIAdapterModule();
116
+ const relay = new FakeRelay();
117
+ const agent = createAgent();
118
+
119
+ onRelay(agent, relay);
120
+
121
+ const inboxTool = agent.tools.find((t: any) => t.tool_name === 'relay_inbox');
122
+ const result = await inboxTool.execute({});
123
+
124
+ assert.equal(result, 'No new relay messages.');
125
+ });
126
+
127
+ test('CrewAI relay_agents tool lists agents', async () => {
128
+ const { onRelay } = await loadCrewAIAdapterModule();
129
+ const relay = new FakeRelay();
130
+ const agent = createAgent();
131
+
132
+ onRelay(agent, relay);
133
+
134
+ const agentsTool = agent.tools.find((t: any) => t.tool_name === 'relay_agents');
135
+ const result = await agentsTool.execute({});
136
+
137
+ assert.equal(result, 'Lead\nWorker-1');
138
+ });
139
+
140
+ test('CrewAI onRelay routes incoming messages via step_callback', async () => {
141
+ const { onRelay } = await loadCrewAIAdapterModule();
142
+ const relay = new FakeRelay();
143
+ const stepCalls: any[] = [];
144
+ const agent = createAgent({
145
+ step_callback: (step: any) => { stepCalls.push(step); },
146
+ });
147
+
148
+ onRelay(agent, relay);
149
+
150
+ await relay.emit({
151
+ sender: 'Lead',
152
+ text: 'Need status update',
153
+ messageId: 'msg-1',
154
+ });
155
+
156
+ assert.equal(stepCalls.length, 1);
157
+ assert.ok(stepCalls[0].relay_message);
158
+ assert.match(stepCalls[0].relay_message, /Lead/);
159
+ assert.match(stepCalls[0].relay_message, /Need status update/);
160
+ });
161
+
162
+ test('CrewAI onRelay unsubscribe stops message routing', async () => {
163
+ const { onRelay } = await loadCrewAIAdapterModule();
164
+ const relay = new FakeRelay();
165
+ const stepCalls: any[] = [];
166
+ const agent = createAgent({
167
+ step_callback: (step: any) => { stepCalls.push(step); },
168
+ });
169
+
170
+ const { unsubscribe } = onRelay(agent, relay);
171
+
172
+ await relay.emit({ sender: 'A', text: 'first', messageId: 'm1' });
173
+ assert.equal(stepCalls.length, 1);
174
+
175
+ unsubscribe();
176
+
177
+ await relay.emit({ sender: 'B', text: 'second', messageId: 'm2' });
178
+ assert.equal(stepCalls.length, 1); // no new calls after unsubscribe
179
+ });
180
+
181
+ test('CrewAI onCrewRelay adds tools to all agents in a crew', async () => {
182
+ const { onCrewRelay } = await loadCrewAIAdapterModule();
183
+ const relay = new FakeRelay();
184
+ const agent1 = createAgent({ role: 'researcher' });
185
+ const agent2 = createAgent({ role: 'writer' });
186
+ const crew = createCrew([agent1, agent2]);
187
+
188
+ onCrewRelay(crew, relay);
189
+
190
+ for (const agent of [agent1, agent2]) {
191
+ const toolNames = agent.tools.map((t: any) => t.tool_name);
192
+ assert.ok(toolNames.includes('relay_send'), `${agent.role} should have relay_send`);
193
+ assert.ok(toolNames.includes('relay_inbox'), `${agent.role} should have relay_inbox`);
194
+ assert.ok(toolNames.includes('relay_post'), `${agent.role} should have relay_post`);
195
+ assert.ok(toolNames.includes('relay_agents'), `${agent.role} should have relay_agents`);
196
+ }
197
+ });
198
+
199
+ test('CrewAI onCrewRelay unsubscribe stops all routing', async () => {
200
+ const { onCrewRelay } = await loadCrewAIAdapterModule();
201
+ const relay = new FakeRelay();
202
+ const stepCalls1: any[] = [];
203
+ const stepCalls2: any[] = [];
204
+ const agent1 = createAgent({ role: 'a1', step_callback: (s: any) => { stepCalls1.push(s); } });
205
+ const agent2 = createAgent({ role: 'a2', step_callback: (s: any) => { stepCalls2.push(s); } });
206
+ const crew = createCrew([agent1, agent2]);
207
+
208
+ const { unsubscribe } = onCrewRelay(crew, relay);
209
+
210
+ await relay.emit({ sender: 'X', text: 'ping', messageId: 'm1' });
211
+ assert.equal(stepCalls1.length, 1);
212
+ assert.equal(stepCalls2.length, 1);
213
+
214
+ unsubscribe();
215
+
216
+ await relay.emit({ sender: 'Y', text: 'pong', messageId: 'm2' });
217
+ assert.equal(stepCalls1.length, 1);
218
+ assert.equal(stepCalls2.length, 1);
219
+ });