calvyn-code 0.14.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 (1718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/README.zh-CN.md +180 -0
  4. package/acp_adapter/__init__.py +1 -0
  5. package/acp_adapter/__main__.py +5 -0
  6. package/acp_adapter/auth.py +68 -0
  7. package/acp_adapter/bootstrap/__init__.py +0 -0
  8. package/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 +288 -0
  9. package/acp_adapter/bootstrap/bootstrap_browser_tools.sh +399 -0
  10. package/acp_adapter/entry.py +292 -0
  11. package/acp_adapter/events.py +265 -0
  12. package/acp_adapter/permissions.py +148 -0
  13. package/acp_adapter/server.py +1713 -0
  14. package/acp_adapter/session.py +629 -0
  15. package/acp_adapter/tools.py +1180 -0
  16. package/agent/__init__.py +6 -0
  17. package/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/agent/__pycache__/account_usage.cpython-312.pyc +0 -0
  19. package/agent/__pycache__/anthropic_adapter.cpython-312.pyc +0 -0
  20. package/agent/__pycache__/async_utils.cpython-312.pyc +0 -0
  21. package/agent/__pycache__/auxiliary_client.cpython-312.pyc +0 -0
  22. package/agent/__pycache__/codex_responses_adapter.cpython-312.pyc +0 -0
  23. package/agent/__pycache__/context_compressor.cpython-312.pyc +0 -0
  24. package/agent/__pycache__/context_engine.cpython-312.pyc +0 -0
  25. package/agent/__pycache__/context_references.cpython-312.pyc +0 -0
  26. package/agent/__pycache__/credential_pool.cpython-312.pyc +0 -0
  27. package/agent/__pycache__/curator.cpython-312.pyc +0 -0
  28. package/agent/__pycache__/display.cpython-312.pyc +0 -0
  29. package/agent/__pycache__/error_classifier.cpython-312.pyc +0 -0
  30. package/agent/__pycache__/file_safety.cpython-312.pyc +0 -0
  31. package/agent/__pycache__/google_code_assist.cpython-312.pyc +0 -0
  32. package/agent/__pycache__/google_oauth.cpython-312.pyc +0 -0
  33. package/agent/__pycache__/i18n.cpython-312.pyc +0 -0
  34. package/agent/__pycache__/image_gen_provider.cpython-312.pyc +0 -0
  35. package/agent/__pycache__/image_gen_registry.cpython-312.pyc +0 -0
  36. package/agent/__pycache__/insights.cpython-312.pyc +0 -0
  37. package/agent/__pycache__/lmstudio_reasoning.cpython-312.pyc +0 -0
  38. package/agent/__pycache__/manual_compression_feedback.cpython-312.pyc +0 -0
  39. package/agent/__pycache__/markdown_tables.cpython-312.pyc +0 -0
  40. package/agent/__pycache__/memory_manager.cpython-312.pyc +0 -0
  41. package/agent/__pycache__/memory_provider.cpython-312.pyc +0 -0
  42. package/agent/__pycache__/model_metadata.cpython-312.pyc +0 -0
  43. package/agent/__pycache__/models_dev.cpython-312.pyc +0 -0
  44. package/agent/__pycache__/moonshot_schema.cpython-312.pyc +0 -0
  45. package/agent/__pycache__/onboarding.cpython-312.pyc +0 -0
  46. package/agent/__pycache__/portal_tags.cpython-312.pyc +0 -0
  47. package/agent/__pycache__/prompt_builder.cpython-312.pyc +0 -0
  48. package/agent/__pycache__/prompt_caching.cpython-312.pyc +0 -0
  49. package/agent/__pycache__/redact.cpython-312.pyc +0 -0
  50. package/agent/__pycache__/retry_utils.cpython-312.pyc +0 -0
  51. package/agent/__pycache__/shell_hooks.cpython-312.pyc +0 -0
  52. package/agent/__pycache__/skill_commands.cpython-312.pyc +0 -0
  53. package/agent/__pycache__/skill_preprocessing.cpython-312.pyc +0 -0
  54. package/agent/__pycache__/skill_utils.cpython-312.pyc +0 -0
  55. package/agent/__pycache__/subdirectory_hints.cpython-312.pyc +0 -0
  56. package/agent/__pycache__/think_scrubber.cpython-312.pyc +0 -0
  57. package/agent/__pycache__/title_generator.cpython-312.pyc +0 -0
  58. package/agent/__pycache__/tool_guardrails.cpython-312.pyc +0 -0
  59. package/agent/__pycache__/tool_result_classification.cpython-312.pyc +0 -0
  60. package/agent/__pycache__/trajectory.cpython-312.pyc +0 -0
  61. package/agent/__pycache__/usage_pricing.cpython-312.pyc +0 -0
  62. package/agent/__pycache__/video_gen_provider.cpython-312.pyc +0 -0
  63. package/agent/__pycache__/video_gen_registry.cpython-312.pyc +0 -0
  64. package/agent/__pycache__/web_search_provider.cpython-312.pyc +0 -0
  65. package/agent/__pycache__/web_search_registry.cpython-312.pyc +0 -0
  66. package/agent/account_usage.py +326 -0
  67. package/agent/anthropic_adapter.py +2087 -0
  68. package/agent/async_utils.py +68 -0
  69. package/agent/auxiliary_client.py +4893 -0
  70. package/agent/bedrock_adapter.py +1276 -0
  71. package/agent/codex_responses_adapter.py +1084 -0
  72. package/agent/context_compressor.py +1583 -0
  73. package/agent/context_engine.py +211 -0
  74. package/agent/context_references.py +519 -0
  75. package/agent/copilot_acp_client.py +684 -0
  76. package/agent/credential_pool.py +1780 -0
  77. package/agent/credential_sources.py +449 -0
  78. package/agent/curator.py +1782 -0
  79. package/agent/curator_backup.py +694 -0
  80. package/agent/display.py +987 -0
  81. package/agent/error_classifier.py +1058 -0
  82. package/agent/file_safety.py +112 -0
  83. package/agent/gemini_cloudcode_adapter.py +909 -0
  84. package/agent/gemini_native_adapter.py +971 -0
  85. package/agent/gemini_schema.py +99 -0
  86. package/agent/google_code_assist.py +452 -0
  87. package/agent/google_oauth.py +1062 -0
  88. package/agent/i18n.py +258 -0
  89. package/agent/image_gen_provider.py +243 -0
  90. package/agent/image_gen_registry.py +145 -0
  91. package/agent/image_routing.py +301 -0
  92. package/agent/insights.py +931 -0
  93. package/agent/lmstudio_reasoning.py +48 -0
  94. package/agent/lsp/__init__.py +106 -0
  95. package/agent/lsp/__pycache__/__init__.cpython-312.pyc +0 -0
  96. package/agent/lsp/__pycache__/cli.cpython-312.pyc +0 -0
  97. package/agent/lsp/__pycache__/client.cpython-312.pyc +0 -0
  98. package/agent/lsp/__pycache__/eventlog.cpython-312.pyc +0 -0
  99. package/agent/lsp/__pycache__/manager.cpython-312.pyc +0 -0
  100. package/agent/lsp/__pycache__/protocol.cpython-312.pyc +0 -0
  101. package/agent/lsp/__pycache__/servers.cpython-312.pyc +0 -0
  102. package/agent/lsp/__pycache__/workspace.cpython-312.pyc +0 -0
  103. package/agent/lsp/cli.py +308 -0
  104. package/agent/lsp/client.py +930 -0
  105. package/agent/lsp/eventlog.py +213 -0
  106. package/agent/lsp/install.py +376 -0
  107. package/agent/lsp/manager.py +644 -0
  108. package/agent/lsp/protocol.py +196 -0
  109. package/agent/lsp/range_shift.py +149 -0
  110. package/agent/lsp/reporter.py +78 -0
  111. package/agent/lsp/servers.py +1040 -0
  112. package/agent/lsp/workspace.py +223 -0
  113. package/agent/manual_compression_feedback.py +49 -0
  114. package/agent/markdown_tables.py +309 -0
  115. package/agent/memory_manager.py +556 -0
  116. package/agent/memory_provider.py +279 -0
  117. package/agent/model_metadata.py +1827 -0
  118. package/agent/models_dev.py +724 -0
  119. package/agent/moonshot_schema.py +231 -0
  120. package/agent/nous_rate_guard.py +326 -0
  121. package/agent/onboarding.py +193 -0
  122. package/agent/plugin_llm.py +1046 -0
  123. package/agent/portal_tags.py +64 -0
  124. package/agent/prompt_builder.py +1457 -0
  125. package/agent/prompt_caching.py +79 -0
  126. package/agent/rate_limit_tracker.py +246 -0
  127. package/agent/redact.py +403 -0
  128. package/agent/retry_utils.py +57 -0
  129. package/agent/shell_hooks.py +837 -0
  130. package/agent/skill_commands.py +502 -0
  131. package/agent/skill_preprocessing.py +131 -0
  132. package/agent/skill_utils.py +512 -0
  133. package/agent/subdirectory_hints.py +224 -0
  134. package/agent/think_scrubber.py +386 -0
  135. package/agent/title_generator.py +171 -0
  136. package/agent/tool_guardrails.py +458 -0
  137. package/agent/tool_result_classification.py +26 -0
  138. package/agent/trajectory.py +56 -0
  139. package/agent/transports/__init__.py +68 -0
  140. package/agent/transports/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/agent/transports/__pycache__/anthropic.cpython-312.pyc +0 -0
  142. package/agent/transports/__pycache__/base.cpython-312.pyc +0 -0
  143. package/agent/transports/__pycache__/bedrock.cpython-312.pyc +0 -0
  144. package/agent/transports/__pycache__/chat_completions.cpython-312.pyc +0 -0
  145. package/agent/transports/__pycache__/codex.cpython-312.pyc +0 -0
  146. package/agent/transports/__pycache__/types.cpython-312.pyc +0 -0
  147. package/agent/transports/anthropic.py +179 -0
  148. package/agent/transports/base.py +89 -0
  149. package/agent/transports/bedrock.py +154 -0
  150. package/agent/transports/chat_completions.py +614 -0
  151. package/agent/transports/codex.py +283 -0
  152. package/agent/transports/codex_app_server.py +368 -0
  153. package/agent/transports/codex_app_server_session.py +810 -0
  154. package/agent/transports/codex_event_projector.py +312 -0
  155. package/agent/transports/hermes_tools_mcp_server.py +233 -0
  156. package/agent/transports/types.py +162 -0
  157. package/agent/usage_pricing.py +877 -0
  158. package/agent/video_gen_provider.py +300 -0
  159. package/agent/video_gen_registry.py +117 -0
  160. package/agent/web_search_provider.py +221 -0
  161. package/agent/web_search_registry.py +262 -0
  162. package/assets/banner.png +0 -0
  163. package/batch_runner.py +1303 -0
  164. package/bin/calvyn.js +67 -0
  165. package/calvyn_bootstrap.py +130 -0
  166. package/calvyn_constants.py +346 -0
  167. package/calvyn_logging.py +390 -0
  168. package/calvyn_state.py +2967 -0
  169. package/calvyn_time.py +105 -0
  170. package/cli.py +14160 -0
  171. package/cron/__init__.py +42 -0
  172. package/cron/__pycache__/__init__.cpython-312.pyc +0 -0
  173. package/cron/__pycache__/jobs.cpython-312.pyc +0 -0
  174. package/cron/__pycache__/scheduler.cpython-312.pyc +0 -0
  175. package/cron/jobs.py +1160 -0
  176. package/cron/scheduler.py +1832 -0
  177. package/gateway/__init__.py +35 -0
  178. package/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  179. package/gateway/__pycache__/channel_directory.cpython-312.pyc +0 -0
  180. package/gateway/__pycache__/config.cpython-312.pyc +0 -0
  181. package/gateway/__pycache__/delivery.cpython-312.pyc +0 -0
  182. package/gateway/__pycache__/display_config.cpython-312.pyc +0 -0
  183. package/gateway/__pycache__/hooks.cpython-312.pyc +0 -0
  184. package/gateway/__pycache__/pairing.cpython-312.pyc +0 -0
  185. package/gateway/__pycache__/platform_registry.cpython-312.pyc +0 -0
  186. package/gateway/__pycache__/restart.cpython-312.pyc +0 -0
  187. package/gateway/__pycache__/run.cpython-312.pyc +0 -0
  188. package/gateway/__pycache__/runtime_footer.cpython-312.pyc +0 -0
  189. package/gateway/__pycache__/session.cpython-312.pyc +0 -0
  190. package/gateway/__pycache__/session_context.cpython-312.pyc +0 -0
  191. package/gateway/__pycache__/shutdown_forensics.cpython-312.pyc +0 -0
  192. package/gateway/__pycache__/slash_access.cpython-312.pyc +0 -0
  193. package/gateway/__pycache__/status.cpython-312.pyc +0 -0
  194. package/gateway/__pycache__/stream_consumer.cpython-312.pyc +0 -0
  195. package/gateway/__pycache__/whatsapp_identity.cpython-312.pyc +0 -0
  196. package/gateway/assets/telegram-botfather-threads-settings.jpg +0 -0
  197. package/gateway/builtin_hooks/__init__.py +1 -0
  198. package/gateway/channel_directory.py +357 -0
  199. package/gateway/config.py +1873 -0
  200. package/gateway/delivery.py +258 -0
  201. package/gateway/display_config.py +206 -0
  202. package/gateway/hooks.py +210 -0
  203. package/gateway/mirror.py +179 -0
  204. package/gateway/pairing.py +322 -0
  205. package/gateway/platform_registry.py +260 -0
  206. package/gateway/platforms/ADDING_A_PLATFORM.md +374 -0
  207. package/gateway/platforms/__init__.py +45 -0
  208. package/gateway/platforms/__pycache__/__init__.cpython-312.pyc +0 -0
  209. package/gateway/platforms/__pycache__/base.cpython-312.pyc +0 -0
  210. package/gateway/platforms/__pycache__/helpers.cpython-312.pyc +0 -0
  211. package/gateway/platforms/__pycache__/telegram.cpython-312.pyc +0 -0
  212. package/gateway/platforms/__pycache__/telegram_network.cpython-312.pyc +0 -0
  213. package/gateway/platforms/__pycache__/yuanbao.cpython-312.pyc +0 -0
  214. package/gateway/platforms/__pycache__/yuanbao_media.cpython-312.pyc +0 -0
  215. package/gateway/platforms/__pycache__/yuanbao_proto.cpython-312.pyc +0 -0
  216. package/gateway/platforms/_http_client_limits.py +84 -0
  217. package/gateway/platforms/api_server.py +3488 -0
  218. package/gateway/platforms/base.py +3747 -0
  219. package/gateway/platforms/bluebubbles.py +937 -0
  220. package/gateway/platforms/dingtalk.py +1473 -0
  221. package/gateway/platforms/discord.py +5584 -0
  222. package/gateway/platforms/email.py +773 -0
  223. package/gateway/platforms/feishu.py +5059 -0
  224. package/gateway/platforms/feishu_comment.py +1382 -0
  225. package/gateway/platforms/feishu_comment_rules.py +430 -0
  226. package/gateway/platforms/helpers.py +279 -0
  227. package/gateway/platforms/homeassistant.py +449 -0
  228. package/gateway/platforms/matrix.py +2777 -0
  229. package/gateway/platforms/mattermost.py +852 -0
  230. package/gateway/platforms/msgraph_webhook.py +397 -0
  231. package/gateway/platforms/qqbot/__init__.py +91 -0
  232. package/gateway/platforms/qqbot/adapter.py +3072 -0
  233. package/gateway/platforms/qqbot/chunked_upload.py +602 -0
  234. package/gateway/platforms/qqbot/constants.py +74 -0
  235. package/gateway/platforms/qqbot/crypto.py +45 -0
  236. package/gateway/platforms/qqbot/keyboards.py +473 -0
  237. package/gateway/platforms/qqbot/onboard.py +220 -0
  238. package/gateway/platforms/qqbot/utils.py +71 -0
  239. package/gateway/platforms/signal.py +1518 -0
  240. package/gateway/platforms/signal_rate_limit.py +369 -0
  241. package/gateway/platforms/slack.py +3028 -0
  242. package/gateway/platforms/sms.py +377 -0
  243. package/gateway/platforms/telegram.py +4836 -0
  244. package/gateway/platforms/telegram_network.py +249 -0
  245. package/gateway/platforms/webhook.py +806 -0
  246. package/gateway/platforms/wecom.py +1610 -0
  247. package/gateway/platforms/wecom_callback.py +403 -0
  248. package/gateway/platforms/wecom_crypto.py +142 -0
  249. package/gateway/platforms/weixin.py +2170 -0
  250. package/gateway/platforms/whatsapp.py +1283 -0
  251. package/gateway/platforms/yuanbao.py +4873 -0
  252. package/gateway/platforms/yuanbao_media.py +645 -0
  253. package/gateway/platforms/yuanbao_proto.py +1209 -0
  254. package/gateway/platforms/yuanbao_sticker.py +558 -0
  255. package/gateway/restart.py +20 -0
  256. package/gateway/run.py +17074 -0
  257. package/gateway/runtime_footer.py +150 -0
  258. package/gateway/session.py +1399 -0
  259. package/gateway/session_context.py +156 -0
  260. package/gateway/shutdown_forensics.py +462 -0
  261. package/gateway/slash_access.py +229 -0
  262. package/gateway/status.py +972 -0
  263. package/gateway/sticker_cache.py +111 -0
  264. package/gateway/stream_consumer.py +1286 -0
  265. package/gateway/whatsapp_identity.py +156 -0
  266. package/hermes_cli/__init__.py +47 -0
  267. package/hermes_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  268. package/hermes_cli/__pycache__/_parser.cpython-312.pyc +0 -0
  269. package/hermes_cli/__pycache__/auth.cpython-312.pyc +0 -0
  270. package/hermes_cli/__pycache__/banner.cpython-312.pyc +0 -0
  271. package/hermes_cli/__pycache__/browser_connect.cpython-312.pyc +0 -0
  272. package/hermes_cli/__pycache__/callbacks.cpython-312.pyc +0 -0
  273. package/hermes_cli/__pycache__/checkpoints.cpython-312.pyc +0 -0
  274. package/hermes_cli/__pycache__/cli_output.cpython-312.pyc +0 -0
  275. package/hermes_cli/__pycache__/codex_models.cpython-312.pyc +0 -0
  276. package/hermes_cli/__pycache__/codex_runtime_switch.cpython-312.pyc +0 -0
  277. package/hermes_cli/__pycache__/colors.cpython-312.pyc +0 -0
  278. package/hermes_cli/__pycache__/commands.cpython-312.pyc +0 -0
  279. package/hermes_cli/__pycache__/config.cpython-312.pyc +0 -0
  280. package/hermes_cli/__pycache__/copilot_auth.cpython-312.pyc +0 -0
  281. package/hermes_cli/__pycache__/curator.cpython-312.pyc +0 -0
  282. package/hermes_cli/__pycache__/curses_ui.cpython-312.pyc +0 -0
  283. package/hermes_cli/__pycache__/debug.cpython-312.pyc +0 -0
  284. package/hermes_cli/__pycache__/default_soul.cpython-312.pyc +0 -0
  285. package/hermes_cli/__pycache__/env_loader.cpython-312.pyc +0 -0
  286. package/hermes_cli/__pycache__/fallback_cmd.cpython-312.pyc +0 -0
  287. package/hermes_cli/__pycache__/gateway.cpython-312.pyc +0 -0
  288. package/hermes_cli/__pycache__/gateway_windows.cpython-312.pyc +0 -0
  289. package/hermes_cli/__pycache__/goals.cpython-312.pyc +0 -0
  290. package/hermes_cli/__pycache__/inventory.cpython-312.pyc +0 -0
  291. package/hermes_cli/__pycache__/kanban.cpython-312.pyc +0 -0
  292. package/hermes_cli/__pycache__/kanban_db.cpython-312.pyc +0 -0
  293. package/hermes_cli/__pycache__/main.cpython-312.pyc +0 -0
  294. package/hermes_cli/__pycache__/model_catalog.cpython-312.pyc +0 -0
  295. package/hermes_cli/__pycache__/model_normalize.cpython-312.pyc +0 -0
  296. package/hermes_cli/__pycache__/model_switch.cpython-312.pyc +0 -0
  297. package/hermes_cli/__pycache__/models.cpython-312.pyc +0 -0
  298. package/hermes_cli/__pycache__/nous_subscription.cpython-312.pyc +0 -0
  299. package/hermes_cli/__pycache__/pairing.cpython-312.pyc +0 -0
  300. package/hermes_cli/__pycache__/platforms.cpython-312.pyc +0 -0
  301. package/hermes_cli/__pycache__/plugins.cpython-312.pyc +0 -0
  302. package/hermes_cli/__pycache__/profiles.cpython-312.pyc +0 -0
  303. package/hermes_cli/__pycache__/providers.cpython-312.pyc +0 -0
  304. package/hermes_cli/__pycache__/pt_input_extras.cpython-312.pyc +0 -0
  305. package/hermes_cli/__pycache__/runtime_provider.cpython-312.pyc +0 -0
  306. package/hermes_cli/__pycache__/security_advisories.cpython-312.pyc +0 -0
  307. package/hermes_cli/__pycache__/setup.cpython-312.pyc +0 -0
  308. package/hermes_cli/__pycache__/skills_hub.cpython-312.pyc +0 -0
  309. package/hermes_cli/__pycache__/skin_engine.cpython-312.pyc +0 -0
  310. package/hermes_cli/__pycache__/stdio.cpython-312.pyc +0 -0
  311. package/hermes_cli/__pycache__/timeouts.cpython-312.pyc +0 -0
  312. package/hermes_cli/__pycache__/tips.cpython-312.pyc +0 -0
  313. package/hermes_cli/__pycache__/tools_config.cpython-312.pyc +0 -0
  314. package/hermes_cli/__pycache__/voice.cpython-312.pyc +0 -0
  315. package/hermes_cli/_parser.py +365 -0
  316. package/hermes_cli/_subprocess_compat.py +175 -0
  317. package/hermes_cli/auth.py +6299 -0
  318. package/hermes_cli/auth_commands.py +749 -0
  319. package/hermes_cli/azure_detect.py +300 -0
  320. package/hermes_cli/backup.py +938 -0
  321. package/hermes_cli/banner.py +703 -0
  322. package/hermes_cli/browser_connect.py +139 -0
  323. package/hermes_cli/callbacks.py +243 -0
  324. package/hermes_cli/checkpoints.py +244 -0
  325. package/hermes_cli/claw.py +810 -0
  326. package/hermes_cli/cli_output.py +78 -0
  327. package/hermes_cli/clipboard.py +495 -0
  328. package/hermes_cli/codex_models.py +198 -0
  329. package/hermes_cli/codex_runtime_plugin_migration.py +757 -0
  330. package/hermes_cli/codex_runtime_switch.py +266 -0
  331. package/hermes_cli/colors.py +38 -0
  332. package/hermes_cli/commands.py +1728 -0
  333. package/hermes_cli/completion.py +315 -0
  334. package/hermes_cli/config.py +5382 -0
  335. package/hermes_cli/copilot_auth.py +392 -0
  336. package/hermes_cli/cron.py +313 -0
  337. package/hermes_cli/curator.py +598 -0
  338. package/hermes_cli/curses_ui.py +472 -0
  339. package/hermes_cli/debug.py +747 -0
  340. package/hermes_cli/default_soul.py +11 -0
  341. package/hermes_cli/dep_ensure.py +107 -0
  342. package/hermes_cli/dingtalk_auth.py +293 -0
  343. package/hermes_cli/doctor.py +1863 -0
  344. package/hermes_cli/dump.py +326 -0
  345. package/hermes_cli/env_loader.py +175 -0
  346. package/hermes_cli/fallback_cmd.py +361 -0
  347. package/hermes_cli/gateway.py +5422 -0
  348. package/hermes_cli/gateway_windows.py +692 -0
  349. package/hermes_cli/goals.py +757 -0
  350. package/hermes_cli/hooks.py +385 -0
  351. package/hermes_cli/inventory.py +240 -0
  352. package/hermes_cli/kanban.py +2252 -0
  353. package/hermes_cli/kanban_db.py +4840 -0
  354. package/hermes_cli/kanban_diagnostics.py +776 -0
  355. package/hermes_cli/kanban_specify.py +266 -0
  356. package/hermes_cli/logs.py +391 -0
  357. package/hermes_cli/main.py +12396 -0
  358. package/hermes_cli/mcp_config.py +781 -0
  359. package/hermes_cli/memory_setup.py +465 -0
  360. package/hermes_cli/model_catalog.py +330 -0
  361. package/hermes_cli/model_normalize.py +473 -0
  362. package/hermes_cli/model_switch.py +1777 -0
  363. package/hermes_cli/models.py +3789 -0
  364. package/hermes_cli/nous_subscription.py +799 -0
  365. package/hermes_cli/oneshot.py +351 -0
  366. package/hermes_cli/pairing.py +115 -0
  367. package/hermes_cli/platforms.py +83 -0
  368. package/hermes_cli/plugins.py +1562 -0
  369. package/hermes_cli/plugins_cmd.py +1587 -0
  370. package/hermes_cli/profile_distribution.py +703 -0
  371. package/hermes_cli/profiles.py +1319 -0
  372. package/hermes_cli/providers.py +720 -0
  373. package/hermes_cli/proxy/__init__.py +20 -0
  374. package/hermes_cli/proxy/adapters/__init__.py +35 -0
  375. package/hermes_cli/proxy/adapters/base.py +94 -0
  376. package/hermes_cli/proxy/adapters/nous_portal.py +137 -0
  377. package/hermes_cli/proxy/cli.py +141 -0
  378. package/hermes_cli/proxy/server.py +265 -0
  379. package/hermes_cli/pt_input_extras.py +83 -0
  380. package/hermes_cli/pty_bridge.py +237 -0
  381. package/hermes_cli/relaunch.py +205 -0
  382. package/hermes_cli/runtime_provider.py +1428 -0
  383. package/hermes_cli/security_advisories.py +452 -0
  384. package/hermes_cli/setup.py +3559 -0
  385. package/hermes_cli/skills_config.py +177 -0
  386. package/hermes_cli/skills_hub.py +1595 -0
  387. package/hermes_cli/skin_engine.py +929 -0
  388. package/hermes_cli/slack_cli.py +160 -0
  389. package/hermes_cli/status.py +550 -0
  390. package/hermes_cli/stdio.py +252 -0
  391. package/hermes_cli/timeouts.py +82 -0
  392. package/hermes_cli/tips.py +487 -0
  393. package/hermes_cli/tools_config.py +3151 -0
  394. package/hermes_cli/uninstall.py +681 -0
  395. package/hermes_cli/vercel_auth.py +70 -0
  396. package/hermes_cli/voice.py +846 -0
  397. package/hermes_cli/web_server.py +4438 -0
  398. package/hermes_cli/webhook.py +275 -0
  399. package/locales/af.yaml +350 -0
  400. package/locales/de.yaml +350 -0
  401. package/locales/en.yaml +365 -0
  402. package/locales/es.yaml +350 -0
  403. package/locales/fr.yaml +350 -0
  404. package/locales/ga.yaml +354 -0
  405. package/locales/hu.yaml +350 -0
  406. package/locales/it.yaml +350 -0
  407. package/locales/ja.yaml +350 -0
  408. package/locales/ko.yaml +350 -0
  409. package/locales/pt.yaml +350 -0
  410. package/locales/ru.yaml +350 -0
  411. package/locales/tr.yaml +350 -0
  412. package/locales/uk.yaml +350 -0
  413. package/locales/zh-hant.yaml +350 -0
  414. package/locales/zh.yaml +350 -0
  415. package/mcp_serve.py +898 -0
  416. package/model_tools.py +899 -0
  417. package/optional-skills/DESCRIPTION.md +24 -0
  418. package/optional-skills/autonomous-ai-agents/DESCRIPTION.md +2 -0
  419. package/optional-skills/autonomous-ai-agents/blackbox/SKILL.md +144 -0
  420. package/optional-skills/autonomous-ai-agents/honcho/SKILL.md +431 -0
  421. package/optional-skills/blockchain/evm/SKILL.md +211 -0
  422. package/optional-skills/blockchain/evm/scripts/evm_client.py +1508 -0
  423. package/optional-skills/blockchain/hyperliquid/SKILL.md +211 -0
  424. package/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1660 -0
  425. package/optional-skills/blockchain/solana/SKILL.md +208 -0
  426. package/optional-skills/blockchain/solana/scripts/solana_client.py +698 -0
  427. package/optional-skills/communication/DESCRIPTION.md +1 -0
  428. package/optional-skills/communication/one-three-one-rule/SKILL.md +104 -0
  429. package/optional-skills/creative/blender-mcp/SKILL.md +117 -0
  430. package/optional-skills/creative/concept-diagrams/SKILL.md +362 -0
  431. package/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  432. package/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  433. package/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  434. package/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  435. package/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  436. package/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  437. package/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  438. package/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  439. package/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  440. package/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  441. package/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  442. package/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  443. package/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  444. package/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  445. package/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  446. package/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md +43 -0
  447. package/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md +144 -0
  448. package/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  449. package/optional-skills/creative/concept-diagrams/templates/template.html +174 -0
  450. package/optional-skills/creative/hyperframes/SKILL.md +191 -0
  451. package/optional-skills/creative/hyperframes/references/cli.md +185 -0
  452. package/optional-skills/creative/hyperframes/references/composition.md +129 -0
  453. package/optional-skills/creative/hyperframes/references/features.md +289 -0
  454. package/optional-skills/creative/hyperframes/references/gsap.md +136 -0
  455. package/optional-skills/creative/hyperframes/references/troubleshooting.md +137 -0
  456. package/optional-skills/creative/hyperframes/references/website-to-video.md +145 -0
  457. package/optional-skills/creative/hyperframes/scripts/setup.sh +135 -0
  458. package/optional-skills/creative/kanban-video-orchestrator/SKILL.md +207 -0
  459. package/optional-skills/creative/kanban-video-orchestrator/assets/brief.md.tmpl +79 -0
  460. package/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +185 -0
  461. package/optional-skills/creative/kanban-video-orchestrator/assets/soul.md.tmpl +38 -0
  462. package/optional-skills/creative/kanban-video-orchestrator/references/examples.md +227 -0
  463. package/optional-skills/creative/kanban-video-orchestrator/references/intake.md +166 -0
  464. package/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +276 -0
  465. package/optional-skills/creative/kanban-video-orchestrator/references/monitoring.md +180 -0
  466. package/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +298 -0
  467. package/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +317 -0
  468. package/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +501 -0
  469. package/optional-skills/creative/kanban-video-orchestrator/scripts/monitor.py +195 -0
  470. package/optional-skills/creative/meme-generation/EXAMPLES.md +46 -0
  471. package/optional-skills/creative/meme-generation/SKILL.md +130 -0
  472. package/optional-skills/creative/meme-generation/scripts/generate_meme.py +471 -0
  473. package/optional-skills/creative/meme-generation/scripts/templates.json +97 -0
  474. package/optional-skills/devops/cli/SKILL.md +156 -0
  475. package/optional-skills/devops/cli/references/app-discovery.md +112 -0
  476. package/optional-skills/devops/cli/references/authentication.md +59 -0
  477. package/optional-skills/devops/cli/references/cli-reference.md +104 -0
  478. package/optional-skills/devops/cli/references/running-apps.md +171 -0
  479. package/optional-skills/devops/docker-management/SKILL.md +281 -0
  480. package/optional-skills/devops/pinggy-tunnel/SKILL.md +309 -0
  481. package/optional-skills/devops/watchers/SKILL.md +112 -0
  482. package/optional-skills/devops/watchers/scripts/_watermark.py +148 -0
  483. package/optional-skills/devops/watchers/scripts/watch_github.py +168 -0
  484. package/optional-skills/devops/watchers/scripts/watch_http_json.py +131 -0
  485. package/optional-skills/devops/watchers/scripts/watch_rss.py +121 -0
  486. package/optional-skills/dogfood/DESCRIPTION.md +3 -0
  487. package/optional-skills/dogfood/adversarial-ux-test/SKILL.md +191 -0
  488. package/optional-skills/email/agentmail/SKILL.md +126 -0
  489. package/optional-skills/finance/3-statement-model/SKILL.md +433 -0
  490. package/optional-skills/finance/3-statement-model/references/formatting.md +118 -0
  491. package/optional-skills/finance/3-statement-model/references/formulas.md +292 -0
  492. package/optional-skills/finance/3-statement-model/references/sec-filings.md +125 -0
  493. package/optional-skills/finance/comps-analysis/SKILL.md +662 -0
  494. package/optional-skills/finance/dcf-model/SKILL.md +1270 -0
  495. package/optional-skills/finance/dcf-model/TROUBLESHOOTING.md +40 -0
  496. package/optional-skills/finance/dcf-model/requirements.txt +7 -0
  497. package/optional-skills/finance/dcf-model/scripts/validate_dcf.py +292 -0
  498. package/optional-skills/finance/excel-author/SKILL.md +244 -0
  499. package/optional-skills/finance/excel-author/scripts/recalc.py +88 -0
  500. package/optional-skills/finance/lbo-model/SKILL.md +291 -0
  501. package/optional-skills/finance/merger-model/SKILL.md +144 -0
  502. package/optional-skills/finance/pptx-author/SKILL.md +173 -0
  503. package/optional-skills/finance/stocks/SKILL.md +95 -0
  504. package/optional-skills/finance/stocks/scripts/stocks_client.py +755 -0
  505. package/optional-skills/health/DESCRIPTION.md +1 -0
  506. package/optional-skills/health/fitness-nutrition/SKILL.md +256 -0
  507. package/optional-skills/health/fitness-nutrition/references/FORMULAS.md +100 -0
  508. package/optional-skills/health/fitness-nutrition/scripts/body_calc.py +210 -0
  509. package/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py +86 -0
  510. package/optional-skills/health/neuroskill-bci/SKILL.md +459 -0
  511. package/optional-skills/health/neuroskill-bci/references/api.md +286 -0
  512. package/optional-skills/health/neuroskill-bci/references/metrics.md +220 -0
  513. package/optional-skills/health/neuroskill-bci/references/protocols.md +452 -0
  514. package/optional-skills/mcp/DESCRIPTION.md +3 -0
  515. package/optional-skills/mcp/fastmcp/SKILL.md +300 -0
  516. package/optional-skills/mcp/fastmcp/references/fastmcp-cli.md +110 -0
  517. package/optional-skills/mcp/fastmcp/scripts/scaffold_fastmcp.py +56 -0
  518. package/optional-skills/mcp/fastmcp/templates/api_wrapper.py +54 -0
  519. package/optional-skills/mcp/fastmcp/templates/database_server.py +77 -0
  520. package/optional-skills/mcp/fastmcp/templates/file_processor.py +55 -0
  521. package/optional-skills/mcp/mcporter/SKILL.md +123 -0
  522. package/optional-skills/migration/DESCRIPTION.md +2 -0
  523. package/optional-skills/migration/openclaw-migration/SKILL.md +298 -0
  524. package/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +3136 -0
  525. package/optional-skills/mlops/accelerate/SKILL.md +336 -0
  526. package/optional-skills/mlops/accelerate/references/custom-plugins.md +453 -0
  527. package/optional-skills/mlops/accelerate/references/megatron-integration.md +489 -0
  528. package/optional-skills/mlops/accelerate/references/performance.md +525 -0
  529. package/optional-skills/mlops/chroma/SKILL.md +410 -0
  530. package/optional-skills/mlops/chroma/references/integration.md +38 -0
  531. package/optional-skills/mlops/clip/SKILL.md +257 -0
  532. package/optional-skills/mlops/clip/references/applications.md +207 -0
  533. package/optional-skills/mlops/faiss/SKILL.md +225 -0
  534. package/optional-skills/mlops/faiss/references/index_types.md +280 -0
  535. package/optional-skills/mlops/flash-attention/SKILL.md +367 -0
  536. package/optional-skills/mlops/flash-attention/references/benchmarks.md +215 -0
  537. package/optional-skills/mlops/flash-attention/references/transformers-integration.md +293 -0
  538. package/optional-skills/mlops/guidance/SKILL.md +576 -0
  539. package/optional-skills/mlops/guidance/references/backends.md +554 -0
  540. package/optional-skills/mlops/guidance/references/constraints.md +674 -0
  541. package/optional-skills/mlops/guidance/references/examples.md +767 -0
  542. package/optional-skills/mlops/huggingface-tokenizers/SKILL.md +520 -0
  543. package/optional-skills/mlops/huggingface-tokenizers/references/algorithms.md +653 -0
  544. package/optional-skills/mlops/huggingface-tokenizers/references/integration.md +637 -0
  545. package/optional-skills/mlops/huggingface-tokenizers/references/pipeline.md +723 -0
  546. package/optional-skills/mlops/huggingface-tokenizers/references/training.md +565 -0
  547. package/optional-skills/mlops/inference/outlines/SKILL.md +656 -0
  548. package/optional-skills/mlops/inference/outlines/references/backends.md +615 -0
  549. package/optional-skills/mlops/inference/outlines/references/examples.md +773 -0
  550. package/optional-skills/mlops/inference/outlines/references/json_generation.md +652 -0
  551. package/optional-skills/mlops/instructor/SKILL.md +744 -0
  552. package/optional-skills/mlops/instructor/references/examples.md +107 -0
  553. package/optional-skills/mlops/instructor/references/providers.md +70 -0
  554. package/optional-skills/mlops/instructor/references/validation.md +606 -0
  555. package/optional-skills/mlops/lambda-labs/SKILL.md +549 -0
  556. package/optional-skills/mlops/lambda-labs/references/advanced-usage.md +611 -0
  557. package/optional-skills/mlops/lambda-labs/references/troubleshooting.md +530 -0
  558. package/optional-skills/mlops/llava/SKILL.md +308 -0
  559. package/optional-skills/mlops/llava/references/training.md +197 -0
  560. package/optional-skills/mlops/modal/SKILL.md +345 -0
  561. package/optional-skills/mlops/modal/references/advanced-usage.md +503 -0
  562. package/optional-skills/mlops/modal/references/troubleshooting.md +494 -0
  563. package/optional-skills/mlops/nemo-curator/SKILL.md +387 -0
  564. package/optional-skills/mlops/nemo-curator/references/deduplication.md +87 -0
  565. package/optional-skills/mlops/nemo-curator/references/filtering.md +102 -0
  566. package/optional-skills/mlops/peft/SKILL.md +435 -0
  567. package/optional-skills/mlops/peft/references/advanced-usage.md +514 -0
  568. package/optional-skills/mlops/peft/references/troubleshooting.md +480 -0
  569. package/optional-skills/mlops/pinecone/SKILL.md +362 -0
  570. package/optional-skills/mlops/pinecone/references/deployment.md +181 -0
  571. package/optional-skills/mlops/pytorch-fsdp/SKILL.md +130 -0
  572. package/optional-skills/mlops/pytorch-fsdp/references/index.md +7 -0
  573. package/optional-skills/mlops/pytorch-fsdp/references/other.md +4261 -0
  574. package/optional-skills/mlops/pytorch-lightning/SKILL.md +350 -0
  575. package/optional-skills/mlops/pytorch-lightning/references/callbacks.md +436 -0
  576. package/optional-skills/mlops/pytorch-lightning/references/distributed.md +490 -0
  577. package/optional-skills/mlops/pytorch-lightning/references/hyperparameter-tuning.md +556 -0
  578. package/optional-skills/mlops/qdrant/SKILL.md +497 -0
  579. package/optional-skills/mlops/qdrant/references/advanced-usage.md +648 -0
  580. package/optional-skills/mlops/qdrant/references/troubleshooting.md +631 -0
  581. package/optional-skills/mlops/saelens/SKILL.md +390 -0
  582. package/optional-skills/mlops/saelens/references/README.md +69 -0
  583. package/optional-skills/mlops/saelens/references/api.md +333 -0
  584. package/optional-skills/mlops/saelens/references/tutorials.md +318 -0
  585. package/optional-skills/mlops/simpo/SKILL.md +223 -0
  586. package/optional-skills/mlops/simpo/references/datasets.md +478 -0
  587. package/optional-skills/mlops/simpo/references/hyperparameters.md +452 -0
  588. package/optional-skills/mlops/simpo/references/loss-functions.md +350 -0
  589. package/optional-skills/mlops/slime/SKILL.md +468 -0
  590. package/optional-skills/mlops/slime/references/api-reference.md +392 -0
  591. package/optional-skills/mlops/slime/references/troubleshooting.md +386 -0
  592. package/optional-skills/mlops/stable-diffusion/SKILL.md +523 -0
  593. package/optional-skills/mlops/stable-diffusion/references/advanced-usage.md +716 -0
  594. package/optional-skills/mlops/stable-diffusion/references/troubleshooting.md +555 -0
  595. package/optional-skills/mlops/tensorrt-llm/SKILL.md +191 -0
  596. package/optional-skills/mlops/tensorrt-llm/references/multi-gpu.md +298 -0
  597. package/optional-skills/mlops/tensorrt-llm/references/optimization.md +242 -0
  598. package/optional-skills/mlops/tensorrt-llm/references/serving.md +470 -0
  599. package/optional-skills/mlops/torchtitan/SKILL.md +362 -0
  600. package/optional-skills/mlops/torchtitan/references/checkpoint.md +181 -0
  601. package/optional-skills/mlops/torchtitan/references/custom-models.md +258 -0
  602. package/optional-skills/mlops/torchtitan/references/float8.md +133 -0
  603. package/optional-skills/mlops/torchtitan/references/fsdp.md +126 -0
  604. package/optional-skills/mlops/training/axolotl/SKILL.md +166 -0
  605. package/optional-skills/mlops/training/axolotl/references/api.md +5548 -0
  606. package/optional-skills/mlops/training/axolotl/references/dataset-formats.md +1029 -0
  607. package/optional-skills/mlops/training/axolotl/references/index.md +15 -0
  608. package/optional-skills/mlops/training/axolotl/references/other.md +3563 -0
  609. package/optional-skills/mlops/training/trl-fine-tuning/SKILL.md +463 -0
  610. package/optional-skills/mlops/training/trl-fine-tuning/references/dpo-variants.md +227 -0
  611. package/optional-skills/mlops/training/trl-fine-tuning/references/grpo-training.md +504 -0
  612. package/optional-skills/mlops/training/trl-fine-tuning/references/online-rl.md +82 -0
  613. package/optional-skills/mlops/training/trl-fine-tuning/references/reward-modeling.md +122 -0
  614. package/optional-skills/mlops/training/trl-fine-tuning/references/sft-training.md +168 -0
  615. package/optional-skills/mlops/training/trl-fine-tuning/templates/basic_grpo_training.py +228 -0
  616. package/optional-skills/mlops/training/unsloth/SKILL.md +84 -0
  617. package/optional-skills/mlops/training/unsloth/references/index.md +7 -0
  618. package/optional-skills/mlops/training/unsloth/references/llms-full.md +16799 -0
  619. package/optional-skills/mlops/training/unsloth/references/llms-txt.md +12044 -0
  620. package/optional-skills/mlops/training/unsloth/references/llms.md +82 -0
  621. package/optional-skills/mlops/whisper/SKILL.md +321 -0
  622. package/optional-skills/mlops/whisper/references/languages.md +189 -0
  623. package/optional-skills/productivity/canvas/SKILL.md +98 -0
  624. package/optional-skills/productivity/canvas/scripts/canvas_api.py +157 -0
  625. package/optional-skills/productivity/here-now/SKILL.md +217 -0
  626. package/optional-skills/productivity/here-now/scripts/drive.sh +406 -0
  627. package/optional-skills/productivity/here-now/scripts/publish.sh +445 -0
  628. package/optional-skills/productivity/memento-flashcards/SKILL.md +324 -0
  629. package/optional-skills/productivity/memento-flashcards/scripts/memento_cards.py +353 -0
  630. package/optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py +88 -0
  631. package/optional-skills/productivity/shop-app/SKILL.md +340 -0
  632. package/optional-skills/productivity/shopify/SKILL.md +373 -0
  633. package/optional-skills/productivity/siyuan/SKILL.md +298 -0
  634. package/optional-skills/productivity/telephony/SKILL.md +418 -0
  635. package/optional-skills/productivity/telephony/scripts/telephony.py +1343 -0
  636. package/optional-skills/research/bioinformatics/SKILL.md +235 -0
  637. package/optional-skills/research/darwinian-evolver/SKILL.md +199 -0
  638. package/optional-skills/research/darwinian-evolver/scripts/parrot_openrouter.py +218 -0
  639. package/optional-skills/research/darwinian-evolver/scripts/show_snapshot.py +69 -0
  640. package/optional-skills/research/darwinian-evolver/templates/custom_problem_template.py +240 -0
  641. package/optional-skills/research/domain-intel/SKILL.md +97 -0
  642. package/optional-skills/research/domain-intel/scripts/domain_intel.py +397 -0
  643. package/optional-skills/research/drug-discovery/SKILL.md +227 -0
  644. package/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md +66 -0
  645. package/optional-skills/research/drug-discovery/scripts/chembl_target.py +53 -0
  646. package/optional-skills/research/drug-discovery/scripts/ro5_screen.py +44 -0
  647. package/optional-skills/research/duckduckgo-search/SKILL.md +238 -0
  648. package/optional-skills/research/duckduckgo-search/scripts/duckduckgo.sh +28 -0
  649. package/optional-skills/research/gitnexus-explorer/SKILL.md +214 -0
  650. package/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs +92 -0
  651. package/optional-skills/research/osint-investigation/SKILL.md +277 -0
  652. package/optional-skills/research/osint-investigation/references/sources/courtlistener.md +98 -0
  653. package/optional-skills/research/osint-investigation/references/sources/gdelt.md +104 -0
  654. package/optional-skills/research/osint-investigation/references/sources/icij-offshore.md +104 -0
  655. package/optional-skills/research/osint-investigation/references/sources/nyc-acris.md +90 -0
  656. package/optional-skills/research/osint-investigation/references/sources/ofac-sdn.md +92 -0
  657. package/optional-skills/research/osint-investigation/references/sources/opencorporates.md +103 -0
  658. package/optional-skills/research/osint-investigation/references/sources/sec-edgar.md +83 -0
  659. package/optional-skills/research/osint-investigation/references/sources/senate-ld.md +89 -0
  660. package/optional-skills/research/osint-investigation/references/sources/usaspending.md +97 -0
  661. package/optional-skills/research/osint-investigation/references/sources/wayback.md +93 -0
  662. package/optional-skills/research/osint-investigation/references/sources/wikipedia.md +107 -0
  663. package/optional-skills/research/osint-investigation/scripts/_http.py +82 -0
  664. package/optional-skills/research/osint-investigation/scripts/_normalize.py +67 -0
  665. package/optional-skills/research/osint-investigation/scripts/build_findings.py +221 -0
  666. package/optional-skills/research/osint-investigation/scripts/entity_resolution.py +228 -0
  667. package/optional-skills/research/osint-investigation/scripts/fetch_courtlistener.py +149 -0
  668. package/optional-skills/research/osint-investigation/scripts/fetch_gdelt.py +162 -0
  669. package/optional-skills/research/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
  670. package/optional-skills/research/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
  671. package/optional-skills/research/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
  672. package/optional-skills/research/osint-investigation/scripts/fetch_opencorporates.py +192 -0
  673. package/optional-skills/research/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
  674. package/optional-skills/research/osint-investigation/scripts/fetch_senate_ld.py +146 -0
  675. package/optional-skills/research/osint-investigation/scripts/fetch_usaspending.py +170 -0
  676. package/optional-skills/research/osint-investigation/scripts/fetch_wayback.py +142 -0
  677. package/optional-skills/research/osint-investigation/scripts/fetch_wikipedia.py +267 -0
  678. package/optional-skills/research/osint-investigation/scripts/timing_analysis.py +253 -0
  679. package/optional-skills/research/osint-investigation/templates/source-template.md +59 -0
  680. package/optional-skills/research/parallel-cli/SKILL.md +391 -0
  681. package/optional-skills/research/qmd/SKILL.md +441 -0
  682. package/optional-skills/research/scrapling/SKILL.md +336 -0
  683. package/optional-skills/research/searxng-search/SKILL.md +212 -0
  684. package/optional-skills/research/searxng-search/scripts/searxng.sh +22 -0
  685. package/optional-skills/security/1password/SKILL.md +163 -0
  686. package/optional-skills/security/1password/references/cli-examples.md +31 -0
  687. package/optional-skills/security/1password/references/get-started.md +21 -0
  688. package/optional-skills/security/DESCRIPTION.md +3 -0
  689. package/optional-skills/security/oss-forensics/SKILL.md +423 -0
  690. package/optional-skills/security/oss-forensics/references/evidence-types.md +89 -0
  691. package/optional-skills/security/oss-forensics/references/github-archive-guide.md +184 -0
  692. package/optional-skills/security/oss-forensics/references/investigation-templates.md +131 -0
  693. package/optional-skills/security/oss-forensics/references/recovery-techniques.md +164 -0
  694. package/optional-skills/security/oss-forensics/scripts/evidence-store.py +313 -0
  695. package/optional-skills/security/oss-forensics/templates/forensic-report.md +151 -0
  696. package/optional-skills/security/oss-forensics/templates/malicious-package-report.md +43 -0
  697. package/optional-skills/security/sherlock/SKILL.md +193 -0
  698. package/optional-skills/software-development/rest-graphql-debug/SKILL.md +514 -0
  699. package/optional-skills/web-development/DESCRIPTION.md +5 -0
  700. package/optional-skills/web-development/page-agent/SKILL.md +190 -0
  701. package/package.json +78 -0
  702. package/plugins/__init__.py +1 -0
  703. package/plugins/__pycache__/__init__.cpython-312.pyc +0 -0
  704. package/plugins/context_engine/__init__.py +219 -0
  705. package/plugins/disk-cleanup/README.md +51 -0
  706. package/plugins/disk-cleanup/__init__.py +316 -0
  707. package/plugins/disk-cleanup/disk_cleanup.py +497 -0
  708. package/plugins/disk-cleanup/plugin.yaml +7 -0
  709. package/plugins/example-dashboard/dashboard/manifest.json +14 -0
  710. package/plugins/example-dashboard/dashboard/plugin_api.py +17 -0
  711. package/plugins/google_meet/README.md +131 -0
  712. package/plugins/google_meet/SKILL.md +148 -0
  713. package/plugins/google_meet/__init__.py +103 -0
  714. package/plugins/google_meet/audio_bridge.py +244 -0
  715. package/plugins/google_meet/cli.py +479 -0
  716. package/plugins/google_meet/meet_bot.py +852 -0
  717. package/plugins/google_meet/node/__init__.py +54 -0
  718. package/plugins/google_meet/node/cli.py +125 -0
  719. package/plugins/google_meet/node/client.py +107 -0
  720. package/plugins/google_meet/node/protocol.py +124 -0
  721. package/plugins/google_meet/node/registry.py +113 -0
  722. package/plugins/google_meet/node/server.py +201 -0
  723. package/plugins/google_meet/plugin.yaml +16 -0
  724. package/plugins/google_meet/process_manager.py +324 -0
  725. package/plugins/google_meet/realtime/__init__.py +10 -0
  726. package/plugins/google_meet/realtime/openai_client.py +332 -0
  727. package/plugins/google_meet/tools.py +348 -0
  728. package/plugins/hermes-achievements/LICENSE +21 -0
  729. package/plugins/hermes-achievements/README.md +150 -0
  730. package/plugins/hermes-achievements/dashboard/dist/index.js +732 -0
  731. package/plugins/hermes-achievements/dashboard/dist/style.css +146 -0
  732. package/plugins/hermes-achievements/dashboard/manifest.json +11 -0
  733. package/plugins/hermes-achievements/dashboard/plugin_api.py +1062 -0
  734. package/plugins/hermes-achievements/docs/achievements-performance-implementation-plan.md +157 -0
  735. package/plugins/hermes-achievements/docs/achievements-performance-implementation-spec.md +219 -0
  736. package/plugins/hermes-achievements/docs/achievements-performance-spec.md +174 -0
  737. package/plugins/hermes-achievements/docs/assets/achievements-dashboard-hd.png +0 -0
  738. package/plugins/hermes-achievements/docs/assets/achievements-tier-showcase-hd.png +0 -0
  739. package/plugins/hermes-achievements/tests/test_achievement_engine.py +156 -0
  740. package/plugins/image_gen/openai/__init__.py +303 -0
  741. package/plugins/image_gen/openai/__pycache__/__init__.cpython-312.pyc +0 -0
  742. package/plugins/image_gen/openai/plugin.yaml +7 -0
  743. package/plugins/image_gen/openai-codex/__init__.py +378 -0
  744. package/plugins/image_gen/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  745. package/plugins/image_gen/openai-codex/plugin.yaml +5 -0
  746. package/plugins/image_gen/xai/__init__.py +316 -0
  747. package/plugins/image_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  748. package/plugins/image_gen/xai/plugin.yaml +7 -0
  749. package/plugins/kanban/dashboard/dist/index.js +3143 -0
  750. package/plugins/kanban/dashboard/dist/style.css +1500 -0
  751. package/plugins/kanban/dashboard/manifest.json +14 -0
  752. package/plugins/kanban/dashboard/plugin_api.py +1612 -0
  753. package/plugins/kanban/systemd/hermes-kanban-dispatcher.service +32 -0
  754. package/plugins/memory/__init__.py +408 -0
  755. package/plugins/memory/byterover/README.md +41 -0
  756. package/plugins/memory/byterover/__init__.py +384 -0
  757. package/plugins/memory/byterover/plugin.yaml +9 -0
  758. package/plugins/memory/hindsight/README.md +138 -0
  759. package/plugins/memory/hindsight/__init__.py +1758 -0
  760. package/plugins/memory/hindsight/plugin.yaml +8 -0
  761. package/plugins/memory/holographic/README.md +36 -0
  762. package/plugins/memory/holographic/__init__.py +409 -0
  763. package/plugins/memory/holographic/holographic.py +203 -0
  764. package/plugins/memory/holographic/plugin.yaml +5 -0
  765. package/plugins/memory/holographic/retrieval.py +593 -0
  766. package/plugins/memory/holographic/store.py +579 -0
  767. package/plugins/memory/honcho/README.md +328 -0
  768. package/plugins/memory/honcho/__init__.py +1329 -0
  769. package/plugins/memory/honcho/cli.py +1452 -0
  770. package/plugins/memory/honcho/client.py +784 -0
  771. package/plugins/memory/honcho/plugin.yaml +7 -0
  772. package/plugins/memory/honcho/session.py +1255 -0
  773. package/plugins/memory/mem0/README.md +38 -0
  774. package/plugins/memory/mem0/__init__.py +374 -0
  775. package/plugins/memory/mem0/plugin.yaml +5 -0
  776. package/plugins/memory/openviking/README.md +40 -0
  777. package/plugins/memory/openviking/__init__.py +945 -0
  778. package/plugins/memory/openviking/plugin.yaml +9 -0
  779. package/plugins/memory/retaindb/README.md +40 -0
  780. package/plugins/memory/retaindb/__init__.py +767 -0
  781. package/plugins/memory/retaindb/plugin.yaml +7 -0
  782. package/plugins/memory/supermemory/README.md +99 -0
  783. package/plugins/memory/supermemory/__init__.py +792 -0
  784. package/plugins/memory/supermemory/plugin.yaml +5 -0
  785. package/plugins/model-providers/README.md +70 -0
  786. package/plugins/model-providers/ai-gateway/__init__.py +43 -0
  787. package/plugins/model-providers/ai-gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  788. package/plugins/model-providers/ai-gateway/plugin.yaml +5 -0
  789. package/plugins/model-providers/alibaba/__init__.py +13 -0
  790. package/plugins/model-providers/alibaba/__pycache__/__init__.cpython-312.pyc +0 -0
  791. package/plugins/model-providers/alibaba/plugin.yaml +5 -0
  792. package/plugins/model-providers/alibaba-coding-plan/__init__.py +21 -0
  793. package/plugins/model-providers/alibaba-coding-plan/__pycache__/__init__.cpython-312.pyc +0 -0
  794. package/plugins/model-providers/alibaba-coding-plan/plugin.yaml +5 -0
  795. package/plugins/model-providers/anthropic/__init__.py +52 -0
  796. package/plugins/model-providers/anthropic/__pycache__/__init__.cpython-312.pyc +0 -0
  797. package/plugins/model-providers/anthropic/plugin.yaml +5 -0
  798. package/plugins/model-providers/arcee/__init__.py +13 -0
  799. package/plugins/model-providers/arcee/__pycache__/__init__.cpython-312.pyc +0 -0
  800. package/plugins/model-providers/arcee/plugin.yaml +5 -0
  801. package/plugins/model-providers/azure-foundry/__init__.py +21 -0
  802. package/plugins/model-providers/azure-foundry/__pycache__/__init__.cpython-312.pyc +0 -0
  803. package/plugins/model-providers/azure-foundry/plugin.yaml +5 -0
  804. package/plugins/model-providers/bedrock/__init__.py +29 -0
  805. package/plugins/model-providers/bedrock/__pycache__/__init__.cpython-312.pyc +0 -0
  806. package/plugins/model-providers/bedrock/plugin.yaml +5 -0
  807. package/plugins/model-providers/copilot/__init__.py +58 -0
  808. package/plugins/model-providers/copilot/__pycache__/__init__.cpython-312.pyc +0 -0
  809. package/plugins/model-providers/copilot/plugin.yaml +5 -0
  810. package/plugins/model-providers/copilot-acp/__init__.py +34 -0
  811. package/plugins/model-providers/copilot-acp/__pycache__/__init__.cpython-312.pyc +0 -0
  812. package/plugins/model-providers/copilot-acp/plugin.yaml +5 -0
  813. package/plugins/model-providers/custom/__init__.py +68 -0
  814. package/plugins/model-providers/custom/__pycache__/__init__.cpython-312.pyc +0 -0
  815. package/plugins/model-providers/custom/plugin.yaml +5 -0
  816. package/plugins/model-providers/deepseek/__init__.py +99 -0
  817. package/plugins/model-providers/deepseek/__pycache__/__init__.cpython-312.pyc +0 -0
  818. package/plugins/model-providers/deepseek/plugin.yaml +5 -0
  819. package/plugins/model-providers/gemini/__init__.py +72 -0
  820. package/plugins/model-providers/gemini/__pycache__/__init__.cpython-312.pyc +0 -0
  821. package/plugins/model-providers/gemini/plugin.yaml +5 -0
  822. package/plugins/model-providers/gmi/__init__.py +31 -0
  823. package/plugins/model-providers/gmi/__pycache__/__init__.cpython-312.pyc +0 -0
  824. package/plugins/model-providers/gmi/plugin.yaml +5 -0
  825. package/plugins/model-providers/huggingface/__init__.py +20 -0
  826. package/plugins/model-providers/huggingface/__pycache__/__init__.cpython-312.pyc +0 -0
  827. package/plugins/model-providers/huggingface/plugin.yaml +5 -0
  828. package/plugins/model-providers/kilocode/__init__.py +14 -0
  829. package/plugins/model-providers/kilocode/__pycache__/__init__.cpython-312.pyc +0 -0
  830. package/plugins/model-providers/kilocode/plugin.yaml +5 -0
  831. package/plugins/model-providers/kimi-coding/__init__.py +71 -0
  832. package/plugins/model-providers/kimi-coding/__pycache__/__init__.cpython-312.pyc +0 -0
  833. package/plugins/model-providers/kimi-coding/plugin.yaml +5 -0
  834. package/plugins/model-providers/minimax/__init__.py +45 -0
  835. package/plugins/model-providers/minimax/__pycache__/__init__.cpython-312.pyc +0 -0
  836. package/plugins/model-providers/minimax/plugin.yaml +5 -0
  837. package/plugins/model-providers/nous/__init__.py +54 -0
  838. package/plugins/model-providers/nous/__pycache__/__init__.cpython-312.pyc +0 -0
  839. package/plugins/model-providers/nous/plugin.yaml +5 -0
  840. package/plugins/model-providers/novita/__init__.py +27 -0
  841. package/plugins/model-providers/novita/__pycache__/__init__.cpython-312.pyc +0 -0
  842. package/plugins/model-providers/novita/plugin.yaml +5 -0
  843. package/plugins/model-providers/nvidia/__init__.py +21 -0
  844. package/plugins/model-providers/nvidia/__pycache__/__init__.cpython-312.pyc +0 -0
  845. package/plugins/model-providers/nvidia/plugin.yaml +5 -0
  846. package/plugins/model-providers/ollama-cloud/__init__.py +14 -0
  847. package/plugins/model-providers/ollama-cloud/__pycache__/__init__.cpython-312.pyc +0 -0
  848. package/plugins/model-providers/ollama-cloud/plugin.yaml +5 -0
  849. package/plugins/model-providers/openai-codex/__init__.py +15 -0
  850. package/plugins/model-providers/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  851. package/plugins/model-providers/openai-codex/plugin.yaml +5 -0
  852. package/plugins/model-providers/opencode-zen/__init__.py +30 -0
  853. package/plugins/model-providers/opencode-zen/__pycache__/__init__.cpython-312.pyc +0 -0
  854. package/plugins/model-providers/opencode-zen/plugin.yaml +5 -0
  855. package/plugins/model-providers/openrouter/__init__.py +115 -0
  856. package/plugins/model-providers/openrouter/__pycache__/__init__.cpython-312.pyc +0 -0
  857. package/plugins/model-providers/openrouter/plugin.yaml +5 -0
  858. package/plugins/model-providers/qwen-oauth/__init__.py +82 -0
  859. package/plugins/model-providers/qwen-oauth/__pycache__/__init__.cpython-312.pyc +0 -0
  860. package/plugins/model-providers/qwen-oauth/plugin.yaml +5 -0
  861. package/plugins/model-providers/stepfun/__init__.py +14 -0
  862. package/plugins/model-providers/stepfun/__pycache__/__init__.cpython-312.pyc +0 -0
  863. package/plugins/model-providers/stepfun/plugin.yaml +5 -0
  864. package/plugins/model-providers/xai/__init__.py +15 -0
  865. package/plugins/model-providers/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  866. package/plugins/model-providers/xai/plugin.yaml +5 -0
  867. package/plugins/model-providers/xiaomi/__init__.py +14 -0
  868. package/plugins/model-providers/xiaomi/__pycache__/__init__.cpython-312.pyc +0 -0
  869. package/plugins/model-providers/xiaomi/plugin.yaml +5 -0
  870. package/plugins/model-providers/zai/__init__.py +21 -0
  871. package/plugins/model-providers/zai/__pycache__/__init__.cpython-312.pyc +0 -0
  872. package/plugins/model-providers/zai/plugin.yaml +5 -0
  873. package/plugins/observability/langfuse/README.md +53 -0
  874. package/plugins/observability/langfuse/__init__.py +1004 -0
  875. package/plugins/observability/langfuse/plugin.yaml +14 -0
  876. package/plugins/platforms/google_chat/__init__.py +3 -0
  877. package/plugins/platforms/google_chat/__pycache__/__init__.cpython-312.pyc +0 -0
  878. package/plugins/platforms/google_chat/__pycache__/adapter.cpython-312.pyc +0 -0
  879. package/plugins/platforms/google_chat/adapter.py +3343 -0
  880. package/plugins/platforms/google_chat/oauth.py +639 -0
  881. package/plugins/platforms/google_chat/plugin.yaml +39 -0
  882. package/plugins/platforms/irc/__init__.py +3 -0
  883. package/plugins/platforms/irc/__pycache__/__init__.cpython-312.pyc +0 -0
  884. package/plugins/platforms/irc/__pycache__/adapter.cpython-312.pyc +0 -0
  885. package/plugins/platforms/irc/adapter.py +969 -0
  886. package/plugins/platforms/irc/plugin.yaml +54 -0
  887. package/plugins/platforms/line/__init__.py +3 -0
  888. package/plugins/platforms/line/__pycache__/__init__.cpython-312.pyc +0 -0
  889. package/plugins/platforms/line/__pycache__/adapter.cpython-312.pyc +0 -0
  890. package/plugins/platforms/line/adapter.py +1639 -0
  891. package/plugins/platforms/line/plugin.yaml +65 -0
  892. package/plugins/platforms/simplex/__init__.py +3 -0
  893. package/plugins/platforms/simplex/__pycache__/__init__.cpython-312.pyc +0 -0
  894. package/plugins/platforms/simplex/__pycache__/adapter.cpython-312.pyc +0 -0
  895. package/plugins/platforms/simplex/adapter.py +746 -0
  896. package/plugins/platforms/simplex/plugin.yaml +37 -0
  897. package/plugins/platforms/teams/__init__.py +3 -0
  898. package/plugins/platforms/teams/__pycache__/__init__.cpython-312.pyc +0 -0
  899. package/plugins/platforms/teams/__pycache__/adapter.cpython-312.pyc +0 -0
  900. package/plugins/platforms/teams/adapter.py +1188 -0
  901. package/plugins/platforms/teams/plugin.yaml +48 -0
  902. package/plugins/spotify/__init__.py +66 -0
  903. package/plugins/spotify/__pycache__/__init__.cpython-312.pyc +0 -0
  904. package/plugins/spotify/__pycache__/client.cpython-312.pyc +0 -0
  905. package/plugins/spotify/__pycache__/tools.cpython-312.pyc +0 -0
  906. package/plugins/spotify/client.py +435 -0
  907. package/plugins/spotify/plugin.yaml +13 -0
  908. package/plugins/spotify/tools.py +454 -0
  909. package/plugins/teams_pipeline/__init__.py +23 -0
  910. package/plugins/teams_pipeline/cli.py +463 -0
  911. package/plugins/teams_pipeline/meetings.py +333 -0
  912. package/plugins/teams_pipeline/models.py +350 -0
  913. package/plugins/teams_pipeline/pipeline.py +692 -0
  914. package/plugins/teams_pipeline/plugin.yaml +9 -0
  915. package/plugins/teams_pipeline/runtime.py +135 -0
  916. package/plugins/teams_pipeline/store.py +194 -0
  917. package/plugins/teams_pipeline/subscriptions.py +249 -0
  918. package/plugins/video_gen/fal/__init__.py +523 -0
  919. package/plugins/video_gen/fal/__pycache__/__init__.cpython-312.pyc +0 -0
  920. package/plugins/video_gen/fal/plugin.yaml +7 -0
  921. package/plugins/video_gen/xai/__init__.py +441 -0
  922. package/plugins/video_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  923. package/plugins/video_gen/xai/plugin.yaml +7 -0
  924. package/plugins/web/__init__.py +7 -0
  925. package/plugins/web/__pycache__/__init__.cpython-312.pyc +0 -0
  926. package/plugins/web/brave_free/__init__.py +14 -0
  927. package/plugins/web/brave_free/__pycache__/__init__.cpython-312.pyc +0 -0
  928. package/plugins/web/brave_free/__pycache__/provider.cpython-312.pyc +0 -0
  929. package/plugins/web/brave_free/plugin.yaml +7 -0
  930. package/plugins/web/brave_free/provider.py +137 -0
  931. package/plugins/web/ddgs/__init__.py +15 -0
  932. package/plugins/web/ddgs/__pycache__/__init__.cpython-312.pyc +0 -0
  933. package/plugins/web/ddgs/__pycache__/provider.cpython-312.pyc +0 -0
  934. package/plugins/web/ddgs/plugin.yaml +7 -0
  935. package/plugins/web/ddgs/provider.py +104 -0
  936. package/plugins/web/exa/__init__.py +15 -0
  937. package/plugins/web/exa/__pycache__/__init__.cpython-312.pyc +0 -0
  938. package/plugins/web/exa/__pycache__/provider.cpython-312.pyc +0 -0
  939. package/plugins/web/exa/plugin.yaml +7 -0
  940. package/plugins/web/exa/provider.py +212 -0
  941. package/plugins/web/firecrawl/__init__.py +28 -0
  942. package/plugins/web/firecrawl/__pycache__/__init__.cpython-312.pyc +0 -0
  943. package/plugins/web/firecrawl/__pycache__/provider.cpython-312.pyc +0 -0
  944. package/plugins/web/firecrawl/plugin.yaml +7 -0
  945. package/plugins/web/firecrawl/provider.py +773 -0
  946. package/plugins/web/parallel/__init__.py +16 -0
  947. package/plugins/web/parallel/__pycache__/__init__.cpython-312.pyc +0 -0
  948. package/plugins/web/parallel/__pycache__/provider.cpython-312.pyc +0 -0
  949. package/plugins/web/parallel/plugin.yaml +7 -0
  950. package/plugins/web/parallel/provider.py +291 -0
  951. package/plugins/web/searxng/__init__.py +15 -0
  952. package/plugins/web/searxng/__pycache__/__init__.cpython-312.pyc +0 -0
  953. package/plugins/web/searxng/__pycache__/provider.cpython-312.pyc +0 -0
  954. package/plugins/web/searxng/plugin.yaml +7 -0
  955. package/plugins/web/searxng/provider.py +140 -0
  956. package/plugins/web/tavily/__init__.py +15 -0
  957. package/plugins/web/tavily/__pycache__/__init__.cpython-312.pyc +0 -0
  958. package/plugins/web/tavily/__pycache__/provider.cpython-312.pyc +0 -0
  959. package/plugins/web/tavily/plugin.yaml +7 -0
  960. package/plugins/web/tavily/provider.py +285 -0
  961. package/providers/README.md +78 -0
  962. package/providers/__init__.py +192 -0
  963. package/providers/__pycache__/__init__.cpython-312.pyc +0 -0
  964. package/providers/__pycache__/base.cpython-312.pyc +0 -0
  965. package/providers/base.py +184 -0
  966. package/pyproject.toml +255 -0
  967. package/run_agent.py +16409 -0
  968. package/scripts/benchmark_browser_eval.py +138 -0
  969. package/scripts/build_model_catalog.py +95 -0
  970. package/scripts/build_skills_index.py +325 -0
  971. package/scripts/check-windows-footguns.py +624 -0
  972. package/scripts/contributor_audit.py +473 -0
  973. package/scripts/discord-voice-doctor.py +396 -0
  974. package/scripts/hermes-gateway +416 -0
  975. package/scripts/install.cmd +28 -0
  976. package/scripts/install.ps1 +1611 -0
  977. package/scripts/install.sh +2007 -0
  978. package/scripts/install_psutil_android.py +117 -0
  979. package/scripts/keystroke_diagnostic.py +81 -0
  980. package/scripts/kill_modal.sh +34 -0
  981. package/scripts/lib/node-bootstrap.sh +238 -0
  982. package/scripts/lint_diff.py +207 -0
  983. package/scripts/postinstall.js +150 -0
  984. package/scripts/profile-tui.py +626 -0
  985. package/scripts/release.py +1680 -0
  986. package/scripts/run_tests.sh +129 -0
  987. package/scripts/sample_and_compress.py +409 -0
  988. package/scripts/setup_open_webui.sh +349 -0
  989. package/scripts/whatsapp-bridge/allowlist.js +88 -0
  990. package/scripts/whatsapp-bridge/allowlist.test.mjs +80 -0
  991. package/scripts/whatsapp-bridge/bridge.js +729 -0
  992. package/scripts/whatsapp-bridge/package-lock.json +2141 -0
  993. package/scripts/whatsapp-bridge/package.json +19 -0
  994. package/skills/apple/DESCRIPTION.md +2 -0
  995. package/skills/apple/apple-notes/SKILL.md +90 -0
  996. package/skills/apple/apple-reminders/SKILL.md +98 -0
  997. package/skills/apple/findmy/SKILL.md +131 -0
  998. package/skills/apple/imessage/SKILL.md +102 -0
  999. package/skills/apple/macos-computer-use/SKILL.md +201 -0
  1000. package/skills/autonomous-ai-agents/DESCRIPTION.md +3 -0
  1001. package/skills/autonomous-ai-agents/claude-code/SKILL.md +745 -0
  1002. package/skills/autonomous-ai-agents/codex/SKILL.md +130 -0
  1003. package/skills/autonomous-ai-agents/hermes-agent/SKILL.md +1014 -0
  1004. package/skills/autonomous-ai-agents/opencode/SKILL.md +219 -0
  1005. package/skills/creative/DESCRIPTION.md +3 -0
  1006. package/skills/creative/architecture-diagram/SKILL.md +148 -0
  1007. package/skills/creative/architecture-diagram/templates/template.html +319 -0
  1008. package/skills/creative/ascii-art/SKILL.md +322 -0
  1009. package/skills/creative/ascii-video/README.md +290 -0
  1010. package/skills/creative/ascii-video/SKILL.md +241 -0
  1011. package/skills/creative/ascii-video/references/architecture.md +802 -0
  1012. package/skills/creative/ascii-video/references/composition.md +892 -0
  1013. package/skills/creative/ascii-video/references/effects.md +1865 -0
  1014. package/skills/creative/ascii-video/references/inputs.md +685 -0
  1015. package/skills/creative/ascii-video/references/optimization.md +688 -0
  1016. package/skills/creative/ascii-video/references/scenes.md +1011 -0
  1017. package/skills/creative/ascii-video/references/shaders.md +1385 -0
  1018. package/skills/creative/ascii-video/references/troubleshooting.md +367 -0
  1019. package/skills/creative/baoyu-comic/PORT_NOTES.md +77 -0
  1020. package/skills/creative/baoyu-comic/SKILL.md +247 -0
  1021. package/skills/creative/baoyu-comic/references/analysis-framework.md +176 -0
  1022. package/skills/creative/baoyu-comic/references/art-styles/chalk.md +101 -0
  1023. package/skills/creative/baoyu-comic/references/art-styles/ink-brush.md +97 -0
  1024. package/skills/creative/baoyu-comic/references/art-styles/ligne-claire.md +75 -0
  1025. package/skills/creative/baoyu-comic/references/art-styles/manga.md +93 -0
  1026. package/skills/creative/baoyu-comic/references/art-styles/minimalist.md +84 -0
  1027. package/skills/creative/baoyu-comic/references/art-styles/realistic.md +89 -0
  1028. package/skills/creative/baoyu-comic/references/auto-selection.md +71 -0
  1029. package/skills/creative/baoyu-comic/references/base-prompt.md +98 -0
  1030. package/skills/creative/baoyu-comic/references/character-template.md +180 -0
  1031. package/skills/creative/baoyu-comic/references/layouts/cinematic.md +23 -0
  1032. package/skills/creative/baoyu-comic/references/layouts/dense.md +23 -0
  1033. package/skills/creative/baoyu-comic/references/layouts/four-panel.md +40 -0
  1034. package/skills/creative/baoyu-comic/references/layouts/mixed.md +23 -0
  1035. package/skills/creative/baoyu-comic/references/layouts/splash.md +23 -0
  1036. package/skills/creative/baoyu-comic/references/layouts/standard.md +23 -0
  1037. package/skills/creative/baoyu-comic/references/layouts/webtoon.md +30 -0
  1038. package/skills/creative/baoyu-comic/references/ohmsha-guide.md +85 -0
  1039. package/skills/creative/baoyu-comic/references/partial-workflows.md +106 -0
  1040. package/skills/creative/baoyu-comic/references/presets/concept-story.md +121 -0
  1041. package/skills/creative/baoyu-comic/references/presets/four-panel.md +107 -0
  1042. package/skills/creative/baoyu-comic/references/presets/ohmsha.md +114 -0
  1043. package/skills/creative/baoyu-comic/references/presets/shoujo.md +116 -0
  1044. package/skills/creative/baoyu-comic/references/presets/wuxia.md +110 -0
  1045. package/skills/creative/baoyu-comic/references/storyboard-template.md +143 -0
  1046. package/skills/creative/baoyu-comic/references/tones/action.md +110 -0
  1047. package/skills/creative/baoyu-comic/references/tones/dramatic.md +95 -0
  1048. package/skills/creative/baoyu-comic/references/tones/energetic.md +105 -0
  1049. package/skills/creative/baoyu-comic/references/tones/neutral.md +63 -0
  1050. package/skills/creative/baoyu-comic/references/tones/romantic.md +100 -0
  1051. package/skills/creative/baoyu-comic/references/tones/vintage.md +104 -0
  1052. package/skills/creative/baoyu-comic/references/tones/warm.md +94 -0
  1053. package/skills/creative/baoyu-comic/references/workflow.md +401 -0
  1054. package/skills/creative/baoyu-infographic/PORT_NOTES.md +43 -0
  1055. package/skills/creative/baoyu-infographic/SKILL.md +237 -0
  1056. package/skills/creative/baoyu-infographic/references/analysis-framework.md +182 -0
  1057. package/skills/creative/baoyu-infographic/references/base-prompt.md +43 -0
  1058. package/skills/creative/baoyu-infographic/references/layouts/bento-grid.md +41 -0
  1059. package/skills/creative/baoyu-infographic/references/layouts/binary-comparison.md +48 -0
  1060. package/skills/creative/baoyu-infographic/references/layouts/bridge.md +41 -0
  1061. package/skills/creative/baoyu-infographic/references/layouts/circular-flow.md +41 -0
  1062. package/skills/creative/baoyu-infographic/references/layouts/comic-strip.md +41 -0
  1063. package/skills/creative/baoyu-infographic/references/layouts/comparison-matrix.md +41 -0
  1064. package/skills/creative/baoyu-infographic/references/layouts/dashboard.md +41 -0
  1065. package/skills/creative/baoyu-infographic/references/layouts/dense-modules.md +72 -0
  1066. package/skills/creative/baoyu-infographic/references/layouts/funnel.md +41 -0
  1067. package/skills/creative/baoyu-infographic/references/layouts/hierarchical-layers.md +48 -0
  1068. package/skills/creative/baoyu-infographic/references/layouts/hub-spoke.md +41 -0
  1069. package/skills/creative/baoyu-infographic/references/layouts/iceberg.md +41 -0
  1070. package/skills/creative/baoyu-infographic/references/layouts/isometric-map.md +41 -0
  1071. package/skills/creative/baoyu-infographic/references/layouts/jigsaw.md +41 -0
  1072. package/skills/creative/baoyu-infographic/references/layouts/linear-progression.md +48 -0
  1073. package/skills/creative/baoyu-infographic/references/layouts/periodic-table.md +41 -0
  1074. package/skills/creative/baoyu-infographic/references/layouts/story-mountain.md +41 -0
  1075. package/skills/creative/baoyu-infographic/references/layouts/structural-breakdown.md +48 -0
  1076. package/skills/creative/baoyu-infographic/references/layouts/tree-branching.md +41 -0
  1077. package/skills/creative/baoyu-infographic/references/layouts/venn-diagram.md +41 -0
  1078. package/skills/creative/baoyu-infographic/references/layouts/winding-roadmap.md +41 -0
  1079. package/skills/creative/baoyu-infographic/references/structured-content-template.md +244 -0
  1080. package/skills/creative/baoyu-infographic/references/styles/aged-academia.md +36 -0
  1081. package/skills/creative/baoyu-infographic/references/styles/bold-graphic.md +36 -0
  1082. package/skills/creative/baoyu-infographic/references/styles/chalkboard.md +61 -0
  1083. package/skills/creative/baoyu-infographic/references/styles/claymation.md +29 -0
  1084. package/skills/creative/baoyu-infographic/references/styles/corporate-memphis.md +29 -0
  1085. package/skills/creative/baoyu-infographic/references/styles/craft-handmade.md +44 -0
  1086. package/skills/creative/baoyu-infographic/references/styles/cyberpunk-neon.md +29 -0
  1087. package/skills/creative/baoyu-infographic/references/styles/hand-drawn-edu.md +63 -0
  1088. package/skills/creative/baoyu-infographic/references/styles/ikea-manual.md +29 -0
  1089. package/skills/creative/baoyu-infographic/references/styles/kawaii.md +29 -0
  1090. package/skills/creative/baoyu-infographic/references/styles/knolling.md +29 -0
  1091. package/skills/creative/baoyu-infographic/references/styles/lego-brick.md +29 -0
  1092. package/skills/creative/baoyu-infographic/references/styles/morandi-journal.md +60 -0
  1093. package/skills/creative/baoyu-infographic/references/styles/origami.md +29 -0
  1094. package/skills/creative/baoyu-infographic/references/styles/pixel-art.md +29 -0
  1095. package/skills/creative/baoyu-infographic/references/styles/pop-laboratory.md +48 -0
  1096. package/skills/creative/baoyu-infographic/references/styles/retro-pop-grid.md +47 -0
  1097. package/skills/creative/baoyu-infographic/references/styles/storybook-watercolor.md +29 -0
  1098. package/skills/creative/baoyu-infographic/references/styles/subway-map.md +29 -0
  1099. package/skills/creative/baoyu-infographic/references/styles/technical-schematic.md +36 -0
  1100. package/skills/creative/baoyu-infographic/references/styles/ui-wireframe.md +29 -0
  1101. package/skills/creative/claude-design/SKILL.md +591 -0
  1102. package/skills/creative/comfyui/SKILL.md +612 -0
  1103. package/skills/creative/comfyui/references/official-cli.md +255 -0
  1104. package/skills/creative/comfyui/references/rest-api.md +312 -0
  1105. package/skills/creative/comfyui/references/template-integrity.md +243 -0
  1106. package/skills/creative/comfyui/references/workflow-format.md +226 -0
  1107. package/skills/creative/comfyui/scripts/_common.py +835 -0
  1108. package/skills/creative/comfyui/scripts/auto_fix_deps.py +225 -0
  1109. package/skills/creative/comfyui/scripts/check_deps.py +437 -0
  1110. package/skills/creative/comfyui/scripts/comfyui_setup.sh +286 -0
  1111. package/skills/creative/comfyui/scripts/extract_schema.py +315 -0
  1112. package/skills/creative/comfyui/scripts/fetch_logs.py +158 -0
  1113. package/skills/creative/comfyui/scripts/hardware_check.py +497 -0
  1114. package/skills/creative/comfyui/scripts/health_check.py +223 -0
  1115. package/skills/creative/comfyui/scripts/run_batch.py +243 -0
  1116. package/skills/creative/comfyui/scripts/run_workflow.py +796 -0
  1117. package/skills/creative/comfyui/scripts/ws_monitor.py +267 -0
  1118. package/skills/creative/comfyui/tests/README.md +50 -0
  1119. package/skills/creative/comfyui/tests/conftest.py +64 -0
  1120. package/skills/creative/comfyui/tests/pytest.ini +5 -0
  1121. package/skills/creative/comfyui/tests/test_check_deps.py +68 -0
  1122. package/skills/creative/comfyui/tests/test_cloud_integration.py +95 -0
  1123. package/skills/creative/comfyui/tests/test_common.py +447 -0
  1124. package/skills/creative/comfyui/tests/test_extract_schema.py +185 -0
  1125. package/skills/creative/comfyui/tests/test_run_workflow.py +213 -0
  1126. package/skills/creative/comfyui/workflows/README.md +86 -0
  1127. package/skills/creative/comfyui/workflows/animatediff_video.json +64 -0
  1128. package/skills/creative/comfyui/workflows/flux_dev_txt2img.json +78 -0
  1129. package/skills/creative/comfyui/workflows/sd15_txt2img.json +49 -0
  1130. package/skills/creative/comfyui/workflows/sdxl_img2img.json +54 -0
  1131. package/skills/creative/comfyui/workflows/sdxl_inpaint.json +59 -0
  1132. package/skills/creative/comfyui/workflows/sdxl_txt2img.json +49 -0
  1133. package/skills/creative/comfyui/workflows/upscale_4x.json +27 -0
  1134. package/skills/creative/comfyui/workflows/wan_video_t2v.json +69 -0
  1135. package/skills/creative/creative-ideation/SKILL.md +152 -0
  1136. package/skills/creative/creative-ideation/references/full-prompt-library.md +110 -0
  1137. package/skills/creative/design-md/SKILL.md +199 -0
  1138. package/skills/creative/design-md/templates/starter.md +99 -0
  1139. package/skills/creative/excalidraw/SKILL.md +199 -0
  1140. package/skills/creative/excalidraw/references/colors.md +44 -0
  1141. package/skills/creative/excalidraw/references/dark-mode.md +68 -0
  1142. package/skills/creative/excalidraw/references/examples.md +141 -0
  1143. package/skills/creative/excalidraw/scripts/upload.py +133 -0
  1144. package/skills/creative/humanizer/LICENSE +21 -0
  1145. package/skills/creative/humanizer/SKILL.md +578 -0
  1146. package/skills/creative/manim-video/README.md +23 -0
  1147. package/skills/creative/manim-video/SKILL.md +269 -0
  1148. package/skills/creative/manim-video/references/animation-design-thinking.md +161 -0
  1149. package/skills/creative/manim-video/references/animations.md +282 -0
  1150. package/skills/creative/manim-video/references/camera-and-3d.md +135 -0
  1151. package/skills/creative/manim-video/references/decorations.md +202 -0
  1152. package/skills/creative/manim-video/references/equations.md +216 -0
  1153. package/skills/creative/manim-video/references/graphs-and-data.md +163 -0
  1154. package/skills/creative/manim-video/references/mobjects.md +333 -0
  1155. package/skills/creative/manim-video/references/paper-explainer.md +255 -0
  1156. package/skills/creative/manim-video/references/production-quality.md +190 -0
  1157. package/skills/creative/manim-video/references/rendering.md +185 -0
  1158. package/skills/creative/manim-video/references/scene-planning.md +118 -0
  1159. package/skills/creative/manim-video/references/troubleshooting.md +135 -0
  1160. package/skills/creative/manim-video/references/updaters-and-trackers.md +260 -0
  1161. package/skills/creative/manim-video/references/visual-design.md +124 -0
  1162. package/skills/creative/manim-video/scripts/setup.sh +14 -0
  1163. package/skills/creative/p5js/README.md +64 -0
  1164. package/skills/creative/p5js/SKILL.md +556 -0
  1165. package/skills/creative/p5js/references/animation.md +439 -0
  1166. package/skills/creative/p5js/references/color-systems.md +352 -0
  1167. package/skills/creative/p5js/references/core-api.md +410 -0
  1168. package/skills/creative/p5js/references/export-pipeline.md +566 -0
  1169. package/skills/creative/p5js/references/interaction.md +398 -0
  1170. package/skills/creative/p5js/references/shapes-and-geometry.md +300 -0
  1171. package/skills/creative/p5js/references/troubleshooting.md +532 -0
  1172. package/skills/creative/p5js/references/typography.md +302 -0
  1173. package/skills/creative/p5js/references/visual-effects.md +895 -0
  1174. package/skills/creative/p5js/references/webgl-and-3d.md +423 -0
  1175. package/skills/creative/p5js/scripts/export-frames.js +179 -0
  1176. package/skills/creative/p5js/scripts/render.sh +108 -0
  1177. package/skills/creative/p5js/scripts/serve.sh +28 -0
  1178. package/skills/creative/p5js/scripts/setup.sh +87 -0
  1179. package/skills/creative/p5js/templates/viewer.html +395 -0
  1180. package/skills/creative/pixel-art/ATTRIBUTION.md +54 -0
  1181. package/skills/creative/pixel-art/SKILL.md +218 -0
  1182. package/skills/creative/pixel-art/references/palettes.md +49 -0
  1183. package/skills/creative/pixel-art/scripts/__init__.py +0 -0
  1184. package/skills/creative/pixel-art/scripts/palettes.py +167 -0
  1185. package/skills/creative/pixel-art/scripts/pixel_art.py +162 -0
  1186. package/skills/creative/pixel-art/scripts/pixel_art_video.py +345 -0
  1187. package/skills/creative/popular-web-designs/SKILL.md +214 -0
  1188. package/skills/creative/popular-web-designs/templates/airbnb.md +259 -0
  1189. package/skills/creative/popular-web-designs/templates/airtable.md +102 -0
  1190. package/skills/creative/popular-web-designs/templates/apple.md +326 -0
  1191. package/skills/creative/popular-web-designs/templates/bmw.md +193 -0
  1192. package/skills/creative/popular-web-designs/templates/cal.md +272 -0
  1193. package/skills/creative/popular-web-designs/templates/claude.md +325 -0
  1194. package/skills/creative/popular-web-designs/templates/clay.md +317 -0
  1195. package/skills/creative/popular-web-designs/templates/clickhouse.md +294 -0
  1196. package/skills/creative/popular-web-designs/templates/cohere.md +279 -0
  1197. package/skills/creative/popular-web-designs/templates/coinbase.md +142 -0
  1198. package/skills/creative/popular-web-designs/templates/composio.md +320 -0
  1199. package/skills/creative/popular-web-designs/templates/cursor.md +322 -0
  1200. package/skills/creative/popular-web-designs/templates/elevenlabs.md +278 -0
  1201. package/skills/creative/popular-web-designs/templates/expo.md +294 -0
  1202. package/skills/creative/popular-web-designs/templates/figma.md +233 -0
  1203. package/skills/creative/popular-web-designs/templates/framer.md +259 -0
  1204. package/skills/creative/popular-web-designs/templates/hashicorp.md +291 -0
  1205. package/skills/creative/popular-web-designs/templates/ibm.md +345 -0
  1206. package/skills/creative/popular-web-designs/templates/intercom.md +159 -0
  1207. package/skills/creative/popular-web-designs/templates/kraken.md +138 -0
  1208. package/skills/creative/popular-web-designs/templates/linear.app.md +380 -0
  1209. package/skills/creative/popular-web-designs/templates/lovable.md +311 -0
  1210. package/skills/creative/popular-web-designs/templates/minimax.md +270 -0
  1211. package/skills/creative/popular-web-designs/templates/mintlify.md +339 -0
  1212. package/skills/creative/popular-web-designs/templates/miro.md +121 -0
  1213. package/skills/creative/popular-web-designs/templates/mistral.ai.md +274 -0
  1214. package/skills/creative/popular-web-designs/templates/mongodb.md +279 -0
  1215. package/skills/creative/popular-web-designs/templates/notion.md +322 -0
  1216. package/skills/creative/popular-web-designs/templates/nvidia.md +306 -0
  1217. package/skills/creative/popular-web-designs/templates/ollama.md +280 -0
  1218. package/skills/creative/popular-web-designs/templates/opencode.ai.md +294 -0
  1219. package/skills/creative/popular-web-designs/templates/pinterest.md +243 -0
  1220. package/skills/creative/popular-web-designs/templates/posthog.md +269 -0
  1221. package/skills/creative/popular-web-designs/templates/raycast.md +281 -0
  1222. package/skills/creative/popular-web-designs/templates/replicate.md +274 -0
  1223. package/skills/creative/popular-web-designs/templates/resend.md +316 -0
  1224. package/skills/creative/popular-web-designs/templates/revolut.md +198 -0
  1225. package/skills/creative/popular-web-designs/templates/runwayml.md +257 -0
  1226. package/skills/creative/popular-web-designs/templates/sanity.md +370 -0
  1227. package/skills/creative/popular-web-designs/templates/sentry.md +275 -0
  1228. package/skills/creative/popular-web-designs/templates/spacex.md +207 -0
  1229. package/skills/creative/popular-web-designs/templates/spotify.md +259 -0
  1230. package/skills/creative/popular-web-designs/templates/stripe.md +335 -0
  1231. package/skills/creative/popular-web-designs/templates/supabase.md +268 -0
  1232. package/skills/creative/popular-web-designs/templates/superhuman.md +265 -0
  1233. package/skills/creative/popular-web-designs/templates/together.ai.md +276 -0
  1234. package/skills/creative/popular-web-designs/templates/uber.md +308 -0
  1235. package/skills/creative/popular-web-designs/templates/vercel.md +323 -0
  1236. package/skills/creative/popular-web-designs/templates/voltagent.md +336 -0
  1237. package/skills/creative/popular-web-designs/templates/warp.md +266 -0
  1238. package/skills/creative/popular-web-designs/templates/webflow.md +105 -0
  1239. package/skills/creative/popular-web-designs/templates/wise.md +186 -0
  1240. package/skills/creative/popular-web-designs/templates/x.ai.md +270 -0
  1241. package/skills/creative/popular-web-designs/templates/zapier.md +341 -0
  1242. package/skills/creative/pretext/SKILL.md +220 -0
  1243. package/skills/creative/pretext/references/patterns.md +258 -0
  1244. package/skills/creative/pretext/templates/donut-orbit.html +1468 -0
  1245. package/skills/creative/pretext/templates/hello-orb-flow.html +95 -0
  1246. package/skills/creative/sketch/SKILL.md +218 -0
  1247. package/skills/creative/songwriting-and-ai-music/SKILL.md +287 -0
  1248. package/skills/creative/touchdesigner-mcp/SKILL.md +356 -0
  1249. package/skills/creative/touchdesigner-mcp/references/3d-scene.md +275 -0
  1250. package/skills/creative/touchdesigner-mcp/references/animation.md +221 -0
  1251. package/skills/creative/touchdesigner-mcp/references/audio-reactive.md +175 -0
  1252. package/skills/creative/touchdesigner-mcp/references/dat-scripting.md +352 -0
  1253. package/skills/creative/touchdesigner-mcp/references/external-data.md +322 -0
  1254. package/skills/creative/touchdesigner-mcp/references/geometry-comp.md +121 -0
  1255. package/skills/creative/touchdesigner-mcp/references/glsl.md +151 -0
  1256. package/skills/creative/touchdesigner-mcp/references/layout-compositor.md +131 -0
  1257. package/skills/creative/touchdesigner-mcp/references/mcp-tools.md +382 -0
  1258. package/skills/creative/touchdesigner-mcp/references/midi-osc.md +211 -0
  1259. package/skills/creative/touchdesigner-mcp/references/network-patterns.md +966 -0
  1260. package/skills/creative/touchdesigner-mcp/references/operator-tips.md +106 -0
  1261. package/skills/creative/touchdesigner-mcp/references/operators.md +239 -0
  1262. package/skills/creative/touchdesigner-mcp/references/panel-ui.md +281 -0
  1263. package/skills/creative/touchdesigner-mcp/references/particles.md +245 -0
  1264. package/skills/creative/touchdesigner-mcp/references/pitfalls.md +704 -0
  1265. package/skills/creative/touchdesigner-mcp/references/postfx.md +183 -0
  1266. package/skills/creative/touchdesigner-mcp/references/projection-mapping.md +211 -0
  1267. package/skills/creative/touchdesigner-mcp/references/python-api.md +463 -0
  1268. package/skills/creative/touchdesigner-mcp/references/replicator.md +198 -0
  1269. package/skills/creative/touchdesigner-mcp/references/troubleshooting.md +244 -0
  1270. package/skills/creative/touchdesigner-mcp/scripts/setup.sh +115 -0
  1271. package/skills/data-science/DESCRIPTION.md +3 -0
  1272. package/skills/data-science/jupyter-live-kernel/SKILL.md +167 -0
  1273. package/skills/devops/kanban-orchestrator/SKILL.md +189 -0
  1274. package/skills/devops/kanban-worker/SKILL.md +184 -0
  1275. package/skills/devops/webhook-subscriptions/SKILL.md +204 -0
  1276. package/skills/diagramming/DESCRIPTION.md +3 -0
  1277. package/skills/dogfood/SKILL.md +162 -0
  1278. package/skills/dogfood/references/issue-taxonomy.md +109 -0
  1279. package/skills/dogfood/templates/dogfood-report-template.md +86 -0
  1280. package/skills/domain/DESCRIPTION.md +24 -0
  1281. package/skills/email/DESCRIPTION.md +3 -0
  1282. package/skills/email/himalaya/SKILL.md +299 -0
  1283. package/skills/email/himalaya/references/configuration.md +227 -0
  1284. package/skills/email/himalaya/references/message-composition.md +199 -0
  1285. package/skills/gaming/DESCRIPTION.md +3 -0
  1286. package/skills/gaming/minecraft-modpack-server/SKILL.md +187 -0
  1287. package/skills/gaming/pokemon-player/SKILL.md +216 -0
  1288. package/skills/gifs/DESCRIPTION.md +3 -0
  1289. package/skills/github/DESCRIPTION.md +3 -0
  1290. package/skills/github/codebase-inspection/SKILL.md +116 -0
  1291. package/skills/github/github-auth/SKILL.md +247 -0
  1292. package/skills/github/github-auth/scripts/gh-env.sh +66 -0
  1293. package/skills/github/github-code-review/SKILL.md +481 -0
  1294. package/skills/github/github-code-review/references/review-output-template.md +74 -0
  1295. package/skills/github/github-issues/SKILL.md +370 -0
  1296. package/skills/github/github-issues/templates/bug-report.md +35 -0
  1297. package/skills/github/github-issues/templates/feature-request.md +31 -0
  1298. package/skills/github/github-pr-workflow/SKILL.md +367 -0
  1299. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
  1300. package/skills/github/github-pr-workflow/references/conventional-commits.md +71 -0
  1301. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
  1302. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
  1303. package/skills/github/github-repo-management/SKILL.md +516 -0
  1304. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
  1305. package/skills/index-cache/anthropics_skills_skills_.json +1 -0
  1306. package/skills/index-cache/claude_marketplace_anthropics_skills.json +1 -0
  1307. package/skills/index-cache/lobehub_index.json +1 -0
  1308. package/skills/index-cache/openai_skills_skills_.json +1 -0
  1309. package/skills/inference-sh/DESCRIPTION.md +19 -0
  1310. package/skills/mcp/DESCRIPTION.md +3 -0
  1311. package/skills/mcp/native-mcp/SKILL.md +357 -0
  1312. package/skills/media/DESCRIPTION.md +3 -0
  1313. package/skills/media/gif-search/SKILL.md +91 -0
  1314. package/skills/media/heartmula/SKILL.md +171 -0
  1315. package/skills/media/songsee/SKILL.md +83 -0
  1316. package/skills/media/spotify/SKILL.md +135 -0
  1317. package/skills/media/youtube-content/SKILL.md +73 -0
  1318. package/skills/media/youtube-content/references/output-formats.md +56 -0
  1319. package/skills/media/youtube-content/scripts/fetch_transcript.py +124 -0
  1320. package/skills/mlops/DESCRIPTION.md +3 -0
  1321. package/skills/mlops/evaluation/DESCRIPTION.md +3 -0
  1322. package/skills/mlops/evaluation/lm-evaluation-harness/SKILL.md +498 -0
  1323. package/skills/mlops/evaluation/lm-evaluation-harness/references/api-evaluation.md +490 -0
  1324. package/skills/mlops/evaluation/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  1325. package/skills/mlops/evaluation/lm-evaluation-harness/references/custom-tasks.md +602 -0
  1326. package/skills/mlops/evaluation/lm-evaluation-harness/references/distributed-eval.md +519 -0
  1327. package/skills/mlops/evaluation/weights-and-biases/SKILL.md +594 -0
  1328. package/skills/mlops/evaluation/weights-and-biases/references/artifacts.md +584 -0
  1329. package/skills/mlops/evaluation/weights-and-biases/references/integrations.md +700 -0
  1330. package/skills/mlops/evaluation/weights-and-biases/references/sweeps.md +847 -0
  1331. package/skills/mlops/huggingface-hub/SKILL.md +81 -0
  1332. package/skills/mlops/inference/DESCRIPTION.md +3 -0
  1333. package/skills/mlops/inference/llama-cpp/SKILL.md +249 -0
  1334. package/skills/mlops/inference/llama-cpp/references/advanced-usage.md +504 -0
  1335. package/skills/mlops/inference/llama-cpp/references/hub-discovery.md +168 -0
  1336. package/skills/mlops/inference/llama-cpp/references/optimization.md +89 -0
  1337. package/skills/mlops/inference/llama-cpp/references/quantization.md +243 -0
  1338. package/skills/mlops/inference/llama-cpp/references/server.md +150 -0
  1339. package/skills/mlops/inference/llama-cpp/references/troubleshooting.md +442 -0
  1340. package/skills/mlops/inference/obliteratus/SKILL.md +342 -0
  1341. package/skills/mlops/inference/obliteratus/references/analysis-modules.md +166 -0
  1342. package/skills/mlops/inference/obliteratus/references/methods-guide.md +141 -0
  1343. package/skills/mlops/inference/obliteratus/templates/abliteration-config.yaml +33 -0
  1344. package/skills/mlops/inference/obliteratus/templates/analysis-study.yaml +40 -0
  1345. package/skills/mlops/inference/obliteratus/templates/batch-abliteration.yaml +41 -0
  1346. package/skills/mlops/inference/vllm/SKILL.md +372 -0
  1347. package/skills/mlops/inference/vllm/references/optimization.md +226 -0
  1348. package/skills/mlops/inference/vllm/references/quantization.md +284 -0
  1349. package/skills/mlops/inference/vllm/references/server-deployment.md +255 -0
  1350. package/skills/mlops/inference/vllm/references/troubleshooting.md +447 -0
  1351. package/skills/mlops/models/DESCRIPTION.md +3 -0
  1352. package/skills/mlops/models/audiocraft/SKILL.md +568 -0
  1353. package/skills/mlops/models/audiocraft/references/advanced-usage.md +666 -0
  1354. package/skills/mlops/models/audiocraft/references/troubleshooting.md +504 -0
  1355. package/skills/mlops/models/segment-anything/SKILL.md +506 -0
  1356. package/skills/mlops/models/segment-anything/references/advanced-usage.md +589 -0
  1357. package/skills/mlops/models/segment-anything/references/troubleshooting.md +484 -0
  1358. package/skills/mlops/research/DESCRIPTION.md +3 -0
  1359. package/skills/mlops/research/dspy/SKILL.md +594 -0
  1360. package/skills/mlops/research/dspy/references/examples.md +663 -0
  1361. package/skills/mlops/research/dspy/references/modules.md +475 -0
  1362. package/skills/mlops/research/dspy/references/optimizers.md +566 -0
  1363. package/skills/mlops/training/DESCRIPTION.md +3 -0
  1364. package/skills/mlops/vector-databases/DESCRIPTION.md +3 -0
  1365. package/skills/note-taking/DESCRIPTION.md +3 -0
  1366. package/skills/note-taking/obsidian/SKILL.md +61 -0
  1367. package/skills/productivity/DESCRIPTION.md +3 -0
  1368. package/skills/productivity/airtable/SKILL.md +229 -0
  1369. package/skills/productivity/google-workspace/SKILL.md +335 -0
  1370. package/skills/productivity/google-workspace/references/gmail-search-syntax.md +63 -0
  1371. package/skills/productivity/google-workspace/scripts/_hermes_home.py +43 -0
  1372. package/skills/productivity/google-workspace/scripts/google_api.py +1221 -0
  1373. package/skills/productivity/google-workspace/scripts/gws_bridge.py +108 -0
  1374. package/skills/productivity/google-workspace/scripts/setup.py +454 -0
  1375. package/skills/productivity/linear/SKILL.md +380 -0
  1376. package/skills/productivity/linear/scripts/linear_api.py +445 -0
  1377. package/skills/productivity/maps/SKILL.md +195 -0
  1378. package/skills/productivity/maps/scripts/maps_client.py +1298 -0
  1379. package/skills/productivity/nano-pdf/SKILL.md +52 -0
  1380. package/skills/productivity/notion/SKILL.md +448 -0
  1381. package/skills/productivity/notion/references/block-types.md +112 -0
  1382. package/skills/productivity/ocr-and-documents/DESCRIPTION.md +3 -0
  1383. package/skills/productivity/ocr-and-documents/SKILL.md +172 -0
  1384. package/skills/productivity/ocr-and-documents/scripts/extract_marker.py +87 -0
  1385. package/skills/productivity/ocr-and-documents/scripts/extract_pymupdf.py +98 -0
  1386. package/skills/productivity/powerpoint/LICENSE.txt +30 -0
  1387. package/skills/productivity/powerpoint/SKILL.md +237 -0
  1388. package/skills/productivity/powerpoint/editing.md +205 -0
  1389. package/skills/productivity/powerpoint/pptxgenjs.md +420 -0
  1390. package/skills/productivity/powerpoint/scripts/__init__.py +0 -0
  1391. package/skills/productivity/powerpoint/scripts/add_slide.py +195 -0
  1392. package/skills/productivity/powerpoint/scripts/clean.py +286 -0
  1393. package/skills/productivity/powerpoint/scripts/office/helpers/__init__.py +0 -0
  1394. package/skills/productivity/powerpoint/scripts/office/helpers/merge_runs.py +199 -0
  1395. package/skills/productivity/powerpoint/scripts/office/helpers/simplify_redlines.py +197 -0
  1396. package/skills/productivity/powerpoint/scripts/office/pack.py +159 -0
  1397. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  1398. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  1399. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  1400. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  1401. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  1402. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  1403. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  1404. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  1405. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  1406. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  1407. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  1408. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  1409. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  1410. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  1411. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  1412. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  1413. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  1414. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  1415. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  1416. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  1417. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  1418. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  1419. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  1420. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  1421. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  1422. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  1423. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  1424. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-contentTypes.xsd +42 -0
  1425. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-coreProperties.xsd +50 -0
  1426. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-digSig.xsd +49 -0
  1427. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-relationships.xsd +33 -0
  1428. package/skills/productivity/powerpoint/scripts/office/schemas/mce/mc.xsd +75 -0
  1429. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  1430. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  1431. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  1432. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  1433. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  1434. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  1435. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  1436. package/skills/productivity/teams-meeting-pipeline/SKILL.md +116 -0
  1437. package/skills/red-teaming/godmode/SKILL.md +404 -0
  1438. package/skills/red-teaming/godmode/references/jailbreak-templates.md +128 -0
  1439. package/skills/red-teaming/godmode/references/refusal-detection.md +142 -0
  1440. package/skills/red-teaming/godmode/scripts/auto_jailbreak.py +769 -0
  1441. package/skills/red-teaming/godmode/scripts/godmode_race.py +530 -0
  1442. package/skills/red-teaming/godmode/scripts/load_godmode.py +45 -0
  1443. package/skills/red-teaming/godmode/scripts/parseltongue.py +550 -0
  1444. package/skills/red-teaming/godmode/templates/prefill-subtle.json +10 -0
  1445. package/skills/red-teaming/godmode/templates/prefill.json +18 -0
  1446. package/skills/research/DESCRIPTION.md +3 -0
  1447. package/skills/research/arxiv/SKILL.md +282 -0
  1448. package/skills/research/arxiv/scripts/search_arxiv.py +114 -0
  1449. package/skills/research/blogwatcher/SKILL.md +137 -0
  1450. package/skills/research/llm-wiki/SKILL.md +507 -0
  1451. package/skills/research/polymarket/SKILL.md +77 -0
  1452. package/skills/research/polymarket/references/api-endpoints.md +220 -0
  1453. package/skills/research/polymarket/scripts/polymarket.py +284 -0
  1454. package/skills/research/research-paper-writing/SKILL.md +2377 -0
  1455. package/skills/research/research-paper-writing/references/autoreason-methodology.md +394 -0
  1456. package/skills/research/research-paper-writing/references/checklists.md +434 -0
  1457. package/skills/research/research-paper-writing/references/citation-workflow.md +564 -0
  1458. package/skills/research/research-paper-writing/references/experiment-patterns.md +728 -0
  1459. package/skills/research/research-paper-writing/references/human-evaluation.md +476 -0
  1460. package/skills/research/research-paper-writing/references/paper-types.md +481 -0
  1461. package/skills/research/research-paper-writing/references/reviewer-guidelines.md +433 -0
  1462. package/skills/research/research-paper-writing/references/sources.md +191 -0
  1463. package/skills/research/research-paper-writing/references/writing-guide.md +474 -0
  1464. package/skills/research/research-paper-writing/templates/README.md +251 -0
  1465. package/skills/research/research-paper-writing/templates/aaai2026/README.md +534 -0
  1466. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  1467. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  1468. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  1469. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  1470. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  1471. package/skills/research/research-paper-writing/templates/acl/README.md +50 -0
  1472. package/skills/research/research-paper-writing/templates/acl/acl.sty +312 -0
  1473. package/skills/research/research-paper-writing/templates/acl/acl_latex.tex +377 -0
  1474. package/skills/research/research-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  1475. package/skills/research/research-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  1476. package/skills/research/research-paper-writing/templates/acl/anthology.bib.txt +26 -0
  1477. package/skills/research/research-paper-writing/templates/acl/custom.bib +70 -0
  1478. package/skills/research/research-paper-writing/templates/acl/formatting.md +326 -0
  1479. package/skills/research/research-paper-writing/templates/colm2025/README.md +3 -0
  1480. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  1481. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  1482. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  1483. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  1484. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  1485. package/skills/research/research-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  1486. package/skills/research/research-paper-writing/templates/colm2025/math_commands.tex +508 -0
  1487. package/skills/research/research-paper-writing/templates/colm2025/natbib.sty +1246 -0
  1488. package/skills/research/research-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  1489. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  1490. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  1491. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  1492. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  1493. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  1494. package/skills/research/research-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  1495. package/skills/research/research-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  1496. package/skills/research/research-paper-writing/templates/icml2026/algorithm.sty +79 -0
  1497. package/skills/research/research-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  1498. package/skills/research/research-paper-writing/templates/icml2026/example_paper.bib +75 -0
  1499. package/skills/research/research-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  1500. package/skills/research/research-paper-writing/templates/icml2026/example_paper.tex +662 -0
  1501. package/skills/research/research-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  1502. package/skills/research/research-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  1503. package/skills/research/research-paper-writing/templates/icml2026/icml2026.sty +767 -0
  1504. package/skills/research/research-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  1505. package/skills/research/research-paper-writing/templates/neurips2025/Makefile +36 -0
  1506. package/skills/research/research-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  1507. package/skills/research/research-paper-writing/templates/neurips2025/main.tex +38 -0
  1508. package/skills/research/research-paper-writing/templates/neurips2025/neurips.sty +382 -0
  1509. package/skills/smart-home/DESCRIPTION.md +3 -0
  1510. package/skills/smart-home/openhue/SKILL.md +109 -0
  1511. package/skills/social-media/DESCRIPTION.md +3 -0
  1512. package/skills/social-media/xurl/SKILL.md +414 -0
  1513. package/skills/software-development/debugging-hermes-tui-commands/SKILL.md +152 -0
  1514. package/skills/software-development/hermes-agent-skill-authoring/SKILL.md +165 -0
  1515. package/skills/software-development/node-inspect-debugger/SKILL.md +319 -0
  1516. package/skills/software-development/plan/SKILL.md +58 -0
  1517. package/skills/software-development/python-debugpy/SKILL.md +375 -0
  1518. package/skills/software-development/requesting-code-review/SKILL.md +280 -0
  1519. package/skills/software-development/spike/SKILL.md +197 -0
  1520. package/skills/software-development/subagent-driven-development/SKILL.md +352 -0
  1521. package/skills/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
  1522. package/skills/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
  1523. package/skills/software-development/systematic-debugging/SKILL.md +367 -0
  1524. package/skills/software-development/test-driven-development/SKILL.md +343 -0
  1525. package/skills/software-development/writing-plans/SKILL.md +297 -0
  1526. package/skills/yuanbao/SKILL.md +108 -0
  1527. package/tools/__init__.py +25 -0
  1528. package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  1529. package/tools/__pycache__/approval.cpython-312.pyc +0 -0
  1530. package/tools/__pycache__/binary_extensions.cpython-312.pyc +0 -0
  1531. package/tools/__pycache__/browser_camofox.cpython-312.pyc +0 -0
  1532. package/tools/__pycache__/browser_camofox_state.cpython-312.pyc +0 -0
  1533. package/tools/__pycache__/browser_cdp_tool.cpython-312.pyc +0 -0
  1534. package/tools/__pycache__/browser_dialog_tool.cpython-312.pyc +0 -0
  1535. package/tools/__pycache__/browser_supervisor.cpython-312.pyc +0 -0
  1536. package/tools/__pycache__/browser_tool.cpython-312.pyc +0 -0
  1537. package/tools/__pycache__/budget_config.cpython-312.pyc +0 -0
  1538. package/tools/__pycache__/checkpoint_manager.cpython-312.pyc +0 -0
  1539. package/tools/__pycache__/clarify_gateway.cpython-312.pyc +0 -0
  1540. package/tools/__pycache__/clarify_tool.cpython-312.pyc +0 -0
  1541. package/tools/__pycache__/code_execution_tool.cpython-312.pyc +0 -0
  1542. package/tools/__pycache__/computer_use_tool.cpython-312.pyc +0 -0
  1543. package/tools/__pycache__/cronjob_tools.cpython-312.pyc +0 -0
  1544. package/tools/__pycache__/debug_helpers.cpython-312.pyc +0 -0
  1545. package/tools/__pycache__/delegate_tool.cpython-312.pyc +0 -0
  1546. package/tools/__pycache__/discord_tool.cpython-312.pyc +0 -0
  1547. package/tools/__pycache__/feishu_doc_tool.cpython-312.pyc +0 -0
  1548. package/tools/__pycache__/feishu_drive_tool.cpython-312.pyc +0 -0
  1549. package/tools/__pycache__/file_operations.cpython-312.pyc +0 -0
  1550. package/tools/__pycache__/file_state.cpython-312.pyc +0 -0
  1551. package/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  1552. package/tools/__pycache__/homeassistant_tool.cpython-312.pyc +0 -0
  1553. package/tools/__pycache__/image_generation_tool.cpython-312.pyc +0 -0
  1554. package/tools/__pycache__/interrupt.cpython-312.pyc +0 -0
  1555. package/tools/__pycache__/kanban_tools.cpython-312.pyc +0 -0
  1556. package/tools/__pycache__/lazy_deps.cpython-312.pyc +0 -0
  1557. package/tools/__pycache__/managed_tool_gateway.cpython-312.pyc +0 -0
  1558. package/tools/__pycache__/mcp_tool.cpython-312.pyc +0 -0
  1559. package/tools/__pycache__/memory_tool.cpython-312.pyc +0 -0
  1560. package/tools/__pycache__/mixture_of_agents_tool.cpython-312.pyc +0 -0
  1561. package/tools/__pycache__/openrouter_client.cpython-312.pyc +0 -0
  1562. package/tools/__pycache__/process_registry.cpython-312.pyc +0 -0
  1563. package/tools/__pycache__/registry.cpython-312.pyc +0 -0
  1564. package/tools/__pycache__/schema_sanitizer.cpython-312.pyc +0 -0
  1565. package/tools/__pycache__/send_message_tool.cpython-312.pyc +0 -0
  1566. package/tools/__pycache__/session_search_tool.cpython-312.pyc +0 -0
  1567. package/tools/__pycache__/skill_manager_tool.cpython-312.pyc +0 -0
  1568. package/tools/__pycache__/skill_provenance.cpython-312.pyc +0 -0
  1569. package/tools/__pycache__/skill_usage.cpython-312.pyc +0 -0
  1570. package/tools/__pycache__/skills_guard.cpython-312.pyc +0 -0
  1571. package/tools/__pycache__/skills_sync.cpython-312.pyc +0 -0
  1572. package/tools/__pycache__/skills_tool.cpython-312.pyc +0 -0
  1573. package/tools/__pycache__/slash_confirm.cpython-312.pyc +0 -0
  1574. package/tools/__pycache__/terminal_tool.cpython-312.pyc +0 -0
  1575. package/tools/__pycache__/tirith_security.cpython-312.pyc +0 -0
  1576. package/tools/__pycache__/todo_tool.cpython-312.pyc +0 -0
  1577. package/tools/__pycache__/tool_backend_helpers.cpython-312.pyc +0 -0
  1578. package/tools/__pycache__/tool_result_storage.cpython-312.pyc +0 -0
  1579. package/tools/__pycache__/tts_tool.cpython-312.pyc +0 -0
  1580. package/tools/__pycache__/url_safety.cpython-312.pyc +0 -0
  1581. package/tools/__pycache__/video_generation_tool.cpython-312.pyc +0 -0
  1582. package/tools/__pycache__/vision_tools.cpython-312.pyc +0 -0
  1583. package/tools/__pycache__/voice_mode.cpython-312.pyc +0 -0
  1584. package/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  1585. package/tools/__pycache__/website_policy.cpython-312.pyc +0 -0
  1586. package/tools/__pycache__/x_search_tool.cpython-312.pyc +0 -0
  1587. package/tools/__pycache__/xai_http.cpython-312.pyc +0 -0
  1588. package/tools/__pycache__/yuanbao_tools.cpython-312.pyc +0 -0
  1589. package/tools/ansi_strip.py +44 -0
  1590. package/tools/approval.py +1392 -0
  1591. package/tools/binary_extensions.py +42 -0
  1592. package/tools/browser_camofox.py +700 -0
  1593. package/tools/browser_camofox_state.py +48 -0
  1594. package/tools/browser_cdp_tool.py +569 -0
  1595. package/tools/browser_dialog_tool.py +148 -0
  1596. package/tools/browser_providers/__init__.py +10 -0
  1597. package/tools/browser_providers/__pycache__/__init__.cpython-312.pyc +0 -0
  1598. package/tools/browser_providers/__pycache__/base.cpython-312.pyc +0 -0
  1599. package/tools/browser_providers/__pycache__/browser_use.cpython-312.pyc +0 -0
  1600. package/tools/browser_providers/__pycache__/browserbase.cpython-312.pyc +0 -0
  1601. package/tools/browser_providers/__pycache__/firecrawl.cpython-312.pyc +0 -0
  1602. package/tools/browser_providers/base.py +59 -0
  1603. package/tools/browser_providers/browser_use.py +225 -0
  1604. package/tools/browser_providers/browserbase.py +222 -0
  1605. package/tools/browser_providers/firecrawl.py +112 -0
  1606. package/tools/browser_supervisor.py +1457 -0
  1607. package/tools/browser_tool.py +3676 -0
  1608. package/tools/budget_config.py +51 -0
  1609. package/tools/checkpoint_manager.py +1639 -0
  1610. package/tools/clarify_gateway.py +278 -0
  1611. package/tools/clarify_tool.py +141 -0
  1612. package/tools/code_execution_tool.py +1782 -0
  1613. package/tools/computer_use/__init__.py +43 -0
  1614. package/tools/computer_use/__pycache__/__init__.cpython-312.pyc +0 -0
  1615. package/tools/computer_use/__pycache__/backend.cpython-312.pyc +0 -0
  1616. package/tools/computer_use/__pycache__/schema.cpython-312.pyc +0 -0
  1617. package/tools/computer_use/__pycache__/tool.cpython-312.pyc +0 -0
  1618. package/tools/computer_use/backend.py +150 -0
  1619. package/tools/computer_use/cua_backend.py +682 -0
  1620. package/tools/computer_use/schema.py +191 -0
  1621. package/tools/computer_use/tool.py +521 -0
  1622. package/tools/computer_use_tool.py +39 -0
  1623. package/tools/credential_files.py +437 -0
  1624. package/tools/cronjob_tools.py +719 -0
  1625. package/tools/debug_helpers.py +106 -0
  1626. package/tools/delegate_tool.py +2797 -0
  1627. package/tools/discord_tool.py +959 -0
  1628. package/tools/env_passthrough.py +145 -0
  1629. package/tools/environments/__init__.py +14 -0
  1630. package/tools/environments/__pycache__/__init__.cpython-312.pyc +0 -0
  1631. package/tools/environments/__pycache__/base.cpython-312.pyc +0 -0
  1632. package/tools/environments/__pycache__/docker.cpython-312.pyc +0 -0
  1633. package/tools/environments/__pycache__/file_sync.cpython-312.pyc +0 -0
  1634. package/tools/environments/__pycache__/local.cpython-312.pyc +0 -0
  1635. package/tools/environments/__pycache__/managed_modal.cpython-312.pyc +0 -0
  1636. package/tools/environments/__pycache__/modal.cpython-312.pyc +0 -0
  1637. package/tools/environments/__pycache__/modal_utils.cpython-312.pyc +0 -0
  1638. package/tools/environments/__pycache__/singularity.cpython-312.pyc +0 -0
  1639. package/tools/environments/__pycache__/ssh.cpython-312.pyc +0 -0
  1640. package/tools/environments/base.py +844 -0
  1641. package/tools/environments/daytona.py +270 -0
  1642. package/tools/environments/docker.py +656 -0
  1643. package/tools/environments/file_sync.py +400 -0
  1644. package/tools/environments/local.py +658 -0
  1645. package/tools/environments/managed_modal.py +282 -0
  1646. package/tools/environments/modal.py +479 -0
  1647. package/tools/environments/modal_utils.py +199 -0
  1648. package/tools/environments/singularity.py +263 -0
  1649. package/tools/environments/ssh.py +295 -0
  1650. package/tools/environments/vercel_sandbox.py +655 -0
  1651. package/tools/feishu_doc_tool.py +138 -0
  1652. package/tools/feishu_drive_tool.py +431 -0
  1653. package/tools/file_operations.py +1825 -0
  1654. package/tools/file_state.py +332 -0
  1655. package/tools/file_tools.py +1172 -0
  1656. package/tools/fuzzy_match.py +703 -0
  1657. package/tools/homeassistant_tool.py +513 -0
  1658. package/tools/image_generation_tool.py +1098 -0
  1659. package/tools/interrupt.py +98 -0
  1660. package/tools/kanban_tools.py +1139 -0
  1661. package/tools/lazy_deps.py +608 -0
  1662. package/tools/managed_tool_gateway.py +168 -0
  1663. package/tools/mcp_oauth.py +633 -0
  1664. package/tools/mcp_oauth_manager.py +607 -0
  1665. package/tools/mcp_tool.py +3483 -0
  1666. package/tools/memory_tool.py +584 -0
  1667. package/tools/microsoft_graph_auth.py +245 -0
  1668. package/tools/microsoft_graph_client.py +408 -0
  1669. package/tools/mixture_of_agents_tool.py +542 -0
  1670. package/tools/neutts_samples/jo.txt +1 -0
  1671. package/tools/neutts_samples/jo.wav +0 -0
  1672. package/tools/neutts_synth.py +104 -0
  1673. package/tools/openrouter_client.py +33 -0
  1674. package/tools/osv_check.py +155 -0
  1675. package/tools/patch_parser.py +592 -0
  1676. package/tools/path_security.py +43 -0
  1677. package/tools/process_registry.py +1534 -0
  1678. package/tools/registry.py +589 -0
  1679. package/tools/schema_sanitizer.py +370 -0
  1680. package/tools/send_message_tool.py +1900 -0
  1681. package/tools/session_search_tool.py +613 -0
  1682. package/tools/skill_manager_tool.py +932 -0
  1683. package/tools/skill_provenance.py +78 -0
  1684. package/tools/skill_usage.py +610 -0
  1685. package/tools/skills_guard.py +932 -0
  1686. package/tools/skills_hub.py +3263 -0
  1687. package/tools/skills_sync.py +432 -0
  1688. package/tools/skills_tool.py +1569 -0
  1689. package/tools/slash_confirm.py +167 -0
  1690. package/tools/terminal_tool.py +2376 -0
  1691. package/tools/tirith_security.py +775 -0
  1692. package/tools/todo_tool.py +277 -0
  1693. package/tools/tool_backend_helpers.py +144 -0
  1694. package/tools/tool_output_limits.py +92 -0
  1695. package/tools/tool_result_storage.py +232 -0
  1696. package/tools/transcription_tools.py +936 -0
  1697. package/tools/tts_tool.py +2285 -0
  1698. package/tools/url_safety.py +330 -0
  1699. package/tools/video_generation_tool.py +561 -0
  1700. package/tools/vision_tools.py +1422 -0
  1701. package/tools/voice_mode.py +1019 -0
  1702. package/tools/web_tools.py +1551 -0
  1703. package/tools/website_policy.py +283 -0
  1704. package/tools/x_search_tool.py +424 -0
  1705. package/tools/xai_http.py +83 -0
  1706. package/tools/yuanbao_tools.py +736 -0
  1707. package/toolset_distributions.py +364 -0
  1708. package/toolsets.py +866 -0
  1709. package/trajectory_compressor.py +1509 -0
  1710. package/tui_gateway/__init__.py +0 -0
  1711. package/tui_gateway/entry.py +251 -0
  1712. package/tui_gateway/event_publisher.py +126 -0
  1713. package/tui_gateway/render.py +49 -0
  1714. package/tui_gateway/server.py +6623 -0
  1715. package/tui_gateway/slash_worker.py +76 -0
  1716. package/tui_gateway/transport.py +219 -0
  1717. package/tui_gateway/ws.py +178 -0
  1718. package/utils.py +361 -0
@@ -0,0 +1,3488 @@
1
+ """
2
+ OpenAI-compatible API server platform adapter.
3
+
4
+ Exposes an HTTP server with endpoints:
5
+ - POST /v1/chat/completions — OpenAI Chat Completions format (stateless; opt-in session continuity via X-Hermes-Session-Id header; opt-in long-term memory scoping via X-Hermes-Session-Key header)
6
+ - POST /v1/responses — OpenAI Responses API format (stateful via previous_response_id; X-Hermes-Session-Key supported)
7
+ - GET /v1/responses/{response_id} — Retrieve a stored response
8
+ - DELETE /v1/responses/{response_id} — Delete a stored response
9
+ - GET /v1/models — lists hermes-agent as an available model
10
+ - GET /v1/capabilities — machine-readable API capabilities for external UIs
11
+ - POST /v1/runs — start a run, returns run_id immediately (202)
12
+ - GET /v1/runs/{run_id} — retrieve current run status
13
+ - GET /v1/runs/{run_id}/events — SSE stream of structured lifecycle events
14
+ - POST /v1/runs/{run_id}/approval — resolve a pending run approval
15
+ - POST /v1/runs/{run_id}/stop — interrupt a running agent
16
+ - GET /health — health check
17
+ - GET /health/detailed — rich status for cross-container dashboard probing
18
+
19
+ Any OpenAI-compatible frontend (Open WebUI, LobeChat, LibreChat,
20
+ AnythingLLM, NextChat, ChatBox, etc.) can connect to hermes-agent
21
+ through this adapter by pointing at http://localhost:8642/v1.
22
+
23
+ Requires:
24
+ - aiohttp (already available in the gateway)
25
+ """
26
+
27
+ import asyncio
28
+ import hashlib
29
+ import hmac
30
+ import json
31
+ import logging
32
+ import os
33
+ import socket as _socket
34
+ import re
35
+ import sqlite3
36
+ import time
37
+ import uuid
38
+ from typing import Any, Dict, List, Optional
39
+
40
+ try:
41
+ from aiohttp import web
42
+ AIOHTTP_AVAILABLE = True
43
+ except ImportError:
44
+ AIOHTTP_AVAILABLE = False
45
+ web = None # type: ignore[assignment]
46
+
47
+ from gateway.config import Platform, PlatformConfig
48
+ from gateway.platforms.base import (
49
+ BasePlatformAdapter,
50
+ SendResult,
51
+ is_network_accessible,
52
+ )
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+ # Default settings
57
+ DEFAULT_HOST = "127.0.0.1"
58
+ DEFAULT_PORT = 8642
59
+ MAX_STORED_RESPONSES = 100
60
+ MAX_REQUEST_BYTES = 10_000_000 # 10 MB — accommodates long agent conversations with tool calls
61
+ CHAT_COMPLETIONS_SSE_KEEPALIVE_SECONDS = 30.0
62
+ MAX_NORMALIZED_TEXT_LENGTH = 65_536 # 64 KB cap for normalized content parts
63
+ MAX_CONTENT_LIST_SIZE = 1_000 # Max items when content is an array
64
+
65
+
66
+ def _coerce_port(value: Any, default: int = DEFAULT_PORT) -> int:
67
+ """Parse a listen port without letting malformed env/config values crash startup."""
68
+ try:
69
+ return int(value)
70
+ except (TypeError, ValueError):
71
+ return default
72
+
73
+
74
+ def _normalize_chat_content(
75
+ content: Any, *, _max_depth: int = 10, _depth: int = 0,
76
+ ) -> str:
77
+ """Normalize OpenAI chat message content into a plain text string.
78
+
79
+ Some clients (Open WebUI, LobeChat, etc.) send content as an array of
80
+ typed parts instead of a plain string::
81
+
82
+ [{"type": "text", "text": "hello"}, {"type": "input_text", "text": "..."}]
83
+
84
+ This function flattens those into a single string so the agent pipeline
85
+ (which expects strings) doesn't choke.
86
+
87
+ Defensive limits prevent abuse: recursion depth, list size, and output
88
+ length are all bounded.
89
+ """
90
+ if _depth > _max_depth:
91
+ return ""
92
+ if content is None:
93
+ return ""
94
+ if isinstance(content, str):
95
+ return content[:MAX_NORMALIZED_TEXT_LENGTH] if len(content) > MAX_NORMALIZED_TEXT_LENGTH else content
96
+
97
+ if isinstance(content, list):
98
+ parts: List[str] = []
99
+ items = content[:MAX_CONTENT_LIST_SIZE] if len(content) > MAX_CONTENT_LIST_SIZE else content
100
+ for item in items:
101
+ if isinstance(item, str):
102
+ if item:
103
+ parts.append(item[:MAX_NORMALIZED_TEXT_LENGTH])
104
+ elif isinstance(item, dict):
105
+ item_type = str(item.get("type") or "").strip().lower()
106
+ if item_type in {"text", "input_text", "output_text"}:
107
+ text = item.get("text", "")
108
+ if text:
109
+ try:
110
+ parts.append(str(text)[:MAX_NORMALIZED_TEXT_LENGTH])
111
+ except Exception:
112
+ pass
113
+ # Silently skip image_url / other non-text parts
114
+ elif isinstance(item, list):
115
+ nested = _normalize_chat_content(item, _max_depth=_max_depth, _depth=_depth + 1)
116
+ if nested:
117
+ parts.append(nested)
118
+ # Check accumulated size
119
+ if sum(len(p) for p in parts) >= MAX_NORMALIZED_TEXT_LENGTH:
120
+ break
121
+ result = "\n".join(parts)
122
+ return result[:MAX_NORMALIZED_TEXT_LENGTH] if len(result) > MAX_NORMALIZED_TEXT_LENGTH else result
123
+
124
+ # Fallback for unexpected types (int, float, bool, etc.)
125
+ try:
126
+ result = str(content)
127
+ return result[:MAX_NORMALIZED_TEXT_LENGTH] if len(result) > MAX_NORMALIZED_TEXT_LENGTH else result
128
+ except Exception:
129
+ return ""
130
+
131
+
132
+ # Content part type aliases used by the OpenAI Chat Completions and Responses
133
+ # APIs. We accept both spellings on input and emit a single canonical internal
134
+ # shape (``{"type": "text", ...}`` / ``{"type": "image_url", ...}``) that the
135
+ # rest of the agent pipeline already understands.
136
+ _TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text"})
137
+ _IMAGE_PART_TYPES = frozenset({"image_url", "input_image"})
138
+ _FILE_PART_TYPES = frozenset({"file", "input_file"})
139
+
140
+
141
+ def _normalize_multimodal_content(content: Any) -> Any:
142
+ """Validate and normalize multimodal content for the API server.
143
+
144
+ Returns a plain string when the content is text-only, or a list of
145
+ ``{"type": "text"|"image_url", ...}`` parts when images are present.
146
+ The output shape is the native OpenAI Chat Completions vision format,
147
+ which the agent pipeline accepts verbatim (OpenAI-wire providers) or
148
+ converts (``_preprocess_anthropic_content`` for Anthropic).
149
+
150
+ Raises ``ValueError`` with an OpenAI-style code on invalid input:
151
+ * ``unsupported_content_type`` — file/input_file/file_id parts, or
152
+ non-image ``data:`` URLs.
153
+ * ``invalid_image_url`` — missing URL or unsupported scheme.
154
+ * ``invalid_content_part`` — malformed text/image objects.
155
+
156
+ Callers translate the ValueError into a 400 response.
157
+ """
158
+ # Scalar passthrough mirrors ``_normalize_chat_content``.
159
+ if content is None:
160
+ return ""
161
+ if isinstance(content, str):
162
+ return content[:MAX_NORMALIZED_TEXT_LENGTH] if len(content) > MAX_NORMALIZED_TEXT_LENGTH else content
163
+ if not isinstance(content, list):
164
+ # Mirror the legacy text-normalizer's fallback so callers that
165
+ # pre-existed image support still get a string back.
166
+ return _normalize_chat_content(content)
167
+
168
+ items = content[:MAX_CONTENT_LIST_SIZE] if len(content) > MAX_CONTENT_LIST_SIZE else content
169
+ normalized_parts: List[Dict[str, Any]] = []
170
+ text_accum_len = 0
171
+
172
+ for part in items:
173
+ if isinstance(part, str):
174
+ if part:
175
+ trimmed = part[:MAX_NORMALIZED_TEXT_LENGTH]
176
+ normalized_parts.append({"type": "text", "text": trimmed})
177
+ text_accum_len += len(trimmed)
178
+ continue
179
+
180
+ if not isinstance(part, dict):
181
+ # Ignore unknown scalars for forward compatibility with future
182
+ # Responses API additions (e.g. ``refusal``). The same policy
183
+ # the text normalizer applies.
184
+ continue
185
+
186
+ raw_type = part.get("type")
187
+ part_type = str(raw_type or "").strip().lower()
188
+
189
+ if part_type in _TEXT_PART_TYPES:
190
+ text = part.get("text")
191
+ if text is None:
192
+ continue
193
+ if not isinstance(text, str):
194
+ text = str(text)
195
+ if text:
196
+ trimmed = text[:MAX_NORMALIZED_TEXT_LENGTH]
197
+ normalized_parts.append({"type": "text", "text": trimmed})
198
+ text_accum_len += len(trimmed)
199
+ continue
200
+
201
+ if part_type in _IMAGE_PART_TYPES:
202
+ detail = part.get("detail")
203
+ image_ref = part.get("image_url")
204
+ # OpenAI Responses sends ``input_image`` with a top-level
205
+ # ``image_url`` string; Chat Completions sends ``image_url`` as
206
+ # ``{"url": "...", "detail": "..."}``. Support both.
207
+ if isinstance(image_ref, dict):
208
+ url_value = image_ref.get("url")
209
+ detail = image_ref.get("detail", detail)
210
+ else:
211
+ url_value = image_ref
212
+ if not isinstance(url_value, str) or not url_value.strip():
213
+ raise ValueError("invalid_image_url:Image parts must include a non-empty image URL.")
214
+ url_value = url_value.strip()
215
+ lowered = url_value.lower()
216
+ if lowered.startswith("data:"):
217
+ if not lowered.startswith("data:image/") or "," not in url_value:
218
+ raise ValueError(
219
+ "unsupported_content_type:Only image data URLs are supported. "
220
+ "Non-image data payloads are not supported."
221
+ )
222
+ elif not (lowered.startswith("http://") or lowered.startswith("https://")):
223
+ raise ValueError(
224
+ "invalid_image_url:Image inputs must use http(s) URLs or data:image/... URLs."
225
+ )
226
+ image_part: Dict[str, Any] = {"type": "image_url", "image_url": {"url": url_value}}
227
+ if detail is not None:
228
+ if not isinstance(detail, str) or not detail.strip():
229
+ raise ValueError("invalid_content_part:Image detail must be a non-empty string when provided.")
230
+ image_part["image_url"]["detail"] = detail.strip()
231
+ normalized_parts.append(image_part)
232
+ continue
233
+
234
+ if part_type in _FILE_PART_TYPES:
235
+ raise ValueError(
236
+ "unsupported_content_type:Inline image inputs are supported, "
237
+ "but uploaded files and document inputs are not supported on this endpoint."
238
+ )
239
+
240
+ # Unknown part type — reject explicitly so clients get a clear error
241
+ # instead of a silently dropped turn.
242
+ raise ValueError(
243
+ f"unsupported_content_type:Unsupported content part type {raw_type!r}. "
244
+ "Only text and image_url/input_image parts are supported."
245
+ )
246
+
247
+ if not normalized_parts:
248
+ return ""
249
+
250
+ # Text-only: collapse to a plain string so downstream logging/trajectory
251
+ # code sees the native shape and prompt caching on text-only turns is
252
+ # unaffected.
253
+ if all(p.get("type") == "text" for p in normalized_parts):
254
+ return "\n".join(p["text"] for p in normalized_parts if p.get("text"))
255
+
256
+ return normalized_parts
257
+
258
+
259
+ def _content_has_visible_payload(content: Any) -> bool:
260
+ """True when content has any text or image attachment. Used to reject empty turns."""
261
+ if isinstance(content, str):
262
+ return bool(content.strip())
263
+ if isinstance(content, list):
264
+ for part in content:
265
+ if isinstance(part, dict):
266
+ ptype = str(part.get("type") or "").strip().lower()
267
+ if ptype in _TEXT_PART_TYPES and str(part.get("text") or "").strip():
268
+ return True
269
+ if ptype in _IMAGE_PART_TYPES:
270
+ return True
271
+ return False
272
+
273
+
274
+ def _multimodal_validation_error(exc: ValueError, *, param: str) -> "web.Response":
275
+ """Translate a ``_normalize_multimodal_content`` ValueError into a 400 response."""
276
+ raw = str(exc)
277
+ code, _, message = raw.partition(":")
278
+ if not message:
279
+ code, message = "invalid_content_part", raw
280
+ return web.json_response(
281
+ _openai_error(message, code=code, param=param),
282
+ status=400,
283
+ )
284
+
285
+
286
+ def check_api_server_requirements() -> bool:
287
+ """Check if API server dependencies are available."""
288
+ return AIOHTTP_AVAILABLE
289
+
290
+
291
+ class ResponseStore:
292
+ """
293
+ SQLite-backed LRU store for Responses API state.
294
+
295
+ Each stored response includes the full internal conversation history
296
+ (with tool calls and results) so it can be reconstructed on subsequent
297
+ requests via previous_response_id.
298
+
299
+ Persists across gateway restarts. Falls back to in-memory SQLite
300
+ if the on-disk path is unavailable.
301
+ """
302
+
303
+ def __init__(self, max_size: int = MAX_STORED_RESPONSES, db_path: str = None):
304
+ self._max_size = max_size
305
+ if db_path is None:
306
+ try:
307
+ from hermes_cli.config import get_hermes_home
308
+ db_path = str(get_hermes_home() / "response_store.db")
309
+ except Exception:
310
+ db_path = ":memory:"
311
+ try:
312
+ self._conn = sqlite3.connect(db_path, check_same_thread=False)
313
+ except Exception:
314
+ self._conn = sqlite3.connect(":memory:", check_same_thread=False)
315
+ # Use shared WAL-fallback helper so response_store.db degrades
316
+ # gracefully on NFS/SMB/FUSE-mounted HERMES_HOME (same filesystem
317
+ # issue addressed for state.db/kanban.db — see
318
+ # calvyn_state._WAL_INCOMPAT_MARKERS).
319
+ from calvyn_state import apply_wal_with_fallback
320
+ apply_wal_with_fallback(self._conn, db_label="response_store.db")
321
+ self._conn.execute(
322
+ """CREATE TABLE IF NOT EXISTS responses (
323
+ response_id TEXT PRIMARY KEY,
324
+ data TEXT NOT NULL,
325
+ accessed_at REAL NOT NULL
326
+ )"""
327
+ )
328
+ self._conn.execute(
329
+ """CREATE TABLE IF NOT EXISTS conversations (
330
+ name TEXT PRIMARY KEY,
331
+ response_id TEXT NOT NULL
332
+ )"""
333
+ )
334
+ self._conn.commit()
335
+
336
+ def get(self, response_id: str) -> Optional[Dict[str, Any]]:
337
+ """Retrieve a stored response by ID (updates access time for LRU)."""
338
+ row = self._conn.execute(
339
+ "SELECT data FROM responses WHERE response_id = ?", (response_id,)
340
+ ).fetchone()
341
+ if row is None:
342
+ return None
343
+ self._conn.execute(
344
+ "UPDATE responses SET accessed_at = ? WHERE response_id = ?",
345
+ (time.time(), response_id),
346
+ )
347
+ self._conn.commit()
348
+ return json.loads(row[0])
349
+
350
+ def put(self, response_id: str, data: Dict[str, Any]) -> None:
351
+ """Store a response, evicting the oldest if at capacity."""
352
+ self._conn.execute(
353
+ "INSERT OR REPLACE INTO responses (response_id, data, accessed_at) VALUES (?, ?, ?)",
354
+ (response_id, json.dumps(data, default=str), time.time()),
355
+ )
356
+ # Evict oldest entries beyond max_size
357
+ count = self._conn.execute("SELECT COUNT(*) FROM responses").fetchone()[0]
358
+ if count > self._max_size:
359
+ # Collect IDs that will be evicted
360
+ evict_ids = [
361
+ row[0]
362
+ for row in self._conn.execute(
363
+ "SELECT response_id FROM responses ORDER BY accessed_at ASC LIMIT ?",
364
+ (count - self._max_size,),
365
+ ).fetchall()
366
+ ]
367
+ if evict_ids:
368
+ placeholders = ",".join("?" for _ in evict_ids)
369
+ # Clear conversation mappings pointing to evicted responses
370
+ self._conn.execute(
371
+ f"DELETE FROM conversations WHERE response_id IN ({placeholders})",
372
+ evict_ids,
373
+ )
374
+ # Delete evicted responses
375
+ self._conn.execute(
376
+ f"DELETE FROM responses WHERE response_id IN ({placeholders})",
377
+ evict_ids,
378
+ )
379
+ self._conn.commit()
380
+
381
+ def delete(self, response_id: str) -> bool:
382
+ """Remove a response from the store. Returns True if found and deleted."""
383
+ # Clear conversation mappings pointing to this response
384
+ self._conn.execute(
385
+ "DELETE FROM conversations WHERE response_id = ?", (response_id,)
386
+ )
387
+ cursor = self._conn.execute(
388
+ "DELETE FROM responses WHERE response_id = ?", (response_id,)
389
+ )
390
+ self._conn.commit()
391
+ return cursor.rowcount > 0
392
+
393
+ def get_conversation(self, name: str) -> Optional[str]:
394
+ """Get the latest response_id for a conversation name."""
395
+ row = self._conn.execute(
396
+ "SELECT response_id FROM conversations WHERE name = ?", (name,)
397
+ ).fetchone()
398
+ return row[0] if row else None
399
+
400
+ def set_conversation(self, name: str, response_id: str) -> None:
401
+ """Map a conversation name to its latest response_id."""
402
+ self._conn.execute(
403
+ "INSERT OR REPLACE INTO conversations (name, response_id) VALUES (?, ?)",
404
+ (name, response_id),
405
+ )
406
+ self._conn.commit()
407
+
408
+ def close(self) -> None:
409
+ """Close the database connection."""
410
+ try:
411
+ self._conn.close()
412
+ except Exception:
413
+ pass
414
+
415
+ def __len__(self) -> int:
416
+ row = self._conn.execute("SELECT COUNT(*) FROM responses").fetchone()
417
+ return row[0] if row else 0
418
+
419
+
420
+ # ---------------------------------------------------------------------------
421
+ # CORS middleware
422
+ # ---------------------------------------------------------------------------
423
+
424
+ _CORS_HEADERS = {
425
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
426
+ "Access-Control-Allow-Headers": "Authorization, Content-Type, Idempotency-Key",
427
+ }
428
+
429
+
430
+ if AIOHTTP_AVAILABLE:
431
+ @web.middleware
432
+ async def cors_middleware(request, handler):
433
+ """Add CORS headers for explicitly allowed origins; handle OPTIONS preflight."""
434
+ adapter = request.app.get("api_server_adapter")
435
+ origin = request.headers.get("Origin", "")
436
+ cors_headers = None
437
+ if adapter is not None:
438
+ if not adapter._origin_allowed(origin):
439
+ return web.Response(status=403)
440
+ cors_headers = adapter._cors_headers_for_origin(origin)
441
+
442
+ if request.method == "OPTIONS":
443
+ if cors_headers is None:
444
+ return web.Response(status=403)
445
+ return web.Response(status=200, headers=cors_headers)
446
+
447
+ response = await handler(request)
448
+ if cors_headers is not None:
449
+ response.headers.update(cors_headers)
450
+ return response
451
+ else:
452
+ cors_middleware = None # type: ignore[assignment]
453
+
454
+
455
+ def _openai_error(message: str, err_type: str = "invalid_request_error", param: str = None, code: str = None) -> Dict[str, Any]:
456
+ """OpenAI-style error envelope."""
457
+ return {
458
+ "error": {
459
+ "message": message,
460
+ "type": err_type,
461
+ "param": param,
462
+ "code": code,
463
+ }
464
+ }
465
+
466
+
467
+ if AIOHTTP_AVAILABLE:
468
+ @web.middleware
469
+ async def body_limit_middleware(request, handler):
470
+ """Reject overly large request bodies early based on Content-Length."""
471
+ if request.method in {"POST", "PUT", "PATCH"}:
472
+ cl = request.headers.get("Content-Length")
473
+ if cl is not None:
474
+ try:
475
+ if int(cl) > MAX_REQUEST_BYTES:
476
+ return web.json_response(_openai_error("Request body too large.", code="body_too_large"), status=413)
477
+ except ValueError:
478
+ return web.json_response(_openai_error("Invalid Content-Length header.", code="invalid_content_length"), status=400)
479
+ return await handler(request)
480
+ else:
481
+ body_limit_middleware = None # type: ignore[assignment]
482
+
483
+ _SECURITY_HEADERS = {
484
+ "X-Content-Type-Options": "nosniff",
485
+ "Referrer-Policy": "no-referrer",
486
+ }
487
+
488
+
489
+ if AIOHTTP_AVAILABLE:
490
+ @web.middleware
491
+ async def security_headers_middleware(request, handler):
492
+ """Add security headers to all responses (including errors)."""
493
+ response = await handler(request)
494
+ for k, v in _SECURITY_HEADERS.items():
495
+ response.headers.setdefault(k, v)
496
+ return response
497
+ else:
498
+ security_headers_middleware = None # type: ignore[assignment]
499
+
500
+
501
+ class _IdempotencyCache:
502
+ """In-memory idempotency cache with TTL and basic LRU semantics."""
503
+ def __init__(self, max_items: int = 1000, ttl_seconds: int = 300):
504
+ from collections import OrderedDict
505
+ self._store = OrderedDict()
506
+ self._inflight: Dict[tuple[str, str], "asyncio.Task[Any]"] = {}
507
+ self._ttl = ttl_seconds
508
+ self._max = max_items
509
+
510
+ def _purge(self):
511
+ now = time.time()
512
+ expired = [k for k, v in self._store.items() if now - v["ts"] > self._ttl]
513
+ for k in expired:
514
+ self._store.pop(k, None)
515
+ while len(self._store) > self._max:
516
+ self._store.popitem(last=False)
517
+
518
+ async def get_or_set(self, key: str, fingerprint: str, compute_coro):
519
+ self._purge()
520
+ item = self._store.get(key)
521
+ if item and item["fp"] == fingerprint:
522
+ return item["resp"]
523
+
524
+ inflight_key = (key, fingerprint)
525
+ task = self._inflight.get(inflight_key)
526
+ if task is None:
527
+ async def _compute_and_store():
528
+ resp = await compute_coro()
529
+ import time as _t
530
+ self._store[key] = {"resp": resp, "fp": fingerprint, "ts": _t.time()}
531
+ self._purge()
532
+ return resp
533
+
534
+ task = asyncio.create_task(_compute_and_store())
535
+ self._inflight[inflight_key] = task
536
+
537
+ def _clear_inflight(done_task: "asyncio.Task[Any]") -> None:
538
+ if self._inflight.get(inflight_key) is done_task:
539
+ self._inflight.pop(inflight_key, None)
540
+
541
+ task.add_done_callback(_clear_inflight)
542
+
543
+ return await asyncio.shield(task)
544
+
545
+
546
+ _idem_cache = _IdempotencyCache()
547
+
548
+
549
+ def _make_request_fingerprint(body: Dict[str, Any], keys: List[str]) -> str:
550
+ from hashlib import sha256
551
+ subset = {k: body.get(k) for k in keys}
552
+ return sha256(repr(subset).encode("utf-8")).hexdigest()
553
+
554
+
555
+ def _derive_chat_session_id(
556
+ system_prompt: Optional[str],
557
+ first_user_message: str,
558
+ ) -> str:
559
+ """Derive a stable session ID from the conversation's first user message.
560
+
561
+ OpenAI-compatible frontends (Open WebUI, LibreChat, etc.) send the full
562
+ conversation history with every request. The system prompt and first user
563
+ message are constant across all turns of the same conversation, so hashing
564
+ them produces a deterministic session ID that lets the API server reuse
565
+ the same Hermes session (and therefore the same Docker container sandbox
566
+ directory) across turns.
567
+ """
568
+ seed = f"{system_prompt or ''}\n{first_user_message}"
569
+ digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:16]
570
+ return f"api-{digest}"
571
+
572
+
573
+ _CRON_AVAILABLE = False
574
+ try:
575
+ from cron.jobs import (
576
+ list_jobs as _cron_list,
577
+ get_job as _cron_get,
578
+ create_job as _cron_create,
579
+ update_job as _cron_update,
580
+ remove_job as _cron_remove,
581
+ pause_job as _cron_pause,
582
+ resume_job as _cron_resume,
583
+ trigger_job as _cron_trigger,
584
+ )
585
+ _CRON_AVAILABLE = True
586
+ except ImportError:
587
+ _cron_list = None
588
+ _cron_get = None
589
+ _cron_create = None
590
+ _cron_update = None
591
+ _cron_remove = None
592
+ _cron_pause = None
593
+ _cron_resume = None
594
+ _cron_trigger = None
595
+
596
+
597
+ class APIServerAdapter(BasePlatformAdapter):
598
+ """
599
+ OpenAI-compatible HTTP API server adapter.
600
+
601
+ Runs an aiohttp web server that accepts OpenAI-format requests
602
+ and routes them through hermes-agent's AIAgent.
603
+ """
604
+
605
+ def __init__(self, config: PlatformConfig):
606
+ super().__init__(config, Platform.API_SERVER)
607
+ extra = config.extra or {}
608
+ self._host: str = extra.get("host", os.getenv("API_SERVER_HOST", DEFAULT_HOST))
609
+ raw_port = extra.get("port")
610
+ if raw_port is None:
611
+ raw_port = os.getenv("API_SERVER_PORT", str(DEFAULT_PORT))
612
+ self._port: int = _coerce_port(raw_port, DEFAULT_PORT)
613
+ self._api_key: str = extra.get("key", os.getenv("API_SERVER_KEY", ""))
614
+ self._cors_origins: tuple[str, ...] = self._parse_cors_origins(
615
+ extra.get("cors_origins", os.getenv("API_SERVER_CORS_ORIGINS", "")),
616
+ )
617
+ self._model_name: str = self._resolve_model_name(
618
+ extra.get("model_name", os.getenv("API_SERVER_MODEL_NAME", "")),
619
+ )
620
+ self._app: Optional["web.Application"] = None
621
+ self._runner: Optional["web.AppRunner"] = None
622
+ self._site: Optional["web.TCPSite"] = None
623
+ self._response_store = ResponseStore()
624
+ # Active run streams: run_id -> asyncio.Queue of SSE event dicts
625
+ self._run_streams: Dict[str, "asyncio.Queue[Optional[Dict]]"] = {}
626
+ # Creation timestamps for orphaned-run TTL sweep
627
+ self._run_streams_created: Dict[str, float] = {}
628
+ # Active run agent/task references for stop support
629
+ self._active_run_agents: Dict[str, Any] = {}
630
+ self._active_run_tasks: Dict[str, "asyncio.Task"] = {}
631
+ # Pollable run status for dashboards and external control-plane UIs.
632
+ self._run_statuses: Dict[str, Dict[str, Any]] = {}
633
+ # Active approval session key for each run_id. The approval core
634
+ # resolves requests by session key, while API clients address the
635
+ # in-flight run by run_id.
636
+ self._run_approval_sessions: Dict[str, str] = {}
637
+ self._session_db: Optional[Any] = None # Lazy-init SessionDB for session continuity
638
+
639
+ @staticmethod
640
+ def _parse_cors_origins(value: Any) -> tuple[str, ...]:
641
+ """Normalize configured CORS origins into a stable tuple."""
642
+ if not value:
643
+ return ()
644
+
645
+ if isinstance(value, str):
646
+ items = value.split(",")
647
+ elif isinstance(value, (list, tuple, set)):
648
+ items = value
649
+ else:
650
+ items = [str(value)]
651
+
652
+ return tuple(str(item).strip() for item in items if str(item).strip())
653
+
654
+ @staticmethod
655
+ def _resolve_model_name(explicit: str) -> str:
656
+ """Derive the advertised model name for /v1/models.
657
+
658
+ Priority:
659
+ 1. Explicit override (config extra or API_SERVER_MODEL_NAME env var)
660
+ 2. Active profile name (so each profile advertises a distinct model)
661
+ 3. Fallback: "hermes-agent"
662
+ """
663
+ if explicit and explicit.strip():
664
+ return explicit.strip()
665
+ try:
666
+ from hermes_cli.profiles import get_active_profile_name
667
+ profile = get_active_profile_name()
668
+ if profile and profile not in {"default", "custom"}:
669
+ return profile
670
+ except Exception:
671
+ pass
672
+ return "hermes-agent"
673
+
674
+ def _cors_headers_for_origin(self, origin: str) -> Optional[Dict[str, str]]:
675
+ """Return CORS headers for an allowed browser origin."""
676
+ if not origin or not self._cors_origins:
677
+ return None
678
+
679
+ if "*" in self._cors_origins:
680
+ headers = dict(_CORS_HEADERS)
681
+ headers["Access-Control-Allow-Origin"] = "*"
682
+ headers["Access-Control-Max-Age"] = "600"
683
+ return headers
684
+
685
+ if origin not in self._cors_origins:
686
+ return None
687
+
688
+ headers = dict(_CORS_HEADERS)
689
+ headers["Access-Control-Allow-Origin"] = origin
690
+ headers["Vary"] = "Origin"
691
+ headers["Access-Control-Max-Age"] = "600"
692
+ return headers
693
+
694
+ def _origin_allowed(self, origin: str) -> bool:
695
+ """Allow non-browser clients and explicitly configured browser origins."""
696
+ if not origin:
697
+ return True
698
+
699
+ if not self._cors_origins:
700
+ return False
701
+
702
+ return "*" in self._cors_origins or origin in self._cors_origins
703
+
704
+ # ------------------------------------------------------------------
705
+ # Auth helper
706
+ # ------------------------------------------------------------------
707
+
708
+ def _check_auth(self, request: "web.Request") -> Optional["web.Response"]:
709
+ """
710
+ Validate Bearer token from Authorization header.
711
+
712
+ Returns None if auth is OK, or a 401 web.Response on failure.
713
+ If no API key is configured, all requests are allowed (only when API
714
+ server is local).
715
+ """
716
+ if not self._api_key:
717
+ return None # No key configured — allow all (local-only use)
718
+
719
+ auth_header = request.headers.get("Authorization", "")
720
+ if auth_header.startswith("Bearer "):
721
+ token = auth_header[7:].strip()
722
+ if hmac.compare_digest(token, self._api_key):
723
+ return None # Auth OK
724
+
725
+ return web.json_response(
726
+ {"error": {"message": "Invalid API key", "type": "invalid_request_error", "code": "invalid_api_key"}},
727
+ status=401,
728
+ )
729
+
730
+ # ------------------------------------------------------------------
731
+ # Session header helpers
732
+ # ------------------------------------------------------------------
733
+
734
+ # Soft length cap for session identifiers. Headers are bounded in
735
+ # aggregate by aiohttp (``client_max_size`` / default 8 KiB per
736
+ # header), but we impose a tighter limit on the session headers so a
737
+ # caller can't burn memory by passing a multi-kilobyte "session key".
738
+ # 256 chars is well above any realistic stable channel identifier
739
+ # (e.g. ``agent:main:webui:dm:user-42``) while staying small enough
740
+ # that the sanitized form is safe to pass into Honcho / state.db.
741
+ _MAX_SESSION_HEADER_LEN = 256
742
+
743
+ def _parse_session_key_header(
744
+ self, request: "web.Request"
745
+ ) -> tuple[Optional[str], Optional["web.Response"]]:
746
+ """Extract and validate the ``X-Hermes-Session-Key`` header.
747
+
748
+ The session key is a stable per-channel identifier that scopes
749
+ long-term memory (e.g. Honcho sessions) across transcripts. It
750
+ is independent of ``X-Hermes-Session-Id``: callers may send
751
+ either, both, or neither.
752
+
753
+ Returns ``(session_key, None)`` on success (with an empty/absent
754
+ header yielding ``None`` for the key), or ``(None, error_response)``
755
+ on validation failure.
756
+
757
+ Security: like session continuation, accepting a caller-supplied
758
+ memory scope requires API-key authentication so that an
759
+ unauthenticated client on a local-only server can't inject itself
760
+ into another user's long-term memory scope by guessing a key.
761
+ """
762
+ raw = request.headers.get("X-Hermes-Session-Key", "").strip()
763
+ if not raw:
764
+ return None, None
765
+
766
+ if not self._api_key:
767
+ logger.warning(
768
+ "X-Hermes-Session-Key rejected: no API key configured. "
769
+ "Set API_SERVER_KEY to enable long-term memory scoping."
770
+ )
771
+ return None, web.json_response(
772
+ _openai_error(
773
+ "X-Hermes-Session-Key requires API key authentication. "
774
+ "Configure API_SERVER_KEY to enable this feature."
775
+ ),
776
+ status=403,
777
+ )
778
+
779
+ # Reject control characters that could enable header injection on
780
+ # the echo path.
781
+ if re.search(r'[\r\n\x00]', raw):
782
+ return None, web.json_response(
783
+ {"error": {"message": "Invalid session key", "type": "invalid_request_error"}},
784
+ status=400,
785
+ )
786
+
787
+ if len(raw) > self._MAX_SESSION_HEADER_LEN:
788
+ return None, web.json_response(
789
+ {"error": {"message": "Session key too long", "type": "invalid_request_error"}},
790
+ status=400,
791
+ )
792
+
793
+ return raw, None
794
+
795
+ # ------------------------------------------------------------------
796
+ # Session DB helper
797
+ # ------------------------------------------------------------------
798
+
799
+ def _ensure_session_db(self):
800
+ """Lazily initialise and return the shared SessionDB instance.
801
+
802
+ Sessions are persisted to ``state.db`` so that ``hermes sessions list``
803
+ shows API-server conversations alongside CLI and gateway ones.
804
+ """
805
+ if self._session_db is None:
806
+ try:
807
+ from calvyn_state import SessionDB
808
+ self._session_db = SessionDB()
809
+ except Exception as e:
810
+ logger.debug("SessionDB unavailable for API server: %s", e)
811
+ return self._session_db
812
+
813
+ # ------------------------------------------------------------------
814
+ # Agent creation helper
815
+ # ------------------------------------------------------------------
816
+
817
+ def _create_agent(
818
+ self,
819
+ ephemeral_system_prompt: Optional[str] = None,
820
+ session_id: Optional[str] = None,
821
+ stream_delta_callback=None,
822
+ tool_progress_callback=None,
823
+ tool_start_callback=None,
824
+ tool_complete_callback=None,
825
+ gateway_session_key: Optional[str] = None,
826
+ ) -> Any:
827
+ """
828
+ Create an AIAgent instance using the gateway's runtime config.
829
+
830
+ Uses _resolve_runtime_agent_kwargs() to pick up model, api_key,
831
+ base_url, etc. from config.yaml / env vars. Toolsets are resolved
832
+ from config.yaml platform_toolsets.api_server (same as all other
833
+ gateway platforms), falling back to the hermes-api-server default.
834
+
835
+ ``gateway_session_key`` is a stable per-channel identifier supplied
836
+ by the client (via ``X-Hermes-Session-Key``). Unlike ``session_id``
837
+ which scopes the short-term transcript and rotates on /new, this
838
+ key is meant to persist across transcripts so long-term memory
839
+ providers (e.g. Honcho) can scope their per-chat state correctly
840
+ — matching the semantics of the native gateway's ``session_key``.
841
+ """
842
+ from run_agent import AIAgent
843
+ from gateway.run import _resolve_runtime_agent_kwargs, _resolve_gateway_model, _load_gateway_config, GatewayRunner
844
+ from hermes_cli.tools_config import _get_platform_tools
845
+
846
+ runtime_kwargs = _resolve_runtime_agent_kwargs()
847
+ reasoning_config = GatewayRunner._load_reasoning_config()
848
+ model = _resolve_gateway_model()
849
+
850
+ user_config = _load_gateway_config()
851
+ enabled_toolsets = sorted(_get_platform_tools(user_config, "api_server"))
852
+
853
+ max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
854
+
855
+ # Load fallback provider chain so the API server platform has the
856
+ # same fallback behaviour as Telegram/Discord/Slack (fixes #4954).
857
+ fallback_model = GatewayRunner._load_fallback_model()
858
+
859
+ agent = AIAgent(
860
+ model=model,
861
+ **runtime_kwargs,
862
+ max_iterations=max_iterations,
863
+ quiet_mode=True,
864
+ verbose_logging=False,
865
+ ephemeral_system_prompt=ephemeral_system_prompt or None,
866
+ enabled_toolsets=enabled_toolsets,
867
+ session_id=session_id,
868
+ platform="api_server",
869
+ stream_delta_callback=stream_delta_callback,
870
+ tool_progress_callback=tool_progress_callback,
871
+ tool_start_callback=tool_start_callback,
872
+ tool_complete_callback=tool_complete_callback,
873
+ session_db=self._ensure_session_db(),
874
+ fallback_model=fallback_model,
875
+ reasoning_config=reasoning_config,
876
+ gateway_session_key=gateway_session_key,
877
+ )
878
+ return agent
879
+
880
+ # ------------------------------------------------------------------
881
+ # HTTP Handlers
882
+ # ------------------------------------------------------------------
883
+
884
+ async def _handle_health(self, request: "web.Request") -> "web.Response":
885
+ """GET /health — simple health check."""
886
+ return web.json_response({"status": "ok", "platform": "hermes-agent"})
887
+
888
+ async def _handle_health_detailed(self, request: "web.Request") -> "web.Response":
889
+ """GET /health/detailed — rich status for cross-container dashboard probing.
890
+
891
+ Returns gateway state, connected platforms, PID, and uptime so the
892
+ dashboard can display full status without needing a shared PID file or
893
+ /proc access. No authentication required.
894
+ """
895
+ from gateway.status import read_runtime_status
896
+
897
+ runtime = read_runtime_status() or {}
898
+ return web.json_response({
899
+ "status": "ok",
900
+ "platform": "hermes-agent",
901
+ "gateway_state": runtime.get("gateway_state"),
902
+ "platforms": runtime.get("platforms", {}),
903
+ "active_agents": runtime.get("active_agents", 0),
904
+ "exit_reason": runtime.get("exit_reason"),
905
+ "updated_at": runtime.get("updated_at"),
906
+ "pid": os.getpid(),
907
+ })
908
+
909
+ async def _handle_models(self, request: "web.Request") -> "web.Response":
910
+ """GET /v1/models — return hermes-agent as an available model."""
911
+ auth_err = self._check_auth(request)
912
+ if auth_err:
913
+ return auth_err
914
+
915
+ return web.json_response({
916
+ "object": "list",
917
+ "data": [
918
+ {
919
+ "id": self._model_name,
920
+ "object": "model",
921
+ "created": int(time.time()),
922
+ "owned_by": "hermes",
923
+ "permission": [],
924
+ "root": self._model_name,
925
+ "parent": None,
926
+ }
927
+ ],
928
+ })
929
+
930
+ async def _handle_capabilities(self, request: "web.Request") -> "web.Response":
931
+ """GET /v1/capabilities — advertise the stable API surface.
932
+
933
+ External UIs and orchestrators use this endpoint to discover the API
934
+ server's plugin-safe contract without scraping docs or assuming that
935
+ every Hermes version exposes the same endpoints.
936
+ """
937
+ auth_err = self._check_auth(request)
938
+ if auth_err:
939
+ return auth_err
940
+
941
+ return web.json_response({
942
+ "object": "hermes.api_server.capabilities",
943
+ "platform": "hermes-agent",
944
+ "model": self._model_name,
945
+ "auth": {
946
+ "type": "bearer",
947
+ "required": bool(self._api_key),
948
+ },
949
+ "runtime": {
950
+ "mode": "server_agent",
951
+ "tool_execution": "server",
952
+ "split_runtime": False,
953
+ "description": (
954
+ "The API server creates a server-side Hermes AIAgent; "
955
+ "tools execute on the API-server host unless a future "
956
+ "explicit split-runtime mode is enabled."
957
+ ),
958
+ },
959
+ "features": {
960
+ "chat_completions": True,
961
+ "chat_completions_streaming": True,
962
+ "responses_api": True,
963
+ "responses_streaming": True,
964
+ "run_submission": True,
965
+ "run_status": True,
966
+ "run_events_sse": True,
967
+ "run_stop": True,
968
+ "run_approval_response": True,
969
+ "tool_progress_events": True,
970
+ "approval_events": True,
971
+ "session_continuity_header": "X-Hermes-Session-Id",
972
+ "session_key_header": "X-Hermes-Session-Key",
973
+ "cors": bool(self._cors_origins),
974
+ },
975
+ "endpoints": {
976
+ "health": {"method": "GET", "path": "/health"},
977
+ "health_detailed": {"method": "GET", "path": "/health/detailed"},
978
+ "models": {"method": "GET", "path": "/v1/models"},
979
+ "chat_completions": {"method": "POST", "path": "/v1/chat/completions"},
980
+ "responses": {"method": "POST", "path": "/v1/responses"},
981
+ "runs": {"method": "POST", "path": "/v1/runs"},
982
+ "run_status": {"method": "GET", "path": "/v1/runs/{run_id}"},
983
+ "run_events": {"method": "GET", "path": "/v1/runs/{run_id}/events"},
984
+ "run_approval": {"method": "POST", "path": "/v1/runs/{run_id}/approval"},
985
+ "run_stop": {"method": "POST", "path": "/v1/runs/{run_id}/stop"},
986
+ },
987
+ })
988
+
989
+ async def _handle_chat_completions(self, request: "web.Request") -> "web.Response":
990
+ """POST /v1/chat/completions — OpenAI Chat Completions format."""
991
+ auth_err = self._check_auth(request)
992
+ if auth_err:
993
+ return auth_err
994
+
995
+ # Parse request body
996
+ try:
997
+ body = await request.json()
998
+ except (json.JSONDecodeError, Exception):
999
+ return web.json_response(_openai_error("Invalid JSON in request body"), status=400)
1000
+
1001
+ messages = body.get("messages")
1002
+ if not messages or not isinstance(messages, list):
1003
+ return web.json_response(
1004
+ {"error": {"message": "Missing or invalid 'messages' field", "type": "invalid_request_error"}},
1005
+ status=400,
1006
+ )
1007
+
1008
+ stream = body.get("stream", False)
1009
+
1010
+ # Extract system message (becomes ephemeral system prompt layered ON TOP of core)
1011
+ system_prompt = None
1012
+ conversation_messages: List[Dict[str, str]] = []
1013
+
1014
+ for idx, msg in enumerate(messages):
1015
+ role = msg.get("role", "")
1016
+ raw_content = msg.get("content", "")
1017
+ if role == "system":
1018
+ # System messages don't support images (Anthropic rejects, OpenAI
1019
+ # text-model systems don't render them). Flatten to text.
1020
+ content = _normalize_chat_content(raw_content)
1021
+ if system_prompt is None:
1022
+ system_prompt = content
1023
+ else:
1024
+ system_prompt = system_prompt + "\n" + content
1025
+ elif role in {"user", "assistant"}:
1026
+ try:
1027
+ content = _normalize_multimodal_content(raw_content)
1028
+ except ValueError as exc:
1029
+ return _multimodal_validation_error(exc, param=f"messages[{idx}].content")
1030
+ conversation_messages.append({"role": role, "content": content})
1031
+
1032
+ # Extract the last user message as the primary input
1033
+ user_message: Any = ""
1034
+ history = []
1035
+ if conversation_messages:
1036
+ user_message = conversation_messages[-1].get("content", "")
1037
+ history = conversation_messages[:-1]
1038
+
1039
+ if not _content_has_visible_payload(user_message):
1040
+ return web.json_response(
1041
+ {"error": {"message": "No user message found in messages", "type": "invalid_request_error"}},
1042
+ status=400,
1043
+ )
1044
+
1045
+ # Allow caller to scope long-term memory (e.g. Honcho) with a
1046
+ # stable per-channel identifier via X-Hermes-Session-Key. This
1047
+ # is independent of X-Hermes-Session-Id: the key persists across
1048
+ # transcripts while the id rotates when the caller starts a new
1049
+ # transcript (i.e. /new semantics). See _parse_session_key_header.
1050
+ gateway_session_key, key_err = self._parse_session_key_header(request)
1051
+ if key_err is not None:
1052
+ return key_err
1053
+
1054
+ # Allow caller to continue an existing session by passing X-Hermes-Session-Id.
1055
+ # When provided, history is loaded from state.db instead of from the request body.
1056
+ #
1057
+ # Security: session continuation exposes conversation history, so it is
1058
+ # only allowed when the API key is configured and the request is
1059
+ # authenticated. Without this gate, any unauthenticated client could
1060
+ # read arbitrary session history by guessing/enumerating session IDs.
1061
+ provided_session_id = request.headers.get("X-Hermes-Session-Id", "").strip()
1062
+ if provided_session_id:
1063
+ if not self._api_key:
1064
+ logger.warning(
1065
+ "Session continuation via X-Hermes-Session-Id rejected: "
1066
+ "no API key configured. Set API_SERVER_KEY to enable "
1067
+ "session continuity."
1068
+ )
1069
+ return web.json_response(
1070
+ _openai_error(
1071
+ "Session continuation requires API key authentication. "
1072
+ "Configure API_SERVER_KEY to enable this feature."
1073
+ ),
1074
+ status=403,
1075
+ )
1076
+ # Sanitize: reject control characters that could enable header injection.
1077
+ if re.search(r'[\r\n\x00]', provided_session_id):
1078
+ return web.json_response(
1079
+ {"error": {"message": "Invalid session ID", "type": "invalid_request_error"}},
1080
+ status=400,
1081
+ )
1082
+ session_id = provided_session_id
1083
+ try:
1084
+ db = self._ensure_session_db()
1085
+ if db is not None:
1086
+ history = db.get_messages_as_conversation(session_id)
1087
+ except Exception as e:
1088
+ logger.warning("Failed to load session history for %s: %s", session_id, e)
1089
+ history = []
1090
+ else:
1091
+ # Derive a stable session ID from the conversation fingerprint so
1092
+ # that consecutive messages from the same Open WebUI (or similar)
1093
+ # conversation map to the same Hermes session. The first user
1094
+ # message + system prompt are constant across all turns.
1095
+ first_user = ""
1096
+ for cm in conversation_messages:
1097
+ if cm.get("role") == "user":
1098
+ first_user = cm.get("content", "")
1099
+ break
1100
+ session_id = _derive_chat_session_id(system_prompt, first_user)
1101
+ # history already set from request body above
1102
+
1103
+ completion_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
1104
+ model_name = body.get("model", self._model_name)
1105
+ created = int(time.time())
1106
+
1107
+ if stream:
1108
+ import queue as _q
1109
+ _stream_q: _q.Queue = _q.Queue()
1110
+
1111
+ def _on_delta(delta):
1112
+ # Filter out None — the agent fires stream_delta_callback(None)
1113
+ # to signal the CLI display to close its response box before
1114
+ # tool execution, but the SSE writer uses None as end-of-stream
1115
+ # sentinel. Forwarding it would prematurely close the HTTP
1116
+ # response, causing Open WebUI (and similar frontends) to miss
1117
+ # the final answer after tool calls. The SSE loop detects
1118
+ # completion via agent_task.done() instead.
1119
+ if delta is not None:
1120
+ _stream_q.put(delta)
1121
+
1122
+ # Track which tool_call_ids we've emitted a "running" lifecycle
1123
+ # event for, so a "completed" event without a matching "running"
1124
+ # (e.g. internal/filtered tools) is silently dropped instead of
1125
+ # producing an orphaned event clients can't correlate.
1126
+ _started_tool_call_ids: set[str] = set()
1127
+
1128
+ def _on_tool_start(tool_call_id, function_name, function_args):
1129
+ """Emit ``hermes.tool.progress`` with ``status: running``.
1130
+
1131
+ Replaces the old ``tool_progress_callback("tool.started",
1132
+ ...)`` emit so SSE consumers receive a single event per
1133
+ tool start, carrying both the legacy ``tool``/``emoji``/
1134
+ ``label`` payload (for #6972 frontends) and the new
1135
+ ``toolCallId``/``status`` correlation fields (#16588).
1136
+
1137
+ Skips tools whose names start with ``_`` so internal
1138
+ events (``_thinking``, …) stay off the wire — matching
1139
+ the prior ``_on_tool_progress`` filter exactly.
1140
+ """
1141
+ if not tool_call_id or function_name.startswith("_"):
1142
+ return
1143
+ _started_tool_call_ids.add(tool_call_id)
1144
+ from agent.display import build_tool_preview, get_tool_emoji
1145
+ label = build_tool_preview(function_name, function_args) or function_name
1146
+ _stream_q.put(("__tool_progress__", {
1147
+ "tool": function_name,
1148
+ "emoji": get_tool_emoji(function_name),
1149
+ "label": label,
1150
+ "toolCallId": tool_call_id,
1151
+ "status": "running",
1152
+ }))
1153
+
1154
+ def _on_tool_complete(tool_call_id, function_name, function_args, function_result):
1155
+ """Emit the matching ``status: completed`` event.
1156
+
1157
+ Dropped if the start was filtered (internal tool, missing
1158
+ id, or never seen) so clients never get an orphaned
1159
+ ``completed`` they can't correlate to a prior ``running``.
1160
+ """
1161
+ if not tool_call_id or tool_call_id not in _started_tool_call_ids:
1162
+ return
1163
+ _started_tool_call_ids.discard(tool_call_id)
1164
+ _stream_q.put(("__tool_progress__", {
1165
+ "tool": function_name,
1166
+ "toolCallId": tool_call_id,
1167
+ "status": "completed",
1168
+ }))
1169
+
1170
+ # Start agent in background. agent_ref is a mutable container
1171
+ # so the SSE writer can interrupt the agent on client disconnect.
1172
+ #
1173
+ # ``tool_progress_callback`` is intentionally not wired here:
1174
+ # it would duplicate every emit because ``run_agent`` fires it
1175
+ # side-by-side with ``tool_start_callback``/``tool_complete_callback``.
1176
+ # The structured callbacks are strictly richer (they carry the
1177
+ # tool_call id), so they own the chat-completions SSE channel.
1178
+ agent_ref = [None]
1179
+ agent_task = asyncio.ensure_future(self._run_agent(
1180
+ user_message=user_message,
1181
+ conversation_history=history,
1182
+ ephemeral_system_prompt=system_prompt,
1183
+ session_id=session_id,
1184
+ stream_delta_callback=_on_delta,
1185
+ tool_start_callback=_on_tool_start,
1186
+ tool_complete_callback=_on_tool_complete,
1187
+ agent_ref=agent_ref,
1188
+ gateway_session_key=gateway_session_key,
1189
+ ))
1190
+ # Ensure SSE drain loops can terminate without relying on polling
1191
+ # agent_task.done(), which can race with queue timeout checks.
1192
+ agent_task.add_done_callback(lambda _fut: _stream_q.put(None))
1193
+
1194
+ return await self._write_sse_chat_completion(
1195
+ request, completion_id, model_name, created, _stream_q,
1196
+ agent_task, agent_ref, session_id=session_id,
1197
+ gateway_session_key=gateway_session_key,
1198
+ )
1199
+
1200
+ # Non-streaming: run the agent (with optional Idempotency-Key)
1201
+ async def _compute_completion():
1202
+ return await self._run_agent(
1203
+ user_message=user_message,
1204
+ conversation_history=history,
1205
+ ephemeral_system_prompt=system_prompt,
1206
+ session_id=session_id,
1207
+ gateway_session_key=gateway_session_key,
1208
+ )
1209
+
1210
+ idempotency_key = request.headers.get("Idempotency-Key")
1211
+ if idempotency_key:
1212
+ fp = _make_request_fingerprint(body, keys=["model", "messages", "tools", "tool_choice", "stream"])
1213
+ try:
1214
+ result, usage = await _idem_cache.get_or_set(idempotency_key, fp, _compute_completion)
1215
+ except Exception as e:
1216
+ logger.error("Error running agent for chat completions: %s", e, exc_info=True)
1217
+ return web.json_response(
1218
+ _openai_error(f"Internal server error: {e}", err_type="server_error"),
1219
+ status=500,
1220
+ )
1221
+ else:
1222
+ try:
1223
+ result, usage = await _compute_completion()
1224
+ except Exception as e:
1225
+ logger.error("Error running agent for chat completions: %s", e, exc_info=True)
1226
+ return web.json_response(
1227
+ _openai_error(f"Internal server error: {e}", err_type="server_error"),
1228
+ status=500,
1229
+ )
1230
+
1231
+ final_response = result.get("final_response") or ""
1232
+ is_partial = bool(result.get("partial"))
1233
+ is_failed = bool(result.get("failed"))
1234
+ completed = bool(result.get("completed", True))
1235
+ err_msg = result.get("error")
1236
+
1237
+ # Decide finish_reason. OpenAI uses "length" for truncation, "stop"
1238
+ # for normal completion, and downstream SDKs accept "error" / custom
1239
+ # codes. See issue #22496.
1240
+ if is_partial and err_msg and "truncat" in err_msg.lower():
1241
+ finish_reason = "length"
1242
+ elif is_failed or (not completed and err_msg):
1243
+ finish_reason = "error"
1244
+ else:
1245
+ finish_reason = "stop"
1246
+
1247
+ response_headers = {
1248
+ "X-Hermes-Session-Id": result.get("session_id", session_id),
1249
+ }
1250
+ if gateway_session_key:
1251
+ response_headers["X-Hermes-Session-Key"] = gateway_session_key
1252
+
1253
+ # Hard-fail path: no usable assistant text AND a real failure → 5xx
1254
+ # with OpenAI-style error envelope so SDK clients raise instead of
1255
+ # silently rendering the internal failure string as message.content.
1256
+ if not final_response and (is_failed or is_partial):
1257
+ err_body = _openai_error(
1258
+ err_msg or "Agent run did not produce a response.",
1259
+ err_type="server_error",
1260
+ code="agent_incomplete",
1261
+ )
1262
+ err_body["error"]["hermes"] = {
1263
+ "completed": completed,
1264
+ "partial": is_partial,
1265
+ "failed": is_failed,
1266
+ }
1267
+ response_headers["X-Hermes-Completed"] = "false"
1268
+ response_headers["X-Hermes-Partial"] = "true" if is_partial else "false"
1269
+ return web.json_response(err_body, status=502, headers=response_headers)
1270
+
1271
+ # Soft-partial path: we have *some* text but the run did not complete
1272
+ # (e.g. truncation with partial buffered output). Still 200 but signal
1273
+ # truncation via finish_reason="length" + Hermes-specific extras.
1274
+ response_data = {
1275
+ "id": completion_id,
1276
+ "object": "chat.completion",
1277
+ "created": created,
1278
+ "model": model_name,
1279
+ "choices": [
1280
+ {
1281
+ "index": 0,
1282
+ "message": {
1283
+ "role": "assistant",
1284
+ "content": final_response,
1285
+ },
1286
+ "finish_reason": finish_reason,
1287
+ }
1288
+ ],
1289
+ "usage": {
1290
+ "prompt_tokens": usage.get("input_tokens", 0),
1291
+ "completion_tokens": usage.get("output_tokens", 0),
1292
+ "total_tokens": usage.get("total_tokens", 0),
1293
+ },
1294
+ }
1295
+ if is_partial or is_failed or not completed:
1296
+ response_data["hermes"] = {
1297
+ "completed": completed,
1298
+ "partial": is_partial,
1299
+ "failed": is_failed,
1300
+ "error": err_msg,
1301
+ "error_code": "output_truncated" if finish_reason == "length" else "agent_error",
1302
+ }
1303
+ response_headers["X-Hermes-Completed"] = "false"
1304
+ response_headers["X-Hermes-Partial"] = "true" if is_partial else "false"
1305
+ if err_msg:
1306
+ response_headers["X-Hermes-Error"] = err_msg[:200]
1307
+
1308
+ return web.json_response(response_data, headers=response_headers)
1309
+
1310
+ async def _write_sse_chat_completion(
1311
+ self, request: "web.Request", completion_id: str, model: str,
1312
+ created: int, stream_q, agent_task, agent_ref=None, session_id: str = None,
1313
+ gateway_session_key: str = None,
1314
+ ) -> "web.StreamResponse":
1315
+ """Write real streaming SSE from agent's stream_delta_callback queue.
1316
+
1317
+ If the client disconnects mid-stream (network drop, browser tab close),
1318
+ the agent is interrupted via ``agent.interrupt()`` so it stops making
1319
+ LLM API calls, and the asyncio task wrapper is cancelled.
1320
+ """
1321
+ import queue as _q
1322
+
1323
+ sse_headers = {
1324
+ "Content-Type": "text/event-stream",
1325
+ "Cache-Control": "no-cache",
1326
+ "X-Accel-Buffering": "no",
1327
+ }
1328
+ # CORS middleware can't inject headers into StreamResponse after
1329
+ # prepare() flushes them, so resolve CORS headers up front.
1330
+ origin = request.headers.get("Origin", "")
1331
+ cors = self._cors_headers_for_origin(origin) if origin else None
1332
+ if cors:
1333
+ sse_headers.update(cors)
1334
+ if session_id:
1335
+ sse_headers["X-Hermes-Session-Id"] = session_id
1336
+ if gateway_session_key:
1337
+ sse_headers["X-Hermes-Session-Key"] = gateway_session_key
1338
+ response = web.StreamResponse(status=200, headers=sse_headers)
1339
+ await response.prepare(request)
1340
+
1341
+ try:
1342
+ last_activity = time.monotonic()
1343
+
1344
+ # Role chunk
1345
+ role_chunk = {
1346
+ "id": completion_id, "object": "chat.completion.chunk",
1347
+ "created": created, "model": model,
1348
+ "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
1349
+ }
1350
+ await response.write(f"data: {json.dumps(role_chunk)}\n\n".encode())
1351
+ last_activity = time.monotonic()
1352
+
1353
+ # Helper — route a queue item to the correct SSE event.
1354
+ async def _emit(item):
1355
+ """Write a single queue item to the SSE stream.
1356
+
1357
+ Plain strings are sent as normal ``delta.content`` chunks.
1358
+ Tagged tuples ``("__tool_progress__", payload)`` are sent
1359
+ as a custom ``event: hermes.tool.progress`` SSE event so
1360
+ frontends can display them without storing the markers in
1361
+ conversation history. See #6972 for the original event,
1362
+ #16588 for the ``toolCallId``/``status`` lifecycle fields.
1363
+ """
1364
+ if isinstance(item, tuple) and len(item) == 2 and item[0] == "__tool_progress__":
1365
+ event_data = json.dumps(item[1])
1366
+ await response.write(
1367
+ f"event: hermes.tool.progress\ndata: {event_data}\n\n".encode()
1368
+ )
1369
+ else:
1370
+ content_chunk = {
1371
+ "id": completion_id, "object": "chat.completion.chunk",
1372
+ "created": created, "model": model,
1373
+ "choices": [{"index": 0, "delta": {"content": item}, "finish_reason": None}],
1374
+ }
1375
+ await response.write(f"data: {json.dumps(content_chunk)}\n\n".encode())
1376
+ return time.monotonic()
1377
+
1378
+ # Stream content chunks as they arrive from the agent
1379
+ loop = asyncio.get_running_loop()
1380
+ while True:
1381
+ try:
1382
+ delta = await loop.run_in_executor(None, lambda: stream_q.get(timeout=0.5))
1383
+ except _q.Empty:
1384
+ if agent_task.done():
1385
+ # Drain any remaining items
1386
+ while True:
1387
+ try:
1388
+ delta = stream_q.get_nowait()
1389
+ if delta is None:
1390
+ break
1391
+ last_activity = await _emit(delta)
1392
+ except _q.Empty:
1393
+ break
1394
+ break
1395
+ if time.monotonic() - last_activity >= CHAT_COMPLETIONS_SSE_KEEPALIVE_SECONDS:
1396
+ await response.write(b": keepalive\n\n")
1397
+ last_activity = time.monotonic()
1398
+ continue
1399
+
1400
+ if delta is None: # End of stream sentinel
1401
+ break
1402
+
1403
+ last_activity = await _emit(delta)
1404
+
1405
+ # Get usage from completed agent
1406
+ usage = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
1407
+ try:
1408
+ result, agent_usage = await agent_task
1409
+ usage = agent_usage or usage
1410
+ except Exception as exc:
1411
+ logger.warning("Agent task %s failed, usage data lost: %s", completion_id, exc)
1412
+
1413
+ # Finish chunk
1414
+ finish_chunk = {
1415
+ "id": completion_id, "object": "chat.completion.chunk",
1416
+ "created": created, "model": model,
1417
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
1418
+ "usage": {
1419
+ "prompt_tokens": usage.get("input_tokens", 0),
1420
+ "completion_tokens": usage.get("output_tokens", 0),
1421
+ "total_tokens": usage.get("total_tokens", 0),
1422
+ },
1423
+ }
1424
+ await response.write(f"data: {json.dumps(finish_chunk)}\n\n".encode())
1425
+ await response.write(b"data: [DONE]\n\n")
1426
+ except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError, OSError):
1427
+ # Client disconnected mid-stream. Interrupt the agent so it
1428
+ # stops making LLM API calls at the next loop iteration, then
1429
+ # cancel the asyncio task wrapper.
1430
+ agent = agent_ref[0] if agent_ref else None
1431
+ if agent is not None:
1432
+ try:
1433
+ agent.interrupt("SSE client disconnected")
1434
+ except Exception:
1435
+ pass
1436
+ if not agent_task.done():
1437
+ agent_task.cancel()
1438
+ try:
1439
+ await agent_task
1440
+ except (asyncio.CancelledError, Exception):
1441
+ pass
1442
+ logger.info("SSE client disconnected; interrupted agent task %s", completion_id)
1443
+ except Exception as _exc:
1444
+ # Agent crashed mid-stream. Try to emit an error chunk
1445
+ # so the client gets a proper response instead of a
1446
+ # TransferEncodingError from incomplete chunked encoding.
1447
+ import traceback as _tb
1448
+ logger.error("Agent crashed mid-stream for %s: %s", completion_id, _tb.format_exc()[:300])
1449
+ try:
1450
+ error_chunk = {
1451
+ "id": completion_id, "object": "chat.completion.chunk",
1452
+ "created": created, "model": model,
1453
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "error"}],
1454
+ }
1455
+ await response.write(f"data: {json.dumps(error_chunk)}\n\n".encode())
1456
+ await response.write(b"data: [DONE]\n\n")
1457
+ except Exception:
1458
+ pass
1459
+
1460
+ return response
1461
+
1462
+ async def _write_sse_responses(
1463
+ self,
1464
+ request: "web.Request",
1465
+ response_id: str,
1466
+ model: str,
1467
+ created_at: int,
1468
+ stream_q,
1469
+ agent_task,
1470
+ agent_ref,
1471
+ conversation_history: List[Dict[str, str]],
1472
+ user_message: str,
1473
+ instructions: Optional[str],
1474
+ conversation: Optional[str],
1475
+ store: bool,
1476
+ session_id: str,
1477
+ gateway_session_key: Optional[str] = None,
1478
+ ) -> "web.StreamResponse":
1479
+ """Write an SSE stream for POST /v1/responses (OpenAI Responses API).
1480
+
1481
+ Emits spec-compliant event types as the agent runs:
1482
+
1483
+ - ``response.created`` — initial envelope (status=in_progress)
1484
+ - ``response.output_text.delta`` / ``response.output_text.done`` —
1485
+ streamed assistant text
1486
+ - ``response.output_item.added`` / ``response.output_item.done``
1487
+ with ``item.type == "function_call"`` — when the agent invokes a
1488
+ tool (both events fire; the ``done`` event carries the finalized
1489
+ ``arguments`` string)
1490
+ - ``response.output_item.added`` with
1491
+ ``item.type == "function_call_output"`` — tool result with
1492
+ ``{call_id, output, status}``
1493
+ - ``response.completed`` — terminal event carrying the full
1494
+ response object with all output items + usage (same payload
1495
+ shape as the non-streaming path for parity)
1496
+ - ``response.failed`` — terminal event on agent error
1497
+
1498
+ If the client disconnects mid-stream, ``agent.interrupt()`` is
1499
+ called so the agent stops issuing upstream LLM calls, then the
1500
+ asyncio task is cancelled. When ``store=True`` an initial
1501
+ ``in_progress`` snapshot is persisted immediately after
1502
+ ``response.created`` and disconnects update it to an
1503
+ ``incomplete`` snapshot so GET /v1/responses/{id} and
1504
+ ``previous_response_id`` chaining still have something to
1505
+ recover from.
1506
+ """
1507
+ import queue as _q
1508
+
1509
+ sse_headers = {
1510
+ "Content-Type": "text/event-stream",
1511
+ "Cache-Control": "no-cache",
1512
+ "X-Accel-Buffering": "no",
1513
+ }
1514
+ origin = request.headers.get("Origin", "")
1515
+ cors = self._cors_headers_for_origin(origin) if origin else None
1516
+ if cors:
1517
+ sse_headers.update(cors)
1518
+ if session_id:
1519
+ sse_headers["X-Hermes-Session-Id"] = session_id
1520
+ if gateway_session_key:
1521
+ sse_headers["X-Hermes-Session-Key"] = gateway_session_key
1522
+ response = web.StreamResponse(status=200, headers=sse_headers)
1523
+ await response.prepare(request)
1524
+
1525
+ # State accumulated during the stream
1526
+ final_text_parts: List[str] = []
1527
+ # Track open function_call items by name so we can emit a matching
1528
+ # ``done`` event when the tool completes. Order preserved.
1529
+ pending_tool_calls: List[Dict[str, Any]] = []
1530
+ # Output items we've emitted so far (used to build the terminal
1531
+ # response.completed payload). Kept in the order they appeared.
1532
+ emitted_items: List[Dict[str, Any]] = []
1533
+ # Monotonic counter for output_index (spec requires it).
1534
+ output_index = 0
1535
+ # Monotonic counter for call_id generation if the agent doesn't
1536
+ # provide one (it doesn't, from tool_progress_callback).
1537
+ call_counter = 0
1538
+ # Canonical Responses SSE events include a monotonically increasing
1539
+ # sequence_number. Add it server-side for every emitted event so
1540
+ # clients that validate the OpenAI event schema can parse our stream.
1541
+ sequence_number = 0
1542
+ # Track the assistant message item id + content index for text
1543
+ # delta events — the spec ties deltas to a specific item.
1544
+ message_item_id = f"msg_{uuid.uuid4().hex[:24]}"
1545
+ message_output_index: Optional[int] = None
1546
+ message_opened = False
1547
+
1548
+ async def _write_event(event_type: str, data: Dict[str, Any]) -> None:
1549
+ nonlocal sequence_number
1550
+ if "sequence_number" not in data:
1551
+ data["sequence_number"] = sequence_number
1552
+ sequence_number += 1
1553
+ payload = f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
1554
+ await response.write(payload.encode())
1555
+
1556
+ def _envelope(status: str) -> Dict[str, Any]:
1557
+ env: Dict[str, Any] = {
1558
+ "id": response_id,
1559
+ "object": "response",
1560
+ "status": status,
1561
+ "created_at": created_at,
1562
+ "model": model,
1563
+ }
1564
+ return env
1565
+
1566
+ final_response_text = ""
1567
+ agent_error: Optional[str] = None
1568
+ usage: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
1569
+ terminal_snapshot_persisted = False
1570
+
1571
+ def _persist_response_snapshot(
1572
+ response_env: Dict[str, Any],
1573
+ *,
1574
+ conversation_history_snapshot: Optional[List[Dict[str, Any]]] = None,
1575
+ ) -> None:
1576
+ if not store:
1577
+ return
1578
+ if conversation_history_snapshot is None:
1579
+ conversation_history_snapshot = list(conversation_history)
1580
+ conversation_history_snapshot.append({"role": "user", "content": user_message})
1581
+ self._response_store.put(response_id, {
1582
+ "response": response_env,
1583
+ "conversation_history": conversation_history_snapshot,
1584
+ "instructions": instructions,
1585
+ "session_id": session_id,
1586
+ })
1587
+ if conversation:
1588
+ self._response_store.set_conversation(conversation, response_id)
1589
+
1590
+ def _persist_incomplete_if_needed() -> None:
1591
+ """Persist an ``incomplete`` snapshot if no terminal one was written.
1592
+
1593
+ Called from both the client-disconnect (``ConnectionResetError``)
1594
+ and server-cancellation (``asyncio.CancelledError``) paths so
1595
+ GET /v1/responses/{id} and ``previous_response_id`` chaining keep
1596
+ working after abrupt stream termination.
1597
+ """
1598
+ if not store or terminal_snapshot_persisted:
1599
+ return
1600
+ incomplete_text = "".join(final_text_parts) or final_response_text
1601
+ incomplete_items: List[Dict[str, Any]] = list(emitted_items)
1602
+ if incomplete_text:
1603
+ incomplete_items.append({
1604
+ "type": "message",
1605
+ "role": "assistant",
1606
+ "content": [{"type": "output_text", "text": incomplete_text}],
1607
+ })
1608
+ incomplete_env = _envelope("incomplete")
1609
+ incomplete_env["output"] = incomplete_items
1610
+ incomplete_env["usage"] = {
1611
+ "input_tokens": usage.get("input_tokens", 0),
1612
+ "output_tokens": usage.get("output_tokens", 0),
1613
+ "total_tokens": usage.get("total_tokens", 0),
1614
+ }
1615
+ incomplete_history = list(conversation_history)
1616
+ incomplete_history.append({"role": "user", "content": user_message})
1617
+ if incomplete_text:
1618
+ incomplete_history.append({"role": "assistant", "content": incomplete_text})
1619
+ _persist_response_snapshot(
1620
+ incomplete_env,
1621
+ conversation_history_snapshot=incomplete_history,
1622
+ )
1623
+
1624
+ try:
1625
+ # response.created — initial envelope, status=in_progress
1626
+ created_env = _envelope("in_progress")
1627
+ created_env["output"] = []
1628
+ await _write_event("response.created", {
1629
+ "type": "response.created",
1630
+ "response": created_env,
1631
+ })
1632
+ _persist_response_snapshot(created_env)
1633
+ last_activity = time.monotonic()
1634
+
1635
+ async def _open_message_item() -> None:
1636
+ """Emit response.output_item.added for the assistant message
1637
+ the first time any text delta arrives."""
1638
+ nonlocal message_opened, message_output_index, output_index
1639
+ if message_opened:
1640
+ return
1641
+ message_opened = True
1642
+ message_output_index = output_index
1643
+ output_index += 1
1644
+ item = {
1645
+ "id": message_item_id,
1646
+ "type": "message",
1647
+ "status": "in_progress",
1648
+ "role": "assistant",
1649
+ "content": [],
1650
+ }
1651
+ await _write_event("response.output_item.added", {
1652
+ "type": "response.output_item.added",
1653
+ "output_index": message_output_index,
1654
+ "item": item,
1655
+ })
1656
+
1657
+ async def _emit_text_delta(delta_text: str) -> None:
1658
+ await _open_message_item()
1659
+ final_text_parts.append(delta_text)
1660
+ await _write_event("response.output_text.delta", {
1661
+ "type": "response.output_text.delta",
1662
+ "item_id": message_item_id,
1663
+ "output_index": message_output_index,
1664
+ "content_index": 0,
1665
+ "delta": delta_text,
1666
+ "logprobs": [],
1667
+ })
1668
+
1669
+ async def _emit_tool_started(payload: Dict[str, Any]) -> str:
1670
+ """Emit response.output_item.added for a function_call.
1671
+
1672
+ Returns the call_id so the matching completion event can
1673
+ reference it. Prefer the real ``tool_call_id`` from the
1674
+ agent when available; fall back to a generated call id for
1675
+ safety in tests or older code paths.
1676
+ """
1677
+ nonlocal output_index, call_counter
1678
+ call_counter += 1
1679
+ call_id = payload.get("tool_call_id") or f"call_{response_id[5:]}_{call_counter}"
1680
+ args = payload.get("arguments", {})
1681
+ if isinstance(args, dict):
1682
+ arguments_str = json.dumps(args)
1683
+ else:
1684
+ arguments_str = str(args)
1685
+ item = {
1686
+ "id": f"fc_{uuid.uuid4().hex[:24]}",
1687
+ "type": "function_call",
1688
+ "status": "in_progress",
1689
+ "name": payload.get("name", ""),
1690
+ "call_id": call_id,
1691
+ "arguments": arguments_str,
1692
+ }
1693
+ idx = output_index
1694
+ output_index += 1
1695
+ pending_tool_calls.append({
1696
+ "call_id": call_id,
1697
+ "name": payload.get("name", ""),
1698
+ "arguments": arguments_str,
1699
+ "item_id": item["id"],
1700
+ "output_index": idx,
1701
+ })
1702
+ emitted_items.append({
1703
+ "type": "function_call",
1704
+ "name": payload.get("name", ""),
1705
+ "arguments": arguments_str,
1706
+ "call_id": call_id,
1707
+ })
1708
+ await _write_event("response.output_item.added", {
1709
+ "type": "response.output_item.added",
1710
+ "output_index": idx,
1711
+ "item": item,
1712
+ })
1713
+ return call_id
1714
+
1715
+ async def _emit_tool_completed(payload: Dict[str, Any]) -> None:
1716
+ """Emit response.output_item.done (function_call) followed
1717
+ by response.output_item.added (function_call_output)."""
1718
+ nonlocal output_index
1719
+ call_id = payload.get("tool_call_id")
1720
+ result = payload.get("result", "")
1721
+ pending = None
1722
+ if call_id:
1723
+ for i, p in enumerate(pending_tool_calls):
1724
+ if p["call_id"] == call_id:
1725
+ pending = pending_tool_calls.pop(i)
1726
+ break
1727
+ if pending is None:
1728
+ # Completion without a matching start — skip to avoid
1729
+ # emitting orphaned done events.
1730
+ return
1731
+
1732
+ # function_call done
1733
+ done_item = {
1734
+ "id": pending["item_id"],
1735
+ "type": "function_call",
1736
+ "status": "completed",
1737
+ "name": pending["name"],
1738
+ "call_id": pending["call_id"],
1739
+ "arguments": pending["arguments"],
1740
+ }
1741
+ await _write_event("response.output_item.done", {
1742
+ "type": "response.output_item.done",
1743
+ "output_index": pending["output_index"],
1744
+ "item": done_item,
1745
+ })
1746
+
1747
+ # function_call_output added (result)
1748
+ result_str = result if isinstance(result, str) else json.dumps(result)
1749
+ output_parts = [{"type": "input_text", "text": result_str}]
1750
+ output_item = {
1751
+ "id": f"fco_{uuid.uuid4().hex[:24]}",
1752
+ "type": "function_call_output",
1753
+ "call_id": pending["call_id"],
1754
+ "output": output_parts,
1755
+ "status": "completed",
1756
+ }
1757
+ idx = output_index
1758
+ output_index += 1
1759
+ emitted_items.append({
1760
+ "type": "function_call_output",
1761
+ "call_id": pending["call_id"],
1762
+ "output": output_parts,
1763
+ })
1764
+ await _write_event("response.output_item.added", {
1765
+ "type": "response.output_item.added",
1766
+ "output_index": idx,
1767
+ "item": output_item,
1768
+ })
1769
+ await _write_event("response.output_item.done", {
1770
+ "type": "response.output_item.done",
1771
+ "output_index": idx,
1772
+ "item": output_item,
1773
+ })
1774
+
1775
+ # Main drain loop — thread-safe queue fed by agent callbacks.
1776
+ async def _dispatch(it) -> None:
1777
+ """Route a queue item to the correct SSE emitter.
1778
+
1779
+ Plain strings are text deltas — they are batched (50ms)
1780
+ to reduce Open WebUI re-render storms. Tagged tuples
1781
+ with ``__tool_started__`` / ``__tool_completed__``
1782
+ prefixes are tool lifecycle events and flush the buffer
1783
+ before emitting.
1784
+ """
1785
+ nonlocal _batch_timer
1786
+ if isinstance(it, tuple) and len(it) == 2 and isinstance(it[0], str):
1787
+ tag, payload = it
1788
+ # Flush batched text before tool events
1789
+ if _batch_buf:
1790
+ await _flush_batch()
1791
+ if tag == "__tool_started__":
1792
+ await _emit_tool_started(payload)
1793
+ elif tag == "__tool_completed__":
1794
+ await _emit_tool_completed(payload)
1795
+ elif isinstance(it, str):
1796
+ # Batch text deltas — append to buffer, flush on timer
1797
+ _batch_buf.append(it)
1798
+ if _batch_timer is None:
1799
+ _batch_timer = asyncio.create_task(_batch_flush_after(0.05))
1800
+ # Other types are silently dropped.
1801
+
1802
+ # ── Batching state ──
1803
+ _batch_buf: List[str] = []
1804
+ _batch_timer: Optional[asyncio.Task] = None
1805
+ _batch_lock = asyncio.Lock()
1806
+
1807
+ async def _batch_flush_after(delay: float) -> None:
1808
+ """Wait delay seconds, then flush accumulated text deltas."""
1809
+ try:
1810
+ await asyncio.sleep(delay)
1811
+ except asyncio.CancelledError:
1812
+ return
1813
+ # Clear timer reference BEFORE flush so new deltas
1814
+ # can start a fresh timer while we emit
1815
+ nonlocal _batch_buf, _batch_timer
1816
+ _batch_timer = None
1817
+ await _flush_batch()
1818
+
1819
+ async def _flush_batch() -> None:
1820
+ """Emit a single SSE delta for all accumulated text."""
1821
+ nonlocal _batch_buf
1822
+ async with _batch_lock:
1823
+ if _batch_buf:
1824
+ combined = "".join(_batch_buf)
1825
+ _batch_buf = []
1826
+ await _emit_text_delta(combined)
1827
+
1828
+ loop = asyncio.get_running_loop()
1829
+ while True:
1830
+ try:
1831
+ item = await loop.run_in_executor(None, lambda: stream_q.get(timeout=0.5))
1832
+ except _q.Empty:
1833
+ if agent_task.done():
1834
+ # Drain remaining
1835
+ while True:
1836
+ try:
1837
+ item = stream_q.get_nowait()
1838
+ if item is None:
1839
+ break
1840
+ await _dispatch(item)
1841
+ last_activity = time.monotonic()
1842
+ except _q.Empty:
1843
+ break
1844
+ break
1845
+ if time.monotonic() - last_activity >= CHAT_COMPLETIONS_SSE_KEEPALIVE_SECONDS:
1846
+ await response.write(b": keepalive\n\n")
1847
+ last_activity = time.monotonic()
1848
+ continue
1849
+
1850
+ if item is None: # EOS sentinel
1851
+ # Cancel pending timer and flush remaining batched text
1852
+ if _batch_timer and not _batch_timer.done():
1853
+ _batch_timer.cancel()
1854
+ _batch_timer = None
1855
+ if _batch_buf:
1856
+ await _flush_batch()
1857
+ break
1858
+
1859
+ await _dispatch(item)
1860
+ last_activity = time.monotonic()
1861
+
1862
+ # Flush any final batched text before processing result
1863
+ if _batch_buf:
1864
+ await _flush_batch()
1865
+
1866
+ # Pick up agent result + usage from the completed task
1867
+ try:
1868
+ result, agent_usage = await agent_task
1869
+ usage = agent_usage or usage
1870
+ # If the agent produced a final_response but no text
1871
+ # deltas were streamed (e.g. some providers only emit
1872
+ # the full response at the end), emit a single fallback
1873
+ # delta so Responses clients still receive a live text part.
1874
+ agent_final = result.get("final_response", "") if isinstance(result, dict) else ""
1875
+ if agent_final and not final_text_parts:
1876
+ await _emit_text_delta(agent_final)
1877
+ if agent_final and not final_response_text:
1878
+ final_response_text = agent_final
1879
+ if isinstance(result, dict) and result.get("error") and not final_response_text:
1880
+ agent_error = result["error"]
1881
+ except Exception as e: # noqa: BLE001
1882
+ logger.error("Error running agent for streaming responses: %s", e, exc_info=True)
1883
+ agent_error = str(e)
1884
+
1885
+ # Close the message item if it was opened
1886
+ final_response_text = "".join(final_text_parts) or final_response_text
1887
+ if message_opened:
1888
+ await _write_event("response.output_text.done", {
1889
+ "type": "response.output_text.done",
1890
+ "item_id": message_item_id,
1891
+ "output_index": message_output_index,
1892
+ "content_index": 0,
1893
+ "text": final_response_text,
1894
+ "logprobs": [],
1895
+ })
1896
+ msg_done_item = {
1897
+ "id": message_item_id,
1898
+ "type": "message",
1899
+ "status": "completed",
1900
+ "role": "assistant",
1901
+ "content": [
1902
+ {"type": "output_text", "text": final_response_text}
1903
+ ],
1904
+ }
1905
+ await _write_event("response.output_item.done", {
1906
+ "type": "response.output_item.done",
1907
+ "output_index": message_output_index,
1908
+ "item": msg_done_item,
1909
+ })
1910
+
1911
+ # Always append a final message item in the completed
1912
+ # response envelope so clients that only parse the terminal
1913
+ # payload still see the assistant text. This mirrors the
1914
+ # shape produced by _extract_output_items in the batch path.
1915
+ final_items: List[Dict[str, Any]] = list(emitted_items)
1916
+
1917
+ # Trim large content from tool call arguments to keep the
1918
+ # response.completed event under ~100KB. Clients already
1919
+ # received full details via incremental events.
1920
+ for _item in final_items:
1921
+ if _item.get("type") == "function_call":
1922
+ try:
1923
+ _args = json.loads(_item.get("arguments", "{}")) if isinstance(_item.get("arguments"), str) else _item.get("arguments", {})
1924
+ if isinstance(_args, dict):
1925
+ for _k in ("content", "query", "pattern", "old_string", "new_string"):
1926
+ if isinstance(_args.get(_k), str) and len(_args[_k]) > 500:
1927
+ _args[_k] = "[" + str(len(_args[_k])) + " chars — truncated for response.completed]"
1928
+ _item["arguments"] = json.dumps(_args)
1929
+ except Exception:
1930
+ pass
1931
+ elif _item.get("type") == "function_call_output":
1932
+ _output = _item.get("output", [])
1933
+ if isinstance(_output, list) and _output:
1934
+ _first = _output[0]
1935
+ if isinstance(_first, dict) and _first.get("type") == "input_text":
1936
+ _text = _first.get("text", "")
1937
+ if len(_text) > 1000:
1938
+ _first["text"] = _text[:500] + "...[" + str(len(_text) - 500) + " more chars]"
1939
+ _item["output"] = [_first]
1940
+
1941
+ final_items.append({
1942
+ "type": "message",
1943
+ "role": "assistant",
1944
+ "content": [
1945
+ {"type": "output_text", "text": final_response_text or (agent_error or "")}
1946
+ ],
1947
+ })
1948
+
1949
+ if agent_error:
1950
+ failed_env = _envelope("failed")
1951
+ failed_env["output"] = final_items
1952
+ failed_env["error"] = {"message": agent_error, "type": "server_error"}
1953
+ failed_env["usage"] = {
1954
+ "input_tokens": usage.get("input_tokens", 0),
1955
+ "output_tokens": usage.get("output_tokens", 0),
1956
+ "total_tokens": usage.get("total_tokens", 0),
1957
+ }
1958
+ _failed_history = list(conversation_history)
1959
+ _failed_history.append({"role": "user", "content": user_message})
1960
+ if final_response_text or agent_error:
1961
+ _failed_history.append({
1962
+ "role": "assistant",
1963
+ "content": final_response_text or agent_error,
1964
+ })
1965
+ _persist_response_snapshot(
1966
+ failed_env,
1967
+ conversation_history_snapshot=_failed_history,
1968
+ )
1969
+ terminal_snapshot_persisted = True
1970
+ await _write_event("response.failed", {
1971
+ "type": "response.failed",
1972
+ "response": failed_env,
1973
+ })
1974
+ else:
1975
+ completed_env = _envelope("completed")
1976
+ completed_env["output"] = final_items
1977
+ completed_env["usage"] = {
1978
+ "input_tokens": usage.get("input_tokens", 0),
1979
+ "output_tokens": usage.get("output_tokens", 0),
1980
+ "total_tokens": usage.get("total_tokens", 0),
1981
+ }
1982
+ full_history = self._build_response_conversation_history(
1983
+ conversation_history,
1984
+ user_message,
1985
+ result,
1986
+ final_response_text,
1987
+ )
1988
+ _persist_response_snapshot(
1989
+ completed_env,
1990
+ conversation_history_snapshot=full_history,
1991
+ )
1992
+ terminal_snapshot_persisted = True
1993
+ await _write_event("response.completed", {
1994
+ "type": "response.completed",
1995
+ "response": completed_env,
1996
+ })
1997
+
1998
+ except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError, OSError):
1999
+ _persist_incomplete_if_needed()
2000
+ # Client disconnected — interrupt the agent so it stops
2001
+ # making upstream LLM calls, then cancel the task.
2002
+ agent = agent_ref[0] if agent_ref else None
2003
+ if agent is not None:
2004
+ try:
2005
+ agent.interrupt("SSE client disconnected")
2006
+ except Exception:
2007
+ pass
2008
+ if not agent_task.done():
2009
+ agent_task.cancel()
2010
+ try:
2011
+ await agent_task
2012
+ except (asyncio.CancelledError, Exception):
2013
+ pass
2014
+ logger.info("SSE client disconnected; interrupted agent task %s", response_id)
2015
+ except asyncio.CancelledError:
2016
+ # Server-side cancellation (e.g. shutdown, request timeout) —
2017
+ # persist an incomplete snapshot so GET /v1/responses/{id} and
2018
+ # previous_response_id chaining still work, then re-raise so the
2019
+ # runtime's cancellation semantics are respected.
2020
+ _persist_incomplete_if_needed()
2021
+ agent = agent_ref[0] if agent_ref else None
2022
+ if agent is not None:
2023
+ try:
2024
+ agent.interrupt("SSE task cancelled")
2025
+ except Exception:
2026
+ pass
2027
+ if not agent_task.done():
2028
+ agent_task.cancel()
2029
+ logger.info("SSE task cancelled; persisted incomplete snapshot for %s", response_id)
2030
+ raise
2031
+ except Exception as _exc:
2032
+ # Agent crashed with an unhandled error (e.g. model API error like
2033
+ # BadRequestError, AuthenticationError). Emit a response.failed
2034
+ # event and properly terminate the SSE stream so the client doesn't
2035
+ # get a TransferEncodingError from incomplete chunked encoding.
2036
+ import traceback as _tb
2037
+ _persist_incomplete_if_needed()
2038
+ agent_error = _tb.format_exc()
2039
+ try:
2040
+ failed_env = _envelope("failed")
2041
+ failed_env["output"] = list(emitted_items)
2042
+ failed_env["error"] = {"message": str(_exc)[:500], "type": "server_error"}
2043
+ failed_env["usage"] = {
2044
+ "input_tokens": usage.get("input_tokens", 0),
2045
+ "output_tokens": usage.get("output_tokens", 0),
2046
+ "total_tokens": usage.get("total_tokens", 0),
2047
+ }
2048
+ await _write_event("response.failed", {
2049
+ "type": "response.failed",
2050
+ "response": failed_env,
2051
+ })
2052
+ except Exception:
2053
+ pass
2054
+ logger.error("Agent crashed mid-stream for %s: %s", response_id, str(agent_error)[:300])
2055
+
2056
+ return response
2057
+
2058
+ async def _handle_responses(self, request: "web.Request") -> "web.Response":
2059
+ """POST /v1/responses — OpenAI Responses API format."""
2060
+ auth_err = self._check_auth(request)
2061
+ if auth_err:
2062
+ return auth_err
2063
+
2064
+ # Long-term memory scope header (see chat_completions for details).
2065
+ gateway_session_key, key_err = self._parse_session_key_header(request)
2066
+ if key_err is not None:
2067
+ return key_err
2068
+
2069
+ # Parse request body
2070
+ try:
2071
+ body = await request.json()
2072
+ except (json.JSONDecodeError, Exception):
2073
+ return web.json_response(
2074
+ {"error": {"message": "Invalid JSON in request body", "type": "invalid_request_error"}},
2075
+ status=400,
2076
+ )
2077
+
2078
+ raw_input = body.get("input")
2079
+ if raw_input is None:
2080
+ return web.json_response(_openai_error("Missing 'input' field"), status=400)
2081
+
2082
+ instructions = body.get("instructions")
2083
+ previous_response_id = body.get("previous_response_id")
2084
+ conversation = body.get("conversation")
2085
+ store = body.get("store", True)
2086
+
2087
+ # conversation and previous_response_id are mutually exclusive
2088
+ if conversation and previous_response_id:
2089
+ return web.json_response(_openai_error("Cannot use both 'conversation' and 'previous_response_id'"), status=400)
2090
+
2091
+ # Resolve conversation name to latest response_id
2092
+ if conversation:
2093
+ previous_response_id = self._response_store.get_conversation(conversation)
2094
+ # No error if conversation doesn't exist yet — it's a new conversation
2095
+
2096
+ # Normalize input to message list
2097
+ input_messages: List[Dict[str, Any]] = []
2098
+ if isinstance(raw_input, str):
2099
+ input_messages = [{"role": "user", "content": raw_input}]
2100
+ elif isinstance(raw_input, list):
2101
+ for idx, item in enumerate(raw_input):
2102
+ if isinstance(item, str):
2103
+ input_messages.append({"role": "user", "content": item})
2104
+ elif isinstance(item, dict):
2105
+ role = item.get("role", "user")
2106
+ try:
2107
+ content = _normalize_multimodal_content(item.get("content", ""))
2108
+ except ValueError as exc:
2109
+ return _multimodal_validation_error(exc, param=f"input[{idx}].content")
2110
+ input_messages.append({"role": role, "content": content})
2111
+ else:
2112
+ return web.json_response(_openai_error("'input' must be a string or array"), status=400)
2113
+
2114
+ # Accept explicit conversation_history from the request body.
2115
+ # This lets stateless clients supply their own history instead of
2116
+ # relying on server-side response chaining via previous_response_id.
2117
+ # Precedence: explicit conversation_history > previous_response_id.
2118
+ conversation_history: List[Dict[str, Any]] = []
2119
+ raw_history = body.get("conversation_history")
2120
+ if raw_history:
2121
+ if not isinstance(raw_history, list):
2122
+ return web.json_response(
2123
+ _openai_error("'conversation_history' must be an array of message objects"),
2124
+ status=400,
2125
+ )
2126
+ for i, entry in enumerate(raw_history):
2127
+ if not isinstance(entry, dict) or "role" not in entry or "content" not in entry:
2128
+ return web.json_response(
2129
+ _openai_error(f"conversation_history[{i}] must have 'role' and 'content' fields"),
2130
+ status=400,
2131
+ )
2132
+ try:
2133
+ entry_content = _normalize_multimodal_content(entry["content"])
2134
+ except ValueError as exc:
2135
+ return _multimodal_validation_error(exc, param=f"conversation_history[{i}].content")
2136
+ conversation_history.append({"role": str(entry["role"]), "content": entry_content})
2137
+ if previous_response_id:
2138
+ logger.debug("Both conversation_history and previous_response_id provided; using conversation_history")
2139
+
2140
+ stored_session_id = None
2141
+ if not conversation_history and previous_response_id:
2142
+ stored = self._response_store.get(previous_response_id)
2143
+ if stored is None:
2144
+ return web.json_response(_openai_error(f"Previous response not found: {previous_response_id}"), status=404)
2145
+ conversation_history = list(stored.get("conversation_history", []))
2146
+ stored_session_id = stored.get("session_id")
2147
+ # If no instructions provided, carry forward from previous
2148
+ if instructions is None:
2149
+ instructions = stored.get("instructions")
2150
+
2151
+ # Append new input messages to history (all but the last become history)
2152
+ for msg in input_messages[:-1]:
2153
+ conversation_history.append(msg)
2154
+
2155
+ # Last input message is the user_message
2156
+ user_message: Any = input_messages[-1].get("content", "") if input_messages else ""
2157
+ if not _content_has_visible_payload(user_message):
2158
+ return web.json_response(_openai_error("No user message found in input"), status=400)
2159
+
2160
+ # Truncation support
2161
+ if body.get("truncation") == "auto" and len(conversation_history) > 100:
2162
+ conversation_history = conversation_history[-100:]
2163
+
2164
+ # Reuse session from previous_response_id chain so the dashboard
2165
+ # groups the entire conversation under one session entry.
2166
+ session_id = stored_session_id or str(uuid.uuid4())
2167
+
2168
+ stream = bool(body.get("stream", False))
2169
+ if stream:
2170
+ # Streaming branch — emit OpenAI Responses SSE events as the
2171
+ # agent runs so frontends can render text deltas and tool
2172
+ # calls in real time. See _write_sse_responses for details.
2173
+ import queue as _q
2174
+ _stream_q: _q.Queue = _q.Queue()
2175
+
2176
+ def _on_delta(delta):
2177
+ # None from the agent is a CLI box-close signal, not EOS.
2178
+ # Forwarding would kill the SSE stream prematurely; the
2179
+ # SSE writer detects completion via agent_task.done().
2180
+ if delta is not None:
2181
+ _stream_q.put(delta)
2182
+
2183
+ def _on_tool_progress(event_type, name, preview, args, **kwargs):
2184
+ """Queue non-start tool progress events if needed in future.
2185
+
2186
+ The structured Responses stream uses ``tool_start_callback``
2187
+ and ``tool_complete_callback`` for exact call-id correlation,
2188
+ so progress events are currently ignored here.
2189
+ """
2190
+ return
2191
+
2192
+ def _on_tool_start(tool_call_id, function_name, function_args):
2193
+ """Queue a started tool for live function_call streaming."""
2194
+ _stream_q.put(("__tool_started__", {
2195
+ "tool_call_id": tool_call_id,
2196
+ "name": function_name,
2197
+ "arguments": function_args or {},
2198
+ }))
2199
+
2200
+ def _on_tool_complete(tool_call_id, function_name, function_args, function_result):
2201
+ """Queue a completed tool result for live function_call_output streaming."""
2202
+ _stream_q.put(("__tool_completed__", {
2203
+ "tool_call_id": tool_call_id,
2204
+ "name": function_name,
2205
+ "arguments": function_args or {},
2206
+ "result": function_result,
2207
+ }))
2208
+
2209
+ agent_ref = [None]
2210
+ agent_task = asyncio.ensure_future(self._run_agent(
2211
+ user_message=user_message,
2212
+ conversation_history=conversation_history,
2213
+ ephemeral_system_prompt=instructions,
2214
+ session_id=session_id,
2215
+ stream_delta_callback=_on_delta,
2216
+ tool_progress_callback=_on_tool_progress,
2217
+ tool_start_callback=_on_tool_start,
2218
+ tool_complete_callback=_on_tool_complete,
2219
+ agent_ref=agent_ref,
2220
+ gateway_session_key=gateway_session_key,
2221
+ ))
2222
+ # Ensure SSE drain loops can terminate without relying on polling
2223
+ # agent_task.done(), which can race with queue timeout checks.
2224
+ agent_task.add_done_callback(lambda _fut: _stream_q.put(None))
2225
+
2226
+ response_id = f"resp_{uuid.uuid4().hex[:28]}"
2227
+ model_name = body.get("model", self._model_name)
2228
+ created_at = int(time.time())
2229
+
2230
+ return await self._write_sse_responses(
2231
+ request=request,
2232
+ response_id=response_id,
2233
+ model=model_name,
2234
+ created_at=created_at,
2235
+ stream_q=_stream_q,
2236
+ agent_task=agent_task,
2237
+ agent_ref=agent_ref,
2238
+ conversation_history=conversation_history,
2239
+ user_message=user_message,
2240
+ instructions=instructions,
2241
+ conversation=conversation,
2242
+ store=store,
2243
+ session_id=session_id,
2244
+ gateway_session_key=gateway_session_key,
2245
+ )
2246
+
2247
+ async def _compute_response():
2248
+ return await self._run_agent(
2249
+ user_message=user_message,
2250
+ conversation_history=conversation_history,
2251
+ ephemeral_system_prompt=instructions,
2252
+ session_id=session_id,
2253
+ gateway_session_key=gateway_session_key,
2254
+ )
2255
+
2256
+ idempotency_key = request.headers.get("Idempotency-Key")
2257
+ if idempotency_key:
2258
+ fp = _make_request_fingerprint(
2259
+ body,
2260
+ keys=["input", "instructions", "previous_response_id", "conversation", "model", "tools"],
2261
+ )
2262
+ try:
2263
+ result, usage = await _idem_cache.get_or_set(idempotency_key, fp, _compute_response)
2264
+ except Exception as e:
2265
+ logger.error("Error running agent for responses: %s", e, exc_info=True)
2266
+ return web.json_response(
2267
+ _openai_error(f"Internal server error: {e}", err_type="server_error"),
2268
+ status=500,
2269
+ )
2270
+ else:
2271
+ try:
2272
+ result, usage = await _compute_response()
2273
+ except Exception as e:
2274
+ logger.error("Error running agent for responses: %s", e, exc_info=True)
2275
+ return web.json_response(
2276
+ _openai_error(f"Internal server error: {e}", err_type="server_error"),
2277
+ status=500,
2278
+ )
2279
+
2280
+ final_response = result.get("final_response", "")
2281
+ if not final_response:
2282
+ final_response = result.get("error", "(No response generated)")
2283
+
2284
+ response_id = f"resp_{uuid.uuid4().hex[:28]}"
2285
+ created_at = int(time.time())
2286
+
2287
+ # Build the full conversation history for storage
2288
+ # (includes tool calls from the agent run)
2289
+ full_history = self._build_response_conversation_history(
2290
+ conversation_history,
2291
+ user_message,
2292
+ result,
2293
+ final_response,
2294
+ )
2295
+
2296
+ # Build output items from the current turn only. AIAgent returns a
2297
+ # full transcript in result["messages"], while older/mocked paths may
2298
+ # return only the current turn suffix.
2299
+ output_start_index = self._response_messages_turn_start_index(
2300
+ conversation_history,
2301
+ user_message,
2302
+ result,
2303
+ )
2304
+ output_items = self._extract_output_items(result, start_index=output_start_index)
2305
+
2306
+ response_data = {
2307
+ "id": response_id,
2308
+ "object": "response",
2309
+ "status": "completed",
2310
+ "created_at": created_at,
2311
+ "model": body.get("model", self._model_name),
2312
+ "output": output_items,
2313
+ "usage": {
2314
+ "input_tokens": usage.get("input_tokens", 0),
2315
+ "output_tokens": usage.get("output_tokens", 0),
2316
+ "total_tokens": usage.get("total_tokens", 0),
2317
+ },
2318
+ }
2319
+
2320
+ # Store the complete response object for future chaining / GET retrieval
2321
+ if store:
2322
+ self._response_store.put(response_id, {
2323
+ "response": response_data,
2324
+ "conversation_history": full_history,
2325
+ "instructions": instructions,
2326
+ "session_id": session_id,
2327
+ })
2328
+ # Update conversation mapping so the next request with the same
2329
+ # conversation name automatically chains to this response
2330
+ if conversation:
2331
+ self._response_store.set_conversation(conversation, response_id)
2332
+
2333
+ response_headers = {"X-Hermes-Session-Id": session_id}
2334
+ if gateway_session_key:
2335
+ response_headers["X-Hermes-Session-Key"] = gateway_session_key
2336
+ return web.json_response(response_data, headers=response_headers)
2337
+
2338
+ # ------------------------------------------------------------------
2339
+ # GET / DELETE response endpoints
2340
+ # ------------------------------------------------------------------
2341
+
2342
+ async def _handle_get_response(self, request: "web.Request") -> "web.Response":
2343
+ """GET /v1/responses/{response_id} — retrieve a stored response."""
2344
+ auth_err = self._check_auth(request)
2345
+ if auth_err:
2346
+ return auth_err
2347
+
2348
+ response_id = request.match_info["response_id"]
2349
+ stored = self._response_store.get(response_id)
2350
+ if stored is None:
2351
+ return web.json_response(_openai_error(f"Response not found: {response_id}"), status=404)
2352
+
2353
+ return web.json_response(stored["response"])
2354
+
2355
+ async def _handle_delete_response(self, request: "web.Request") -> "web.Response":
2356
+ """DELETE /v1/responses/{response_id} — delete a stored response."""
2357
+ auth_err = self._check_auth(request)
2358
+ if auth_err:
2359
+ return auth_err
2360
+
2361
+ response_id = request.match_info["response_id"]
2362
+ deleted = self._response_store.delete(response_id)
2363
+ if not deleted:
2364
+ return web.json_response(_openai_error(f"Response not found: {response_id}"), status=404)
2365
+
2366
+ return web.json_response({
2367
+ "id": response_id,
2368
+ "object": "response",
2369
+ "deleted": True,
2370
+ })
2371
+
2372
+ # ------------------------------------------------------------------
2373
+ # Cron jobs API
2374
+ # ------------------------------------------------------------------
2375
+
2376
+ _JOB_ID_RE = __import__("re").compile(r"[a-f0-9]{12}")
2377
+ # Allowed fields for update — prevents clients injecting arbitrary keys
2378
+ _UPDATE_ALLOWED_FIELDS = {"name", "schedule", "prompt", "deliver", "skills", "skill", "repeat", "enabled"}
2379
+ _MAX_NAME_LENGTH = 200
2380
+ _MAX_PROMPT_LENGTH = 5000
2381
+
2382
+ @staticmethod
2383
+ def _check_jobs_available() -> Optional["web.Response"]:
2384
+ """Return error response if cron module isn't available."""
2385
+ if not _CRON_AVAILABLE:
2386
+ return web.json_response(
2387
+ {"error": "Cron module not available"}, status=501,
2388
+ )
2389
+ return None
2390
+
2391
+ def _check_job_id(self, request: "web.Request") -> tuple:
2392
+ """Validate and extract job_id. Returns (job_id, error_response)."""
2393
+ job_id = request.match_info["job_id"]
2394
+ if not self._JOB_ID_RE.fullmatch(job_id):
2395
+ return job_id, web.json_response(
2396
+ {"error": "Invalid job ID format"}, status=400,
2397
+ )
2398
+ return job_id, None
2399
+
2400
+ async def _handle_list_jobs(self, request: "web.Request") -> "web.Response":
2401
+ """GET /api/jobs — list all cron jobs."""
2402
+ auth_err = self._check_auth(request)
2403
+ if auth_err:
2404
+ return auth_err
2405
+ cron_err = self._check_jobs_available()
2406
+ if cron_err:
2407
+ return cron_err
2408
+ try:
2409
+ include_disabled = request.query.get("include_disabled", "").lower() in {"true", "1"}
2410
+ jobs = _cron_list(include_disabled=include_disabled)
2411
+ return web.json_response({"jobs": jobs})
2412
+ except Exception as e:
2413
+ return web.json_response({"error": str(e)}, status=500)
2414
+
2415
+ async def _handle_create_job(self, request: "web.Request") -> "web.Response":
2416
+ """POST /api/jobs — create a new cron job."""
2417
+ auth_err = self._check_auth(request)
2418
+ if auth_err:
2419
+ return auth_err
2420
+ cron_err = self._check_jobs_available()
2421
+ if cron_err:
2422
+ return cron_err
2423
+ try:
2424
+ body = await request.json()
2425
+ name = (body.get("name") or "").strip()
2426
+ schedule = (body.get("schedule") or "").strip()
2427
+ prompt = body.get("prompt", "")
2428
+ deliver = body.get("deliver", "local")
2429
+ skills = body.get("skills")
2430
+ repeat = body.get("repeat")
2431
+
2432
+ if not name:
2433
+ return web.json_response({"error": "Name is required"}, status=400)
2434
+ if len(name) > self._MAX_NAME_LENGTH:
2435
+ return web.json_response(
2436
+ {"error": f"Name must be ≤ {self._MAX_NAME_LENGTH} characters"}, status=400,
2437
+ )
2438
+ if not schedule:
2439
+ return web.json_response({"error": "Schedule is required"}, status=400)
2440
+ if len(prompt) > self._MAX_PROMPT_LENGTH:
2441
+ return web.json_response(
2442
+ {"error": f"Prompt must be ≤ {self._MAX_PROMPT_LENGTH} characters"}, status=400,
2443
+ )
2444
+ if repeat is not None and (not isinstance(repeat, int) or repeat < 1):
2445
+ return web.json_response({"error": "Repeat must be a positive integer"}, status=400)
2446
+
2447
+ kwargs = {
2448
+ "prompt": prompt,
2449
+ "schedule": schedule,
2450
+ "name": name,
2451
+ "deliver": deliver,
2452
+ }
2453
+ if skills:
2454
+ kwargs["skills"] = skills
2455
+ if repeat is not None:
2456
+ kwargs["repeat"] = repeat
2457
+
2458
+ job = _cron_create(**kwargs)
2459
+ return web.json_response({"job": job})
2460
+ except Exception as e:
2461
+ return web.json_response({"error": str(e)}, status=500)
2462
+
2463
+ async def _handle_get_job(self, request: "web.Request") -> "web.Response":
2464
+ """GET /api/jobs/{job_id} — get a single cron job."""
2465
+ auth_err = self._check_auth(request)
2466
+ if auth_err:
2467
+ return auth_err
2468
+ cron_err = self._check_jobs_available()
2469
+ if cron_err:
2470
+ return cron_err
2471
+ job_id, id_err = self._check_job_id(request)
2472
+ if id_err:
2473
+ return id_err
2474
+ try:
2475
+ job = _cron_get(job_id)
2476
+ if not job:
2477
+ return web.json_response({"error": "Job not found"}, status=404)
2478
+ return web.json_response({"job": job})
2479
+ except Exception as e:
2480
+ return web.json_response({"error": str(e)}, status=500)
2481
+
2482
+ async def _handle_update_job(self, request: "web.Request") -> "web.Response":
2483
+ """PATCH /api/jobs/{job_id} — update a cron job."""
2484
+ auth_err = self._check_auth(request)
2485
+ if auth_err:
2486
+ return auth_err
2487
+ cron_err = self._check_jobs_available()
2488
+ if cron_err:
2489
+ return cron_err
2490
+ job_id, id_err = self._check_job_id(request)
2491
+ if id_err:
2492
+ return id_err
2493
+ try:
2494
+ body = await request.json()
2495
+ # Whitelist allowed fields to prevent arbitrary key injection
2496
+ sanitized = {k: v for k, v in body.items() if k in self._UPDATE_ALLOWED_FIELDS}
2497
+ if not sanitized:
2498
+ return web.json_response({"error": "No valid fields to update"}, status=400)
2499
+ # Validate lengths if present
2500
+ if "name" in sanitized and len(sanitized["name"]) > self._MAX_NAME_LENGTH:
2501
+ return web.json_response(
2502
+ {"error": f"Name must be ≤ {self._MAX_NAME_LENGTH} characters"}, status=400,
2503
+ )
2504
+ if "prompt" in sanitized and len(sanitized["prompt"]) > self._MAX_PROMPT_LENGTH:
2505
+ return web.json_response(
2506
+ {"error": f"Prompt must be ≤ {self._MAX_PROMPT_LENGTH} characters"}, status=400,
2507
+ )
2508
+ job = _cron_update(job_id, sanitized)
2509
+ if not job:
2510
+ return web.json_response({"error": "Job not found"}, status=404)
2511
+ return web.json_response({"job": job})
2512
+ except Exception as e:
2513
+ return web.json_response({"error": str(e)}, status=500)
2514
+
2515
+ async def _handle_delete_job(self, request: "web.Request") -> "web.Response":
2516
+ """DELETE /api/jobs/{job_id} — delete a cron job."""
2517
+ auth_err = self._check_auth(request)
2518
+ if auth_err:
2519
+ return auth_err
2520
+ cron_err = self._check_jobs_available()
2521
+ if cron_err:
2522
+ return cron_err
2523
+ job_id, id_err = self._check_job_id(request)
2524
+ if id_err:
2525
+ return id_err
2526
+ try:
2527
+ success = _cron_remove(job_id)
2528
+ if not success:
2529
+ return web.json_response({"error": "Job not found"}, status=404)
2530
+ return web.json_response({"ok": True})
2531
+ except Exception as e:
2532
+ return web.json_response({"error": str(e)}, status=500)
2533
+
2534
+ async def _handle_pause_job(self, request: "web.Request") -> "web.Response":
2535
+ """POST /api/jobs/{job_id}/pause — pause a cron job."""
2536
+ auth_err = self._check_auth(request)
2537
+ if auth_err:
2538
+ return auth_err
2539
+ cron_err = self._check_jobs_available()
2540
+ if cron_err:
2541
+ return cron_err
2542
+ job_id, id_err = self._check_job_id(request)
2543
+ if id_err:
2544
+ return id_err
2545
+ try:
2546
+ job = _cron_pause(job_id)
2547
+ if not job:
2548
+ return web.json_response({"error": "Job not found"}, status=404)
2549
+ return web.json_response({"job": job})
2550
+ except Exception as e:
2551
+ return web.json_response({"error": str(e)}, status=500)
2552
+
2553
+ async def _handle_resume_job(self, request: "web.Request") -> "web.Response":
2554
+ """POST /api/jobs/{job_id}/resume — resume a paused cron job."""
2555
+ auth_err = self._check_auth(request)
2556
+ if auth_err:
2557
+ return auth_err
2558
+ cron_err = self._check_jobs_available()
2559
+ if cron_err:
2560
+ return cron_err
2561
+ job_id, id_err = self._check_job_id(request)
2562
+ if id_err:
2563
+ return id_err
2564
+ try:
2565
+ job = _cron_resume(job_id)
2566
+ if not job:
2567
+ return web.json_response({"error": "Job not found"}, status=404)
2568
+ return web.json_response({"job": job})
2569
+ except Exception as e:
2570
+ return web.json_response({"error": str(e)}, status=500)
2571
+
2572
+ async def _handle_run_job(self, request: "web.Request") -> "web.Response":
2573
+ """POST /api/jobs/{job_id}/run — trigger immediate execution."""
2574
+ auth_err = self._check_auth(request)
2575
+ if auth_err:
2576
+ return auth_err
2577
+ cron_err = self._check_jobs_available()
2578
+ if cron_err:
2579
+ return cron_err
2580
+ job_id, id_err = self._check_job_id(request)
2581
+ if id_err:
2582
+ return id_err
2583
+ try:
2584
+ job = _cron_trigger(job_id)
2585
+ if not job:
2586
+ return web.json_response({"error": "Job not found"}, status=404)
2587
+ return web.json_response({"job": job})
2588
+ except Exception as e:
2589
+ return web.json_response({"error": str(e)}, status=500)
2590
+
2591
+ # ------------------------------------------------------------------
2592
+ # Output extraction helper
2593
+ # ------------------------------------------------------------------
2594
+
2595
+ @staticmethod
2596
+ def _build_response_conversation_history(
2597
+ conversation_history: List[Dict[str, Any]],
2598
+ user_message: Any,
2599
+ result: Dict[str, Any],
2600
+ final_response: Any,
2601
+ ) -> List[Dict[str, Any]]:
2602
+ """Build the stored Responses transcript without duplicating history."""
2603
+ prior = list(conversation_history)
2604
+ current_user = {"role": "user", "content": user_message}
2605
+ agent_messages = result.get("messages") if isinstance(result, dict) else None
2606
+
2607
+ if isinstance(agent_messages, list) and agent_messages:
2608
+ turn_start = APIServerAdapter._response_messages_turn_start_index(
2609
+ conversation_history,
2610
+ user_message,
2611
+ result,
2612
+ )
2613
+ if turn_start:
2614
+ return list(agent_messages)
2615
+
2616
+ full_history = prior
2617
+ full_history.append(current_user)
2618
+ full_history.extend(agent_messages)
2619
+ return full_history
2620
+
2621
+ full_history = prior
2622
+ full_history.append(current_user)
2623
+ full_history.append({"role": "assistant", "content": final_response})
2624
+ return full_history
2625
+
2626
+ @staticmethod
2627
+ def _response_messages_turn_start_index(
2628
+ conversation_history: List[Dict[str, Any]],
2629
+ user_message: Any,
2630
+ result: Dict[str, Any],
2631
+ ) -> int:
2632
+ """Detect transcript-shaped result["messages"] and return turn start."""
2633
+ agent_messages = result.get("messages") if isinstance(result, dict) else None
2634
+ if not isinstance(agent_messages, list) or not agent_messages:
2635
+ return 0
2636
+
2637
+ prior = list(conversation_history)
2638
+ current_user = {"role": "user", "content": user_message}
2639
+ expected_prefix = prior + [current_user]
2640
+ if agent_messages[:len(expected_prefix)] == expected_prefix:
2641
+ return len(expected_prefix)
2642
+ if prior and agent_messages[:len(prior)] == prior:
2643
+ return len(prior)
2644
+ return 0
2645
+
2646
+ @staticmethod
2647
+ def _extract_output_items(result: Dict[str, Any], start_index: int = 0) -> List[Dict[str, Any]]:
2648
+ """
2649
+ Build the output item array from the agent's messages.
2650
+
2651
+ Walks *result["messages"]* starting at *start_index* and emits:
2652
+ - ``function_call`` items for each tool_call on assistant messages
2653
+ - ``function_call_output`` items for each tool-role message
2654
+ - a final ``message`` item with the assistant's text reply
2655
+ """
2656
+ items: List[Dict[str, Any]] = []
2657
+ messages = result.get("messages", [])
2658
+ if start_index > 0:
2659
+ messages = messages[start_index:]
2660
+
2661
+ for msg in messages:
2662
+ role = msg.get("role")
2663
+ if role == "assistant" and msg.get("tool_calls"):
2664
+ for tc in msg["tool_calls"]:
2665
+ func = tc.get("function", {})
2666
+ items.append({
2667
+ "type": "function_call",
2668
+ "name": func.get("name", ""),
2669
+ "arguments": func.get("arguments", ""),
2670
+ "call_id": tc.get("id", ""),
2671
+ })
2672
+ elif role == "tool":
2673
+ items.append({
2674
+ "type": "function_call_output",
2675
+ "call_id": msg.get("tool_call_id", ""),
2676
+ "output": msg.get("content", ""),
2677
+ })
2678
+
2679
+ # Final assistant message
2680
+ final = result.get("final_response", "")
2681
+ if not final:
2682
+ final = result.get("error", "(No response generated)")
2683
+
2684
+ items.append({
2685
+ "type": "message",
2686
+ "role": "assistant",
2687
+ "content": [
2688
+ {
2689
+ "type": "output_text",
2690
+ "text": final,
2691
+ }
2692
+ ],
2693
+ })
2694
+ return items
2695
+
2696
+ # ------------------------------------------------------------------
2697
+ # Agent execution
2698
+ # ------------------------------------------------------------------
2699
+
2700
+ async def _run_agent(
2701
+ self,
2702
+ user_message: str,
2703
+ conversation_history: List[Dict[str, str]],
2704
+ ephemeral_system_prompt: Optional[str] = None,
2705
+ session_id: Optional[str] = None,
2706
+ stream_delta_callback=None,
2707
+ tool_progress_callback=None,
2708
+ tool_start_callback=None,
2709
+ tool_complete_callback=None,
2710
+ agent_ref: Optional[list] = None,
2711
+ gateway_session_key: Optional[str] = None,
2712
+ ) -> tuple:
2713
+ """
2714
+ Create an agent and run a conversation in a thread executor.
2715
+
2716
+ Returns ``(result_dict, usage_dict)`` where *usage_dict* contains
2717
+ ``input_tokens``, ``output_tokens`` and ``total_tokens``.
2718
+
2719
+ If *agent_ref* is a one-element list, the AIAgent instance is stored
2720
+ at ``agent_ref[0]`` before ``run_conversation`` begins. This allows
2721
+ callers (e.g. the SSE writer) to call ``agent.interrupt()`` from
2722
+ another thread to stop in-progress LLM calls.
2723
+ """
2724
+ loop = asyncio.get_running_loop()
2725
+
2726
+ def _run():
2727
+ agent = self._create_agent(
2728
+ ephemeral_system_prompt=ephemeral_system_prompt,
2729
+ session_id=session_id,
2730
+ stream_delta_callback=stream_delta_callback,
2731
+ tool_progress_callback=tool_progress_callback,
2732
+ tool_start_callback=tool_start_callback,
2733
+ tool_complete_callback=tool_complete_callback,
2734
+ gateway_session_key=gateway_session_key,
2735
+ )
2736
+ if agent_ref is not None:
2737
+ agent_ref[0] = agent
2738
+ effective_task_id = session_id or str(uuid.uuid4())
2739
+ result = agent.run_conversation(
2740
+ user_message=user_message,
2741
+ conversation_history=conversation_history,
2742
+ task_id=effective_task_id,
2743
+ )
2744
+ usage = {
2745
+ "input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
2746
+ "output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
2747
+ "total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
2748
+ }
2749
+ # Include the effective session ID in the result so callers
2750
+ # (e.g. X-Hermes-Session-Id header) can track compression-
2751
+ # triggered session rotations. (#16938)
2752
+ _eff_sid = getattr(agent, "session_id", session_id)
2753
+ if isinstance(_eff_sid, str) and _eff_sid:
2754
+ result["session_id"] = _eff_sid
2755
+ return result, usage
2756
+
2757
+ return await loop.run_in_executor(None, _run)
2758
+
2759
+ # ------------------------------------------------------------------
2760
+ # /v1/runs — structured event streaming
2761
+ # ------------------------------------------------------------------
2762
+
2763
+ _MAX_CONCURRENT_RUNS = 10 # Prevent unbounded resource allocation
2764
+ _RUN_STREAM_TTL = 300 # seconds before orphaned runs are swept
2765
+ _RUN_STATUS_TTL = 3600 # seconds to retain terminal run status for polling
2766
+
2767
+ def _set_run_status(self, run_id: str, status: str, **fields: Any) -> Dict[str, Any]:
2768
+ """Update pollable run status without exposing private agent objects."""
2769
+ now = time.time()
2770
+ current = self._run_statuses.get(run_id, {})
2771
+ current.update({
2772
+ "object": "hermes.run",
2773
+ "run_id": run_id,
2774
+ "status": status,
2775
+ "updated_at": now,
2776
+ })
2777
+ current.setdefault("created_at", fields.pop("created_at", now))
2778
+ current.update(fields)
2779
+ self._run_statuses[run_id] = current
2780
+ return current
2781
+
2782
+ def _make_run_event_callback(self, run_id: str, loop: "asyncio.AbstractEventLoop"):
2783
+ """Return a tool_progress_callback that pushes structured events to the run's SSE queue."""
2784
+ def _push(event: Dict[str, Any]) -> None:
2785
+ self._set_run_status(
2786
+ run_id,
2787
+ self._run_statuses.get(run_id, {}).get("status", "running"),
2788
+ last_event=event.get("event"),
2789
+ )
2790
+ q = self._run_streams.get(run_id)
2791
+ if q is None:
2792
+ return
2793
+ try:
2794
+ loop.call_soon_threadsafe(q.put_nowait, event)
2795
+ except Exception:
2796
+ pass
2797
+
2798
+ def _callback(event_type: str, tool_name: str = None, preview: str = None, args=None, **kwargs):
2799
+ ts = time.time()
2800
+ if event_type == "tool.started":
2801
+ _push({
2802
+ "event": "tool.started",
2803
+ "run_id": run_id,
2804
+ "timestamp": ts,
2805
+ "tool": tool_name,
2806
+ "preview": preview,
2807
+ })
2808
+ elif event_type == "tool.completed":
2809
+ _push({
2810
+ "event": "tool.completed",
2811
+ "run_id": run_id,
2812
+ "timestamp": ts,
2813
+ "tool": tool_name,
2814
+ "duration": round(kwargs.get("duration", 0), 3),
2815
+ "error": kwargs.get("is_error", False),
2816
+ })
2817
+ elif event_type == "reasoning.available":
2818
+ _push({
2819
+ "event": "reasoning.available",
2820
+ "run_id": run_id,
2821
+ "timestamp": ts,
2822
+ "text": preview or "",
2823
+ })
2824
+ # _thinking and subagent_progress are intentionally not forwarded
2825
+
2826
+ return _callback
2827
+
2828
+ async def _handle_runs(self, request: "web.Request") -> "web.Response":
2829
+ """POST /v1/runs — start an agent run, return run_id immediately."""
2830
+ auth_err = self._check_auth(request)
2831
+ if auth_err:
2832
+ return auth_err
2833
+
2834
+ # Long-term memory scope header (see chat_completions for details).
2835
+ gateway_session_key, key_err = self._parse_session_key_header(request)
2836
+ if key_err is not None:
2837
+ return key_err
2838
+
2839
+ # Enforce concurrency limit
2840
+ if len(self._run_streams) >= self._MAX_CONCURRENT_RUNS:
2841
+ return web.json_response(
2842
+ _openai_error(f"Too many concurrent runs (max {self._MAX_CONCURRENT_RUNS})", code="rate_limit_exceeded"),
2843
+ status=429,
2844
+ )
2845
+
2846
+ try:
2847
+ body = await request.json()
2848
+ except Exception:
2849
+ return web.json_response(_openai_error("Invalid JSON"), status=400)
2850
+
2851
+ raw_input = body.get("input")
2852
+ if not raw_input:
2853
+ return web.json_response(_openai_error("Missing 'input' field"), status=400)
2854
+
2855
+ user_message = raw_input if isinstance(raw_input, str) else (raw_input[-1].get("content", "") if isinstance(raw_input, list) else "")
2856
+ if not user_message:
2857
+ return web.json_response(_openai_error("No user message found in input"), status=400)
2858
+
2859
+ instructions = body.get("instructions")
2860
+ previous_response_id = body.get("previous_response_id")
2861
+
2862
+ # Accept explicit conversation_history from the request body.
2863
+ # Precedence: explicit conversation_history > previous_response_id.
2864
+ conversation_history: List[Dict[str, str]] = []
2865
+ raw_history = body.get("conversation_history")
2866
+ if raw_history:
2867
+ if not isinstance(raw_history, list):
2868
+ return web.json_response(
2869
+ _openai_error("'conversation_history' must be an array of message objects"),
2870
+ status=400,
2871
+ )
2872
+ for i, entry in enumerate(raw_history):
2873
+ if not isinstance(entry, dict) or "role" not in entry or "content" not in entry:
2874
+ return web.json_response(
2875
+ _openai_error(f"conversation_history[{i}] must have 'role' and 'content' fields"),
2876
+ status=400,
2877
+ )
2878
+ conversation_history.append({"role": str(entry["role"]), "content": str(entry["content"])})
2879
+ if previous_response_id:
2880
+ logger.debug("Both conversation_history and previous_response_id provided; using conversation_history")
2881
+
2882
+ stored_session_id = None
2883
+ if not conversation_history and previous_response_id:
2884
+ stored = self._response_store.get(previous_response_id)
2885
+ if stored:
2886
+ conversation_history = list(stored.get("conversation_history", []))
2887
+ stored_session_id = stored.get("session_id")
2888
+ if instructions is None:
2889
+ instructions = stored.get("instructions")
2890
+
2891
+ # When input is a multi-message array, extract all but the last
2892
+ # message as conversation history (the last becomes user_message).
2893
+ # Only fires when no explicit history was provided.
2894
+ if not conversation_history and isinstance(raw_input, list) and len(raw_input) > 1:
2895
+ for msg in raw_input[:-1]:
2896
+ if isinstance(msg, dict) and msg.get("role") and msg.get("content"):
2897
+ content = msg["content"]
2898
+ if isinstance(content, list):
2899
+ # Flatten multi-part content blocks to text
2900
+ content = " ".join(
2901
+ part.get("text", "") for part in content
2902
+ if isinstance(part, dict) and part.get("type") == "text"
2903
+ )
2904
+ conversation_history.append({"role": msg["role"], "content": str(content)})
2905
+
2906
+ run_id = f"run_{uuid.uuid4().hex}"
2907
+ session_id = body.get("session_id") or stored_session_id or run_id
2908
+ approval_session_key = gateway_session_key or session_id or run_id
2909
+ ephemeral_system_prompt = instructions
2910
+ loop = asyncio.get_running_loop()
2911
+ q: "asyncio.Queue[Optional[Dict]]" = asyncio.Queue()
2912
+ created_at = time.time()
2913
+ self._run_streams[run_id] = q
2914
+ self._run_streams_created[run_id] = created_at
2915
+ self._run_approval_sessions[run_id] = approval_session_key
2916
+
2917
+ event_cb = self._make_run_event_callback(run_id, loop)
2918
+
2919
+ # Also wire stream_delta_callback so message.delta events flow through.
2920
+ def _text_cb(delta: Optional[str]) -> None:
2921
+ if delta is None:
2922
+ return
2923
+ try:
2924
+ loop.call_soon_threadsafe(q.put_nowait, {
2925
+ "event": "message.delta",
2926
+ "run_id": run_id,
2927
+ "timestamp": time.time(),
2928
+ "delta": delta,
2929
+ })
2930
+ except Exception:
2931
+ pass
2932
+
2933
+ self._set_run_status(
2934
+ run_id,
2935
+ "queued",
2936
+ created_at=created_at,
2937
+ session_id=session_id,
2938
+ model=body.get("model", self._model_name),
2939
+ )
2940
+
2941
+ async def _run_and_close():
2942
+ try:
2943
+ self._set_run_status(run_id, "running")
2944
+ agent = self._create_agent(
2945
+ ephemeral_system_prompt=ephemeral_system_prompt,
2946
+ session_id=session_id,
2947
+ stream_delta_callback=_text_cb,
2948
+ tool_progress_callback=event_cb,
2949
+ gateway_session_key=gateway_session_key,
2950
+ )
2951
+ self._active_run_agents[run_id] = agent
2952
+
2953
+ def _approval_notify(approval_data: Dict[str, Any]) -> None:
2954
+ event = dict(approval_data or {})
2955
+ event.update({
2956
+ "event": "approval.request",
2957
+ "run_id": run_id,
2958
+ "timestamp": time.time(),
2959
+ "choices": ["once", "session", "always", "deny"],
2960
+ })
2961
+ self._set_run_status(
2962
+ run_id,
2963
+ "waiting_for_approval",
2964
+ last_event="approval.request",
2965
+ )
2966
+ try:
2967
+ loop.call_soon_threadsafe(q.put_nowait, event)
2968
+ except Exception:
2969
+ pass
2970
+
2971
+ def _run_sync():
2972
+ from gateway.session_context import clear_session_vars, set_session_vars
2973
+ from tools.approval import (
2974
+ register_gateway_notify,
2975
+ reset_current_session_key,
2976
+ set_current_session_key,
2977
+ unregister_gateway_notify,
2978
+ )
2979
+
2980
+ effective_task_id = session_id or run_id
2981
+ approval_token = None
2982
+ session_tokens = []
2983
+ try:
2984
+ # Bind approval/session identity for this API run via
2985
+ # contextvars so concurrent runs do not share process
2986
+ # environment state.
2987
+ approval_token = set_current_session_key(approval_session_key)
2988
+ session_tokens = set_session_vars(
2989
+ platform="api_server",
2990
+ session_key=approval_session_key,
2991
+ )
2992
+ register_gateway_notify(approval_session_key, _approval_notify)
2993
+ r = agent.run_conversation(
2994
+ user_message=user_message,
2995
+ conversation_history=conversation_history,
2996
+ task_id=effective_task_id,
2997
+ )
2998
+ finally:
2999
+ try:
3000
+ unregister_gateway_notify(approval_session_key)
3001
+ finally:
3002
+ if approval_token is not None:
3003
+ try:
3004
+ reset_current_session_key(approval_token)
3005
+ except Exception:
3006
+ pass
3007
+ if session_tokens:
3008
+ try:
3009
+ clear_session_vars(session_tokens)
3010
+ except Exception:
3011
+ pass
3012
+ u = {
3013
+ "input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
3014
+ "output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
3015
+ "total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
3016
+ }
3017
+ return r, u
3018
+
3019
+ result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)
3020
+ # Check for structured failure (non-retryable client errors like
3021
+ # 401/400 return failed=True instead of raising, so the except
3022
+ # block below never fires — issue #15561).
3023
+ if isinstance(result, dict) and result.get("failed"):
3024
+ error_msg = result.get("error") or "agent run failed"
3025
+ q.put_nowait({
3026
+ "event": "run.failed",
3027
+ "run_id": run_id,
3028
+ "timestamp": time.time(),
3029
+ "error": error_msg,
3030
+ })
3031
+ self._set_run_status(
3032
+ run_id,
3033
+ "failed",
3034
+ error=error_msg,
3035
+ last_event="run.failed",
3036
+ )
3037
+ else:
3038
+ final_response = result.get("final_response", "") if isinstance(result, dict) else ""
3039
+ q.put_nowait({
3040
+ "event": "run.completed",
3041
+ "run_id": run_id,
3042
+ "timestamp": time.time(),
3043
+ "output": final_response,
3044
+ "usage": usage,
3045
+ })
3046
+ self._set_run_status(
3047
+ run_id,
3048
+ "completed",
3049
+ output=final_response,
3050
+ usage=usage,
3051
+ last_event="run.completed",
3052
+ )
3053
+ except asyncio.CancelledError:
3054
+ self._set_run_status(
3055
+ run_id,
3056
+ "cancelled",
3057
+ last_event="run.cancelled",
3058
+ )
3059
+ try:
3060
+ q.put_nowait({
3061
+ "event": "run.cancelled",
3062
+ "run_id": run_id,
3063
+ "timestamp": time.time(),
3064
+ })
3065
+ except Exception:
3066
+ pass
3067
+ raise
3068
+ except Exception as exc:
3069
+ logger.exception("[api_server] run %s failed", run_id)
3070
+ self._set_run_status(
3071
+ run_id,
3072
+ "failed",
3073
+ error=str(exc),
3074
+ last_event="run.failed",
3075
+ )
3076
+ try:
3077
+ q.put_nowait({
3078
+ "event": "run.failed",
3079
+ "run_id": run_id,
3080
+ "timestamp": time.time(),
3081
+ "error": str(exc),
3082
+ })
3083
+ except Exception:
3084
+ pass
3085
+ finally:
3086
+ # If the asyncio wrapper is cancelled (for example via
3087
+ # /stop), the executor thread can still be blocked waiting
3088
+ # on an approval Event. Unregistering here releases those
3089
+ # waits immediately; the in-thread unregister is harmlessly
3090
+ # idempotent on normal completion.
3091
+ try:
3092
+ from tools.approval import unregister_gateway_notify
3093
+
3094
+ unregister_gateway_notify(approval_session_key)
3095
+ except Exception:
3096
+ pass
3097
+ # Sentinel: signal SSE stream to close
3098
+ try:
3099
+ q.put_nowait(None)
3100
+ except Exception:
3101
+ pass
3102
+ self._active_run_agents.pop(run_id, None)
3103
+ self._active_run_tasks.pop(run_id, None)
3104
+ self._run_approval_sessions.pop(run_id, None)
3105
+
3106
+ task = asyncio.create_task(_run_and_close())
3107
+ self._active_run_tasks[run_id] = task
3108
+ try:
3109
+ self._background_tasks.add(task)
3110
+ except TypeError:
3111
+ pass
3112
+ if hasattr(task, "add_done_callback"):
3113
+ task.add_done_callback(self._background_tasks.discard)
3114
+
3115
+ response_headers = (
3116
+ {"X-Hermes-Session-Key": gateway_session_key} if gateway_session_key else {}
3117
+ )
3118
+ return web.json_response(
3119
+ {"run_id": run_id, "status": "started"},
3120
+ status=202,
3121
+ headers=response_headers,
3122
+ )
3123
+
3124
+ async def _handle_get_run(self, request: "web.Request") -> "web.Response":
3125
+ """GET /v1/runs/{run_id} — return pollable run status for external UIs."""
3126
+ auth_err = self._check_auth(request)
3127
+ if auth_err:
3128
+ return auth_err
3129
+
3130
+ run_id = request.match_info["run_id"]
3131
+ status = self._run_statuses.get(run_id)
3132
+ if status is None:
3133
+ return web.json_response(
3134
+ _openai_error(f"Run not found: {run_id}", code="run_not_found"),
3135
+ status=404,
3136
+ )
3137
+ return web.json_response(status)
3138
+
3139
+ async def _handle_run_events(self, request: "web.Request") -> "web.StreamResponse":
3140
+ """GET /v1/runs/{run_id}/events — SSE stream of structured agent lifecycle events."""
3141
+ auth_err = self._check_auth(request)
3142
+ if auth_err:
3143
+ return auth_err
3144
+
3145
+ run_id = request.match_info["run_id"]
3146
+
3147
+ # Allow subscribing slightly before the run is registered (race condition window)
3148
+ for _ in range(20):
3149
+ if run_id in self._run_streams:
3150
+ break
3151
+ await asyncio.sleep(0.05)
3152
+ else:
3153
+ return web.json_response(_openai_error(f"Run not found: {run_id}", code="run_not_found"), status=404)
3154
+
3155
+ q = self._run_streams[run_id]
3156
+
3157
+ response = web.StreamResponse(
3158
+ status=200,
3159
+ headers={
3160
+ "Content-Type": "text/event-stream",
3161
+ "Cache-Control": "no-cache",
3162
+ "X-Accel-Buffering": "no",
3163
+ },
3164
+ )
3165
+ await response.prepare(request)
3166
+
3167
+ try:
3168
+ while True:
3169
+ try:
3170
+ event = await asyncio.wait_for(q.get(), timeout=30.0)
3171
+ except asyncio.TimeoutError:
3172
+ await response.write(b": keepalive\n\n")
3173
+ continue
3174
+ if event is None:
3175
+ # Run finished — send final SSE comment and close
3176
+ await response.write(b": stream closed\n\n")
3177
+ break
3178
+ payload = f"data: {json.dumps(event)}\n\n"
3179
+ await response.write(payload.encode())
3180
+ except Exception as exc:
3181
+ logger.debug("[api_server] SSE stream error for run %s: %s", run_id, exc)
3182
+ finally:
3183
+ self._run_streams.pop(run_id, None)
3184
+ self._run_streams_created.pop(run_id, None)
3185
+
3186
+ return response
3187
+
3188
+
3189
+ async def _handle_run_approval(self, request: "web.Request") -> "web.Response":
3190
+ """POST /v1/runs/{run_id}/approval — resolve a pending run approval."""
3191
+ auth_err = self._check_auth(request)
3192
+ if auth_err:
3193
+ return auth_err
3194
+
3195
+ run_id = request.match_info["run_id"]
3196
+ status = self._run_statuses.get(run_id)
3197
+ if status is None:
3198
+ return web.json_response(
3199
+ _openai_error(f"Run not found: {run_id}", code="run_not_found"),
3200
+ status=404,
3201
+ )
3202
+
3203
+ try:
3204
+ body = await request.json()
3205
+ except Exception:
3206
+ return web.json_response(_openai_error("Invalid JSON"), status=400)
3207
+
3208
+ raw_choice = str(body.get("choice", "")).strip().lower()
3209
+ aliases = {"approve": "once", "approved": "once", "allow": "once"}
3210
+ choice = aliases.get(raw_choice, raw_choice)
3211
+ allowed = {"once", "session", "always", "deny"}
3212
+ if choice not in allowed:
3213
+ return web.json_response(
3214
+ _openai_error(
3215
+ "Invalid approval choice; expected one of: once, session, always, deny",
3216
+ code="invalid_approval_choice",
3217
+ ),
3218
+ status=400,
3219
+ )
3220
+
3221
+ approval_session_key = self._run_approval_sessions.get(run_id)
3222
+ if not approval_session_key:
3223
+ return web.json_response(
3224
+ _openai_error(
3225
+ f"Run has no active approval session: {run_id}",
3226
+ code="approval_not_active",
3227
+ ),
3228
+ status=409,
3229
+ )
3230
+
3231
+ resolve_all = bool(body.get("all") or body.get("resolve_all"))
3232
+ try:
3233
+ from tools.approval import resolve_gateway_approval
3234
+
3235
+ resolved = resolve_gateway_approval(
3236
+ approval_session_key,
3237
+ choice,
3238
+ resolve_all=resolve_all,
3239
+ )
3240
+ except Exception as exc:
3241
+ logger.exception("[api_server] approval resolution failed for run %s", run_id)
3242
+ return web.json_response(_openai_error(str(exc)), status=500)
3243
+
3244
+ if resolved <= 0:
3245
+ return web.json_response(
3246
+ _openai_error(
3247
+ f"Run has no pending approval: {run_id}",
3248
+ code="approval_not_pending",
3249
+ ),
3250
+ status=409,
3251
+ )
3252
+
3253
+ self._set_run_status(run_id, "running", last_event="approval.responded")
3254
+ q = self._run_streams.get(run_id)
3255
+ if q is not None:
3256
+ try:
3257
+ q.put_nowait({
3258
+ "event": "approval.responded",
3259
+ "run_id": run_id,
3260
+ "timestamp": time.time(),
3261
+ "choice": choice,
3262
+ "resolved": resolved,
3263
+ })
3264
+ except Exception:
3265
+ pass
3266
+
3267
+ return web.json_response({
3268
+ "object": "hermes.run.approval_response",
3269
+ "run_id": run_id,
3270
+ "choice": choice,
3271
+ "resolved": resolved,
3272
+ })
3273
+
3274
+ async def _handle_stop_run(self, request: "web.Request") -> "web.Response":
3275
+ """POST /v1/runs/{run_id}/stop — interrupt a running agent."""
3276
+ auth_err = self._check_auth(request)
3277
+ if auth_err:
3278
+ return auth_err
3279
+
3280
+ run_id = request.match_info["run_id"]
3281
+ agent = self._active_run_agents.get(run_id)
3282
+ task = self._active_run_tasks.get(run_id)
3283
+
3284
+ if agent is None and task is None:
3285
+ return web.json_response(_openai_error(f"Run not found: {run_id}", code="run_not_found"), status=404)
3286
+
3287
+ self._set_run_status(run_id, "stopping", last_event="run.stopping")
3288
+
3289
+ if agent is not None:
3290
+ try:
3291
+ agent.interrupt("Stop requested via API")
3292
+ except Exception:
3293
+ pass
3294
+
3295
+ if task is not None and not task.done():
3296
+ task.cancel()
3297
+ # Bounded wait: run_conversation() executes in the default
3298
+ # executor thread which task.cancel() cannot preempt — we rely on
3299
+ # agent.interrupt() above to break the loop. Cap the wait so a
3300
+ # slow/unresponsive interrupt can't hang this handler.
3301
+ try:
3302
+ await asyncio.wait_for(asyncio.shield(task), timeout=5.0)
3303
+ except asyncio.TimeoutError:
3304
+ logger.warning(
3305
+ "[api_server] stop for run %s timed out after 5s; "
3306
+ "agent may still be finishing the current step",
3307
+ run_id,
3308
+ )
3309
+ except (asyncio.CancelledError, Exception):
3310
+ pass
3311
+
3312
+ return web.json_response({"run_id": run_id, "status": "stopping"})
3313
+
3314
+ async def _sweep_orphaned_runs(self) -> None:
3315
+ """Periodically clean up run streams that were never consumed."""
3316
+ while True:
3317
+ await asyncio.sleep(60)
3318
+ now = time.time()
3319
+ stale = [
3320
+ run_id
3321
+ for run_id, created_at in list(self._run_streams_created.items())
3322
+ if now - created_at > self._RUN_STREAM_TTL
3323
+ ]
3324
+ for run_id in stale:
3325
+ logger.debug("[api_server] sweeping orphaned run %s", run_id)
3326
+ try:
3327
+ from tools.approval import unregister_gateway_notify
3328
+
3329
+ approval_session_key = self._run_approval_sessions.get(run_id)
3330
+ if approval_session_key:
3331
+ unregister_gateway_notify(approval_session_key)
3332
+ except Exception:
3333
+ pass
3334
+ self._run_streams.pop(run_id, None)
3335
+ self._run_streams_created.pop(run_id, None)
3336
+ self._active_run_agents.pop(run_id, None)
3337
+ self._active_run_tasks.pop(run_id, None)
3338
+ self._run_approval_sessions.pop(run_id, None)
3339
+
3340
+ stale_statuses = [
3341
+ run_id
3342
+ for run_id, status in list(self._run_statuses.items())
3343
+ if status.get("status") in {"completed", "failed", "cancelled"}
3344
+ and now - float(status.get("updated_at", 0) or 0) > self._RUN_STATUS_TTL
3345
+ ]
3346
+ for run_id in stale_statuses:
3347
+ self._run_statuses.pop(run_id, None)
3348
+
3349
+ # ------------------------------------------------------------------
3350
+ # BasePlatformAdapter interface
3351
+ # ------------------------------------------------------------------
3352
+
3353
+ async def connect(self) -> bool:
3354
+ """Start the aiohttp web server."""
3355
+ if not AIOHTTP_AVAILABLE:
3356
+ logger.warning("[%s] aiohttp not installed", self.name)
3357
+ return False
3358
+
3359
+ try:
3360
+ mws = [mw for mw in (cors_middleware, body_limit_middleware, security_headers_middleware) if mw is not None]
3361
+ self._app = web.Application(middlewares=mws, client_max_size=MAX_REQUEST_BYTES)
3362
+ self._app["api_server_adapter"] = self
3363
+ self._app.router.add_get("/health", self._handle_health)
3364
+ self._app.router.add_get("/health/detailed", self._handle_health_detailed)
3365
+ self._app.router.add_get("/v1/health", self._handle_health)
3366
+ self._app.router.add_get("/v1/models", self._handle_models)
3367
+ self._app.router.add_get("/v1/capabilities", self._handle_capabilities)
3368
+ self._app.router.add_post("/v1/chat/completions", self._handle_chat_completions)
3369
+ self._app.router.add_post("/v1/responses", self._handle_responses)
3370
+ self._app.router.add_get("/v1/responses/{response_id}", self._handle_get_response)
3371
+ self._app.router.add_delete("/v1/responses/{response_id}", self._handle_delete_response)
3372
+ # Cron jobs management API
3373
+ self._app.router.add_get("/api/jobs", self._handle_list_jobs)
3374
+ self._app.router.add_post("/api/jobs", self._handle_create_job)
3375
+ self._app.router.add_get("/api/jobs/{job_id}", self._handle_get_job)
3376
+ self._app.router.add_patch("/api/jobs/{job_id}", self._handle_update_job)
3377
+ self._app.router.add_delete("/api/jobs/{job_id}", self._handle_delete_job)
3378
+ self._app.router.add_post("/api/jobs/{job_id}/pause", self._handle_pause_job)
3379
+ self._app.router.add_post("/api/jobs/{job_id}/resume", self._handle_resume_job)
3380
+ self._app.router.add_post("/api/jobs/{job_id}/run", self._handle_run_job)
3381
+ # Structured event streaming
3382
+ self._app.router.add_post("/v1/runs", self._handle_runs)
3383
+ self._app.router.add_get("/v1/runs/{run_id}", self._handle_get_run)
3384
+ self._app.router.add_get("/v1/runs/{run_id}/events", self._handle_run_events)
3385
+ self._app.router.add_post("/v1/runs/{run_id}/approval", self._handle_run_approval)
3386
+ self._app.router.add_post("/v1/runs/{run_id}/stop", self._handle_stop_run)
3387
+ # Start background sweep to clean up orphaned (unconsumed) run streams
3388
+ sweep_task = asyncio.create_task(self._sweep_orphaned_runs())
3389
+ try:
3390
+ self._background_tasks.add(sweep_task)
3391
+ except TypeError:
3392
+ pass
3393
+ if hasattr(sweep_task, "add_done_callback"):
3394
+ sweep_task.add_done_callback(self._background_tasks.discard)
3395
+
3396
+ # Refuse to start network-accessible without authentication
3397
+ if is_network_accessible(self._host) and not self._api_key:
3398
+ logger.error(
3399
+ "[%s] Refusing to start: binding to %s requires API_SERVER_KEY. "
3400
+ "Set API_SERVER_KEY or use the default 127.0.0.1.",
3401
+ self.name, self._host,
3402
+ )
3403
+ return False
3404
+
3405
+ # Refuse to start network-accessible with a placeholder key.
3406
+ # Ported from openclaw/openclaw#64586.
3407
+ if is_network_accessible(self._host) and self._api_key:
3408
+ try:
3409
+ from hermes_cli.auth import has_usable_secret
3410
+ if not has_usable_secret(self._api_key, min_length=8):
3411
+ logger.error(
3412
+ "[%s] Refusing to start: API_SERVER_KEY is set to a "
3413
+ "placeholder value. Generate a real secret "
3414
+ "(e.g. `openssl rand -hex 32`) and set API_SERVER_KEY "
3415
+ "before exposing the API server on %s.",
3416
+ self.name, self._host,
3417
+ )
3418
+ return False
3419
+ except ImportError:
3420
+ pass
3421
+
3422
+ # Port conflict detection — fail fast if port is already in use
3423
+ try:
3424
+ with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as _s:
3425
+ _s.settimeout(1)
3426
+ _s.connect(('127.0.0.1', self._port))
3427
+ logger.error('[%s] Port %d already in use. Set a different port in config.yaml: platforms.api_server.port', self.name, self._port)
3428
+ return False
3429
+ except (ConnectionRefusedError, OSError):
3430
+ pass # port is free
3431
+
3432
+ self._runner = web.AppRunner(self._app)
3433
+ await self._runner.setup()
3434
+ self._site = web.TCPSite(self._runner, self._host, self._port)
3435
+ await self._site.start()
3436
+
3437
+ self._mark_connected()
3438
+ if not self._api_key:
3439
+ logger.warning(
3440
+ "[%s] ⚠️ No API key configured (API_SERVER_KEY / platforms.api_server.key). "
3441
+ "All requests will be accepted without authentication. "
3442
+ "Set an API key for production deployments to prevent "
3443
+ "unauthorized access to sessions, responses, and cron jobs.",
3444
+ self.name,
3445
+ )
3446
+ logger.info(
3447
+ "[%s] API server listening on http://%s:%d (model: %s)",
3448
+ self.name, self._host, self._port, self._model_name,
3449
+ )
3450
+ return True
3451
+
3452
+ except Exception as e:
3453
+ logger.error("[%s] Failed to start API server: %s", self.name, e)
3454
+ return False
3455
+
3456
+ async def disconnect(self) -> None:
3457
+ """Stop the aiohttp web server."""
3458
+ self._mark_disconnected()
3459
+ if self._site:
3460
+ await self._site.stop()
3461
+ self._site = None
3462
+ if self._runner:
3463
+ await self._runner.cleanup()
3464
+ self._runner = None
3465
+ self._app = None
3466
+ logger.info("[%s] API server stopped", self.name)
3467
+
3468
+ async def send(
3469
+ self,
3470
+ chat_id: str,
3471
+ content: str,
3472
+ reply_to: Optional[str] = None,
3473
+ metadata: Optional[Dict[str, Any]] = None,
3474
+ ) -> SendResult:
3475
+ """
3476
+ Not used — HTTP request/response cycle handles delivery directly.
3477
+ """
3478
+ return SendResult(success=False, error="API server uses HTTP request/response, not send()")
3479
+
3480
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
3481
+ """Return basic info about the API server."""
3482
+ return {
3483
+ "name": "API Server",
3484
+ "type": "api",
3485
+ "host": self._host,
3486
+ "port": self._port,
3487
+ }
3488
+