enya-agent 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 (389) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.github/workflows/publish.yml +250 -0
  4. package/.gitmodules +3 -0
  5. package/Cargo.lock +3584 -0
  6. package/Cargo.toml +97 -0
  7. package/crates/enact/Cargo.toml +27 -0
  8. package/crates/enact/src/lib.rs +60 -0
  9. package/crates/enact-a2a/Cargo.toml +25 -0
  10. package/crates/enact-a2a/src/lib.rs +411 -0
  11. package/crates/enact-channels/Cargo.toml +64 -0
  12. package/crates/enact-channels/examples/README.md +80 -0
  13. package/crates/enact-channels/examples/channel_bot.rs +169 -0
  14. package/crates/enact-channels/examples/telegram-echo.rs +34 -0
  15. package/crates/enact-channels/examples/whatsapp-echo.rs +142 -0
  16. package/crates/enact-channels/src/config.rs +213 -0
  17. package/crates/enact-channels/src/lib.rs +25 -0
  18. package/crates/enact-channels/src/runtime.rs +237 -0
  19. package/crates/enact-channels/src/security/mod.rs +5 -0
  20. package/crates/enact-channels/src/security/pairing.rs +205 -0
  21. package/crates/enact-channels/src/teams.rs +601 -0
  22. package/crates/enact-channels/src/telegram.rs +2833 -0
  23. package/crates/enact-channels/src/traits.rs +200 -0
  24. package/crates/enact-channels/src/webhook.rs +262 -0
  25. package/crates/enact-channels/src/whatsapp.rs +310 -0
  26. package/crates/enact-cli/Cargo.toml +40 -0
  27. package/crates/enact-cli/src/commands/doctor.rs +62 -0
  28. package/crates/enact-cli/src/commands/mod.rs +3 -0
  29. package/crates/enact-cli/src/commands/run.rs +69 -0
  30. package/crates/enact-cli/src/commands/serve.rs +81 -0
  31. package/crates/enact-cli/src/config.rs +2 -0
  32. package/crates/enact-cli/src/main.rs +79 -0
  33. package/crates/enact-config/Cargo.toml +36 -0
  34. package/crates/enact-config/ENV_VAR_MAPPING.md +135 -0
  35. package/crates/enact-config/QUICK_REFERENCE.md +92 -0
  36. package/crates/enact-config/README.md +107 -0
  37. package/crates/enact-config/TESTING.md +161 -0
  38. package/crates/enact-config/examples/test-env-vars.rs +100 -0
  39. package/crates/enact-config/src/config.rs +399 -0
  40. package/crates/enact-config/src/encrypted_store.rs +211 -0
  41. package/crates/enact-config/src/lib.rs +298 -0
  42. package/crates/enact-config/src/secrets.rs +149 -0
  43. package/crates/enact-config/src/sync.rs +260 -0
  44. package/crates/enact-config/test-env-vars.sh +34 -0
  45. package/crates/enact-config/tests/README.md +99 -0
  46. package/crates/enact-config/tests/config_integration_test.rs +202 -0
  47. package/crates/enact-config/tests/security_test.rs +140 -0
  48. package/crates/enact-context/Cargo.toml +41 -0
  49. package/crates/enact-context/src/budget.rs +314 -0
  50. package/crates/enact-context/src/calibrator.rs +535 -0
  51. package/crates/enact-context/src/compactor.rs +392 -0
  52. package/crates/enact-context/src/condenser.rs +826 -0
  53. package/crates/enact-context/src/lib.rs +94 -0
  54. package/crates/enact-context/src/segment.rs +238 -0
  55. package/crates/enact-context/src/step_context.rs +645 -0
  56. package/crates/enact-context/src/token_counter.rs +148 -0
  57. package/crates/enact-context/src/window.rs +372 -0
  58. package/crates/enact-core/Cargo.toml +42 -0
  59. package/crates/enact-core/README.md +98 -0
  60. package/crates/enact-core/src/background/executor.rs +524 -0
  61. package/crates/enact-core/src/background/mod.rs +48 -0
  62. package/crates/enact-core/src/background/target_binding.rs +390 -0
  63. package/crates/enact-core/src/background/trigger.rs +511 -0
  64. package/crates/enact-core/src/callable/callable.rs +152 -0
  65. package/crates/enact-core/src/callable/composite.rs +817 -0
  66. package/crates/enact-core/src/callable/graph.rs +104 -0
  67. package/crates/enact-core/src/callable/llm.rs +211 -0
  68. package/crates/enact-core/src/callable/mod.rs +64 -0
  69. package/crates/enact-core/src/callable/registry.rs +206 -0
  70. package/crates/enact-core/src/context/execution_context.rs +757 -0
  71. package/crates/enact-core/src/context/invocation.rs +99 -0
  72. package/crates/enact-core/src/context/mod.rs +50 -0
  73. package/crates/enact-core/src/context/tenant.rs +175 -0
  74. package/crates/enact-core/src/context/trace.rs +127 -0
  75. package/crates/enact-core/src/flow/conditional.rs +293 -0
  76. package/crates/enact-core/src/flow/mod.rs +43 -0
  77. package/crates/enact-core/src/flow/parallel.rs +437 -0
  78. package/crates/enact-core/src/flow/repeat.rs +534 -0
  79. package/crates/enact-core/src/flow/sequential.rs +248 -0
  80. package/crates/enact-core/src/graph/checkpoint.rs +79 -0
  81. package/crates/enact-core/src/graph/checkpoint_store.rs +76 -0
  82. package/crates/enact-core/src/graph/compiled.rs +189 -0
  83. package/crates/enact-core/src/graph/edge.rs +59 -0
  84. package/crates/enact-core/src/graph/graph_schema.rs +218 -0
  85. package/crates/enact-core/src/graph/loader.rs +155 -0
  86. package/crates/enact-core/src/graph/mod.rs +18 -0
  87. package/crates/enact-core/src/graph/node/function.rs +49 -0
  88. package/crates/enact-core/src/graph/node/mod.rs +48 -0
  89. package/crates/enact-core/src/graph/schema.rs +62 -0
  90. package/crates/enact-core/src/inbox/message.rs +405 -0
  91. package/crates/enact-core/src/inbox/mod.rs +31 -0
  92. package/crates/enact-core/src/inbox/store.rs +355 -0
  93. package/crates/enact-core/src/kernel/artifact/filesystem.rs +546 -0
  94. package/crates/enact-core/src/kernel/artifact/metadata.rs +283 -0
  95. package/crates/enact-core/src/kernel/artifact/mod.rs +27 -0
  96. package/crates/enact-core/src/kernel/artifact/store.rs +427 -0
  97. package/crates/enact-core/src/kernel/enforcement.rs +1315 -0
  98. package/crates/enact-core/src/kernel/error.rs +1200 -0
  99. package/crates/enact-core/src/kernel/event.rs +1394 -0
  100. package/crates/enact-core/src/kernel/execution_model.rs +831 -0
  101. package/crates/enact-core/src/kernel/execution_state.rs +189 -0
  102. package/crates/enact-core/src/kernel/execution_strategy.rs +117 -0
  103. package/crates/enact-core/src/kernel/ids.rs +2086 -0
  104. package/crates/enact-core/src/kernel/interrupt.rs +125 -0
  105. package/crates/enact-core/src/kernel/kernel.rs +1283 -0
  106. package/crates/enact-core/src/kernel/mod.rs +205 -0
  107. package/crates/enact-core/src/kernel/persistence/event_store.rs +270 -0
  108. package/crates/enact-core/src/kernel/persistence/message_store.rs +908 -0
  109. package/crates/enact-core/src/kernel/persistence/mod.rs +102 -0
  110. package/crates/enact-core/src/kernel/persistence/state_store.rs +228 -0
  111. package/crates/enact-core/src/kernel/persistence/vector_store.rs +299 -0
  112. package/crates/enact-core/src/kernel/reducer.rs +808 -0
  113. package/crates/enact-core/src/kernel/replay.rs +153 -0
  114. package/crates/enact-core/src/lib.rs +413 -0
  115. package/crates/enact-core/src/memory/episodic.rs +0 -0
  116. package/crates/enact-core/src/memory/mod.rs +6 -0
  117. package/crates/enact-core/src/memory/semantic.rs +0 -0
  118. package/crates/enact-core/src/memory/trait.rs +0 -0
  119. package/crates/enact-core/src/memory/vector_db.rs +0 -0
  120. package/crates/enact-core/src/memory/working.rs +0 -0
  121. package/crates/enact-core/src/policy/execution_policy.rs +292 -0
  122. package/crates/enact-core/src/policy/filters.rs +458 -0
  123. package/crates/enact-core/src/policy/input_processor.rs +407 -0
  124. package/crates/enact-core/src/policy/long_running.rs +134 -0
  125. package/crates/enact-core/src/policy/mod.rs +193 -0
  126. package/crates/enact-core/src/policy/pii_input.rs +274 -0
  127. package/crates/enact-core/src/policy/tenant_policy.rs +453 -0
  128. package/crates/enact-core/src/policy/tool_policy.rs +407 -0
  129. package/crates/enact-core/src/providers/mod.rs +63 -0
  130. package/crates/enact-core/src/providers/trait.rs +292 -0
  131. package/crates/enact-core/src/runner/callbacks.rs +6 -0
  132. package/crates/enact-core/src/runner/execution_runner.rs +476 -0
  133. package/crates/enact-core/src/runner/loop.rs +117 -0
  134. package/crates/enact-core/src/runner/mod.rs +58 -0
  135. package/crates/enact-core/src/runner/protected_runner.rs +280 -0
  136. package/crates/enact-core/src/signal/inmemory.rs +231 -0
  137. package/crates/enact-core/src/signal/mod.rs +108 -0
  138. package/crates/enact-core/src/streaming/event_logger.rs +195 -0
  139. package/crates/enact-core/src/streaming/event_stream.rs +1423 -0
  140. package/crates/enact-core/src/streaming/mod.rs +108 -0
  141. package/crates/enact-core/src/streaming/pause_cancel.rs +0 -0
  142. package/crates/enact-core/src/streaming/protected_emitter.rs +173 -0
  143. package/crates/enact-core/src/streaming/protection/context.rs +136 -0
  144. package/crates/enact-core/src/streaming/protection/encryption.rs +289 -0
  145. package/crates/enact-core/src/streaming/protection/mod.rs +43 -0
  146. package/crates/enact-core/src/streaming/protection/pii_protection.rs +243 -0
  147. package/crates/enact-core/src/streaming/protection/processor.rs +166 -0
  148. package/crates/enact-core/src/streaming/sse.rs +0 -0
  149. package/crates/enact-core/src/telemetry/exporter.rs +0 -0
  150. package/crates/enact-core/src/telemetry/init.rs +0 -0
  151. package/crates/enact-core/src/telemetry/mod.rs +49 -0
  152. package/crates/enact-core/src/telemetry/spans.rs +245 -0
  153. package/crates/enact-core/src/tool/agent_tool.rs +177 -0
  154. package/crates/enact-core/src/tool/browser/mod.rs +0 -0
  155. package/crates/enact-core/src/tool/browser/webdriver.rs +0 -0
  156. package/crates/enact-core/src/tool/cost.rs +247 -0
  157. package/crates/enact-core/src/tool/discovery.rs +0 -0
  158. package/crates/enact-core/src/tool/dispatcher.rs +347 -0
  159. package/crates/enact-core/src/tool/filesystem.rs +231 -0
  160. package/crates/enact-core/src/tool/function.rs +99 -0
  161. package/crates/enact-core/src/tool/git.rs +162 -0
  162. package/crates/enact-core/src/tool/http.rs +214 -0
  163. package/crates/enact-core/src/tool/mcp/client.rs +0 -0
  164. package/crates/enact-core/src/tool/mcp/mod.rs +0 -0
  165. package/crates/enact-core/src/tool/mod.rs +51 -0
  166. package/crates/enact-core/src/tool/reasoning/debugging.rs +0 -0
  167. package/crates/enact-core/src/tool/reasoning/mcts.rs +0 -0
  168. package/crates/enact-core/src/tool/reasoning/mod.rs +0 -0
  169. package/crates/enact-core/src/tool/reasoning/sequential.rs +0 -0
  170. package/crates/enact-core/src/tool/sandbox/dagger.rs +0 -0
  171. package/crates/enact-core/src/tool/sandbox/mod.rs +0 -0
  172. package/crates/enact-core/src/tool/shell.rs +147 -0
  173. package/crates/enact-core/src/tool/trait.rs +33 -0
  174. package/crates/enact-core/src/tool/web_search.rs +277 -0
  175. package/crates/enact-core/src/util/config.rs +0 -0
  176. package/crates/enact-core/src/util/errors.rs +0 -0
  177. package/crates/enact-core/src/util/mod.rs +6 -0
  178. package/crates/enact-core/tests/airgapped_e2e_test.rs +291 -0
  179. package/crates/enact-core/tests/e2e_agentic_loop.rs +119 -0
  180. package/crates/enact-core/tests/e2e_test.rs +259 -0
  181. package/crates/enact-core/tests/graph_test.rs +130 -0
  182. package/crates/enact-core/tests/stream_event_id_validation.rs +435 -0
  183. package/crates/enact-cron/Cargo.toml +28 -0
  184. package/crates/enact-cron/src/lib.rs +44 -0
  185. package/crates/enact-cron/src/schedule.rs +156 -0
  186. package/crates/enact-cron/src/store.rs +589 -0
  187. package/crates/enact-cron/src/types.rs +148 -0
  188. package/crates/enact-gateway/Cargo.toml +31 -0
  189. package/crates/enact-gateway/README.md +30 -0
  190. package/crates/enact-gateway/examples/whatsapp-gateway-runner-mock.rs +59 -0
  191. package/crates/enact-gateway/examples/whatsapp-gateway.rs +42 -0
  192. package/crates/enact-gateway/src/lib.rs +582 -0
  193. package/crates/enact-mcp/Cargo.toml +24 -0
  194. package/crates/enact-mcp/src/lib.rs +178 -0
  195. package/crates/enact-memory/Cargo.toml +25 -0
  196. package/crates/enact-memory/src/backend.rs +20 -0
  197. package/crates/enact-memory/src/chunker.rs +230 -0
  198. package/crates/enact-memory/src/embeddings.rs +221 -0
  199. package/crates/enact-memory/src/lib.rs +67 -0
  200. package/crates/enact-memory/src/markdown.rs +127 -0
  201. package/crates/enact-memory/src/none.rs +61 -0
  202. package/crates/enact-memory/src/sqlite.rs +276 -0
  203. package/crates/enact-memory/src/traits.rs +65 -0
  204. package/crates/enact-memory/src/vector.rs +198 -0
  205. package/crates/enact-oauth/Cargo.toml +27 -0
  206. package/crates/enact-oauth/src/lib.rs +584 -0
  207. package/crates/enact-observability/Cargo.toml +22 -0
  208. package/crates/enact-observability/src/lib.rs +197 -0
  209. package/crates/enact-providers/Cargo.toml +33 -0
  210. package/crates/enact-providers/examples/hello-agent.rs +33 -0
  211. package/crates/enact-providers/src/anthropic.rs +182 -0
  212. package/crates/enact-providers/src/azure.rs +96 -0
  213. package/crates/enact-providers/src/bridge.rs +221 -0
  214. package/crates/enact-providers/src/gemini.rs +227 -0
  215. package/crates/enact-providers/src/http.rs +78 -0
  216. package/crates/enact-providers/src/lib.rs +53 -0
  217. package/crates/enact-providers/src/openai_compatible.rs +167 -0
  218. package/crates/enact-providers/src/openrouter.rs +33 -0
  219. package/crates/enact-runner/Cargo.toml +24 -0
  220. package/crates/enact-runner/README.md +76 -0
  221. package/crates/enact-runner/src/compaction.rs +225 -0
  222. package/crates/enact-runner/src/config.rs +118 -0
  223. package/crates/enact-runner/src/lib.rs +63 -0
  224. package/crates/enact-runner/src/loop_driver.rs +414 -0
  225. package/crates/enact-runner/src/parser.rs +421 -0
  226. package/crates/enact-runner/src/retry.rs +262 -0
  227. package/crates/enact-runner/tests/integration.rs +278 -0
  228. package/crates/enact-security/Cargo.toml +22 -0
  229. package/crates/enact-security/src/audit.rs +375 -0
  230. package/crates/enact-security/src/lib.rs +37 -0
  231. package/crates/enact-security/src/policy.rs +406 -0
  232. package/crates/enact-skills/Cargo.toml +25 -0
  233. package/crates/enact-skills/src/lib.rs +506 -0
  234. package/crates/enact-tools/Cargo.toml +22 -0
  235. package/crates/enact-tools/src/file_read.rs +166 -0
  236. package/crates/enact-tools/src/file_write.rs +216 -0
  237. package/crates/enact-tools/src/git_operations.rs +513 -0
  238. package/crates/enact-tools/src/http_request.rs +417 -0
  239. package/crates/enact-tools/src/lib.rs +104 -0
  240. package/crates/enact-tools/src/security.rs +227 -0
  241. package/crates/enact-tools/src/shell.rs +191 -0
  242. package/crates/enact-tools/src/traits.rs +159 -0
  243. package/docs/Makefile +74 -0
  244. package/docs/config.toml +62 -0
  245. package/docs/content/_index.md +174 -0
  246. package/docs/content/a2a/_index.md +431 -0
  247. package/docs/content/api/_index.md +323 -0
  248. package/docs/content/channels/_index.md +160 -0
  249. package/docs/content/channels/teams.md +205 -0
  250. package/docs/content/channels/telegram.md +182 -0
  251. package/docs/content/channels/webhook.md +423 -0
  252. package/docs/content/channels/whatsapp.md +240 -0
  253. package/docs/content/cli/_index.md +261 -0
  254. package/docs/content/concepts/_index.md +273 -0
  255. package/docs/content/configuration/_index.md +241 -0
  256. package/docs/content/cron/_index.md +248 -0
  257. package/docs/content/developers/_index.md +278 -0
  258. package/docs/content/getting-started/_index.md +180 -0
  259. package/docs/content/installation/_index.md +186 -0
  260. package/docs/content/installation/uninstall.md +101 -0
  261. package/docs/content/installation/updating.md +120 -0
  262. package/docs/content/mcp/_index.md +215 -0
  263. package/docs/content/memory/_index.md +163 -0
  264. package/docs/content/oauth/_index.md +515 -0
  265. package/docs/content/providers/_index.md +206 -0
  266. package/docs/content/roadmap/_index.md +199 -0
  267. package/docs/content/security/_index.md +219 -0
  268. package/docs/content/skills/_index.md +228 -0
  269. package/docs/content/tools/_index.md +485 -0
  270. package/docs/content/troubleshooting/_index.md +259 -0
  271. package/docs/content/yaml-schema/_index.md +294 -0
  272. package/docs/static/giallo-dark.css +91 -0
  273. package/docs/static/giallo-light.css +91 -0
  274. package/docs/themes/tanuki/.github/workflows/deploy.yml +44 -0
  275. package/docs/themes/tanuki/LICENSE +21 -0
  276. package/docs/themes/tanuki/README.md +166 -0
  277. package/docs/themes/tanuki/examples/blog/config.toml +58 -0
  278. package/docs/themes/tanuki/examples/blog/content/_index.md +4 -0
  279. package/docs/themes/tanuki/examples/blog/content/about.md +33 -0
  280. package/docs/themes/tanuki/examples/blog/content/blog/_index.md +7 -0
  281. package/docs/themes/tanuki/examples/blog/content/blog/api-design-best-practices.md +245 -0
  282. package/docs/themes/tanuki/examples/blog/content/blog/building-accessible-websites.md +147 -0
  283. package/docs/themes/tanuki/examples/blog/content/blog/css-grid-vs-flexbox.md +165 -0
  284. package/docs/themes/tanuki/examples/blog/content/blog/customizing-catppuccin-colors.md +137 -0
  285. package/docs/themes/tanuki/examples/blog/content/blog/dark-mode-best-practices.md +82 -0
  286. package/docs/themes/tanuki/examples/blog/content/blog/docker-essentials.md +301 -0
  287. package/docs/themes/tanuki/examples/blog/content/blog/getting-started-with-zola.md +129 -0
  288. package/docs/themes/tanuki/examples/blog/content/blog/git-workflow-for-content.md +112 -0
  289. package/docs/themes/tanuki/examples/blog/content/blog/introduction-to-webassembly.md +183 -0
  290. package/docs/themes/tanuki/examples/blog/content/blog/modern-javascript-features.md +234 -0
  291. package/docs/themes/tanuki/examples/blog/content/blog/testing-strategies.md +311 -0
  292. package/docs/themes/tanuki/examples/blog/content/blog/typography-for-developers.md +104 -0
  293. package/docs/themes/tanuki/examples/blog/content/blog/welcome-to-tanuki.md +67 -0
  294. package/docs/themes/tanuki/examples/blog/content/blog/why-static-sites.md +85 -0
  295. package/docs/themes/tanuki/examples/blog/content/projects.md +64 -0
  296. package/docs/themes/tanuki/examples/book/config.toml +17 -0
  297. package/docs/themes/tanuki/examples/book/content/_index.md +12 -0
  298. package/docs/themes/tanuki/examples/book/content/chapter-1.md +90 -0
  299. package/docs/themes/tanuki/examples/book/content/chapter-2.md +143 -0
  300. package/docs/themes/tanuki/examples/book/content/chapter-3.md +217 -0
  301. package/docs/themes/tanuki/examples/book/content/chapter-4.md +224 -0
  302. package/docs/themes/tanuki/examples/book/content/chapter-5.md +297 -0
  303. package/docs/themes/tanuki/examples/book/content/print.md +6 -0
  304. package/docs/themes/tanuki/examples/docs/config.toml +28 -0
  305. package/docs/themes/tanuki/examples/docs/content/_index.md +20 -0
  306. package/docs/themes/tanuki/examples/docs/content/components.md +156 -0
  307. package/docs/themes/tanuki/examples/docs/content/configuration.md +94 -0
  308. package/docs/themes/tanuki/examples/docs/content/customization.md +202 -0
  309. package/docs/themes/tanuki/examples/docs/content/deployment.md +204 -0
  310. package/docs/themes/tanuki/examples/docs/content/installation.md +59 -0
  311. package/docs/themes/tanuki/examples/docs/content/print.md +6 -0
  312. package/docs/themes/tanuki/examples/docs/static/img/tanuki-icon.avif +0 -0
  313. package/docs/themes/tanuki/examples/index.html +2104 -0
  314. package/docs/themes/tanuki/mise.toml +108 -0
  315. package/docs/themes/tanuki/sass/base/_catppuccin.scss +164 -0
  316. package/docs/themes/tanuki/sass/base/_fonts.scss +64 -0
  317. package/docs/themes/tanuki/sass/base/_reset.scss +152 -0
  318. package/docs/themes/tanuki/sass/base/_typography.scss +523 -0
  319. package/docs/themes/tanuki/sass/components/_buttons.scss +209 -0
  320. package/docs/themes/tanuki/sass/components/_code.scss +457 -0
  321. package/docs/themes/tanuki/sass/components/_landing.scss +633 -0
  322. package/docs/themes/tanuki/sass/components/_layout.scss +294 -0
  323. package/docs/themes/tanuki/sass/components/_navigation.scss +1200 -0
  324. package/docs/themes/tanuki/sass/components/_print.scss +237 -0
  325. package/docs/themes/tanuki/sass/components/_search.scss +224 -0
  326. package/docs/themes/tanuki/sass/components/_sidebar.scss +473 -0
  327. package/docs/themes/tanuki/sass/components/_theme-toggle.scss +186 -0
  328. package/docs/themes/tanuki/sass/modes/_blog.scss +366 -0
  329. package/docs/themes/tanuki/sass/modes/_product.scss +875 -0
  330. package/docs/themes/tanuki/sass/modes/_raskell.scss +1696 -0
  331. package/docs/themes/tanuki/sass/patterns/_buttons.scss +183 -0
  332. package/docs/themes/tanuki/sass/patterns/_cards.scss +144 -0
  333. package/docs/themes/tanuki/sass/patterns/_index.scss +9 -0
  334. package/docs/themes/tanuki/sass/patterns/_lists.scss +259 -0
  335. package/docs/themes/tanuki/sass/patterns/_sections.scss +243 -0
  336. package/docs/themes/tanuki/sass/style.scss +47 -0
  337. package/docs/themes/tanuki/sass/tokens/_colors.scss +139 -0
  338. package/docs/themes/tanuki/sass/tokens/_spacing.scss +100 -0
  339. package/docs/themes/tanuki/sass/tokens/_typography.scss +186 -0
  340. package/docs/themes/tanuki/screenshot.png +0 -0
  341. package/docs/themes/tanuki/sentinel.kdl +59 -0
  342. package/docs/themes/tanuki/static/elasticlunr.min.js +10 -0
  343. package/docs/themes/tanuki/static/fonts/GEIST-LICENSE.txt +92 -0
  344. package/docs/themes/tanuki/static/fonts/Geist-Variable.woff2 +0 -0
  345. package/docs/themes/tanuki/static/fonts/GeistMono-Variable.woff2 +0 -0
  346. package/docs/themes/tanuki/static/img/tanuki-icon.avif +0 -0
  347. package/docs/themes/tanuki/static/img/tanuki-icon.png +0 -0
  348. package/docs/themes/tanuki/static/js/anchors.js +18 -0
  349. package/docs/themes/tanuki/static/js/app.js +274 -0
  350. package/docs/themes/tanuki/static/js/code.js +394 -0
  351. package/docs/themes/tanuki/static/js/navigation.js +778 -0
  352. package/docs/themes/tanuki/static/js/scroll-to-top.js +33 -0
  353. package/docs/themes/tanuki/static/js/search-raskell.js +240 -0
  354. package/docs/themes/tanuki/static/js/search.js +215 -0
  355. package/docs/themes/tanuki/static/js/theme.js +169 -0
  356. package/docs/themes/tanuki/static/syntax-dark.css +151 -0
  357. package/docs/themes/tanuki/static/syntax-light.css +151 -0
  358. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm.js +486 -0
  359. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm_bg.wasm +0 -0
  360. package/docs/themes/tanuki/templates/404.html +52 -0
  361. package/docs/themes/tanuki/templates/base.html +428 -0
  362. package/docs/themes/tanuki/templates/blog.html +66 -0
  363. package/docs/themes/tanuki/templates/home.html +108 -0
  364. package/docs/themes/tanuki/templates/index.html +178 -0
  365. package/docs/themes/tanuki/templates/landing.html +168 -0
  366. package/docs/themes/tanuki/templates/macros/nav.html +128 -0
  367. package/docs/themes/tanuki/templates/macros/posts.html +101 -0
  368. package/docs/themes/tanuki/templates/macros/ui.html +159 -0
  369. package/docs/themes/tanuki/templates/page.html +135 -0
  370. package/docs/themes/tanuki/templates/partials/footer.html +38 -0
  371. package/docs/themes/tanuki/templates/partials/header.html +366 -0
  372. package/docs/themes/tanuki/templates/partials/nav-buttons.html +55 -0
  373. package/docs/themes/tanuki/templates/partials/nav-overlay.html +81 -0
  374. package/docs/themes/tanuki/templates/partials/page-toc-panel.html +43 -0
  375. package/docs/themes/tanuki/templates/partials/search.html +52 -0
  376. package/docs/themes/tanuki/templates/partials/sidebar.html +107 -0
  377. package/docs/themes/tanuki/templates/partials/theme-toggle.html +35 -0
  378. package/docs/themes/tanuki/templates/partials/toc-overlay.html +146 -0
  379. package/docs/themes/tanuki/templates/partials/version-picker.html +38 -0
  380. package/docs/themes/tanuki/templates/print.html +244 -0
  381. package/docs/themes/tanuki/templates/section.html +186 -0
  382. package/docs/themes/tanuki/templates/taxonomy_list.html +18 -0
  383. package/docs/themes/tanuki/templates/taxonomy_single.html +31 -0
  384. package/docs/themes/tanuki/theme.toml +58 -0
  385. package/examples/hello-agent.rs +55 -0
  386. package/package.json +36 -0
  387. package/proto/config.proto +60 -0
  388. package/proto/events.proto +0 -0
  389. package/proto/runtime.proto +215 -0
@@ -0,0 +1,601 @@
1
+ //! Microsoft Teams channel implementation.
2
+ //!
3
+ //! Integrates with the Bot Framework REST API for Teams messaging.
4
+
5
+ use crate::config::{build_runtime_proxy_client, StreamMode, TeamsConfig};
6
+ use crate::traits::{Channel, ChannelMessage, SendMessage};
7
+ use anyhow::Context;
8
+ use async_trait::async_trait;
9
+ use parking_lot::Mutex;
10
+ use serde::{Deserialize, Serialize};
11
+ use std::sync::Arc;
12
+ use std::time::{Duration, Instant};
13
+ use tokio::sync::mpsc;
14
+
15
+ /// Bot Framework token endpoint.
16
+ const BOT_FRAMEWORK_TOKEN_URL: &str =
17
+ "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token";
18
+
19
+ /// Bot Framework API scope.
20
+ const BOT_FRAMEWORK_SCOPE: &str = "https://api.botframework.com/.default";
21
+
22
+ /// Teams message activity type.
23
+ const ACTIVITY_TYPE_MESSAGE: &str = "message";
24
+
25
+ /// Teams Activity from Bot Framework.
26
+ #[derive(Debug, Clone, Serialize, Deserialize)]
27
+ #[serde(rename_all = "camelCase")]
28
+ pub struct TeamsActivity {
29
+ #[serde(rename = "type")]
30
+ pub activity_type: String,
31
+ pub id: String,
32
+ pub timestamp: Option<String>,
33
+ pub service_url: String,
34
+ pub channel_id: String,
35
+ pub from: TeamsAccount,
36
+ pub conversation: TeamsConversation,
37
+ pub recipient: Option<TeamsAccount>,
38
+ pub text: Option<String>,
39
+ pub text_format: Option<String>,
40
+ #[serde(default)]
41
+ pub attachments: Vec<TeamsAttachment>,
42
+ pub reply_to_id: Option<String>,
43
+ }
44
+
45
+ /// Teams account (user or bot).
46
+ #[derive(Debug, Clone, Serialize, Deserialize)]
47
+ #[serde(rename_all = "camelCase")]
48
+ pub struct TeamsAccount {
49
+ pub id: String,
50
+ pub name: Option<String>,
51
+ #[serde(rename = "aadObjectId")]
52
+ pub aad_object_id: Option<String>,
53
+ }
54
+
55
+ /// Teams conversation reference.
56
+ #[derive(Debug, Clone, Serialize, Deserialize)]
57
+ #[serde(rename_all = "camelCase")]
58
+ pub struct TeamsConversation {
59
+ pub id: String,
60
+ pub conversation_type: Option<String>,
61
+ pub tenant_id: Option<String>,
62
+ pub name: Option<String>,
63
+ }
64
+
65
+ /// Teams attachment.
66
+ #[derive(Debug, Clone, Serialize, Deserialize)]
67
+ #[serde(rename_all = "camelCase")]
68
+ pub struct TeamsAttachment {
69
+ pub content_type: String,
70
+ pub content_url: Option<String>,
71
+ pub name: Option<String>,
72
+ }
73
+
74
+ /// Token response from Bot Framework.
75
+ #[derive(Debug, Deserialize)]
76
+ struct TokenResponse {
77
+ access_token: String,
78
+ expires_in: u64,
79
+ }
80
+
81
+ /// Cached access token.
82
+ struct CachedToken {
83
+ token: String,
84
+ expires_at: Instant,
85
+ }
86
+
87
+ /// Microsoft Teams channel.
88
+ pub struct TeamsChannel {
89
+ /// Microsoft App ID.
90
+ app_id: String,
91
+ /// Microsoft App Password.
92
+ app_password: String,
93
+ /// Bot display name.
94
+ bot_name: String,
95
+ /// Allowed user IDs (empty = allow all).
96
+ allowed_users: Vec<String>,
97
+ /// HTTP client for API calls.
98
+ client: reqwest::Client,
99
+ /// Cached access token.
100
+ token_cache: Arc<Mutex<Option<CachedToken>>>,
101
+ /// Pending activities from webhook (for listen to consume).
102
+ pending_activities: Arc<Mutex<Vec<TeamsActivity>>>,
103
+ /// Webhook receiver channel.
104
+ webhook_rx: Arc<tokio::sync::Mutex<Option<mpsc::Receiver<TeamsActivity>>>>,
105
+ /// Webhook sender (for external webhook handler to send activities).
106
+ webhook_tx: mpsc::Sender<TeamsActivity>,
107
+ /// Stream mode for draft updates.
108
+ stream_mode: StreamMode,
109
+ /// Draft update interval in milliseconds.
110
+ draft_update_interval_ms: u64,
111
+ }
112
+
113
+ impl TeamsChannel {
114
+ /// Create a new Teams channel.
115
+ pub fn new(config: &TeamsConfig) -> Self {
116
+ let (webhook_tx, webhook_rx) = mpsc::channel(100);
117
+
118
+ Self {
119
+ app_id: config.app_id.clone(),
120
+ app_password: config.app_password.clone(),
121
+ bot_name: config.bot_name.clone(),
122
+ allowed_users: config.allowed_users.clone(),
123
+ client: build_runtime_proxy_client("channel.teams"),
124
+ token_cache: Arc::new(Mutex::new(None)),
125
+ pending_activities: Arc::new(Mutex::new(Vec::new())),
126
+ webhook_rx: Arc::new(tokio::sync::Mutex::new(Some(webhook_rx))),
127
+ webhook_tx,
128
+ stream_mode: StreamMode::Off,
129
+ draft_update_interval_ms: 1000,
130
+ }
131
+ }
132
+
133
+ /// Create with custom streaming settings.
134
+ pub fn with_streaming(mut self, mode: StreamMode, interval_ms: u64) -> Self {
135
+ self.stream_mode = mode;
136
+ self.draft_update_interval_ms = interval_ms;
137
+ self
138
+ }
139
+
140
+ /// Get the webhook sender for external webhook handlers.
141
+ pub fn webhook_sender(&self) -> mpsc::Sender<TeamsActivity> {
142
+ self.webhook_tx.clone()
143
+ }
144
+
145
+ /// Get a valid access token, refreshing if needed.
146
+ async fn get_access_token(&self) -> anyhow::Result<String> {
147
+ // Check cache first
148
+ {
149
+ let cache = self.token_cache.lock();
150
+ if let Some(ref cached) = *cache {
151
+ if Instant::now() < cached.expires_at {
152
+ return Ok(cached.token.clone());
153
+ }
154
+ }
155
+ }
156
+
157
+ // Fetch new token
158
+ tracing::debug!("Teams: fetching new access token");
159
+
160
+ let params = [
161
+ ("grant_type", "client_credentials"),
162
+ ("client_id", &self.app_id),
163
+ ("client_secret", &self.app_password),
164
+ ("scope", BOT_FRAMEWORK_SCOPE),
165
+ ];
166
+
167
+ let response = self
168
+ .client
169
+ .post(BOT_FRAMEWORK_TOKEN_URL)
170
+ .form(&params)
171
+ .send()
172
+ .await
173
+ .context("Failed to request Teams access token")?;
174
+
175
+ if !response.status().is_success() {
176
+ let status = response.status();
177
+ let body = response.text().await.unwrap_or_default();
178
+ anyhow::bail!("Teams token request failed ({}): {}", status, body);
179
+ }
180
+
181
+ let token_response: TokenResponse = response
182
+ .json()
183
+ .await
184
+ .context("Failed to parse Teams token response")?;
185
+
186
+ // Cache the token (with 60 second buffer)
187
+ let expires_at = Instant::now() + Duration::from_secs(token_response.expires_in.saturating_sub(60));
188
+ let token = token_response.access_token.clone();
189
+
190
+ {
191
+ let mut cache = self.token_cache.lock();
192
+ *cache = Some(CachedToken {
193
+ token: token.clone(),
194
+ expires_at,
195
+ });
196
+ }
197
+
198
+ tracing::debug!("Teams: access token acquired");
199
+ Ok(token)
200
+ }
201
+
202
+ /// Send a reply to a conversation.
203
+ async fn send_reply(
204
+ &self,
205
+ service_url: &str,
206
+ conversation_id: &str,
207
+ text: &str,
208
+ reply_to_id: Option<&str>,
209
+ ) -> anyhow::Result<String> {
210
+ let token = self.get_access_token().await?;
211
+
212
+ // Ensure service URL ends with /
213
+ let service_url = if service_url.ends_with('/') {
214
+ service_url.to_string()
215
+ } else {
216
+ format!("{}/", service_url)
217
+ };
218
+
219
+ let url = format!(
220
+ "{}v3/conversations/{}/activities",
221
+ service_url, conversation_id
222
+ );
223
+
224
+ let mut activity = serde_json::json!({
225
+ "type": "message",
226
+ "from": {
227
+ "id": self.app_id,
228
+ "name": self.bot_name
229
+ },
230
+ "conversation": {
231
+ "id": conversation_id
232
+ },
233
+ "text": text,
234
+ "textFormat": "markdown"
235
+ });
236
+
237
+ if let Some(reply_id) = reply_to_id {
238
+ activity["replyToId"] = serde_json::json!(reply_id);
239
+ }
240
+
241
+ let response = self
242
+ .client
243
+ .post(&url)
244
+ .bearer_auth(&token)
245
+ .json(&activity)
246
+ .send()
247
+ .await
248
+ .context("Failed to send Teams message")?;
249
+
250
+ if !response.status().is_success() {
251
+ let status = response.status();
252
+ let body = response.text().await.unwrap_or_default();
253
+ anyhow::bail!("Teams send failed ({}): {}", status, body);
254
+ }
255
+
256
+ // Parse response to get activity ID
257
+ let result: serde_json::Value = response
258
+ .json()
259
+ .await
260
+ .unwrap_or_else(|_| serde_json::json!({}));
261
+
262
+ let activity_id = result["id"]
263
+ .as_str()
264
+ .unwrap_or("unknown")
265
+ .to_string();
266
+
267
+ Ok(activity_id)
268
+ }
269
+
270
+ /// Update an existing message.
271
+ async fn update_message(
272
+ &self,
273
+ service_url: &str,
274
+ conversation_id: &str,
275
+ activity_id: &str,
276
+ text: &str,
277
+ ) -> anyhow::Result<()> {
278
+ let token = self.get_access_token().await?;
279
+
280
+ let service_url = if service_url.ends_with('/') {
281
+ service_url.to_string()
282
+ } else {
283
+ format!("{}/", service_url)
284
+ };
285
+
286
+ let url = format!(
287
+ "{}v3/conversations/{}/activities/{}",
288
+ service_url, conversation_id, activity_id
289
+ );
290
+
291
+ let activity = serde_json::json!({
292
+ "type": "message",
293
+ "id": activity_id,
294
+ "from": {
295
+ "id": self.app_id,
296
+ "name": self.bot_name
297
+ },
298
+ "conversation": {
299
+ "id": conversation_id
300
+ },
301
+ "text": text,
302
+ "textFormat": "markdown"
303
+ });
304
+
305
+ let response = self
306
+ .client
307
+ .put(&url)
308
+ .bearer_auth(&token)
309
+ .json(&activity)
310
+ .send()
311
+ .await
312
+ .context("Failed to update Teams message")?;
313
+
314
+ if !response.status().is_success() {
315
+ let status = response.status();
316
+ let body = response.text().await.unwrap_or_default();
317
+ anyhow::bail!("Teams update failed ({}): {}", status, body);
318
+ }
319
+
320
+ Ok(())
321
+ }
322
+
323
+ /// Send typing indicator.
324
+ async fn send_typing_indicator(
325
+ &self,
326
+ service_url: &str,
327
+ conversation_id: &str,
328
+ ) -> anyhow::Result<()> {
329
+ let token = self.get_access_token().await?;
330
+
331
+ let service_url = if service_url.ends_with('/') {
332
+ service_url.to_string()
333
+ } else {
334
+ format!("{}/", service_url)
335
+ };
336
+
337
+ let url = format!(
338
+ "{}v3/conversations/{}/activities",
339
+ service_url, conversation_id
340
+ );
341
+
342
+ let activity = serde_json::json!({
343
+ "type": "typing",
344
+ "from": {
345
+ "id": self.app_id,
346
+ "name": self.bot_name
347
+ },
348
+ "conversation": {
349
+ "id": conversation_id
350
+ }
351
+ });
352
+
353
+ let _ = self
354
+ .client
355
+ .post(&url)
356
+ .bearer_auth(&token)
357
+ .json(&activity)
358
+ .send()
359
+ .await;
360
+
361
+ Ok(())
362
+ }
363
+
364
+ /// Check if a user is allowed.
365
+ fn is_user_allowed(&self, user_id: &str) -> bool {
366
+ if self.allowed_users.is_empty() {
367
+ return true;
368
+ }
369
+ self.allowed_users.iter().any(|u| u == user_id)
370
+ }
371
+
372
+ /// Strip bot mentions from message text.
373
+ fn strip_mentions(text: &str) -> String {
374
+ // Teams mentions are in format <at>BotName</at>
375
+ let re = regex::Regex::new(r"<at>[^<]*</at>").unwrap();
376
+ re.replace_all(text, "").trim().to_string()
377
+ }
378
+
379
+ /// Parse a conversation reference for reply routing.
380
+ fn parse_recipient(recipient: &str) -> Option<(String, String)> {
381
+ // Format: "service_url|conversation_id"
382
+ let parts: Vec<&str> = recipient.split('|').collect();
383
+ if parts.len() == 2 {
384
+ Some((parts[0].to_string(), parts[1].to_string()))
385
+ } else {
386
+ None
387
+ }
388
+ }
389
+
390
+ /// Build a recipient string from service URL and conversation ID.
391
+ fn build_recipient(service_url: &str, conversation_id: &str) -> String {
392
+ format!("{}|{}", service_url, conversation_id)
393
+ }
394
+ }
395
+
396
+ #[async_trait]
397
+ impl Channel for TeamsChannel {
398
+ fn name(&self) -> &str {
399
+ "teams"
400
+ }
401
+
402
+ async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
403
+ let (service_url, conversation_id) = Self::parse_recipient(&message.recipient)
404
+ .ok_or_else(|| anyhow::anyhow!("Invalid Teams recipient format"))?;
405
+
406
+ self.send_reply(&service_url, &conversation_id, &message.content, None)
407
+ .await?;
408
+
409
+ Ok(())
410
+ }
411
+
412
+ async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
413
+ // Take ownership of the webhook receiver
414
+ let mut webhook_rx = {
415
+ let mut guard = self.webhook_rx.lock().await;
416
+ guard.take().ok_or_else(|| anyhow::anyhow!("Teams listener already started"))?
417
+ };
418
+
419
+ tracing::info!("Teams: channel listener started, waiting for webhook activities");
420
+
421
+ while let Some(activity) = webhook_rx.recv().await {
422
+ // Only process message activities
423
+ if activity.activity_type != ACTIVITY_TYPE_MESSAGE {
424
+ tracing::debug!(
425
+ "Teams: ignoring activity type: {}",
426
+ activity.activity_type
427
+ );
428
+ continue;
429
+ }
430
+
431
+ // Check if text is present
432
+ let Some(text) = activity.text.as_ref() else {
433
+ continue;
434
+ };
435
+
436
+ // Check user authorization
437
+ if !self.is_user_allowed(&activity.from.id) {
438
+ tracing::debug!(
439
+ "Teams: ignoring message from unauthorized user: {}",
440
+ activity.from.id
441
+ );
442
+ continue;
443
+ }
444
+
445
+ // Strip bot mentions
446
+ let clean_text = Self::strip_mentions(text);
447
+ if clean_text.is_empty() {
448
+ continue;
449
+ }
450
+
451
+ // Build recipient for replies
452
+ let recipient = Self::build_recipient(
453
+ &activity.service_url,
454
+ &activity.conversation.id,
455
+ );
456
+
457
+ let channel_message = ChannelMessage {
458
+ id: activity.id.clone(),
459
+ sender: activity.from.id.clone(),
460
+ reply_target: recipient,
461
+ content: clean_text,
462
+ channel: "teams".to_string(),
463
+ timestamp: chrono::Utc::now().timestamp() as u64,
464
+ };
465
+
466
+ tracing::debug!(
467
+ "Teams: received message from {} in conversation {}",
468
+ activity.from.name.as_deref().unwrap_or("unknown"),
469
+ activity.conversation.id
470
+ );
471
+
472
+ if tx.send(channel_message).await.is_err() {
473
+ tracing::warn!("Teams: message channel closed, stopping listener");
474
+ break;
475
+ }
476
+ }
477
+
478
+ tracing::info!("Teams: channel listener stopped");
479
+ Ok(())
480
+ }
481
+
482
+ async fn health_check(&self) -> bool {
483
+ // Try to get an access token as health check
484
+ self.get_access_token().await.is_ok()
485
+ }
486
+
487
+ async fn start_typing(&self, recipient: &str) -> anyhow::Result<()> {
488
+ if let Some((service_url, conversation_id)) = Self::parse_recipient(recipient) {
489
+ self.send_typing_indicator(&service_url, &conversation_id).await?;
490
+ }
491
+ Ok(())
492
+ }
493
+
494
+ fn supports_draft_updates(&self) -> bool {
495
+ self.stream_mode != StreamMode::Off
496
+ }
497
+
498
+ async fn send_draft(&self, message: &SendMessage) -> anyhow::Result<Option<String>> {
499
+ let (service_url, conversation_id) = Self::parse_recipient(&message.recipient)
500
+ .ok_or_else(|| anyhow::anyhow!("Invalid Teams recipient format"))?;
501
+
502
+ // Send initial message with typing indicator
503
+ let activity_id = self
504
+ .send_reply(&service_url, &conversation_id, &message.content, None)
505
+ .await?;
506
+
507
+ // Return composite ID for updates: "service_url|conversation_id|activity_id"
508
+ Ok(Some(format!("{}|{}|{}", service_url, conversation_id, activity_id)))
509
+ }
510
+
511
+ async fn update_draft(
512
+ &self,
513
+ _recipient: &str,
514
+ message_id: &str,
515
+ text: &str,
516
+ ) -> anyhow::Result<()> {
517
+ // Parse composite ID
518
+ let parts: Vec<&str> = message_id.split('|').collect();
519
+ if parts.len() != 3 {
520
+ anyhow::bail!("Invalid Teams draft message ID format");
521
+ }
522
+
523
+ let (service_url, conversation_id, activity_id) = (parts[0], parts[1], parts[2]);
524
+ self.update_message(service_url, conversation_id, activity_id, text).await
525
+ }
526
+
527
+ async fn finalize_draft(
528
+ &self,
529
+ recipient: &str,
530
+ message_id: &str,
531
+ text: &str,
532
+ ) -> anyhow::Result<()> {
533
+ // Same as update for Teams
534
+ self.update_draft(recipient, message_id, text).await
535
+ }
536
+ }
537
+
538
+ /// Axum handler for Teams webhook.
539
+ ///
540
+ /// Use this with your Axum router:
541
+ /// ```ignore
542
+ /// let teams = TeamsChannel::new(&config);
543
+ /// let webhook_tx = teams.webhook_sender();
544
+ ///
545
+ /// let app = Router::new()
546
+ /// .route("/api/messages", post(teams_webhook_handler))
547
+ /// .with_state(webhook_tx);
548
+ /// ```
549
+ pub async fn teams_webhook_handler(
550
+ axum::extract::State(tx): axum::extract::State<mpsc::Sender<TeamsActivity>>,
551
+ axum::extract::Json(activity): axum::extract::Json<TeamsActivity>,
552
+ ) -> impl axum::response::IntoResponse {
553
+ tracing::debug!(
554
+ "Teams webhook: received {} activity from {}",
555
+ activity.activity_type,
556
+ activity.from.name.as_deref().unwrap_or("unknown")
557
+ );
558
+
559
+ if let Err(e) = tx.send(activity).await {
560
+ tracing::error!("Teams webhook: failed to forward activity: {}", e);
561
+ return axum::http::StatusCode::INTERNAL_SERVER_ERROR;
562
+ }
563
+
564
+ axum::http::StatusCode::OK
565
+ }
566
+
567
+ #[cfg(test)]
568
+ mod tests {
569
+ use super::*;
570
+
571
+ #[test]
572
+ fn test_strip_mentions() {
573
+ assert_eq!(
574
+ TeamsChannel::strip_mentions("<at>BotName</at> hello"),
575
+ "hello"
576
+ );
577
+ assert_eq!(
578
+ TeamsChannel::strip_mentions("hello <at>Bot</at> world"),
579
+ "hello world"
580
+ );
581
+ assert_eq!(TeamsChannel::strip_mentions("no mentions"), "no mentions");
582
+ }
583
+
584
+ #[test]
585
+ fn test_parse_recipient() {
586
+ let result = TeamsChannel::parse_recipient("https://smba.trafficmanager.net/|19:abc123");
587
+ assert!(result.is_some());
588
+ let (url, conv) = result.unwrap();
589
+ assert_eq!(url, "https://smba.trafficmanager.net/");
590
+ assert_eq!(conv, "19:abc123");
591
+ }
592
+
593
+ #[test]
594
+ fn test_build_recipient() {
595
+ let recipient = TeamsChannel::build_recipient(
596
+ "https://smba.trafficmanager.net/",
597
+ "19:abc123",
598
+ );
599
+ assert_eq!(recipient, "https://smba.trafficmanager.net/|19:abc123");
600
+ }
601
+ }