signalwire-agents 0.1.0

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 (245) hide show
  1. package/CLAUDE.md +112 -0
  2. package/README.md +417 -0
  3. package/dist/AgentBase.d.ts +481 -0
  4. package/dist/AgentBase.js +1155 -0
  5. package/dist/AgentBase.js.map +1 -0
  6. package/dist/AgentServer.d.ts +60 -0
  7. package/dist/AgentServer.js +128 -0
  8. package/dist/AgentServer.js.map +1 -0
  9. package/dist/AuthHandler.d.ts +56 -0
  10. package/dist/AuthHandler.js +119 -0
  11. package/dist/AuthHandler.js.map +1 -0
  12. package/dist/ConfigLoader.d.ts +68 -0
  13. package/dist/ConfigLoader.js +150 -0
  14. package/dist/ConfigLoader.js.map +1 -0
  15. package/dist/ContextBuilder.d.ts +431 -0
  16. package/dist/ContextBuilder.js +751 -0
  17. package/dist/ContextBuilder.js.map +1 -0
  18. package/dist/DataMap.d.ts +179 -0
  19. package/dist/DataMap.js +321 -0
  20. package/dist/DataMap.js.map +1 -0
  21. package/dist/Logger.d.ts +91 -0
  22. package/dist/Logger.js +191 -0
  23. package/dist/Logger.js.map +1 -0
  24. package/dist/PomBuilder.d.ts +144 -0
  25. package/dist/PomBuilder.js +249 -0
  26. package/dist/PomBuilder.js.map +1 -0
  27. package/dist/PromptManager.d.ts +85 -0
  28. package/dist/PromptManager.js +108 -0
  29. package/dist/PromptManager.js.map +1 -0
  30. package/dist/SchemaUtils.d.ts +42 -0
  31. package/dist/SchemaUtils.js +148 -0
  32. package/dist/SchemaUtils.js.map +1 -0
  33. package/dist/ServerlessAdapter.d.ts +100 -0
  34. package/dist/ServerlessAdapter.js +166 -0
  35. package/dist/ServerlessAdapter.js.map +1 -0
  36. package/dist/SessionManager.d.ts +86 -0
  37. package/dist/SessionManager.js +155 -0
  38. package/dist/SessionManager.js.map +1 -0
  39. package/dist/SslConfig.d.ts +77 -0
  40. package/dist/SslConfig.js +100 -0
  41. package/dist/SslConfig.js.map +1 -0
  42. package/dist/SwaigFunction.d.ts +88 -0
  43. package/dist/SwaigFunction.js +125 -0
  44. package/dist/SwaigFunction.js.map +1 -0
  45. package/dist/SwaigFunctionResult.d.ts +442 -0
  46. package/dist/SwaigFunctionResult.js +735 -0
  47. package/dist/SwaigFunctionResult.js.map +1 -0
  48. package/dist/SwmlBuilder.d.ts +37 -0
  49. package/dist/SwmlBuilder.js +55 -0
  50. package/dist/SwmlBuilder.js.map +1 -0
  51. package/dist/cli/agent-loader.d.ts +22 -0
  52. package/dist/cli/agent-loader.js +106 -0
  53. package/dist/cli/agent-loader.js.map +1 -0
  54. package/dist/cli/mock-data.d.ts +37 -0
  55. package/dist/cli/mock-data.js +79 -0
  56. package/dist/cli/mock-data.js.map +1 -0
  57. package/dist/cli/swaig-test.d.ts +32 -0
  58. package/dist/cli/swaig-test.js +405 -0
  59. package/dist/cli/swaig-test.js.map +1 -0
  60. package/dist/index.d.ts +36 -0
  61. package/dist/index.js +44 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/prefabs/ConciergeAgent.d.ts +64 -0
  64. package/dist/prefabs/ConciergeAgent.js +188 -0
  65. package/dist/prefabs/ConciergeAgent.js.map +1 -0
  66. package/dist/prefabs/FAQBotAgent.d.ts +68 -0
  67. package/dist/prefabs/FAQBotAgent.js +206 -0
  68. package/dist/prefabs/FAQBotAgent.js.map +1 -0
  69. package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
  70. package/dist/prefabs/InfoGathererAgent.js +202 -0
  71. package/dist/prefabs/InfoGathererAgent.js.map +1 -0
  72. package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
  73. package/dist/prefabs/ReceptionistAgent.js +205 -0
  74. package/dist/prefabs/ReceptionistAgent.js.map +1 -0
  75. package/dist/prefabs/SurveyAgent.d.ts +84 -0
  76. package/dist/prefabs/SurveyAgent.js +324 -0
  77. package/dist/prefabs/SurveyAgent.js.map +1 -0
  78. package/dist/prefabs/index.d.ts +13 -0
  79. package/dist/prefabs/index.js +14 -0
  80. package/dist/prefabs/index.js.map +1 -0
  81. package/dist/skills/SkillBase.d.ts +134 -0
  82. package/dist/skills/SkillBase.js +105 -0
  83. package/dist/skills/SkillBase.js.map +1 -0
  84. package/dist/skills/SkillManager.d.ts +83 -0
  85. package/dist/skills/SkillManager.js +159 -0
  86. package/dist/skills/SkillManager.js.map +1 -0
  87. package/dist/skills/SkillRegistry.d.ts +105 -0
  88. package/dist/skills/SkillRegistry.js +195 -0
  89. package/dist/skills/SkillRegistry.js.map +1 -0
  90. package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
  91. package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
  92. package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
  93. package/dist/skills/builtin/claude_skills.d.ts +34 -0
  94. package/dist/skills/builtin/claude_skills.js +156 -0
  95. package/dist/skills/builtin/claude_skills.js.map +1 -0
  96. package/dist/skills/builtin/custom_skills.d.ts +59 -0
  97. package/dist/skills/builtin/custom_skills.js +231 -0
  98. package/dist/skills/builtin/custom_skills.js.map +1 -0
  99. package/dist/skills/builtin/datasphere.d.ts +34 -0
  100. package/dist/skills/builtin/datasphere.js +163 -0
  101. package/dist/skills/builtin/datasphere.js.map +1 -0
  102. package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
  103. package/dist/skills/builtin/datasphere_serverless.js +155 -0
  104. package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
  105. package/dist/skills/builtin/datetime.d.ts +31 -0
  106. package/dist/skills/builtin/datetime.js +95 -0
  107. package/dist/skills/builtin/datetime.js.map +1 -0
  108. package/dist/skills/builtin/google_maps.d.ts +33 -0
  109. package/dist/skills/builtin/google_maps.js +233 -0
  110. package/dist/skills/builtin/google_maps.js.map +1 -0
  111. package/dist/skills/builtin/index.d.ts +28 -0
  112. package/dist/skills/builtin/index.js +75 -0
  113. package/dist/skills/builtin/index.js.map +1 -0
  114. package/dist/skills/builtin/info_gatherer.d.ts +52 -0
  115. package/dist/skills/builtin/info_gatherer.js +237 -0
  116. package/dist/skills/builtin/info_gatherer.js.map +1 -0
  117. package/dist/skills/builtin/joke.d.ts +32 -0
  118. package/dist/skills/builtin/joke.js +142 -0
  119. package/dist/skills/builtin/joke.js.map +1 -0
  120. package/dist/skills/builtin/math.d.ts +33 -0
  121. package/dist/skills/builtin/math.js +120 -0
  122. package/dist/skills/builtin/math.js.map +1 -0
  123. package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
  124. package/dist/skills/builtin/mcp_gateway.js +85 -0
  125. package/dist/skills/builtin/mcp_gateway.js.map +1 -0
  126. package/dist/skills/builtin/native_vector_search.d.ts +44 -0
  127. package/dist/skills/builtin/native_vector_search.js +248 -0
  128. package/dist/skills/builtin/native_vector_search.js.map +1 -0
  129. package/dist/skills/builtin/play_background_file.d.ts +34 -0
  130. package/dist/skills/builtin/play_background_file.js +135 -0
  131. package/dist/skills/builtin/play_background_file.js.map +1 -0
  132. package/dist/skills/builtin/spider.d.ts +34 -0
  133. package/dist/skills/builtin/spider.js +164 -0
  134. package/dist/skills/builtin/spider.js.map +1 -0
  135. package/dist/skills/builtin/swml_transfer.d.ts +44 -0
  136. package/dist/skills/builtin/swml_transfer.js +204 -0
  137. package/dist/skills/builtin/swml_transfer.js.map +1 -0
  138. package/dist/skills/builtin/weather_api.d.ts +34 -0
  139. package/dist/skills/builtin/weather_api.js +136 -0
  140. package/dist/skills/builtin/weather_api.js.map +1 -0
  141. package/dist/skills/builtin/web_search.d.ts +34 -0
  142. package/dist/skills/builtin/web_search.js +141 -0
  143. package/dist/skills/builtin/web_search.js.map +1 -0
  144. package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
  145. package/dist/skills/builtin/wikipedia_search.js +166 -0
  146. package/dist/skills/builtin/wikipedia_search.js.map +1 -0
  147. package/dist/skills/index.d.ts +11 -0
  148. package/dist/skills/index.js +10 -0
  149. package/dist/skills/index.js.map +1 -0
  150. package/dist/types.d.ts +85 -0
  151. package/dist/types.js +5 -0
  152. package/dist/types.js.map +1 -0
  153. package/docs/agent-guide.md +869 -0
  154. package/docs/api-reference.md +2419 -0
  155. package/docs/architecture.md +843 -0
  156. package/docs/cli-guide.md +579 -0
  157. package/docs/configuration.md +422 -0
  158. package/docs/contexts-guide.md +787 -0
  159. package/docs/datamap-guide.md +1141 -0
  160. package/docs/prefabs-guide.md +830 -0
  161. package/docs/security.md +436 -0
  162. package/docs/serverless-guide.md +513 -0
  163. package/docs/skills-guide.md +845 -0
  164. package/docs/swaig-reference.md +1627 -0
  165. package/examples/call-flow.ts +60 -0
  166. package/examples/contexts-steps.ts +62 -0
  167. package/examples/datamap-tools.ts +52 -0
  168. package/examples/dynamic-config.ts +51 -0
  169. package/examples/multi-agent.ts +67 -0
  170. package/examples/pom-prompt.ts +68 -0
  171. package/examples/simple-agent.ts +33 -0
  172. package/package.json +35 -0
  173. package/src/AgentBase.ts +1279 -0
  174. package/src/AgentServer.ts +149 -0
  175. package/src/AuthHandler.ts +141 -0
  176. package/src/ConfigLoader.ts +165 -0
  177. package/src/ContextBuilder.ts +796 -0
  178. package/src/DataMap.ts +365 -0
  179. package/src/Logger.ts +197 -0
  180. package/src/PomBuilder.ts +298 -0
  181. package/src/PromptManager.ts +133 -0
  182. package/src/SchemaUtils.ts +170 -0
  183. package/src/ServerlessAdapter.ts +218 -0
  184. package/src/SessionManager.ts +161 -0
  185. package/src/SslConfig.ts +118 -0
  186. package/src/SwaigFunction.ts +168 -0
  187. package/src/SwaigFunctionResult.ts +864 -0
  188. package/src/SwmlBuilder.ts +62 -0
  189. package/src/cli/agent-loader.ts +122 -0
  190. package/src/cli/mock-data.ts +110 -0
  191. package/src/cli/swaig-test.ts +440 -0
  192. package/src/index.ts +99 -0
  193. package/src/prefabs/ConciergeAgent.ts +251 -0
  194. package/src/prefabs/FAQBotAgent.ts +263 -0
  195. package/src/prefabs/InfoGathererAgent.ts +276 -0
  196. package/src/prefabs/ReceptionistAgent.ts +277 -0
  197. package/src/prefabs/SurveyAgent.ts +413 -0
  198. package/src/prefabs/index.ts +23 -0
  199. package/src/skills/SkillBase.ts +184 -0
  200. package/src/skills/SkillManager.ts +177 -0
  201. package/src/skills/SkillRegistry.ts +231 -0
  202. package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
  203. package/src/skills/builtin/claude_skills.ts +239 -0
  204. package/src/skills/builtin/custom_skills.ts +326 -0
  205. package/src/skills/builtin/datasphere.ts +225 -0
  206. package/src/skills/builtin/datasphere_serverless.ts +194 -0
  207. package/src/skills/builtin/datetime.ts +115 -0
  208. package/src/skills/builtin/google_maps.ts +347 -0
  209. package/src/skills/builtin/index.ts +79 -0
  210. package/src/skills/builtin/info_gatherer.ts +331 -0
  211. package/src/skills/builtin/joke.ts +180 -0
  212. package/src/skills/builtin/math.ts +150 -0
  213. package/src/skills/builtin/mcp_gateway.ts +99 -0
  214. package/src/skills/builtin/native_vector_search.ts +327 -0
  215. package/src/skills/builtin/play_background_file.ts +189 -0
  216. package/src/skills/builtin/spider.ts +232 -0
  217. package/src/skills/builtin/swml_transfer.ts +285 -0
  218. package/src/skills/builtin/weather_api.ts +193 -0
  219. package/src/skills/builtin/web_search.ts +212 -0
  220. package/src/skills/builtin/wikipedia_search.ts +248 -0
  221. package/src/skills/index.ts +12 -0
  222. package/src/types.ts +99 -0
  223. package/tests/AgentBase.test.ts +693 -0
  224. package/tests/AgentServer.test.ts +102 -0
  225. package/tests/AuthHandler.test.ts +121 -0
  226. package/tests/ConfigLoader.test.ts +121 -0
  227. package/tests/DataMap.test.ts +286 -0
  228. package/tests/Logger.test.ts +218 -0
  229. package/tests/PomBuilder.test.ts +164 -0
  230. package/tests/ProxyDetection.test.ts +185 -0
  231. package/tests/SchemaUtils.test.ts +106 -0
  232. package/tests/ServerlessAdapter.test.ts +200 -0
  233. package/tests/SessionManager.test.ts +139 -0
  234. package/tests/SslConfig.test.ts +97 -0
  235. package/tests/SwaigFunctionResult.test.ts +298 -0
  236. package/tests/cli/mock-data.test.ts +94 -0
  237. package/tests/cli/swaig-test.test.ts +96 -0
  238. package/tests/prefabs/prefabs.test.ts +854 -0
  239. package/tests/setup.ts +6 -0
  240. package/tests/skills/SkillBase.test.ts +118 -0
  241. package/tests/skills/SkillManager.test.ts +164 -0
  242. package/tests/skills/SkillRegistry.test.ts +124 -0
  243. package/tests/skills/builtin.test.ts +924 -0
  244. package/tsconfig.json +18 -0
  245. package/vitest.config.ts +9 -0
@@ -0,0 +1,854 @@
1
+ import { describe, it, expect, vi, beforeAll } from 'vitest';
2
+ import { suppressAllLogs } from '../../src/Logger.js';
3
+ import { InfoGathererAgent, createInfoGathererAgent } from '../../src/prefabs/InfoGathererAgent.js';
4
+ import { SurveyAgent, createSurveyAgent } from '../../src/prefabs/SurveyAgent.js';
5
+ import { FAQBotAgent, createFAQBotAgent } from '../../src/prefabs/FAQBotAgent.js';
6
+ import { ConciergeAgent, createConciergeAgent } from '../../src/prefabs/ConciergeAgent.js';
7
+ import { ReceptionistAgent, createReceptionistAgent } from '../../src/prefabs/ReceptionistAgent.js';
8
+
9
+ beforeAll(() => {
10
+ suppressAllLogs(true);
11
+ });
12
+
13
+ // ============================================================================
14
+ // InfoGathererAgent
15
+ // ============================================================================
16
+
17
+ describe('InfoGathererAgent', () => {
18
+ const baseFields = [
19
+ { name: 'full_name', description: 'Full name of the caller', required: true },
20
+ { name: 'email', description: 'Email address', required: true, validation: '^[^@]+@[^@]+\\.[^@]+$' },
21
+ { name: 'phone', description: 'Phone number', required: true, validation: /^\d{10}$/ },
22
+ ];
23
+
24
+ function createAgent(overrides?: Record<string, unknown>) {
25
+ return new InfoGathererAgent({
26
+ fields: baseFields,
27
+ ...overrides,
28
+ });
29
+ }
30
+
31
+ it('creates with fields config', () => {
32
+ const agent = createAgent();
33
+ expect(agent).toBeInstanceOf(InfoGathererAgent);
34
+ });
35
+
36
+ it('renders valid SWML', () => {
37
+ const agent = createAgent();
38
+ const swml = agent.renderSwml('test-call');
39
+ const parsed = JSON.parse(swml);
40
+ expect(parsed.version).toBe('1.0.0');
41
+ expect(parsed.sections.main).toBeDefined();
42
+ });
43
+
44
+ it('has save_field and get_status tools', () => {
45
+ const agent = createAgent();
46
+ expect(agent.getTool('save_field')).toBeDefined();
47
+ expect(agent.getTool('get_status')).toBeDefined();
48
+ });
49
+
50
+ it('save_field handler stores data', async () => {
51
+ const agent = createAgent();
52
+ const tool = agent.getTool('save_field')!;
53
+ const result = await tool.execute(
54
+ { field_name: 'full_name', value: 'John Doe' },
55
+ { call_id: 'call-1' },
56
+ );
57
+ expect(result.response).toContain('saved');
58
+ expect(result.response).toContain('full_name');
59
+ });
60
+
61
+ it('save_field validates against regex string pattern', async () => {
62
+ const agent = createAgent();
63
+ const tool = agent.getTool('save_field')!;
64
+ const result = await tool.execute(
65
+ { field_name: 'email', value: 'not-an-email' },
66
+ { call_id: 'call-2' },
67
+ );
68
+ expect(result.response).toContain('not valid');
69
+ });
70
+
71
+ it('save_field validates against RegExp pattern', async () => {
72
+ const agent = createAgent();
73
+ const tool = agent.getTool('save_field')!;
74
+ const result = await tool.execute(
75
+ { field_name: 'phone', value: '123' },
76
+ { call_id: 'call-3' },
77
+ );
78
+ expect(result.response).toContain('not valid');
79
+
80
+ // Valid phone passes
81
+ const result2 = await tool.execute(
82
+ { field_name: 'phone', value: '5551234567' },
83
+ { call_id: 'call-3' },
84
+ );
85
+ expect(result2.response).toContain('saved');
86
+ });
87
+
88
+ it('get_status returns collected and remaining', async () => {
89
+ const agent = createAgent();
90
+ const saveTool = agent.getTool('save_field')!;
91
+ await saveTool.execute(
92
+ { field_name: 'full_name', value: 'Jane Doe' },
93
+ { call_id: 'call-4' },
94
+ );
95
+
96
+ const statusTool = agent.getTool('get_status')!;
97
+ const status = await statusTool.execute({}, { call_id: 'call-4' });
98
+ expect(status.response).toContain('full_name: Jane Doe');
99
+ expect(status.response).toContain('email');
100
+ expect(status.response).toContain('phone');
101
+ });
102
+
103
+ it('all required fields triggers completion message', async () => {
104
+ const agent = createAgent();
105
+ const tool = agent.getTool('save_field')!;
106
+ const callData = { call_id: 'call-5' };
107
+
108
+ await tool.execute({ field_name: 'full_name', value: 'Jane Doe' }, callData);
109
+ await tool.execute({ field_name: 'email', value: 'jane@example.com' }, callData);
110
+ const result = await tool.execute({ field_name: 'phone', value: '5551234567' }, callData);
111
+
112
+ expect(result.response).toContain('All required fields are now collected');
113
+ expect(result.response).toContain('Thank you');
114
+ });
115
+
116
+ it('prompt mentions field names', () => {
117
+ const agent = createAgent();
118
+ const swml = agent.renderSwml('test-call');
119
+ const parsed = JSON.parse(swml);
120
+ const promptText = JSON.stringify(parsed);
121
+ expect(promptText).toContain('full_name');
122
+ expect(promptText).toContain('email');
123
+ expect(promptText).toContain('phone');
124
+ });
125
+
126
+ it('works with optional fields', async () => {
127
+ const agent = new InfoGathererAgent({
128
+ fields: [
129
+ { name: 'name', description: 'Name', required: true },
130
+ { name: 'nickname', description: 'Nickname', required: false },
131
+ ],
132
+ });
133
+
134
+ const saveTool = agent.getTool('save_field')!;
135
+ const callData = { call_id: 'call-6' };
136
+
137
+ // Completing only the required field should trigger completion
138
+ const result = await saveTool.execute({ field_name: 'name', value: 'Alice' }, callData);
139
+ expect(result.response).toContain('All required fields are now collected');
140
+
141
+ // get_status should mention optional field not yet collected
142
+ const statusTool = agent.getTool('get_status')!;
143
+ const status = await statusTool.execute({}, callData);
144
+ expect(status.response).toContain('Optional');
145
+ expect(status.response).toContain('nickname');
146
+ });
147
+
148
+ it('onComplete callback fires when all required fields collected', async () => {
149
+ const onComplete = vi.fn();
150
+ const agent = new InfoGathererAgent({
151
+ fields: [{ name: 'name', description: 'Name' }],
152
+ onComplete,
153
+ });
154
+
155
+ const tool = agent.getTool('save_field')!;
156
+ await tool.execute({ field_name: 'name', value: 'Bob' }, { call_id: 'call-7' });
157
+
158
+ expect(onComplete).toHaveBeenCalledTimes(1);
159
+ expect(onComplete).toHaveBeenCalledWith({ name: 'Bob' });
160
+ });
161
+
162
+ it('returns error for invalid field name', async () => {
163
+ const agent = createAgent();
164
+ const tool = agent.getTool('save_field')!;
165
+ const result = await tool.execute(
166
+ { field_name: 'nonexistent', value: 'val' },
167
+ { call_id: 'call-8' },
168
+ );
169
+ expect(result.response).toContain('Unknown field');
170
+ expect(result.response).toContain('Available fields');
171
+ });
172
+
173
+ it('factory function creates agent', () => {
174
+ const agent = createInfoGathererAgent({ fields: baseFields });
175
+ expect(agent).toBeInstanceOf(InfoGathererAgent);
176
+ expect(agent.getTool('save_field')).toBeDefined();
177
+ });
178
+
179
+ it('custom name and messages are applied', () => {
180
+ const agent = new InfoGathererAgent({
181
+ name: 'CustomGatherer',
182
+ fields: [{ name: 'age', description: 'Age' }],
183
+ introMessage: 'Hi there!',
184
+ confirmationMessage: 'All done!',
185
+ });
186
+ const swml = agent.renderSwml('test-call');
187
+ expect(swml).toContain('Hi there!');
188
+ });
189
+ });
190
+
191
+ // ============================================================================
192
+ // SurveyAgent
193
+ // ============================================================================
194
+
195
+ describe('SurveyAgent', () => {
196
+ const baseQuestions = [
197
+ {
198
+ id: 'q1',
199
+ text: 'How satisfied are you with our service?',
200
+ type: 'rating' as const,
201
+ points: 1,
202
+ },
203
+ {
204
+ id: 'q2',
205
+ text: 'Would you recommend us?',
206
+ type: 'yes_no' as const,
207
+ points: { yes: 10, no: 0 },
208
+ },
209
+ {
210
+ id: 'q3',
211
+ text: 'Which feature do you like most?',
212
+ type: 'multiple_choice' as const,
213
+ options: ['Speed', 'Price', 'Support'],
214
+ },
215
+ {
216
+ id: 'q4',
217
+ text: 'Any additional comments?',
218
+ type: 'open_ended' as const,
219
+ },
220
+ ];
221
+
222
+ function createAgent(overrides?: Record<string, unknown>) {
223
+ return new SurveyAgent({
224
+ questions: baseQuestions,
225
+ ...overrides,
226
+ });
227
+ }
228
+
229
+ it('creates with questions config', () => {
230
+ const agent = createAgent();
231
+ expect(agent).toBeInstanceOf(SurveyAgent);
232
+ });
233
+
234
+ it('renders valid SWML', () => {
235
+ const agent = createAgent();
236
+ const swml = agent.renderSwml('test-call');
237
+ const parsed = JSON.parse(swml);
238
+ expect(parsed.version).toBe('1.0.0');
239
+ expect(parsed.sections.main).toBeDefined();
240
+ });
241
+
242
+ it('has answer_question, get_current_question, and get_survey_progress tools', () => {
243
+ const agent = createAgent();
244
+ expect(agent.getTool('answer_question')).toBeDefined();
245
+ expect(agent.getTool('get_current_question')).toBeDefined();
246
+ expect(agent.getTool('get_survey_progress')).toBeDefined();
247
+ });
248
+
249
+ it('get_current_question returns first question initially', async () => {
250
+ const agent = createAgent();
251
+ const tool = agent.getTool('get_current_question')!;
252
+ const result = await tool.execute({}, { call_id: 'survey-1' });
253
+ expect(result.response).toContain('q1');
254
+ expect(result.response).toContain('How satisfied');
255
+ });
256
+
257
+ it('answer_question stores response and advances', async () => {
258
+ const agent = createAgent();
259
+ const tool = agent.getTool('answer_question')!;
260
+ const callData = { call_id: 'survey-2' };
261
+
262
+ const result = await tool.execute({ question_id: 'q1', answer: '8' }, callData);
263
+ expect(result.response).toContain('Answer recorded');
264
+ expect(result.response).toContain('q2');
265
+
266
+ // Verify progress
267
+ const progressTool = agent.getTool('get_survey_progress')!;
268
+ const progress = await progressTool.execute({}, callData);
269
+ expect(progress.response).toContain('1/4');
270
+ expect(progress.response).toContain('q1: 8');
271
+ });
272
+
273
+ it('branching: conditional nextQuestion works', async () => {
274
+ const branchQuestions = [
275
+ {
276
+ id: 'start',
277
+ text: 'Do you like it?',
278
+ type: 'yes_no' as const,
279
+ nextQuestion: { yes: 'positive', no: 'negative' },
280
+ },
281
+ {
282
+ id: 'positive',
283
+ text: 'What do you love?',
284
+ type: 'open_ended' as const,
285
+ },
286
+ {
287
+ id: 'negative',
288
+ text: 'What can we improve?',
289
+ type: 'open_ended' as const,
290
+ },
291
+ ];
292
+
293
+ const agent = new SurveyAgent({ questions: branchQuestions });
294
+ const tool = agent.getTool('answer_question')!;
295
+
296
+ // Answer "yes" should branch to "positive"
297
+ const result = await tool.execute(
298
+ { question_id: 'start', answer: 'yes' },
299
+ { call_id: 'branch-1' },
300
+ );
301
+ expect(result.response).toContain('positive');
302
+ expect(result.response).toContain('What do you love');
303
+
304
+ // A separate call answering "no" should branch to "negative"
305
+ const agent2 = new SurveyAgent({ questions: branchQuestions });
306
+ const tool2 = agent2.getTool('answer_question')!;
307
+ const result2 = await tool2.execute(
308
+ { question_id: 'start', answer: 'no' },
309
+ { call_id: 'branch-2' },
310
+ );
311
+ expect(result2.response).toContain('negative');
312
+ expect(result2.response).toContain('What can we improve');
313
+ });
314
+
315
+ it('scoring: points accumulate', async () => {
316
+ const agent = createAgent();
317
+ const tool = agent.getTool('answer_question')!;
318
+ const callData = { call_id: 'score-1' };
319
+
320
+ // q1 has fixed points=1
321
+ await tool.execute({ question_id: 'q1', answer: '7' }, callData);
322
+ // q2 has per-answer: yes=10
323
+ await tool.execute({ question_id: 'q2', answer: 'yes' }, callData);
324
+
325
+ const progressTool = agent.getTool('get_survey_progress')!;
326
+ const progress = await progressTool.execute({}, callData);
327
+ expect(progress.response).toContain('Current score: 11');
328
+ });
329
+
330
+ it('get_survey_progress shows progress percentage', async () => {
331
+ const agent = createAgent();
332
+ const tool = agent.getTool('answer_question')!;
333
+ const callData = { call_id: 'prog-1' };
334
+
335
+ await tool.execute({ question_id: 'q1', answer: '5' }, callData);
336
+ await tool.execute({ question_id: 'q2', answer: 'yes' }, callData);
337
+
338
+ const progressTool = agent.getTool('get_survey_progress')!;
339
+ const progress = await progressTool.execute({}, callData);
340
+ expect(progress.response).toContain('2/4');
341
+ expect(progress.response).toContain('50%');
342
+ });
343
+
344
+ it('survey completion triggers onComplete', async () => {
345
+ const onComplete = vi.fn();
346
+ const simpleQuestions = [
347
+ { id: 'only', text: 'Rate us', type: 'rating' as const, points: 5 },
348
+ ];
349
+ const agent = new SurveyAgent({ questions: simpleQuestions, onComplete });
350
+
351
+ const tool = agent.getTool('answer_question')!;
352
+ await tool.execute({ question_id: 'only', answer: '9' }, { call_id: 'done-1' });
353
+
354
+ expect(onComplete).toHaveBeenCalledTimes(1);
355
+ expect(onComplete).toHaveBeenCalledWith({ only: '9' }, 5);
356
+ });
357
+
358
+ it('rating type validates range', async () => {
359
+ const agent = createAgent();
360
+ const tool = agent.getTool('answer_question')!;
361
+
362
+ // Out of range
363
+ const result = await tool.execute(
364
+ { question_id: 'q1', answer: '15' },
365
+ { call_id: 'val-1' },
366
+ );
367
+ expect(result.response).toContain('rating between 1 and 10');
368
+
369
+ // Not a number
370
+ const result2 = await tool.execute(
371
+ { question_id: 'q1', answer: 'abc' },
372
+ { call_id: 'val-2' },
373
+ );
374
+ expect(result2.response).toContain('rating between 1 and 10');
375
+ });
376
+
377
+ it('yes/no type normalizes answers', async () => {
378
+ const agent = createAgent();
379
+ const tool = agent.getTool('answer_question')!;
380
+ const callData = { call_id: 'norm-1' };
381
+
382
+ // Answer q1 first (rating)
383
+ await tool.execute({ question_id: 'q1', answer: '7' }, callData);
384
+
385
+ // "yeah" should be normalized to "yes"
386
+ await tool.execute({ question_id: 'q2', answer: 'yeah' }, callData);
387
+
388
+ const progressTool = agent.getTool('get_survey_progress')!;
389
+ const progress = await progressTool.execute({}, callData);
390
+ expect(progress.response).toContain('q2: yes');
391
+ });
392
+
393
+ it('multiple choice validates options', async () => {
394
+ const agent = createAgent();
395
+ const tool = agent.getTool('answer_question')!;
396
+ const callData = { call_id: 'mc-1' };
397
+
398
+ // Skip to q3 by answering q1 and q2 first
399
+ await tool.execute({ question_id: 'q1', answer: '5' }, callData);
400
+ await tool.execute({ question_id: 'q2', answer: 'yes' }, callData);
401
+
402
+ // Invalid option
403
+ const result = await tool.execute(
404
+ { question_id: 'q3', answer: 'InvalidOption' },
405
+ callData,
406
+ );
407
+ expect(result.response).toContain('Invalid answer');
408
+ expect(result.response).toContain('Speed');
409
+ expect(result.response).toContain('Price');
410
+ expect(result.response).toContain('Support');
411
+ });
412
+
413
+ it('factory function creates agent', () => {
414
+ const agent = createSurveyAgent({ questions: baseQuestions });
415
+ expect(agent).toBeInstanceOf(SurveyAgent);
416
+ expect(agent.getTool('answer_question')).toBeDefined();
417
+ });
418
+ });
419
+
420
+ // ============================================================================
421
+ // FAQBotAgent
422
+ // ============================================================================
423
+
424
+ describe('FAQBotAgent', () => {
425
+ const baseFaqs = [
426
+ {
427
+ question: 'What are your business hours?',
428
+ answer: 'We are open Monday through Friday, 9am to 5pm EST.',
429
+ keywords: ['hours', 'open', 'schedule', 'time'],
430
+ },
431
+ {
432
+ question: 'How do I reset my password?',
433
+ answer: 'Go to the login page, click "Forgot Password", and follow the instructions.',
434
+ keywords: ['password', 'reset', 'login', 'forgot'],
435
+ },
436
+ {
437
+ question: 'What is the return policy?',
438
+ answer: 'You can return any item within 30 days of purchase for a full refund.',
439
+ keywords: ['return', 'refund', 'policy'],
440
+ },
441
+ ];
442
+
443
+ function createAgent(overrides?: Record<string, unknown>) {
444
+ return new FAQBotAgent({
445
+ faqs: baseFaqs,
446
+ ...overrides,
447
+ });
448
+ }
449
+
450
+ it('creates with FAQs config', () => {
451
+ const agent = createAgent();
452
+ expect(agent).toBeInstanceOf(FAQBotAgent);
453
+ });
454
+
455
+ it('renders valid SWML', () => {
456
+ const agent = createAgent();
457
+ const swml = agent.renderSwml('test-call');
458
+ const parsed = JSON.parse(swml);
459
+ expect(parsed.version).toBe('1.0.0');
460
+ expect(parsed.sections.main).toBeDefined();
461
+ });
462
+
463
+ it('has search_faq tool and no escalate when not configured', () => {
464
+ const agent = createAgent();
465
+ expect(agent.getTool('search_faq')).toBeDefined();
466
+ expect(agent.getTool('escalate')).toBeUndefined();
467
+ });
468
+
469
+ it('search_faq returns matching FAQ', async () => {
470
+ const agent = createAgent();
471
+ const tool = agent.getTool('search_faq')!;
472
+ const result = await tool.execute({ query: 'What are your business hours?' }, {});
473
+ expect(result.response).toContain('Monday through Friday');
474
+ expect(result.response).toContain('9am to 5pm');
475
+ });
476
+
477
+ it('returns no-match for unrelated query', async () => {
478
+ const agent = createAgent({ threshold: 0.5 });
479
+ const tool = agent.getTool('search_faq')!;
480
+ const result = await tool.execute({ query: 'quantum physics dark matter' }, {});
481
+ expect(result.response).toContain('No FAQ matched');
482
+ });
483
+
484
+ it('threshold filtering works', async () => {
485
+ // High threshold makes it hard to match
486
+ const agent = createAgent({ threshold: 0.99 });
487
+ const tool = agent.getTool('search_faq')!;
488
+ const result = await tool.execute({ query: 'hours open' }, {});
489
+ expect(result.response).toContain('No FAQ matched');
490
+
491
+ // Low threshold makes it easy to match
492
+ const agent2 = createAgent({ threshold: 0.01 });
493
+ const tool2 = agent2.getTool('search_faq')!;
494
+ const result2 = await tool2.execute({ query: 'hours open' }, {});
495
+ expect(result2.response).toContain('FAQ Match');
496
+ });
497
+
498
+ it('escalate tool is not registered without escalation number', () => {
499
+ const agent = createAgent();
500
+ expect(agent.getTool('escalate')).toBeUndefined();
501
+ });
502
+
503
+ it('escalate tool is registered when escalation number is set', () => {
504
+ const agent = createAgent({ escalationNumber: '+15551234567' });
505
+ expect(agent.getTool('escalate')).toBeDefined();
506
+ });
507
+
508
+ it('escalate tool transfers caller', async () => {
509
+ const agent = createAgent({ escalationNumber: '+15551234567' });
510
+ const tool = agent.getTool('escalate')!;
511
+ const result = await tool.execute({ reason: 'Cannot find answer' }, {});
512
+ expect(result.response).toContain('Transferring');
513
+ expect(result.response).toContain('Cannot find answer');
514
+ const actions = result.action as Record<string, unknown>[];
515
+ expect(actions).toBeDefined();
516
+ expect(actions.length).toBeGreaterThan(0);
517
+ });
518
+
519
+ it('search_faq includes escalation message when no match', async () => {
520
+ const agent = createAgent({ escalationMessage: 'Please hold for an agent.' });
521
+ const tool = agent.getTool('search_faq')!;
522
+ const result = await tool.execute({ query: 'completely unrelated topic xyz' }, {});
523
+ // The escalation message is included in the no-match response
524
+ expect(result.response).toContain('No FAQ matched');
525
+ });
526
+
527
+ it('keywords improve matching', async () => {
528
+ const agent = createAgent({ threshold: 0.3 });
529
+ const tool = agent.getTool('search_faq')!;
530
+
531
+ // Query using keywords that match the password FAQ
532
+ const result = await tool.execute({ query: 'forgot password reset' }, {});
533
+ expect(result.response).toContain('FAQ Match');
534
+ expect(result.response).toContain('Forgot Password');
535
+ });
536
+
537
+ it('prompt mentions FAQ topics', () => {
538
+ const agent = createAgent();
539
+ const swml = agent.renderSwml('test-call');
540
+ expect(swml).toContain('business hours');
541
+ expect(swml).toContain('password');
542
+ expect(swml).toContain('return policy');
543
+ });
544
+
545
+ it('factory function creates agent', () => {
546
+ const agent = createFAQBotAgent({ faqs: baseFaqs });
547
+ expect(agent).toBeInstanceOf(FAQBotAgent);
548
+ expect(agent.getTool('search_faq')).toBeDefined();
549
+ });
550
+ });
551
+
552
+ // ============================================================================
553
+ // ConciergeAgent
554
+ // ============================================================================
555
+
556
+ describe('ConciergeAgent', () => {
557
+ const baseDepartments = [
558
+ {
559
+ name: 'Sales',
560
+ description: 'Handles product inquiries and purchases.',
561
+ transferNumber: '+15551001001',
562
+ keywords: ['buy', 'purchase', 'pricing'],
563
+ hoursOfOperation: 'Mon-Fri 9am-5pm EST',
564
+ },
565
+ {
566
+ name: 'Support',
567
+ description: 'Handles technical issues and troubleshooting.',
568
+ transferNumber: '+15551002002',
569
+ keywords: ['help', 'issue', 'problem', 'technical'],
570
+ hoursOfOperation: '24/7',
571
+ },
572
+ {
573
+ name: 'HR',
574
+ description: 'Handles employment and benefits inquiries.',
575
+ keywords: ['employment', 'benefits', 'job'],
576
+ // No transferNumber
577
+ },
578
+ ];
579
+
580
+ function createAgent(overrides?: Record<string, unknown>) {
581
+ return new ConciergeAgent({
582
+ departments: baseDepartments,
583
+ companyName: 'Acme Corp',
584
+ ...overrides,
585
+ });
586
+ }
587
+
588
+ it('creates with departments config', () => {
589
+ const agent = createAgent();
590
+ expect(agent).toBeInstanceOf(ConciergeAgent);
591
+ });
592
+
593
+ it('renders valid SWML', () => {
594
+ const agent = createAgent();
595
+ const swml = agent.renderSwml('test-call');
596
+ const parsed = JSON.parse(swml);
597
+ expect(parsed.version).toBe('1.0.0');
598
+ expect(parsed.sections.main).toBeDefined();
599
+ });
600
+
601
+ it('has list_departments, get_department_info, and transfer_to_department tools', () => {
602
+ const agent = createAgent();
603
+ expect(agent.getTool('list_departments')).toBeDefined();
604
+ expect(agent.getTool('get_department_info')).toBeDefined();
605
+ expect(agent.getTool('transfer_to_department')).toBeDefined();
606
+ });
607
+
608
+ it('list_departments returns all departments', async () => {
609
+ const agent = createAgent();
610
+ const tool = agent.getTool('list_departments')!;
611
+ const result = await tool.execute({}, {});
612
+ expect(result.response).toContain('Sales');
613
+ expect(result.response).toContain('Support');
614
+ expect(result.response).toContain('HR');
615
+ expect(result.response).toContain('Acme Corp');
616
+ });
617
+
618
+ it('get_department_info returns specific department', async () => {
619
+ const agent = createAgent();
620
+ const tool = agent.getTool('get_department_info')!;
621
+ const result = await tool.execute({ department_name: 'Sales' }, {});
622
+ expect(result.response).toContain('Sales');
623
+ expect(result.response).toContain('product inquiries');
624
+ expect(result.response).toContain('Mon-Fri 9am-5pm');
625
+ expect(result.response).toContain('Transfer: available');
626
+ });
627
+
628
+ it('transfer_to_department handles missing transfer number', async () => {
629
+ const agent = createAgent();
630
+ const tool = agent.getTool('transfer_to_department')!;
631
+ const result = await tool.execute({ department_name: 'HR' }, {});
632
+ expect(result.response).toContain('does not have a direct transfer number');
633
+ });
634
+
635
+ it('department lookup by keyword', async () => {
636
+ const agent = createAgent();
637
+ const tool = agent.getTool('get_department_info')!;
638
+ // "buy" is a keyword for "Sales"
639
+ const result = await tool.execute({ department_name: 'buy' }, {});
640
+ expect(result.response).toContain('Sales');
641
+ expect(result.response).toContain('product inquiries');
642
+ });
643
+
644
+ it('prompt mentions company name', () => {
645
+ const agent = createAgent();
646
+ const swml = agent.renderSwml('test-call');
647
+ expect(swml).toContain('Acme Corp');
648
+ });
649
+
650
+ it('general info is included in prompt', () => {
651
+ const agent = createAgent({ generalInfo: 'We are a leading technology provider.' });
652
+ const swml = agent.renderSwml('test-call');
653
+ expect(swml).toContain('leading technology provider');
654
+ });
655
+
656
+ it('department without transfer number shows not available in list', async () => {
657
+ const agent = createAgent();
658
+ const tool = agent.getTool('list_departments')!;
659
+ const result = await tool.execute({}, {});
660
+ expect(result.response).toContain('no direct transfer available');
661
+ });
662
+
663
+ it('transfer_to_department connects successfully', async () => {
664
+ const agent = createAgent();
665
+ const tool = agent.getTool('transfer_to_department')!;
666
+ const result = await tool.execute({ department_name: 'Support' }, {});
667
+ expect(result.response).toContain('Transferring');
668
+ expect(result.response).toContain('Support');
669
+ // Check that a SWML connect action was produced
670
+ const actions = result.action as Record<string, unknown>[];
671
+ expect(actions).toBeDefined();
672
+ expect(actions.length).toBeGreaterThan(0);
673
+ });
674
+
675
+ it('factory function creates agent', () => {
676
+ const agent = createConciergeAgent({ departments: baseDepartments });
677
+ expect(agent).toBeInstanceOf(ConciergeAgent);
678
+ expect(agent.getTool('list_departments')).toBeDefined();
679
+ });
680
+ });
681
+
682
+ // ============================================================================
683
+ // ReceptionistAgent
684
+ // ============================================================================
685
+
686
+ describe('ReceptionistAgent', () => {
687
+ const baseDepartments = [
688
+ { name: 'Engineering', extension: '1001', description: 'Software development team' },
689
+ { name: 'Marketing', extension: '1002', description: 'Marketing and communications' },
690
+ { name: 'Finance', extension: '1003' },
691
+ ];
692
+
693
+ function createAgent(overrides?: Record<string, unknown>) {
694
+ return new ReceptionistAgent({
695
+ companyName: 'TechCo',
696
+ departments: baseDepartments,
697
+ ...overrides,
698
+ });
699
+ }
700
+
701
+ it('creates with departments config', () => {
702
+ const agent = createAgent();
703
+ expect(agent).toBeInstanceOf(ReceptionistAgent);
704
+ });
705
+
706
+ it('renders valid SWML', () => {
707
+ const agent = createAgent();
708
+ const swml = agent.renderSwml('test-call');
709
+ const parsed = JSON.parse(swml);
710
+ expect(parsed.version).toBe('1.0.0');
711
+ expect(parsed.sections.main).toBeDefined();
712
+ });
713
+
714
+ it('has get_department_list and transfer_to_department tools', () => {
715
+ const agent = createAgent();
716
+ expect(agent.getTool('get_department_list')).toBeDefined();
717
+ expect(agent.getTool('transfer_to_department')).toBeDefined();
718
+ });
719
+
720
+ it('check_in_visitor is registered by default', () => {
721
+ const agent = createAgent();
722
+ expect(agent.getTool('check_in_visitor')).toBeDefined();
723
+ });
724
+
725
+ it('get_department_list returns departments', async () => {
726
+ const agent = createAgent();
727
+ const tool = agent.getTool('get_department_list')!;
728
+ const result = await tool.execute({}, {});
729
+ expect(result.response).toContain('Engineering');
730
+ expect(result.response).toContain('1001');
731
+ expect(result.response).toContain('Marketing');
732
+ expect(result.response).toContain('1002');
733
+ expect(result.response).toContain('Finance');
734
+ expect(result.response).toContain('1003');
735
+ expect(result.response).toContain('TechCo');
736
+ });
737
+
738
+ it('transfer_to_department connects to extension', async () => {
739
+ const agent = createAgent();
740
+ const tool = agent.getTool('transfer_to_department')!;
741
+ const result = await tool.execute({ department_name: 'Engineering' }, {});
742
+ expect(result.response).toContain('Transferring');
743
+ expect(result.response).toContain('Engineering');
744
+ expect(result.response).toContain('1001');
745
+ // Check that a SWML connect action is present
746
+ const actions = result.action as Record<string, unknown>[];
747
+ expect(actions).toBeDefined();
748
+ expect(actions.length).toBeGreaterThan(0);
749
+ });
750
+
751
+ it('transfer_to_department returns error for unknown department', async () => {
752
+ const agent = createAgent();
753
+ const tool = agent.getTool('transfer_to_department')!;
754
+ const result = await tool.execute({ department_name: 'Unknown' }, {});
755
+ expect(result.response).toContain('not found');
756
+ expect(result.response).toContain('Available departments');
757
+ });
758
+
759
+ it('check_in_visitor records visitor data', async () => {
760
+ const agent = createAgent();
761
+ const tool = agent.getTool('check_in_visitor')!;
762
+ const result = await tool.execute(
763
+ { visitor_name: 'Alice Smith', purpose: 'Interview', visiting: 'Engineering' },
764
+ { call_id: 'check-1' },
765
+ );
766
+ expect(result.response).toContain('checked in successfully');
767
+ expect(result.response).toContain('Alice Smith');
768
+ expect(result.response).toContain('Interview');
769
+ expect(result.response).toContain('Engineering');
770
+ expect(result.response).toContain('TechCo');
771
+ });
772
+
773
+ it('check_in_visitor absent with checkInEnabled=false', () => {
774
+ const agent = new ReceptionistAgent({
775
+ companyName: 'TechCo',
776
+ departments: baseDepartments,
777
+ checkInEnabled: false,
778
+ });
779
+ expect(agent.getTool('check_in_visitor')).toBeUndefined();
780
+ // Other tools still present
781
+ expect(agent.getTool('get_department_list')).toBeDefined();
782
+ expect(agent.getTool('transfer_to_department')).toBeDefined();
783
+ });
784
+
785
+ it('check_in_visitor present with checkInEnabled=true', () => {
786
+ const agent = new ReceptionistAgent({
787
+ companyName: 'TechCo',
788
+ departments: baseDepartments,
789
+ checkInEnabled: true,
790
+ });
791
+ expect(agent.getTool('check_in_visitor')).toBeDefined();
792
+ });
793
+
794
+ it('onVisitorCheckIn callback fires when visitor checks in', async () => {
795
+ const onVisitorCheckIn = vi.fn();
796
+ const agent = new ReceptionistAgent({
797
+ companyName: 'TechCo',
798
+ departments: baseDepartments,
799
+ onVisitorCheckIn,
800
+ });
801
+ expect(agent).toBeInstanceOf(ReceptionistAgent);
802
+ const tool = agent.getTool('check_in_visitor')!;
803
+ expect(tool).toBeDefined();
804
+ await tool.execute(
805
+ { visitor_name: 'Bob Jones', purpose: 'Meeting', visiting: 'Marketing' },
806
+ { call_id: 'cb-1' },
807
+ );
808
+ expect(onVisitorCheckIn).toHaveBeenCalledTimes(1);
809
+ expect(onVisitorCheckIn).toHaveBeenCalledWith(
810
+ expect.objectContaining({
811
+ visitor_name: 'Bob Jones',
812
+ purpose: 'Meeting',
813
+ visiting: 'Marketing',
814
+ }),
815
+ );
816
+ });
817
+
818
+ it('prompt mentions company name', () => {
819
+ const agent = createAgent();
820
+ const swml = agent.renderSwml('test-call');
821
+ expect(swml).toContain('TechCo');
822
+ });
823
+
824
+ it('works without optional config', () => {
825
+ const agent = new ReceptionistAgent({
826
+ companyName: 'MinimalCo',
827
+ departments: [{ name: 'General', extension: '100' }],
828
+ });
829
+ expect(agent).toBeInstanceOf(ReceptionistAgent);
830
+ expect(agent.getTool('get_department_list')).toBeDefined();
831
+ expect(agent.getTool('transfer_to_department')).toBeDefined();
832
+ const swml = agent.renderSwml('test-call');
833
+ expect(swml).toContain('MinimalCo');
834
+ });
835
+
836
+ it('factory function creates agent', () => {
837
+ const agent = createReceptionistAgent({
838
+ companyName: 'TechCo',
839
+ departments: baseDepartments,
840
+ });
841
+ expect(agent).toBeInstanceOf(ReceptionistAgent);
842
+ expect(agent.getTool('get_department_list')).toBeDefined();
843
+ });
844
+
845
+ it('custom welcome message appears in SWML', () => {
846
+ const agent = new ReceptionistAgent({
847
+ companyName: 'TechCo',
848
+ departments: baseDepartments,
849
+ welcomeMessage: 'Hello and welcome to TechCo headquarters!',
850
+ });
851
+ const swml = agent.renderSwml('test-call');
852
+ expect(swml).toContain('Hello and welcome to TechCo headquarters!');
853
+ });
854
+ });