ciris-agent 1.7.7__py3-none-any.whl

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 (986) hide show
  1. ciris_adapters/README.md +113 -0
  2. ciris_adapters/__init__.py +30 -0
  3. ciris_adapters/ciris_covenant_metrics/README.md +144 -0
  4. ciris_adapters/ciris_covenant_metrics/__init__.py +36 -0
  5. ciris_adapters/ciris_covenant_metrics/adapter.py +249 -0
  6. ciris_adapters/ciris_covenant_metrics/manifest.json +152 -0
  7. ciris_adapters/ciris_covenant_metrics/services.py +403 -0
  8. ciris_adapters/ciris_hosted_tools/__init__.py +24 -0
  9. ciris_adapters/ciris_hosted_tools/adapter.py +169 -0
  10. ciris_adapters/ciris_hosted_tools/manifest.json +94 -0
  11. ciris_adapters/ciris_hosted_tools/services.py +744 -0
  12. ciris_adapters/external_data_sql/README.md +559 -0
  13. ciris_adapters/external_data_sql/__init__.py +43 -0
  14. ciris_adapters/external_data_sql/adapter.py +144 -0
  15. ciris_adapters/external_data_sql/configurable.py +315 -0
  16. ciris_adapters/external_data_sql/dialects/__init__.py +37 -0
  17. ciris_adapters/external_data_sql/dialects/base.py +133 -0
  18. ciris_adapters/external_data_sql/dialects/mysql.py +63 -0
  19. ciris_adapters/external_data_sql/dialects/postgresql.py +59 -0
  20. ciris_adapters/external_data_sql/dialects/sqlite.py +62 -0
  21. ciris_adapters/external_data_sql/example_config.json +88 -0
  22. ciris_adapters/external_data_sql/example_privacy_schema.yaml +127 -0
  23. ciris_adapters/external_data_sql/manifest.json +195 -0
  24. ciris_adapters/external_data_sql/privacy_schema_loader.py +189 -0
  25. ciris_adapters/external_data_sql/protocol.py +101 -0
  26. ciris_adapters/external_data_sql/schemas.py +146 -0
  27. ciris_adapters/external_data_sql/service.py +1547 -0
  28. ciris_adapters/external_data_sql/service_old.py +492 -0
  29. ciris_adapters/home_assistant/__init__.py +63 -0
  30. ciris_adapters/home_assistant/adapter.py +201 -0
  31. ciris_adapters/home_assistant/communication_service.py +347 -0
  32. ciris_adapters/home_assistant/configurable.py +667 -0
  33. ciris_adapters/home_assistant/manifest.json +203 -0
  34. ciris_adapters/home_assistant/schemas.py +129 -0
  35. ciris_adapters/home_assistant/service.py +751 -0
  36. ciris_adapters/home_assistant/tool_service.py +441 -0
  37. ciris_adapters/mcp_client/__init__.py +82 -0
  38. ciris_adapters/mcp_client/adapter.py +847 -0
  39. ciris_adapters/mcp_client/config.py +280 -0
  40. ciris_adapters/mcp_client/configurable.py +422 -0
  41. ciris_adapters/mcp_client/manifest.json +185 -0
  42. ciris_adapters/mcp_client/mcp_communication_service.py +393 -0
  43. ciris_adapters/mcp_client/mcp_tool_service.py +463 -0
  44. ciris_adapters/mcp_client/mcp_wise_service.py +394 -0
  45. ciris_adapters/mcp_client/schemas.py +149 -0
  46. ciris_adapters/mcp_client/security.py +592 -0
  47. ciris_adapters/mcp_common/__init__.py +44 -0
  48. ciris_adapters/mcp_common/manifest.json +25 -0
  49. ciris_adapters/mcp_common/protocol.py +315 -0
  50. ciris_adapters/mcp_common/schemas.py +225 -0
  51. ciris_adapters/mcp_server/__init__.py +47 -0
  52. ciris_adapters/mcp_server/adapter.py +581 -0
  53. ciris_adapters/mcp_server/config.py +260 -0
  54. ciris_adapters/mcp_server/configurable.py +393 -0
  55. ciris_adapters/mcp_server/handlers.py +663 -0
  56. ciris_adapters/mcp_server/manifest.json +211 -0
  57. ciris_adapters/mcp_server/security.py +500 -0
  58. ciris_adapters/mock_llm/README.md +117 -0
  59. ciris_adapters/mock_llm/__init__.py +21 -0
  60. ciris_adapters/mock_llm/adapter.py +131 -0
  61. ciris_adapters/mock_llm/configurable.py +237 -0
  62. ciris_adapters/mock_llm/manifest.json +106 -0
  63. ciris_adapters/mock_llm/protocol.py +37 -0
  64. ciris_adapters/mock_llm/responses.py +520 -0
  65. ciris_adapters/mock_llm/responses_action_selection.py +1041 -0
  66. ciris_adapters/mock_llm/responses_epistemic.py +17 -0
  67. ciris_adapters/mock_llm/responses_feedback.py +27 -0
  68. ciris_adapters/mock_llm/schemas.py +35 -0
  69. ciris_adapters/mock_llm/service.py +294 -0
  70. ciris_adapters/navigation/__init__.py +21 -0
  71. ciris_adapters/navigation/adapter.py +129 -0
  72. ciris_adapters/navigation/configurable.py +239 -0
  73. ciris_adapters/navigation/manifest.json +104 -0
  74. ciris_adapters/navigation/service.py +487 -0
  75. ciris_adapters/reddit/README.md +132 -0
  76. ciris_adapters/reddit/REDDIT_ADAPTER_ANALYSIS.md +715 -0
  77. ciris_adapters/reddit/REDDIT_ADAPTER_SUMMARY.txt +278 -0
  78. ciris_adapters/reddit/REDDIT_ANALYSIS_INDEX.md +307 -0
  79. ciris_adapters/reddit/REDDIT_PRODUCTION_READINESS_PLAN.md +518 -0
  80. ciris_adapters/reddit/__init__.py +15 -0
  81. ciris_adapters/reddit/adapter.py +189 -0
  82. ciris_adapters/reddit/configurable.py +274 -0
  83. ciris_adapters/reddit/error_handler.py +307 -0
  84. ciris_adapters/reddit/manifest.json +218 -0
  85. ciris_adapters/reddit/observer.py +532 -0
  86. ciris_adapters/reddit/protocol.py +34 -0
  87. ciris_adapters/reddit/schemas.py +433 -0
  88. ciris_adapters/reddit/service.py +1471 -0
  89. ciris_adapters/sample_adapter/README.md +474 -0
  90. ciris_adapters/sample_adapter/__init__.py +45 -0
  91. ciris_adapters/sample_adapter/adapter.py +208 -0
  92. ciris_adapters/sample_adapter/configurable.py +469 -0
  93. ciris_adapters/sample_adapter/manifest.json +247 -0
  94. ciris_adapters/sample_adapter/services.py +486 -0
  95. ciris_adapters/weather/__init__.py +16 -0
  96. ciris_adapters/weather/adapter.py +130 -0
  97. ciris_adapters/weather/configurable.py +240 -0
  98. ciris_adapters/weather/manifest.json +156 -0
  99. ciris_adapters/weather/service.py +600 -0
  100. ciris_agent-1.7.7.dist-info/METADATA +284 -0
  101. ciris_agent-1.7.7.dist-info/RECORD +986 -0
  102. ciris_agent-1.7.7.dist-info/WHEEL +5 -0
  103. ciris_agent-1.7.7.dist-info/entry_points.txt +15 -0
  104. ciris_agent-1.7.7.dist-info/licenses/LICENSE +205 -0
  105. ciris_agent-1.7.7.dist-info/licenses/NOTICE +82 -0
  106. ciris_agent-1.7.7.dist-info/top_level.txt +4 -0
  107. ciris_engine/__init__.py +15 -0
  108. ciris_engine/ciris_templates/ally.yaml +632 -0
  109. ciris_engine/ciris_templates/default.yaml +411 -0
  110. ciris_engine/ciris_templates/echo-core.yaml +629 -0
  111. ciris_engine/ciris_templates/echo-speculative.yaml +764 -0
  112. ciris_engine/ciris_templates/echo.yaml +647 -0
  113. ciris_engine/ciris_templates/sage.yaml +332 -0
  114. ciris_engine/ciris_templates/scout.yaml +338 -0
  115. ciris_engine/ciris_templates/test.yaml +168 -0
  116. ciris_engine/cli.py +42 -0
  117. ciris_engine/config/CIRIS_SERVICES.json +19 -0
  118. ciris_engine/config/MODEL_CAPABILITIES.json +419 -0
  119. ciris_engine/config/PRICING_DATA.json +179 -0
  120. ciris_engine/config/__init__.py +50 -0
  121. ciris_engine/config/ciris_services.py +113 -0
  122. ciris_engine/config/model_capabilities.py +388 -0
  123. ciris_engine/config/pricing_models.py +276 -0
  124. ciris_engine/constants.py +35 -0
  125. ciris_engine/data/__init__.py +1 -0
  126. ciris_engine/data/covenant_1.0b.txt +978 -0
  127. ciris_engine/gui_static/11steps.svg +107 -0
  128. ciris_engine/gui_static/2x-schematics.png +0 -0
  129. ciris_engine/gui_static/404/index.html +1 -0
  130. ciris_engine/gui_static/404.html +1 -0
  131. ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_buildManifest.js +1 -0
  132. ciris_engine/gui_static/_next/static/0edhkwDxd5UccTsCmtaBi/_ssgManifest.js +1 -0
  133. ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_buildManifest.js +1 -0
  134. ciris_engine/gui_static/_next/static/U-3xTQao7hc2wnAi-Uekm/_ssgManifest.js +1 -0
  135. ciris_engine/gui_static/_next/static/chunks/3297-60e86ba0f8a7b040.js +1 -0
  136. ciris_engine/gui_static/_next/static/chunks/3835-2aad4b7f5f8e4643.js +1 -0
  137. ciris_engine/gui_static/_next/static/chunks/4499-99a0bc47de0b8975.js +1 -0
  138. ciris_engine/gui_static/_next/static/chunks/4534-af88cd4ba6e99bff.js +1 -0
  139. ciris_engine/gui_static/_next/static/chunks/4541-84b455f9e0dc4cfe.js +1 -0
  140. ciris_engine/gui_static/_next/static/chunks/4789-61412711484754bb.js +1 -0
  141. ciris_engine/gui_static/_next/static/chunks/6539-c6398bc9d7018430.js +1 -0
  142. ciris_engine/gui_static/_next/static/chunks/704-8e827b26cc8c2d32.js +1 -0
  143. ciris_engine/gui_static/_next/static/chunks/704-fb45d630f3192c6f.js +1 -0
  144. ciris_engine/gui_static/_next/static/chunks/8072-de4952a2e6d2b33f.js +1 -0
  145. ciris_engine/gui_static/_next/static/chunks/8315-b91d03a3949db0af.js +1 -0
  146. ciris_engine/gui_static/_next/static/chunks/8386-f93a83ccbd789bd9.js +1 -0
  147. ciris_engine/gui_static/_next/static/chunks/87c73c54-781a7f35148d5433.js +1 -0
  148. ciris_engine/gui_static/_next/static/chunks/8903-fefea3339a02d41b.js +1 -0
  149. ciris_engine/gui_static/_next/static/chunks/9090-e66485adf8d9d990.js +1 -0
  150. ciris_engine/gui_static/_next/static/chunks/app/_not-found/page-a67d9808462c23b1.js +1 -0
  151. ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-2d7ee1583bbbd02e.js +1 -0
  152. ciris_engine/gui_static/_next/static/chunks/app/account/api-keys/page-6a3c2bae6fe92b7b.js +1 -0
  153. ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-2ed3a035136bc4e8.js +1 -0
  154. ciris_engine/gui_static/_next/static/chunks/app/account/consent/page-b2f5c91844a32422.js +1 -0
  155. ciris_engine/gui_static/_next/static/chunks/app/account/page-25b90f89af3ea58c.js +1 -0
  156. ciris_engine/gui_static/_next/static/chunks/app/account/page-b65d16c94ecaf69c.js +1 -0
  157. ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-675b6d05c8f9184f.js +1 -0
  158. ciris_engine/gui_static/_next/static/chunks/app/account/privacy/page-cbee2e1c8ab52145.js +1 -0
  159. ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-0f44da06697cf9f0.js +1 -0
  160. ciris_engine/gui_static/_next/static/chunks/app/account/settings/page-563420253577edbf.js +1 -0
  161. ciris_engine/gui_static/_next/static/chunks/app/adapters/page-1854631018bc32be.js +1 -0
  162. ciris_engine/gui_static/_next/static/chunks/app/agents/page-8353752c176a7c70.js +1 -0
  163. ciris_engine/gui_static/_next/static/chunks/app/agents/page-f61a529f110a6040.js +1 -0
  164. ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-7f19b9d20d39be28.js +1 -0
  165. ciris_engine/gui_static/_next/static/chunks/app/api-demo/page-d1063938f249b8bd.js +1 -0
  166. ciris_engine/gui_static/_next/static/chunks/app/audit/page-321b6728b8fff0bb.js +1 -0
  167. ciris_engine/gui_static/_next/static/chunks/app/audit/page-ebac35ca961a1277.js +1 -0
  168. ciris_engine/gui_static/_next/static/chunks/app/billing/page-6f3dc3bd02924f8e.js +1 -0
  169. ciris_engine/gui_static/_next/static/chunks/app/billing/page-fa4a469f814c821a.js +1 -0
  170. ciris_engine/gui_static/_next/static/chunks/app/comms/page-0d4f734269addd8f.js +1 -0
  171. ciris_engine/gui_static/_next/static/chunks/app/comms/page-79227d426050089c.js +1 -0
  172. ciris_engine/gui_static/_next/static/chunks/app/config/page-018d21d683b6e5bc.js +1 -0
  173. ciris_engine/gui_static/_next/static/chunks/app/config/page-2aa5a5363ca2a371.js +1 -0
  174. ciris_engine/gui_static/_next/static/chunks/app/consent/page-198373205fd316e2.js +1 -0
  175. ciris_engine/gui_static/_next/static/chunks/app/consent/page-f2ca39e7713b13f8.js +1 -0
  176. ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-1dd5a196f643c60d.js +1 -0
  177. ciris_engine/gui_static/_next/static/chunks/app/dashboard/page-530a04d3abbb8cda.js +1 -0
  178. ciris_engine/gui_static/_next/static/chunks/app/docs/page-3193b06d094ab654.js +1 -0
  179. ciris_engine/gui_static/_next/static/chunks/app/docs/page-330e996dedb87aba.js +1 -0
  180. ciris_engine/gui_static/_next/static/chunks/app/layout-0a70f5fc460298b1.js +1 -0
  181. ciris_engine/gui_static/_next/static/chunks/app/layout-21f2f99dd5b336e9.js +1 -0
  182. ciris_engine/gui_static/_next/static/chunks/app/login/page-33240e6c6034a49d.js +1 -0
  183. ciris_engine/gui_static/_next/static/chunks/app/login/page-68ffab6d54a7fdcd.js +1 -0
  184. ciris_engine/gui_static/_next/static/chunks/app/logs/page-8a6167aecc4a475c.js +1 -0
  185. ciris_engine/gui_static/_next/static/chunks/app/memory/page-9ca8c5d0056de3ff.js +1 -0
  186. ciris_engine/gui_static/_next/static/chunks/app/memory/page-e961226941c18f81.js +1 -0
  187. ciris_engine/gui_static/_next/static/chunks/app/page-6fdb065a787a4974.js +1 -0
  188. ciris_engine/gui_static/_next/static/chunks/app/page-89f87d431be6064a.js +1 -0
  189. ciris_engine/gui_static/_next/static/chunks/app/runtime/page-2e728b9c43aa164d.js +1 -0
  190. ciris_engine/gui_static/_next/static/chunks/app/runtime/page-c7dd033dc40a72f0.js +1 -0
  191. ciris_engine/gui_static/_next/static/chunks/app/services/page-ae9f0bdf11d01a95.js +1 -0
  192. ciris_engine/gui_static/_next/static/chunks/app/services/page-b10feb79ca5d75e5.js +1 -0
  193. ciris_engine/gui_static/_next/static/chunks/app/sessions/page-13ebe7ef1c16ae11.js +1 -0
  194. ciris_engine/gui_static/_next/static/chunks/app/sessions/page-e6c82b16d617f785.js +1 -0
  195. ciris_engine/gui_static/_next/static/chunks/app/setup/page-0beb5f5b5a5c20fc.js +1 -0
  196. ciris_engine/gui_static/_next/static/chunks/app/setup/page-2595e729eae30c0e.js +1 -0
  197. ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-1037c987aecc3653.js +1 -0
  198. ciris_engine/gui_static/_next/static/chunks/app/status-dashboard/page-2ffd147f6d3162ff.js +1 -0
  199. ciris_engine/gui_static/_next/static/chunks/app/system/page-2c5798d58cafcd91.js +1 -0
  200. ciris_engine/gui_static/_next/static/chunks/app/system/page-505b1ba4eceb01c3.js +1 -0
  201. ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-b0cad31d5cb1b2fa.js +1 -0
  202. ciris_engine/gui_static/_next/static/chunks/app/test-auth/page-f3ecd7a8012df230.js +1 -0
  203. ciris_engine/gui_static/_next/static/chunks/app/test-login/page-f35117fdc4105801.js +1 -0
  204. ciris_engine/gui_static/_next/static/chunks/app/test-login/page-fb583a7924114906.js +1 -0
  205. ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-50f116fd76935563.js +1 -0
  206. ciris_engine/gui_static/_next/static/chunks/app/test-sdk/page-c37d8aa5ba623a44.js +1 -0
  207. ciris_engine/gui_static/_next/static/chunks/app/tools/page-429aec7a707777ef.js +1 -0
  208. ciris_engine/gui_static/_next/static/chunks/app/tools/page-5f705aad60e0c04e.js +1 -0
  209. ciris_engine/gui_static/_next/static/chunks/app/users/page-13476b8b0f3808cc.js +1 -0
  210. ciris_engine/gui_static/_next/static/chunks/app/users/page-7e500d154ed5bba4.js +1 -0
  211. ciris_engine/gui_static/_next/static/chunks/app/wa/page-cc4a9d8a5cb44d08.js +1 -0
  212. ciris_engine/gui_static/_next/static/chunks/app/wa/page-ec3e429efbc79230.js +1 -0
  213. ciris_engine/gui_static/_next/static/chunks/framework-9d29490f5ba089ba.js +1 -0
  214. ciris_engine/gui_static/_next/static/chunks/main-1f554952e47a82c4.js +1 -0
  215. ciris_engine/gui_static/_next/static/chunks/main-app-26fa8aed029082e5.js +1 -0
  216. ciris_engine/gui_static/_next/static/chunks/main-app-97b0486ef6bcef25.js +1 -0
  217. ciris_engine/gui_static/_next/static/chunks/pages/_app-6ce685456e616eb2.js +1 -0
  218. ciris_engine/gui_static/_next/static/chunks/pages/_error-d4bce98d93fe21e7.js +1 -0
  219. ciris_engine/gui_static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  220. ciris_engine/gui_static/_next/static/chunks/webpack-fcebd240b7f8477d.js +1 -0
  221. ciris_engine/gui_static/_next/static/css/16b94b1fe0cc6e37.css +3 -0
  222. ciris_engine/gui_static/_next/static/css/77a24ceaae86deff.css +3 -0
  223. ciris_engine/gui_static/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  224. ciris_engine/gui_static/_next/static/media/747892c23ea88013-s.woff2 +0 -0
  225. ciris_engine/gui_static/_next/static/media/8d697b304b401681-s.woff2 +0 -0
  226. ciris_engine/gui_static/_next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  227. ciris_engine/gui_static/_next/static/media/9610d9e46709d722-s.woff2 +0 -0
  228. ciris_engine/gui_static/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  229. ciris_engine/gui_static/_next/static/media/d8298875641ec7d4-s.p.woff2 +0 -0
  230. ciris_engine/gui_static/account/api-keys/index.html +1 -0
  231. ciris_engine/gui_static/account/api-keys/index.txt +27 -0
  232. ciris_engine/gui_static/account/consent/index.html +1 -0
  233. ciris_engine/gui_static/account/consent/index.txt +27 -0
  234. ciris_engine/gui_static/account/index.html +1 -0
  235. ciris_engine/gui_static/account/index.txt +27 -0
  236. ciris_engine/gui_static/account/privacy/index.html +1 -0
  237. ciris_engine/gui_static/account/privacy/index.txt +27 -0
  238. ciris_engine/gui_static/account/settings/index.html +1 -0
  239. ciris_engine/gui_static/account/settings/index.txt +27 -0
  240. ciris_engine/gui_static/adapters/index.html +1 -0
  241. ciris_engine/gui_static/adapters/index.txt +27 -0
  242. ciris_engine/gui_static/agents/index.html +1 -0
  243. ciris_engine/gui_static/agents/index.txt +27 -0
  244. ciris_engine/gui_static/andrew-roberts-euBRXcx57T4-unsplash.jpg +0 -0
  245. ciris_engine/gui_static/api-demo/index.html +1 -0
  246. ciris_engine/gui_static/api-demo/index.txt +27 -0
  247. ciris_engine/gui_static/audit/index.html +1 -0
  248. ciris_engine/gui_static/audit/index.txt +27 -0
  249. ciris_engine/gui_static/billing/index.html +1 -0
  250. ciris_engine/gui_static/billing/index.txt +27 -0
  251. ciris_engine/gui_static/blurryinfo.png +0 -0
  252. ciris_engine/gui_static/chip-vincent-PkQDwfl9Flc-unsplash.jpg +0 -0
  253. ciris_engine/gui_static/ciris-architecture.svg +338 -0
  254. ciris_engine/gui_static/comms/index.html +1 -0
  255. ciris_engine/gui_static/comms/index.txt +27 -0
  256. ciris_engine/gui_static/config/index.html +1 -0
  257. ciris_engine/gui_static/config/index.txt +27 -0
  258. ciris_engine/gui_static/consent/index.html +1 -0
  259. ciris_engine/gui_static/consent/index.txt +27 -0
  260. ciris_engine/gui_static/dashboard/index.html +1 -0
  261. ciris_engine/gui_static/dashboard/index.txt +27 -0
  262. ciris_engine/gui_static/docs/index.html +1 -0
  263. ciris_engine/gui_static/docs/index.txt +27 -0
  264. ciris_engine/gui_static/eric.png +0 -0
  265. ciris_engine/gui_static/file.svg +1 -0
  266. ciris_engine/gui_static/globe.svg +1 -0
  267. ciris_engine/gui_static/index.html +1 -0
  268. ciris_engine/gui_static/index.txt +27 -0
  269. ciris_engine/gui_static/infogfx-1@2x.png +0 -0
  270. ciris_engine/gui_static/infogfx-2.png +0 -0
  271. ciris_engine/gui_static/infogfx-dark-1.png +0 -0
  272. ciris_engine/gui_static/kelly-vohs-soSTXmIxTDU-unsplash.jpg +0 -0
  273. ciris_engine/gui_static/login/index.html +1 -0
  274. ciris_engine/gui_static/login/index.txt +27 -0
  275. ciris_engine/gui_static/logs/index.html +1 -0
  276. ciris_engine/gui_static/logs/index.txt +27 -0
  277. ciris_engine/gui_static/memory/index.html +1 -0
  278. ciris_engine/gui_static/memory/index.txt +27 -0
  279. ciris_engine/gui_static/nathan-farrish-ArcTfEoBgzs-unsplash.jpg +0 -0
  280. ciris_engine/gui_static/next.svg +1 -0
  281. ciris_engine/gui_static/overview.svg +512 -0
  282. ciris_engine/gui_static/overview1.svg +407 -0
  283. ciris_engine/gui_static/overview2.svg +370 -0
  284. ciris_engine/gui_static/pipeline-visualization.svg +278 -0
  285. ciris_engine/gui_static/privacy-policy.html +160 -0
  286. ciris_engine/gui_static/runtime/index.html +8 -0
  287. ciris_engine/gui_static/runtime/index.txt +27 -0
  288. ciris_engine/gui_static/services/index.html +1 -0
  289. ciris_engine/gui_static/services/index.txt +27 -0
  290. ciris_engine/gui_static/sessions/index.html +1 -0
  291. ciris_engine/gui_static/sessions/index.txt +27 -0
  292. ciris_engine/gui_static/setup/index.html +1 -0
  293. ciris_engine/gui_static/setup/index.txt +27 -0
  294. ciris_engine/gui_static/status-dashboard/index.html +1 -0
  295. ciris_engine/gui_static/status-dashboard/index.txt +27 -0
  296. ciris_engine/gui_static/system/index.html +1 -0
  297. ciris_engine/gui_static/system/index.txt +27 -0
  298. ciris_engine/gui_static/terms-of-service.html +174 -0
  299. ciris_engine/gui_static/test-auth/index.html +1 -0
  300. ciris_engine/gui_static/test-auth/index.txt +27 -0
  301. ciris_engine/gui_static/test-login/index.html +1 -0
  302. ciris_engine/gui_static/test-login/index.txt +27 -0
  303. ciris_engine/gui_static/test-sdk/index.html +1 -0
  304. ciris_engine/gui_static/test-sdk/index.txt +27 -0
  305. ciris_engine/gui_static/tools/index.html +1 -0
  306. ciris_engine/gui_static/tools/index.txt +27 -0
  307. ciris_engine/gui_static/users/index.html +1 -0
  308. ciris_engine/gui_static/users/index.txt +27 -0
  309. ciris_engine/gui_static/vercel.svg +1 -0
  310. ciris_engine/gui_static/videos/video1.mp4 +0 -0
  311. ciris_engine/gui_static/videos/video3.mp4 +0 -0
  312. ciris_engine/gui_static/wa/index.html +1 -0
  313. ciris_engine/gui_static/wa/index.txt +27 -0
  314. ciris_engine/gui_static/window.svg +1 -0
  315. ciris_engine/logic/__init__.py +8 -0
  316. ciris_engine/logic/adapters/__init__.py +74 -0
  317. ciris_engine/logic/adapters/api/__init__.py +5 -0
  318. ciris_engine/logic/adapters/api/adapter.py +1037 -0
  319. ciris_engine/logic/adapters/api/api_communication.py +370 -0
  320. ciris_engine/logic/adapters/api/api_document.py +330 -0
  321. ciris_engine/logic/adapters/api/api_observer.py +24 -0
  322. ciris_engine/logic/adapters/api/api_runtime_control.py +388 -0
  323. ciris_engine/logic/adapters/api/api_tools.py +299 -0
  324. ciris_engine/logic/adapters/api/api_vision.py +215 -0
  325. ciris_engine/logic/adapters/api/app.py +272 -0
  326. ciris_engine/logic/adapters/api/auth.py +159 -0
  327. ciris_engine/logic/adapters/api/config.py +101 -0
  328. ciris_engine/logic/adapters/api/constants.py +55 -0
  329. ciris_engine/logic/adapters/api/dependencies/__init__.py +1 -0
  330. ciris_engine/logic/adapters/api/dependencies/auth.py +260 -0
  331. ciris_engine/logic/adapters/api/endpoints/__init__.py +1 -0
  332. ciris_engine/logic/adapters/api/endpoints/emergency.py +86 -0
  333. ciris_engine/logic/adapters/api/middleware/__init__.py +1 -0
  334. ciris_engine/logic/adapters/api/middleware/rate_limiter.py +302 -0
  335. ciris_engine/logic/adapters/api/models.py +29 -0
  336. ciris_engine/logic/adapters/api/routes/__init__.py +52 -0
  337. ciris_engine/logic/adapters/api/routes/agent.py +1762 -0
  338. ciris_engine/logic/adapters/api/routes/audit.py +707 -0
  339. ciris_engine/logic/adapters/api/routes/auth.py +1745 -0
  340. ciris_engine/logic/adapters/api/routes/billing.py +895 -0
  341. ciris_engine/logic/adapters/api/routes/config.py +329 -0
  342. ciris_engine/logic/adapters/api/routes/connectors.py +534 -0
  343. ciris_engine/logic/adapters/api/routes/consent.py +637 -0
  344. ciris_engine/logic/adapters/api/routes/dsar.py +637 -0
  345. ciris_engine/logic/adapters/api/routes/dsar_multi_source.py +484 -0
  346. ciris_engine/logic/adapters/api/routes/emergency.py +302 -0
  347. ciris_engine/logic/adapters/api/routes/memory.py +733 -0
  348. ciris_engine/logic/adapters/api/routes/memory_filters.py +230 -0
  349. ciris_engine/logic/adapters/api/routes/memory_models.py +112 -0
  350. ciris_engine/logic/adapters/api/routes/memory_queries.py +236 -0
  351. ciris_engine/logic/adapters/api/routes/memory_query_helpers.py +394 -0
  352. ciris_engine/logic/adapters/api/routes/memory_visualization.py +359 -0
  353. ciris_engine/logic/adapters/api/routes/memory_visualization_helpers.py +110 -0
  354. ciris_engine/logic/adapters/api/routes/partnership.py +541 -0
  355. ciris_engine/logic/adapters/api/routes/setup.py +1374 -0
  356. ciris_engine/logic/adapters/api/routes/system.py +3049 -0
  357. ciris_engine/logic/adapters/api/routes/system_extensions.py +952 -0
  358. ciris_engine/logic/adapters/api/routes/telemetry.py +1987 -0
  359. ciris_engine/logic/adapters/api/routes/telemetry_converters.py +141 -0
  360. ciris_engine/logic/adapters/api/routes/telemetry_helpers.py +111 -0
  361. ciris_engine/logic/adapters/api/routes/telemetry_logs_reader.py +280 -0
  362. ciris_engine/logic/adapters/api/routes/telemetry_metrics.py +131 -0
  363. ciris_engine/logic/adapters/api/routes/telemetry_models.py +190 -0
  364. ciris_engine/logic/adapters/api/routes/telemetry_otlp.py +878 -0
  365. ciris_engine/logic/adapters/api/routes/telemetry_resource_helpers.py +191 -0
  366. ciris_engine/logic/adapters/api/routes/tickets.py +541 -0
  367. ciris_engine/logic/adapters/api/routes/tools.py +556 -0
  368. ciris_engine/logic/adapters/api/routes/transparency.py +281 -0
  369. ciris_engine/logic/adapters/api/routes/users.py +981 -0
  370. ciris_engine/logic/adapters/api/routes/verification.py +373 -0
  371. ciris_engine/logic/adapters/api/routes/wa.py +369 -0
  372. ciris_engine/logic/adapters/api/service_configuration.py +177 -0
  373. ciris_engine/logic/adapters/api/services/__init__.py +1 -0
  374. ciris_engine/logic/adapters/api/services/auth_service.py +1417 -0
  375. ciris_engine/logic/adapters/api/services/oauth_security.py +68 -0
  376. ciris_engine/logic/adapters/base.py +141 -0
  377. ciris_engine/logic/adapters/base_adapter.py +73 -0
  378. ciris_engine/logic/adapters/base_observer.py +1141 -0
  379. ciris_engine/logic/adapters/base_vision.py +312 -0
  380. ciris_engine/logic/adapters/cirisnode_client.py +307 -0
  381. ciris_engine/logic/adapters/cli/__init__.py +3 -0
  382. ciris_engine/logic/adapters/cli/adapter.py +207 -0
  383. ciris_engine/logic/adapters/cli/cli_adapter.py +902 -0
  384. ciris_engine/logic/adapters/cli/cli_observer.py +268 -0
  385. ciris_engine/logic/adapters/cli/cli_tools.py +427 -0
  386. ciris_engine/logic/adapters/cli/cli_wa_service.py +134 -0
  387. ciris_engine/logic/adapters/cli/config.py +73 -0
  388. ciris_engine/logic/adapters/discord/__init__.py +3 -0
  389. ciris_engine/logic/adapters/discord/adapter.py +783 -0
  390. ciris_engine/logic/adapters/discord/ciris_discord_client.py +159 -0
  391. ciris_engine/logic/adapters/discord/config.py +177 -0
  392. ciris_engine/logic/adapters/discord/constants.py +185 -0
  393. ciris_engine/logic/adapters/discord/discord-stubs.pyi +50 -0
  394. ciris_engine/logic/adapters/discord/discord_adapter.py +1584 -0
  395. ciris_engine/logic/adapters/discord/discord_audit.py +150 -0
  396. ciris_engine/logic/adapters/discord/discord_channel_manager.py +351 -0
  397. ciris_engine/logic/adapters/discord/discord_connection_manager.py +313 -0
  398. ciris_engine/logic/adapters/discord/discord_embed_formatter.py +369 -0
  399. ciris_engine/logic/adapters/discord/discord_error_classifier.py +302 -0
  400. ciris_engine/logic/adapters/discord/discord_error_handler.py +316 -0
  401. ciris_engine/logic/adapters/discord/discord_guidance_handler.py +460 -0
  402. ciris_engine/logic/adapters/discord/discord_message_handler.py +207 -0
  403. ciris_engine/logic/adapters/discord/discord_observer.py +670 -0
  404. ciris_engine/logic/adapters/discord/discord_rate_limiter.py +249 -0
  405. ciris_engine/logic/adapters/discord/discord_reaction_handler.py +278 -0
  406. ciris_engine/logic/adapters/discord/discord_tool_handler.py +465 -0
  407. ciris_engine/logic/adapters/discord/discord_tool_service.py +790 -0
  408. ciris_engine/logic/adapters/discord/discord_tools.py +90 -0
  409. ciris_engine/logic/adapters/discord/discord_vision_helper.py +148 -0
  410. ciris_engine/logic/adapters/discord/py.typed +0 -0
  411. ciris_engine/logic/adapters/document_parser.py +320 -0
  412. ciris_engine/logic/audit/__init__.py +10 -0
  413. ciris_engine/logic/audit/hash_chain.py +313 -0
  414. ciris_engine/logic/audit/signature_manager.py +352 -0
  415. ciris_engine/logic/audit/verifier.py +408 -0
  416. ciris_engine/logic/buses/__init__.py +21 -0
  417. ciris_engine/logic/buses/base_bus.py +178 -0
  418. ciris_engine/logic/buses/bus_manager.py +121 -0
  419. ciris_engine/logic/buses/communication_bus.py +387 -0
  420. ciris_engine/logic/buses/llm_bus.py +722 -0
  421. ciris_engine/logic/buses/memory_bus.py +577 -0
  422. ciris_engine/logic/buses/prohibitions.py +502 -0
  423. ciris_engine/logic/buses/runtime_control_bus.py +539 -0
  424. ciris_engine/logic/buses/tool_bus.py +482 -0
  425. ciris_engine/logic/buses/wise_bus.py +684 -0
  426. ciris_engine/logic/config/__init__.py +25 -0
  427. ciris_engine/logic/config/bootstrap.py +255 -0
  428. ciris_engine/logic/config/config_accessor.py +202 -0
  429. ciris_engine/logic/config/db_paths.py +194 -0
  430. ciris_engine/logic/config/env_utils.py +39 -0
  431. ciris_engine/logic/conscience/__init__.py +16 -0
  432. ciris_engine/logic/conscience/build_deferral_package.py +0 -0
  433. ciris_engine/logic/conscience/core.py +688 -0
  434. ciris_engine/logic/conscience/interface.py +33 -0
  435. ciris_engine/logic/conscience/registry.py +76 -0
  436. ciris_engine/logic/conscience/thought_depth_guardrail.py +231 -0
  437. ciris_engine/logic/conscience/updated_status_conscience.py +156 -0
  438. ciris_engine/logic/context/__init__.py +10 -0
  439. ciris_engine/logic/context/batch_context.py +550 -0
  440. ciris_engine/logic/context/builder.py +149 -0
  441. ciris_engine/logic/context/channel_resolution.py +136 -0
  442. ciris_engine/logic/context/secrets_snapshot.py +52 -0
  443. ciris_engine/logic/context/system_snapshot.py +116 -0
  444. ciris_engine/logic/context/system_snapshot_helpers.py +1651 -0
  445. ciris_engine/logic/covenant/__init__.py +33 -0
  446. ciris_engine/logic/covenant/executor.py +303 -0
  447. ciris_engine/logic/covenant/extractor.py +382 -0
  448. ciris_engine/logic/covenant/handler.py +241 -0
  449. ciris_engine/logic/covenant/verifier.py +383 -0
  450. ciris_engine/logic/dma/__init__.py +15 -0
  451. ciris_engine/logic/dma/action_selection/__init__.py +11 -0
  452. ciris_engine/logic/dma/action_selection/action_instruction_generator.py +444 -0
  453. ciris_engine/logic/dma/action_selection/context_builder.py +508 -0
  454. ciris_engine/logic/dma/action_selection/faculty_integration.py +193 -0
  455. ciris_engine/logic/dma/action_selection/special_cases.py +132 -0
  456. ciris_engine/logic/dma/action_selection_pdma.py +365 -0
  457. ciris_engine/logic/dma/base_dma.py +335 -0
  458. ciris_engine/logic/dma/csdma.py +239 -0
  459. ciris_engine/logic/dma/dma_executor.py +575 -0
  460. ciris_engine/logic/dma/dsdma_base.py +410 -0
  461. ciris_engine/logic/dma/exceptions.py +4 -0
  462. ciris_engine/logic/dma/factory.py +150 -0
  463. ciris_engine/logic/dma/pdma.py +120 -0
  464. ciris_engine/logic/dma/prompt_loader.py +189 -0
  465. ciris_engine/logic/dma/prompts/action_selection_pdma.yml +58 -0
  466. ciris_engine/logic/dma/prompts/csdma_common_sense.yml +28 -0
  467. ciris_engine/logic/dma/prompts/dsdma_base.yml +17 -0
  468. ciris_engine/logic/dma/prompts/pdma_ethical.yml +42 -0
  469. ciris_engine/logic/formatters/__init__.py +26 -0
  470. ciris_engine/logic/formatters/crisis_resources.py +80 -0
  471. ciris_engine/logic/formatters/escalation.py +21 -0
  472. ciris_engine/logic/formatters/identity.py +224 -0
  473. ciris_engine/logic/formatters/prompt_blocks.py +64 -0
  474. ciris_engine/logic/formatters/system_snapshot.py +193 -0
  475. ciris_engine/logic/formatters/user_profiles.py +108 -0
  476. ciris_engine/logic/handlers/__init__.py +1 -0
  477. ciris_engine/logic/handlers/control/__init__.py +1 -0
  478. ciris_engine/logic/handlers/control/defer_handler.py +195 -0
  479. ciris_engine/logic/handlers/control/ponder_handler.py +154 -0
  480. ciris_engine/logic/handlers/control/reject_handler.py +81 -0
  481. ciris_engine/logic/handlers/external/__init__.py +1 -0
  482. ciris_engine/logic/handlers/external/observe_handler.py +154 -0
  483. ciris_engine/logic/handlers/external/speak_handler.py +250 -0
  484. ciris_engine/logic/handlers/external/tool_handler.py +148 -0
  485. ciris_engine/logic/handlers/memory/__init__.py +1 -0
  486. ciris_engine/logic/handlers/memory/forget_handler.py +107 -0
  487. ciris_engine/logic/handlers/memory/memorize_handler.py +391 -0
  488. ciris_engine/logic/handlers/memory/recall_handler.py +213 -0
  489. ciris_engine/logic/handlers/terminal/__init__.py +1 -0
  490. ciris_engine/logic/handlers/terminal/task_complete_handler.py +299 -0
  491. ciris_engine/logic/infrastructure/__init__.py +1 -0
  492. ciris_engine/logic/infrastructure/handlers/__init__.py +8 -0
  493. ciris_engine/logic/infrastructure/handlers/action_dispatcher.py +382 -0
  494. ciris_engine/logic/infrastructure/handlers/base_handler.py +450 -0
  495. ciris_engine/logic/infrastructure/handlers/exceptions.py +2 -0
  496. ciris_engine/logic/infrastructure/handlers/handler_registry.py +59 -0
  497. ciris_engine/logic/infrastructure/handlers/helpers.py +55 -0
  498. ciris_engine/logic/infrastructure/step_streaming.py +149 -0
  499. ciris_engine/logic/infrastructure/sub_services/__init__.py +1 -0
  500. ciris_engine/logic/infrastructure/sub_services/identity_variance_monitor.py +1035 -0
  501. ciris_engine/logic/infrastructure/sub_services/pattern_analysis_loop.py +758 -0
  502. ciris_engine/logic/infrastructure/sub_services/wa_cli_bootstrap.py +229 -0
  503. ciris_engine/logic/infrastructure/sub_services/wa_cli_display.py +176 -0
  504. ciris_engine/logic/infrastructure/sub_services/wa_cli_oauth.py +404 -0
  505. ciris_engine/logic/infrastructure/sub_services/wa_cli_wizard.py +181 -0
  506. ciris_engine/logic/persistence/__init__.py +130 -0
  507. ciris_engine/logic/persistence/analytics.py +97 -0
  508. ciris_engine/logic/persistence/db/__init__.py +28 -0
  509. ciris_engine/logic/persistence/db/core.py +520 -0
  510. ciris_engine/logic/persistence/db/dialect.py +380 -0
  511. ciris_engine/logic/persistence/db/execution_helpers.py +216 -0
  512. ciris_engine/logic/persistence/db/migration_runner.py +191 -0
  513. ciris_engine/logic/persistence/db/operations.py +313 -0
  514. ciris_engine/logic/persistence/db/query_builder.py +232 -0
  515. ciris_engine/logic/persistence/db/retry.py +154 -0
  516. ciris_engine/logic/persistence/db/setup.py +18 -0
  517. ciris_engine/logic/persistence/migrations/postgres/001_initial_schema.sql +4 -0
  518. ciris_engine/logic/persistence/migrations/postgres/002_add_retry_status.sql +3 -0
  519. ciris_engine/logic/persistence/migrations/postgres/003_add_task_update_tracking.sql +8 -0
  520. ciris_engine/logic/persistence/migrations/postgres/004_add_occurrence_id.sql +54 -0
  521. ciris_engine/logic/persistence/migrations/postgres/005_add_consolidation_locks.sql +22 -0
  522. ciris_engine/logic/persistence/migrations/postgres/006_add_correlation_id_unique_index.sql +16 -0
  523. ciris_engine/logic/persistence/migrations/postgres/007_add_dsar_tickets.sql +39 -0
  524. ciris_engine/logic/persistence/migrations/postgres/008_rename_to_tickets_add_sop.sql +123 -0
  525. ciris_engine/logic/persistence/migrations/postgres/009_add_ticket_status_columns.sql +39 -0
  526. ciris_engine/logic/persistence/migrations/postgres/010_add_images_to_tasks.sql +5 -0
  527. ciris_engine/logic/persistence/migrations/sqlite/001_initial_schema.sql +357 -0
  528. ciris_engine/logic/persistence/migrations/sqlite/002_add_retry_status.sql +3 -0
  529. ciris_engine/logic/persistence/migrations/sqlite/003_add_task_update_tracking.sql +8 -0
  530. ciris_engine/logic/persistence/migrations/sqlite/004_add_occurrence_id.sql +45 -0
  531. ciris_engine/logic/persistence/migrations/sqlite/005_add_consolidation_locks.sql +22 -0
  532. ciris_engine/logic/persistence/migrations/sqlite/006_add_correlation_id_unique_index.sql +16 -0
  533. ciris_engine/logic/persistence/migrations/sqlite/007_add_dsar_tickets.sql +39 -0
  534. ciris_engine/logic/persistence/migrations/sqlite/008_rename_to_tickets_add_sop.sql +120 -0
  535. ciris_engine/logic/persistence/migrations/sqlite/009_add_ticket_status_columns.sql +129 -0
  536. ciris_engine/logic/persistence/migrations/sqlite/010_add_images_to_tasks.sql +17 -0
  537. ciris_engine/logic/persistence/models/__init__.py +141 -0
  538. ciris_engine/logic/persistence/models/correlations.py +881 -0
  539. ciris_engine/logic/persistence/models/deferral.py +68 -0
  540. ciris_engine/logic/persistence/models/dsar.py +286 -0
  541. ciris_engine/logic/persistence/models/graph.py +362 -0
  542. ciris_engine/logic/persistence/models/identity.py +264 -0
  543. ciris_engine/logic/persistence/models/queue_status.py +139 -0
  544. ciris_engine/logic/persistence/models/tasks.py +1043 -0
  545. ciris_engine/logic/persistence/models/thoughts.py +400 -0
  546. ciris_engine/logic/persistence/models/tickets.py +518 -0
  547. ciris_engine/logic/persistence/stores/__init__.py +13 -0
  548. ciris_engine/logic/persistence/stores/auth_helpers.py +117 -0
  549. ciris_engine/logic/persistence/stores/authentication_store.py +414 -0
  550. ciris_engine/logic/persistence/utils.py +212 -0
  551. ciris_engine/logic/processors/__init__.py +30 -0
  552. ciris_engine/logic/processors/core/__init__.py +1 -0
  553. ciris_engine/logic/processors/core/base_processor.py +280 -0
  554. ciris_engine/logic/processors/core/main_processor.py +1777 -0
  555. ciris_engine/logic/processors/core/step_decorators.py +1583 -0
  556. ciris_engine/logic/processors/core/thought_processor/__init__.py +20 -0
  557. ciris_engine/logic/processors/core/thought_processor/action_execution.py +49 -0
  558. ciris_engine/logic/processors/core/thought_processor/conscience_execution.py +382 -0
  559. ciris_engine/logic/processors/core/thought_processor/finalize_action.py +66 -0
  560. ciris_engine/logic/processors/core/thought_processor/gather_context.py +120 -0
  561. ciris_engine/logic/processors/core/thought_processor/main.py +920 -0
  562. ciris_engine/logic/processors/core/thought_processor/perform_aspdma.py +86 -0
  563. ciris_engine/logic/processors/core/thought_processor/perform_dmas.py +106 -0
  564. ciris_engine/logic/processors/core/thought_processor/recursive_processing.py +237 -0
  565. ciris_engine/logic/processors/core/thought_processor/round_complete.py +52 -0
  566. ciris_engine/logic/processors/core/thought_processor/start_round.py +64 -0
  567. ciris_engine/logic/processors/exceptions.py +59 -0
  568. ciris_engine/logic/processors/states/__init__.py +1 -0
  569. ciris_engine/logic/processors/states/dream_processor.py +1381 -0
  570. ciris_engine/logic/processors/states/play_processor.py +141 -0
  571. ciris_engine/logic/processors/states/shutdown_processor.py +623 -0
  572. ciris_engine/logic/processors/states/solitude_processor.py +305 -0
  573. ciris_engine/logic/processors/states/wakeup_processor.py +802 -0
  574. ciris_engine/logic/processors/states/work_processor.py +742 -0
  575. ciris_engine/logic/processors/support/__init__.py +1 -0
  576. ciris_engine/logic/processors/support/dma_orchestrator.py +336 -0
  577. ciris_engine/logic/processors/support/processing_queue.py +133 -0
  578. ciris_engine/logic/processors/support/shutdown_condition_evaluator.py +294 -0
  579. ciris_engine/logic/processors/support/state_manager.py +358 -0
  580. ciris_engine/logic/processors/support/task_manager.py +303 -0
  581. ciris_engine/logic/processors/support/thought_escalation.py +116 -0
  582. ciris_engine/logic/processors/support/thought_manager.py +328 -0
  583. ciris_engine/logic/processors/support/thought_manager_enhanced.py +105 -0
  584. ciris_engine/logic/registries/__init__.py +34 -0
  585. ciris_engine/logic/registries/base.py +653 -0
  586. ciris_engine/logic/registries/circuit_breaker.py +275 -0
  587. ciris_engine/logic/registries/typed_registries.py +184 -0
  588. ciris_engine/logic/runtime/__init__.py +7 -0
  589. ciris_engine/logic/runtime/adapter_loader.py +261 -0
  590. ciris_engine/logic/runtime/adapter_manager.py +1053 -0
  591. ciris_engine/logic/runtime/ciris_runtime.py +2342 -0
  592. ciris_engine/logic/runtime/ciris_runtime_helpers.py +923 -0
  593. ciris_engine/logic/runtime/component_builder.py +361 -0
  594. ciris_engine/logic/runtime/identity_manager.py +219 -0
  595. ciris_engine/logic/runtime/module_loader.py +207 -0
  596. ciris_engine/logic/runtime/prevent_sideeffects.py +30 -0
  597. ciris_engine/logic/runtime/runtime_interface.py +23 -0
  598. ciris_engine/logic/runtime/service_initializer.py +1623 -0
  599. ciris_engine/logic/secrets/__init__.py +30 -0
  600. ciris_engine/logic/secrets/encryption.py +175 -0
  601. ciris_engine/logic/secrets/filter.py +295 -0
  602. ciris_engine/logic/secrets/service.py +652 -0
  603. ciris_engine/logic/secrets/store.py +669 -0
  604. ciris_engine/logic/services/__init__.py +1 -0
  605. ciris_engine/logic/services/adaptation/__init__.py +3 -0
  606. ciris_engine/logic/services/base_graph_service.py +142 -0
  607. ciris_engine/logic/services/base_infrastructure_service.py +69 -0
  608. ciris_engine/logic/services/base_scheduled_service.py +136 -0
  609. ciris_engine/logic/services/base_service.py +247 -0
  610. ciris_engine/logic/services/governance/__init__.py +3 -0
  611. ciris_engine/logic/services/governance/adaptive_filter/__init__.py +14 -0
  612. ciris_engine/logic/services/governance/adaptive_filter/service.py +818 -0
  613. ciris_engine/logic/services/governance/consent/__init__.py +53 -0
  614. ciris_engine/logic/services/governance/consent/air.py +403 -0
  615. ciris_engine/logic/services/governance/consent/decay.py +324 -0
  616. ciris_engine/logic/services/governance/consent/dsar_automation.py +589 -0
  617. ciris_engine/logic/services/governance/consent/exceptions.py +106 -0
  618. ciris_engine/logic/services/governance/consent/metrics.py +270 -0
  619. ciris_engine/logic/services/governance/consent/partnership.py +533 -0
  620. ciris_engine/logic/services/governance/consent/service.py +1256 -0
  621. ciris_engine/logic/services/governance/dsar/__init__.py +29 -0
  622. ciris_engine/logic/services/governance/dsar/orchestrator.py +977 -0
  623. ciris_engine/logic/services/governance/dsar/schemas.py +141 -0
  624. ciris_engine/logic/services/governance/dsar/signature_service.py +283 -0
  625. ciris_engine/logic/services/governance/self_observation/__init__.py +20 -0
  626. ciris_engine/logic/services/governance/self_observation/service.py +1153 -0
  627. ciris_engine/logic/services/governance/visibility/__init__.py +17 -0
  628. ciris_engine/logic/services/governance/visibility/service.py +512 -0
  629. ciris_engine/logic/services/governance/wise_authority/__init__.py +15 -0
  630. ciris_engine/logic/services/governance/wise_authority/service.py +827 -0
  631. ciris_engine/logic/services/graph/__init__.py +5 -0
  632. ciris_engine/logic/services/graph/audit_service/__init__.py +5 -0
  633. ciris_engine/logic/services/graph/audit_service/service.py +1675 -0
  634. ciris_engine/logic/services/graph/base.py +208 -0
  635. ciris_engine/logic/services/graph/config_service/__init__.py +5 -0
  636. ciris_engine/logic/services/graph/config_service/service.py +372 -0
  637. ciris_engine/logic/services/graph/incident_service/__init__.py +5 -0
  638. ciris_engine/logic/services/graph/incident_service/service.py +803 -0
  639. ciris_engine/logic/services/graph/memory_service.py +1120 -0
  640. ciris_engine/logic/services/graph/telemetry_service/__init__.py +5 -0
  641. ciris_engine/logic/services/graph/telemetry_service/exceptions.py +104 -0
  642. ciris_engine/logic/services/graph/telemetry_service/helpers.py +1337 -0
  643. ciris_engine/logic/services/graph/telemetry_service/service.py +2429 -0
  644. ciris_engine/logic/services/graph/tsdb_consolidation/__init__.py +17 -0
  645. ciris_engine/logic/services/graph/tsdb_consolidation/aggregation_helpers.py +355 -0
  646. ciris_engine/logic/services/graph/tsdb_consolidation/cleanup_helpers.py +438 -0
  647. ciris_engine/logic/services/graph/tsdb_consolidation/compressor.py +260 -0
  648. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/__init__.py +27 -0
  649. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/audit.py +326 -0
  650. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/conversation.py +291 -0
  651. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/memory.py +197 -0
  652. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/metrics.py +251 -0
  653. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/task.py +257 -0
  654. ciris_engine/logic/services/graph/tsdb_consolidation/consolidators/trace.py +363 -0
  655. ciris_engine/logic/services/graph/tsdb_consolidation/data_converter.py +545 -0
  656. ciris_engine/logic/services/graph/tsdb_consolidation/date_calculation_helpers.py +193 -0
  657. ciris_engine/logic/services/graph/tsdb_consolidation/db_query_helpers.py +296 -0
  658. ciris_engine/logic/services/graph/tsdb_consolidation/edge_helpers.py +92 -0
  659. ciris_engine/logic/services/graph/tsdb_consolidation/edge_manager.py +896 -0
  660. ciris_engine/logic/services/graph/tsdb_consolidation/extensive_helpers.py +322 -0
  661. ciris_engine/logic/services/graph/tsdb_consolidation/period_manager.py +152 -0
  662. ciris_engine/logic/services/graph/tsdb_consolidation/profound_helpers.py +277 -0
  663. ciris_engine/logic/services/graph/tsdb_consolidation/query_manager.py +812 -0
  664. ciris_engine/logic/services/graph/tsdb_consolidation/service.py +1692 -0
  665. ciris_engine/logic/services/graph/tsdb_consolidation/sql_builders.py +363 -0
  666. ciris_engine/logic/services/infrastructure/__init__.py +1 -0
  667. ciris_engine/logic/services/infrastructure/authentication/__init__.py +5 -0
  668. ciris_engine/logic/services/infrastructure/authentication/service.py +1634 -0
  669. ciris_engine/logic/services/infrastructure/database_maintenance/__init__.py +15 -0
  670. ciris_engine/logic/services/infrastructure/database_maintenance/service.py +764 -0
  671. ciris_engine/logic/services/infrastructure/resource_monitor/__init__.py +7 -0
  672. ciris_engine/logic/services/infrastructure/resource_monitor/ciris_billing_provider.py +755 -0
  673. ciris_engine/logic/services/infrastructure/resource_monitor/service.py +409 -0
  674. ciris_engine/logic/services/infrastructure/resource_monitor/simple_credit_provider.py +129 -0
  675. ciris_engine/logic/services/lifecycle/__init__.py +3 -0
  676. ciris_engine/logic/services/lifecycle/initialization/__init__.py +10 -0
  677. ciris_engine/logic/services/lifecycle/initialization/service.py +312 -0
  678. ciris_engine/logic/services/lifecycle/scheduler/__init__.py +5 -0
  679. ciris_engine/logic/services/lifecycle/scheduler/service.py +607 -0
  680. ciris_engine/logic/services/lifecycle/shutdown/__init__.py +9 -0
  681. ciris_engine/logic/services/lifecycle/shutdown/service.py +378 -0
  682. ciris_engine/logic/services/lifecycle/time/__init__.py +15 -0
  683. ciris_engine/logic/services/lifecycle/time/service.py +259 -0
  684. ciris_engine/logic/services/memory_service/__init__.py +8 -0
  685. ciris_engine/logic/services/mixins/__init__.py +13 -0
  686. ciris_engine/logic/services/mixins/example_usage.py +200 -0
  687. ciris_engine/logic/services/mixins/request_metrics.py +179 -0
  688. ciris_engine/logic/services/runtime/__init__.py +3 -0
  689. ciris_engine/logic/services/runtime/adapter_configuration/__init__.py +16 -0
  690. ciris_engine/logic/services/runtime/adapter_configuration/service.py +674 -0
  691. ciris_engine/logic/services/runtime/adapter_configuration/session.py +67 -0
  692. ciris_engine/logic/services/runtime/control_service/__init__.py +5 -0
  693. ciris_engine/logic/services/runtime/control_service/service.py +2269 -0
  694. ciris_engine/logic/services/runtime/llm_service/__init__.py +14 -0
  695. ciris_engine/logic/services/runtime/llm_service/pricing_calculator.py +279 -0
  696. ciris_engine/logic/services/runtime/llm_service/service.py +930 -0
  697. ciris_engine/logic/services/tools/__init__.py +5 -0
  698. ciris_engine/logic/services/tools/core_tool_service/__init__.py +8 -0
  699. ciris_engine/logic/services/tools/core_tool_service/service.py +852 -0
  700. ciris_engine/logic/setup/__init__.py +1 -0
  701. ciris_engine/logic/setup/first_run.py +250 -0
  702. ciris_engine/logic/setup/wizard.py +327 -0
  703. ciris_engine/logic/telemetry/__init__.py +46 -0
  704. ciris_engine/logic/telemetry/core.py +239 -0
  705. ciris_engine/logic/telemetry/hot_cold_config.py +133 -0
  706. ciris_engine/logic/telemetry/log_collector.py +190 -0
  707. ciris_engine/logic/telemetry/resource_monitor.py +7 -0
  708. ciris_engine/logic/telemetry/security.py +79 -0
  709. ciris_engine/logic/utils/__init__.py +18 -0
  710. ciris_engine/logic/utils/channel_utils.py +75 -0
  711. ciris_engine/logic/utils/consent/__init__.py +1 -0
  712. ciris_engine/logic/utils/consent/partnership_utils.py +172 -0
  713. ciris_engine/logic/utils/constants.py +92 -0
  714. ciris_engine/logic/utils/context_utils.py +145 -0
  715. ciris_engine/logic/utils/directory_setup.py +533 -0
  716. ciris_engine/logic/utils/graphql_context_provider.py +152 -0
  717. ciris_engine/logic/utils/identity_resolution.py +843 -0
  718. ciris_engine/logic/utils/incident_capture_handler.py +303 -0
  719. ciris_engine/logic/utils/initialization_manager.py +74 -0
  720. ciris_engine/logic/utils/jsondict_helpers.py +290 -0
  721. ciris_engine/logic/utils/log_sanitizer.py +97 -0
  722. ciris_engine/logic/utils/logging_config.py +151 -0
  723. ciris_engine/logic/utils/observability_decorators.py +544 -0
  724. ciris_engine/logic/utils/occurrence_utils.py +155 -0
  725. ciris_engine/logic/utils/path_resolution.py +281 -0
  726. ciris_engine/logic/utils/platform_detection.py +286 -0
  727. ciris_engine/logic/utils/privacy.py +266 -0
  728. ciris_engine/logic/utils/profile_loader.py +124 -0
  729. ciris_engine/logic/utils/profile_manager.py +16 -0
  730. ciris_engine/logic/utils/runtime_utils.py +69 -0
  731. ciris_engine/logic/utils/shutdown_manager.py +107 -0
  732. ciris_engine/logic/utils/task_formatters.py +60 -0
  733. ciris_engine/logic/utils/task_thought_factory.py +404 -0
  734. ciris_engine/logic/utils/thought_utils.py +54 -0
  735. ciris_engine/logic/utils/user_utils.py +70 -0
  736. ciris_engine/protocols/__init__.py +0 -0
  737. ciris_engine/protocols/adapters/__init__.py +35 -0
  738. ciris_engine/protocols/adapters/base.py +149 -0
  739. ciris_engine/protocols/adapters/configurable.py +265 -0
  740. ciris_engine/protocols/adapters/message.py +90 -0
  741. ciris_engine/protocols/audit/__init__.py +1 -0
  742. ciris_engine/protocols/buses/__init__.py +1 -0
  743. ciris_engine/protocols/config/__init__.py +1 -0
  744. ciris_engine/protocols/conscience/__init__.py +1 -0
  745. ciris_engine/protocols/consent.py +88 -0
  746. ciris_engine/protocols/context/__init__.py +1 -0
  747. ciris_engine/protocols/data/__init__.py +1 -0
  748. ciris_engine/protocols/dma/__init__.py +1 -0
  749. ciris_engine/protocols/dma/base.py +107 -0
  750. ciris_engine/protocols/faculties.py +34 -0
  751. ciris_engine/protocols/formatters/__init__.py +1 -0
  752. ciris_engine/protocols/handlers/__init__.py +1 -0
  753. ciris_engine/protocols/infrastructure/__init__.py +25 -0
  754. ciris_engine/protocols/infrastructure/base.py +377 -0
  755. ciris_engine/protocols/persistence/__init__.py +1 -0
  756. ciris_engine/protocols/pipeline_control.py +609 -0
  757. ciris_engine/protocols/processors/__init__.py +19 -0
  758. ciris_engine/protocols/processors/agent.py +299 -0
  759. ciris_engine/protocols/processors/base.py +130 -0
  760. ciris_engine/protocols/processors/orchestration.py +62 -0
  761. ciris_engine/protocols/registries/__init__.py +1 -0
  762. ciris_engine/protocols/runtime/__init__.py +1 -0
  763. ciris_engine/protocols/runtime/base.py +163 -0
  764. ciris_engine/protocols/secrets/__init__.py +1 -0
  765. ciris_engine/protocols/services/__init__.py +80 -0
  766. ciris_engine/protocols/services/adaptation/__init__.py +7 -0
  767. ciris_engine/protocols/services/adaptation/self_observation.py +265 -0
  768. ciris_engine/protocols/services/governance/__init__.py +20 -0
  769. ciris_engine/protocols/services/governance/communication.py +58 -0
  770. ciris_engine/protocols/services/governance/filter.py +56 -0
  771. ciris_engine/protocols/services/governance/visibility.py +32 -0
  772. ciris_engine/protocols/services/governance/wa_auth.py +192 -0
  773. ciris_engine/protocols/services/governance/wise_authority.py +75 -0
  774. ciris_engine/protocols/services/graph/__init__.py +19 -0
  775. ciris_engine/protocols/services/graph/audit.py +92 -0
  776. ciris_engine/protocols/services/graph/config.py +54 -0
  777. ciris_engine/protocols/services/graph/incident_management.py +103 -0
  778. ciris_engine/protocols/services/graph/memory.py +110 -0
  779. ciris_engine/protocols/services/graph/telemetry.py +51 -0
  780. ciris_engine/protocols/services/graph/tsdb_consolidation.py +87 -0
  781. ciris_engine/protocols/services/infrastructure/__init__.py +11 -0
  782. ciris_engine/protocols/services/infrastructure/authentication.py +159 -0
  783. ciris_engine/protocols/services/infrastructure/credit_gate.py +46 -0
  784. ciris_engine/protocols/services/infrastructure/database_maintenance.py +25 -0
  785. ciris_engine/protocols/services/infrastructure/resource_monitor.py +83 -0
  786. ciris_engine/protocols/services/lifecycle/__init__.py +13 -0
  787. ciris_engine/protocols/services/lifecycle/initialization.py +41 -0
  788. ciris_engine/protocols/services/lifecycle/scheduler.py +42 -0
  789. ciris_engine/protocols/services/lifecycle/shutdown.py +50 -0
  790. ciris_engine/protocols/services/lifecycle/time.py +31 -0
  791. ciris_engine/protocols/services/runtime/__init__.py +13 -0
  792. ciris_engine/protocols/services/runtime/llm.py +50 -0
  793. ciris_engine/protocols/services/runtime/runtime_control.py +193 -0
  794. ciris_engine/protocols/services/runtime/secrets.py +100 -0
  795. ciris_engine/protocols/services/runtime/tool.py +123 -0
  796. ciris_engine/protocols/telemetry/__init__.py +1 -0
  797. ciris_engine/protocols/utils/__init__.py +1 -0
  798. ciris_engine/schemas/__init__.py +112 -0
  799. ciris_engine/schemas/actions/__init__.py +37 -0
  800. ciris_engine/schemas/actions/parameters.py +137 -0
  801. ciris_engine/schemas/adapters/__init__.py +13 -0
  802. ciris_engine/schemas/adapters/cirisnode.py +135 -0
  803. ciris_engine/schemas/adapters/cli.py +97 -0
  804. ciris_engine/schemas/adapters/cli_tools.py +98 -0
  805. ciris_engine/schemas/adapters/discord.py +125 -0
  806. ciris_engine/schemas/adapters/graphql_core.py +144 -0
  807. ciris_engine/schemas/adapters/registration.py +47 -0
  808. ciris_engine/schemas/adapters/runtime_context.py +48 -0
  809. ciris_engine/schemas/adapters/tool_execution.py +45 -0
  810. ciris_engine/schemas/adapters/tools.py +96 -0
  811. ciris_engine/schemas/api/__init__.py +1 -0
  812. ciris_engine/schemas/api/agent.py +50 -0
  813. ciris_engine/schemas/api/audit.py +38 -0
  814. ciris_engine/schemas/api/auth.py +351 -0
  815. ciris_engine/schemas/api/config_security.py +242 -0
  816. ciris_engine/schemas/api/emergency.py +111 -0
  817. ciris_engine/schemas/api/responses.py +72 -0
  818. ciris_engine/schemas/api/runtime.py +26 -0
  819. ciris_engine/schemas/api/telemetry.py +109 -0
  820. ciris_engine/schemas/api/wa.py +90 -0
  821. ciris_engine/schemas/audit/__init__.py +13 -0
  822. ciris_engine/schemas/audit/core.py +139 -0
  823. ciris_engine/schemas/audit/hash_chain.py +58 -0
  824. ciris_engine/schemas/audit/verification.py +131 -0
  825. ciris_engine/schemas/buses/__init__.py +1 -0
  826. ciris_engine/schemas/config/__init__.py +41 -0
  827. ciris_engine/schemas/config/agent.py +279 -0
  828. ciris_engine/schemas/config/cognitive_state_behaviors.py +194 -0
  829. ciris_engine/schemas/config/default_dsar_sops.py +178 -0
  830. ciris_engine/schemas/config/essential.py +195 -0
  831. ciris_engine/schemas/config/tickets.py +86 -0
  832. ciris_engine/schemas/conscience/__init__.py +25 -0
  833. ciris_engine/schemas/conscience/context.py +34 -0
  834. ciris_engine/schemas/conscience/core.py +145 -0
  835. ciris_engine/schemas/conscience/results.py +24 -0
  836. ciris_engine/schemas/consent/__init__.py +5 -0
  837. ciris_engine/schemas/consent/core.py +404 -0
  838. ciris_engine/schemas/context/__init__.py +1 -0
  839. ciris_engine/schemas/covenant.py +382 -0
  840. ciris_engine/schemas/data/__init__.py +1 -0
  841. ciris_engine/schemas/dma/__init__.py +16 -0
  842. ciris_engine/schemas/dma/core.py +199 -0
  843. ciris_engine/schemas/dma/faculty.py +192 -0
  844. ciris_engine/schemas/dma/prompts.py +172 -0
  845. ciris_engine/schemas/dma/results.py +103 -0
  846. ciris_engine/schemas/formatters/__init__.py +1 -0
  847. ciris_engine/schemas/handlers/__init__.py +10 -0
  848. ciris_engine/schemas/handlers/context.py +119 -0
  849. ciris_engine/schemas/handlers/contexts.py +100 -0
  850. ciris_engine/schemas/handlers/core.py +167 -0
  851. ciris_engine/schemas/handlers/memory_schemas.py +67 -0
  852. ciris_engine/schemas/handlers/schemas.py +95 -0
  853. ciris_engine/schemas/identity.py +149 -0
  854. ciris_engine/schemas/infrastructure/__init__.py +1 -0
  855. ciris_engine/schemas/infrastructure/base.py +256 -0
  856. ciris_engine/schemas/infrastructure/behavioral_patterns.py +129 -0
  857. ciris_engine/schemas/infrastructure/feedback_loop.py +57 -0
  858. ciris_engine/schemas/infrastructure/identity_variance.py +141 -0
  859. ciris_engine/schemas/infrastructure/oauth.py +175 -0
  860. ciris_engine/schemas/infrastructure/wa_cli_wizard.py +54 -0
  861. ciris_engine/schemas/persistence/__init__.py +34 -0
  862. ciris_engine/schemas/persistence/core.py +140 -0
  863. ciris_engine/schemas/persistence/correlations.py +73 -0
  864. ciris_engine/schemas/persistence/postgres/__init__.py +1 -0
  865. ciris_engine/schemas/persistence/postgres/tables.py +280 -0
  866. ciris_engine/schemas/persistence/sqlite/__init__.py +1 -0
  867. ciris_engine/schemas/persistence/sqlite/tables.py +281 -0
  868. ciris_engine/schemas/platform.py +149 -0
  869. ciris_engine/schemas/processors/__init__.py +26 -0
  870. ciris_engine/schemas/processors/base.py +130 -0
  871. ciris_engine/schemas/processors/cognitive.py +77 -0
  872. ciris_engine/schemas/processors/context.py +35 -0
  873. ciris_engine/schemas/processors/core.py +152 -0
  874. ciris_engine/schemas/processors/dma.py +105 -0
  875. ciris_engine/schemas/processors/error.py +122 -0
  876. ciris_engine/schemas/processors/main.py +109 -0
  877. ciris_engine/schemas/processors/phase_results.py +21 -0
  878. ciris_engine/schemas/processors/results.py +99 -0
  879. ciris_engine/schemas/processors/solitude.py +79 -0
  880. ciris_engine/schemas/processors/state.py +202 -0
  881. ciris_engine/schemas/processors/state_example.py +177 -0
  882. ciris_engine/schemas/processors/states.py +21 -0
  883. ciris_engine/schemas/processors/status.py +34 -0
  884. ciris_engine/schemas/registries/__init__.py +1 -0
  885. ciris_engine/schemas/registries/base.py +66 -0
  886. ciris_engine/schemas/resources/__init__.py +15 -0
  887. ciris_engine/schemas/resources/crisis.py +315 -0
  888. ciris_engine/schemas/runtime/__init__.py +42 -0
  889. ciris_engine/schemas/runtime/adapter_management.py +186 -0
  890. ciris_engine/schemas/runtime/api.py +58 -0
  891. ciris_engine/schemas/runtime/audit.py +50 -0
  892. ciris_engine/schemas/runtime/bootstrap.py +33 -0
  893. ciris_engine/schemas/runtime/contexts.py +61 -0
  894. ciris_engine/schemas/runtime/core.py +161 -0
  895. ciris_engine/schemas/runtime/enums.py +167 -0
  896. ciris_engine/schemas/runtime/extended.py +232 -0
  897. ciris_engine/schemas/runtime/manifest.py +311 -0
  898. ciris_engine/schemas/runtime/memory.py +60 -0
  899. ciris_engine/schemas/runtime/messages.py +108 -0
  900. ciris_engine/schemas/runtime/models.py +156 -0
  901. ciris_engine/schemas/runtime/processing_context.py +43 -0
  902. ciris_engine/schemas/runtime/protocols_core.py +96 -0
  903. ciris_engine/schemas/runtime/resources.py +33 -0
  904. ciris_engine/schemas/runtime/system_context.py +417 -0
  905. ciris_engine/schemas/secrets/__init__.py +1 -0
  906. ciris_engine/schemas/secrets/core.py +267 -0
  907. ciris_engine/schemas/secrets/service.py +95 -0
  908. ciris_engine/schemas/services/__init__.py +33 -0
  909. ciris_engine/schemas/services/audit_summary_node.py +172 -0
  910. ciris_engine/schemas/services/authority/__init__.py +39 -0
  911. ciris_engine/schemas/services/authority/jwt.py +158 -0
  912. ciris_engine/schemas/services/authority/wa_updates.py +138 -0
  913. ciris_engine/schemas/services/authority/wise_authority.py +163 -0
  914. ciris_engine/schemas/services/authority_core.py +370 -0
  915. ciris_engine/schemas/services/capabilities.py +72 -0
  916. ciris_engine/schemas/services/community_core.py +95 -0
  917. ciris_engine/schemas/services/context.py +111 -0
  918. ciris_engine/schemas/services/conversation_summary_node.py +189 -0
  919. ciris_engine/schemas/services/core/__init__.py +153 -0
  920. ciris_engine/schemas/services/core/runtime.py +262 -0
  921. ciris_engine/schemas/services/core/runtime_config.py +117 -0
  922. ciris_engine/schemas/services/core/secrets.py +65 -0
  923. ciris_engine/schemas/services/correlation_node.py +179 -0
  924. ciris_engine/schemas/services/credit_gate.py +92 -0
  925. ciris_engine/schemas/services/discord_nodes.py +299 -0
  926. ciris_engine/schemas/services/feedback_core.py +131 -0
  927. ciris_engine/schemas/services/filters_core.py +270 -0
  928. ciris_engine/schemas/services/governance.py +26 -0
  929. ciris_engine/schemas/services/graph/__init__.py +26 -0
  930. ciris_engine/schemas/services/graph/attributes.py +254 -0
  931. ciris_engine/schemas/services/graph/audit.py +98 -0
  932. ciris_engine/schemas/services/graph/consolidation.py +338 -0
  933. ciris_engine/schemas/services/graph/edge_types.py +43 -0
  934. ciris_engine/schemas/services/graph/edges.py +88 -0
  935. ciris_engine/schemas/services/graph/incident.py +312 -0
  936. ciris_engine/schemas/services/graph/memory.py +84 -0
  937. ciris_engine/schemas/services/graph/node_data.py +174 -0
  938. ciris_engine/schemas/services/graph/query_results.py +82 -0
  939. ciris_engine/schemas/services/graph/telemetry.py +250 -0
  940. ciris_engine/schemas/services/graph/tsdb_consolidation.py +27 -0
  941. ciris_engine/schemas/services/graph/tsdb_models.py +107 -0
  942. ciris_engine/schemas/services/graph_core.py +196 -0
  943. ciris_engine/schemas/services/graph_typed_nodes.py +194 -0
  944. ciris_engine/schemas/services/infrastructure/__init__.py +1 -0
  945. ciris_engine/schemas/services/infrastructure/resource_monitor.py +20 -0
  946. ciris_engine/schemas/services/lifecycle/__init__.py +9 -0
  947. ciris_engine/schemas/services/lifecycle/initialization.py +33 -0
  948. ciris_engine/schemas/services/lifecycle/time.py +50 -0
  949. ciris_engine/schemas/services/llm.py +187 -0
  950. ciris_engine/schemas/services/metadata.py +43 -0
  951. ciris_engine/schemas/services/nodes.py +704 -0
  952. ciris_engine/schemas/services/operations.py +126 -0
  953. ciris_engine/schemas/services/requests.py +128 -0
  954. ciris_engine/schemas/services/resources_core.py +182 -0
  955. ciris_engine/schemas/services/runtime_control.py +1010 -0
  956. ciris_engine/schemas/services/shutdown.py +88 -0
  957. ciris_engine/schemas/services/special/__init__.py +0 -0
  958. ciris_engine/schemas/services/special/self_observation.py +396 -0
  959. ciris_engine/schemas/services/trace_summary_node.py +199 -0
  960. ciris_engine/schemas/services/visibility.py +98 -0
  961. ciris_engine/schemas/streaming/__init__.py +10 -0
  962. ciris_engine/schemas/streaming/reasoning_stream.py +95 -0
  963. ciris_engine/schemas/telemetry/__init__.py +0 -0
  964. ciris_engine/schemas/telemetry/collector.py +67 -0
  965. ciris_engine/schemas/telemetry/core.py +252 -0
  966. ciris_engine/schemas/telemetry/unified.py +59 -0
  967. ciris_engine/schemas/tools.py +72 -0
  968. ciris_engine/schemas/types.py +47 -0
  969. ciris_engine/schemas/utils/__init__.py +1 -0
  970. ciris_engine/schemas/utils/config_validator.py +54 -0
  971. ciris_engine/utils/__init__.py +1 -0
  972. ciris_engine/utils/serialization.py +35 -0
  973. ciris_sdk/__init__.py +124 -0
  974. ciris_sdk/auth_store.py +261 -0
  975. ciris_sdk/client.py +261 -0
  976. ciris_sdk/exceptions.py +73 -0
  977. ciris_sdk/model_types.py +258 -0
  978. ciris_sdk/models.py +354 -0
  979. ciris_sdk/pagination.py +214 -0
  980. ciris_sdk/rate_limiter.py +188 -0
  981. ciris_sdk/setup.py +17 -0
  982. ciris_sdk/telemetry_models.py +257 -0
  983. ciris_sdk/telemetry_responses.py +199 -0
  984. ciris_sdk/transport.py +177 -0
  985. ciris_sdk/websocket.py +400 -0
  986. main.py +766 -0
@@ -0,0 +1,1471 @@
1
+ """Reddit adapter modular service implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import uuid
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import httpx
12
+ from pydantic import BaseModel, ValidationError
13
+
14
+ from ciris_engine.logic.buses import BusManager
15
+ from ciris_engine.logic.secrets.service import SecretsService
16
+ from ciris_engine.logic.services.base_service import BaseService
17
+ from ciris_engine.protocols.services.lifecycle.time import TimeServiceProtocol
18
+ from ciris_engine.schemas.adapters.tools import ToolExecutionResult, ToolExecutionStatus, ToolInfo, ToolParameterSchema
19
+ from ciris_engine.schemas.runtime.enums import ServiceType
20
+ from ciris_engine.schemas.runtime.messages import FetchedMessage
21
+ from ciris_engine.schemas.services.core import ServiceCapabilities
22
+ from ciris_engine.schemas.types import JSONDict, JSONValue
23
+
24
+ from .protocol import RedditCommunicationProtocol, RedditOAuthProtocol, RedditToolProtocol
25
+ from .schemas import (
26
+ RedditChannelReference,
27
+ RedditChannelType,
28
+ RedditCommentResult,
29
+ RedditCommentSummary,
30
+ RedditCredentials,
31
+ RedditDeleteContentRequest,
32
+ RedditDeletionResult,
33
+ RedditDeletionStatus,
34
+ RedditDisclosureRequest,
35
+ RedditGetSubmissionRequest,
36
+ RedditPostResult,
37
+ RedditRemovalResult,
38
+ RedditRemoveContentRequest,
39
+ RedditSubmissionSummary,
40
+ RedditSubmitCommentRequest,
41
+ RedditSubmitPostRequest,
42
+ RedditTimelineEntry,
43
+ RedditTimelineResponse,
44
+ RedditToken,
45
+ RedditUserContext,
46
+ RedditUserContextRequest,
47
+ )
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def _build_channel_reference(
53
+ subreddit: str,
54
+ submission_id: Optional[str] = None,
55
+ comment_id: Optional[str] = None,
56
+ ) -> str:
57
+ """Return a canonical reddit:r/<sub>:post/<id>:comment/<id> reference."""
58
+
59
+ if comment_id:
60
+ reference = RedditChannelReference(
61
+ target=RedditChannelType.COMMENT,
62
+ subreddit=subreddit,
63
+ submission_id=submission_id,
64
+ comment_id=comment_id,
65
+ )
66
+ elif submission_id:
67
+ reference = RedditChannelReference(
68
+ target=RedditChannelType.SUBMISSION,
69
+ subreddit=subreddit,
70
+ submission_id=submission_id,
71
+ )
72
+ else:
73
+ reference = RedditChannelReference(target=RedditChannelType.SUBREDDIT, subreddit=subreddit)
74
+ return reference.to_string()
75
+
76
+
77
+ class RedditAPIClient:
78
+ """Thin wrapper around the Reddit REST API with OAuth management."""
79
+
80
+ _TOKEN_URL = "https://www.reddit.com/api/v1/access_token"
81
+ _API_BASE_URL = "https://oauth.reddit.com"
82
+ _USER_AGENT_FALLBACK = "CIRIS-RedditAdapter/1.0 (+https://ciris.ai)"
83
+
84
+ def __init__(self, credentials: RedditCredentials, time_service: Optional[TimeServiceProtocol] = None) -> None:
85
+ self._credentials = credentials
86
+ self._time_service = time_service
87
+ self._http_client: Optional[httpx.AsyncClient] = None
88
+ self._token: Optional[RedditToken] = None
89
+ self._token_lock = asyncio.Lock()
90
+ self._request_count = 0
91
+ self._error_count = 0
92
+
93
+ # ------------------------------------------------------------------
94
+ # Lifecycle
95
+ # ------------------------------------------------------------------
96
+ async def start(self) -> None:
97
+ headers = {
98
+ "User-Agent": self._credentials.user_agent or self._USER_AGENT_FALLBACK,
99
+ "Accept": "application/json",
100
+ }
101
+ timeout = httpx.Timeout(connect=10.0, read=20.0, write=20.0, pool=10.0)
102
+ self._http_client = httpx.AsyncClient(base_url=self._API_BASE_URL, headers=headers, timeout=timeout)
103
+ await self.refresh_token(force=True)
104
+
105
+ async def stop(self) -> None:
106
+ if self._http_client:
107
+ await self._http_client.aclose()
108
+ self._http_client = None
109
+ self._token = None
110
+
111
+ async def update_credentials(self, credentials: RedditCredentials) -> None:
112
+ self._credentials = credentials
113
+ self._token = None
114
+ if self._http_client:
115
+ await self.refresh_token(force=True)
116
+
117
+ # ------------------------------------------------------------------
118
+ # Helpers
119
+ # ------------------------------------------------------------------
120
+ def _now(self) -> datetime:
121
+ if self._time_service:
122
+ return self._time_service.now()
123
+ return datetime.now(timezone.utc)
124
+
125
+ @property
126
+ def token_active(self) -> bool:
127
+ return bool(self._token and not self._token.is_expired(self._now()))
128
+
129
+ @property
130
+ def metrics(self) -> Dict[str, float]:
131
+ return {"requests": float(self._request_count), "errors": float(self._error_count)}
132
+
133
+ def _add_ciris_attribution(self, text: str, *, max_length: int = 10000) -> str:
134
+ """
135
+ Add CIRIS attribution footer to post/comment text.
136
+
137
+ Args:
138
+ text: Original post/comment text
139
+ max_length: Reddit character limit (10000 for posts/comments)
140
+
141
+ Returns:
142
+ Text with attribution, truncated if necessary to fit within limit
143
+
144
+ Note: Reddit rejects posts/comments longer than 10,000 characters.
145
+ This method ensures attribution is always included by truncating
146
+ the original text if needed.
147
+ """
148
+ attribution = (
149
+ "\n\n"
150
+ "Posted by a CIRIS agent, learn more at https://ciris.ai "
151
+ "or chat with scout at https://scout.ciris.ai"
152
+ )
153
+
154
+ # If text + attribution fits within limit, return as-is
155
+ if len(text) + len(attribution) <= max_length:
156
+ return text + attribution
157
+
158
+ # Otherwise, truncate text to make room for attribution
159
+ # Leave space for attribution + ellipsis + newline
160
+ truncation_marker = "...\n"
161
+ available_space = max_length - len(attribution) - len(truncation_marker)
162
+
163
+ if available_space < 100: # Sanity check: need at least 100 chars for meaningful content
164
+ # If attribution is too large for the limit, skip it (shouldn't happen with 10k limit)
165
+ logger.warning(
166
+ f"Attribution footer ({len(attribution)} chars) too large for limit ({max_length}), "
167
+ "submitting without attribution"
168
+ )
169
+ return text[:max_length]
170
+
171
+ truncated_text = text[:available_space]
172
+ logger.info(
173
+ f"Truncated text from {len(text)} to {len(truncated_text)} chars to fit attribution "
174
+ f"within {max_length} char limit"
175
+ )
176
+ return truncated_text + truncation_marker + attribution
177
+
178
+ async def refresh_token(self, force: bool = False) -> bool:
179
+ async with self._token_lock:
180
+ if not force and self._token and not self._token.is_expired(self._now()):
181
+ return True
182
+
183
+ auth = (self._credentials.client_id, self._credentials.client_secret)
184
+ data = {
185
+ "grant_type": "password",
186
+ "username": self._credentials.username,
187
+ "password": self._credentials.password,
188
+ }
189
+ headers = {"User-Agent": self._credentials.user_agent or self._USER_AGENT_FALLBACK}
190
+
191
+ async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, read=20.0)) as client:
192
+ response = await client.post(self._TOKEN_URL, data=data, auth=auth, headers=headers)
193
+
194
+ if response.status_code >= 300:
195
+ self._error_count += 1
196
+ raise RuntimeError(f"Token request failed ({response.status_code}): {response.text}")
197
+
198
+ payload = self._expect_dict(response.json(), context="token")
199
+ access_token = self._get_str(payload, "access_token")
200
+
201
+ # Validate access token - empty token indicates auth failure (suspended account, invalid credentials)
202
+ if not access_token or access_token.strip() == "":
203
+ error_msg = payload.get("error", "Unknown error")
204
+ error_desc = payload.get("error_description", "No access_token in response")
205
+ logger.error(
206
+ f"Reddit OAuth failed - likely suspended account or invalid credentials. "
207
+ f"Error: {error_msg}, Description: {error_desc}, Response: {payload}"
208
+ )
209
+ raise RuntimeError(
210
+ f"Reddit authentication failed: {error_msg} - {error_desc}. "
211
+ "This may indicate a suspended Reddit account or invalid credentials."
212
+ )
213
+
214
+ expires_in = int(float(self._get_str(payload, "expires_in", default="3600")))
215
+ expires_at = self._now() + timedelta(seconds=expires_in)
216
+ self._token = RedditToken(access_token=access_token, expires_at=expires_at)
217
+ logger.info("Refreshed Reddit OAuth token; expires at %s", expires_at.isoformat())
218
+ return True
219
+
220
+ async def _request(
221
+ self,
222
+ method: str,
223
+ path: str,
224
+ *,
225
+ params: Optional[Dict[str, Any]] = None,
226
+ data: Optional[Dict[str, Any]] = None,
227
+ ) -> httpx.Response:
228
+ if not self._http_client:
229
+ raise RuntimeError("HTTP client not initialized")
230
+
231
+ await self.refresh_token()
232
+ assert self._token is not None
233
+ headers = {"Authorization": f"bearer {self._token.access_token}"}
234
+
235
+ response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
236
+ self._request_count += 1
237
+
238
+ if response.status_code == 401:
239
+ await self.refresh_token(force=True)
240
+ assert self._token is not None
241
+ headers["Authorization"] = f"bearer {self._token.access_token}"
242
+ response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
243
+
244
+ if response.status_code == 429:
245
+ retry_after = float(response.headers.get("Retry-After", "1"))
246
+ await asyncio.sleep(max(retry_after, 0))
247
+ response = await self._http_client.request(method, path, params=params, data=data, headers=headers)
248
+
249
+ if response.status_code >= 400:
250
+ self._error_count += 1
251
+ return response
252
+
253
+ async def _request_json(
254
+ self,
255
+ method: str,
256
+ path: str,
257
+ *,
258
+ params: Optional[Dict[str, Any]] = None,
259
+ data: Optional[Dict[str, Any]] = None,
260
+ ) -> JSONDict:
261
+ response = await self._request(method, path, params=params, data=data)
262
+ payload = response.json()
263
+ return self._expect_dict(payload, context=path)
264
+
265
+ # ------------------------------------------------------------------
266
+ # Expectation helpers
267
+ # ------------------------------------------------------------------
268
+ def _expect_dict(self, value: JSONValue, *, context: str) -> JSONDict:
269
+ if not isinstance(value, dict):
270
+ raise RuntimeError(f"{context}: expected object")
271
+ return value
272
+
273
+ def _get_str(self, data: JSONDict, key: str, *, default: str = "") -> str:
274
+ value = data.get(key, default)
275
+ if value is None:
276
+ return default
277
+ if isinstance(value, str):
278
+ return value
279
+ if isinstance(value, (int, float)):
280
+ return str(value)
281
+ return default
282
+
283
+ def _build_permalink(self, permalink: str) -> str:
284
+ if permalink.startswith("http"):
285
+ return permalink
286
+ return f"https://www.reddit.com{permalink}" if permalink else ""
287
+
288
+ def _strip_prefix(self, value: str, prefix: str) -> str:
289
+ if value.startswith(prefix):
290
+ return value[len(prefix) :]
291
+ return value
292
+
293
+ # ------------------------------------------------------------------
294
+ # High level API methods
295
+ # ------------------------------------------------------------------
296
+ async def fetch_user_context(self, request: RedditUserContextRequest) -> RedditUserContext:
297
+ about_payload = await self._request_json("GET", f"/user/{request.username}/about")
298
+ about_data = self._expect_dict(about_payload.get("data"), context="user_about.data")
299
+
300
+ account_created = datetime.fromtimestamp(
301
+ float(self._get_str(about_data, "created_utc", default="0")), tz=timezone.utc
302
+ )
303
+ context = RedditUserContext(
304
+ username=self._get_str(about_data, "name"),
305
+ user_id=self._get_str(about_data, "id"),
306
+ link_karma=int(float(self._get_str(about_data, "link_karma", default="0"))),
307
+ comment_karma=int(float(self._get_str(about_data, "comment_karma", default="0"))),
308
+ is_mod=bool(about_data.get("is_mod", False)),
309
+ account_created_at=account_created,
310
+ )
311
+
312
+ if request.include_history:
313
+ submissions = await self._fetch_listing(
314
+ f"/user/{request.username}/submitted", limit=request.history_limit, entry_type="submission"
315
+ )
316
+ comments = await self._fetch_listing(
317
+ f"/user/{request.username}/comments", limit=request.history_limit, entry_type="comment"
318
+ )
319
+ context.recent_posts = submissions
320
+ context.recent_comments = comments
321
+
322
+ return context
323
+
324
+ async def submit_post(self, request: RedditSubmitPostRequest) -> RedditSubmissionSummary:
325
+ subreddit = request.subreddit or self._credentials.subreddit
326
+ # Add CIRIS attribution to post body
327
+ body_with_attribution = self._add_ciris_attribution(request.body)
328
+ payload = {
329
+ "sr": subreddit,
330
+ "kind": "self",
331
+ "title": request.title,
332
+ "text": body_with_attribution,
333
+ "resubmit": "true",
334
+ "sendreplies": "true" if request.send_replies else "false",
335
+ }
336
+ if request.flair_id:
337
+ payload["flair_id"] = request.flair_id
338
+ if request.flair_text:
339
+ payload["flair_text"] = request.flair_text
340
+ if request.nsfw:
341
+ payload["nsfw"] = "true"
342
+ if request.spoiler:
343
+ payload["spoiler"] = "true"
344
+
345
+ response = await self._request("POST", "/api/submit", data=payload)
346
+ result = await self._parse_submission_response(
347
+ response, subreddit=subreddit, title=request.title, body=request.body
348
+ )
349
+ if not result:
350
+ raise RuntimeError("Submission failed")
351
+ return result.submission
352
+
353
+ async def submit_comment(self, request: RedditSubmitCommentRequest) -> RedditCommentSummary:
354
+ # Add CIRIS attribution to comment text
355
+ text_with_attribution = self._add_ciris_attribution(request.text)
356
+ payload = {"thing_id": request.parent_fullname, "text": text_with_attribution}
357
+ response = await self._request("POST", "/api/comment", data=payload)
358
+ comment = await self._parse_comment_response(response)
359
+ if not comment:
360
+ raise RuntimeError(
361
+ f"Comment response missing data - status: {response.status_code}, " f"text: {response.text[:200]}"
362
+ )
363
+
364
+ if request.lock_thread and comment.submission_id:
365
+ await self._request("POST", "/api/lock", data={"id": f"t3_{comment.submission_id}"})
366
+ return comment
367
+
368
+ async def remove_content(self, request: RedditRemoveContentRequest) -> RedditRemovalResult:
369
+ payload = {"id": request.thing_fullname, "spam": "true" if request.spam else "false"}
370
+ response = await self._request("POST", "/api/remove", data=payload)
371
+ if response.status_code >= 300:
372
+ raise RuntimeError(f"Removal failed ({response.status_code}): {response.text}")
373
+ return RedditRemovalResult(thing_fullname=request.thing_fullname, removed=True, spam=request.spam)
374
+
375
+ async def delete_content(self, thing_fullname: str) -> bool:
376
+ """
377
+ Permanently delete content from Reddit (Reddit ToS compliance).
378
+
379
+ Args:
380
+ thing_fullname: Reddit thing fullname (t3_xxxxx or t1_xxxxx)
381
+
382
+ Returns:
383
+ True if deletion successful
384
+
385
+ Note: This uses DELETE /api/del which permanently removes content.
386
+ This is different from remove_content which hides content.
387
+ """
388
+ payload = {"id": thing_fullname}
389
+ response = await self._request("POST", "/api/del", data=payload)
390
+ if response.status_code >= 300:
391
+ raise RuntimeError(f"Deletion failed ({response.status_code}): {response.text}")
392
+ return True
393
+
394
+ async def get_submission_summary(
395
+ self,
396
+ submission_id: str,
397
+ *,
398
+ include_comments: bool,
399
+ comment_limit: int,
400
+ ) -> RedditSubmissionSummary:
401
+ fullname = f"t3_{submission_id}"
402
+ metadata = await self._fetch_item_metadata(fullname)
403
+ if not metadata:
404
+ raise RuntimeError("Submission not found")
405
+ return await self._build_submission_summary(
406
+ metadata, include_comments=include_comments, comment_limit=comment_limit
407
+ )
408
+
409
+ async def fetch_subreddit_new(self, subreddit: str, *, limit: int) -> List[RedditTimelineEntry]:
410
+ return await self._fetch_listing(f"/r/{subreddit}/new", limit=limit, entry_type="submission")
411
+
412
+ async def fetch_subreddit_comments(self, subreddit: str, *, limit: int) -> List[RedditTimelineEntry]:
413
+ return await self._fetch_listing(f"/r/{subreddit}/comments", limit=limit, entry_type="comment")
414
+
415
+ async def fetch_submission_comments(self, submission_id: str, *, limit: int) -> List[RedditCommentSummary]:
416
+ params = {"limit": str(limit)}
417
+ response = await self._request("GET", f"/comments/{submission_id}", params=params)
418
+ if response.status_code >= 300:
419
+ raise RuntimeError(f"Failed to fetch comments: {response.status_code}")
420
+
421
+ payload = response.json()
422
+ if not isinstance(payload, list) or len(payload) < 2:
423
+ return []
424
+
425
+ comments_listing = payload[1]
426
+ listing_data = self._expect_dict(comments_listing.get("data"), context="comments.data")
427
+ children = listing_data.get("children", [])
428
+ summaries: List[RedditCommentSummary] = []
429
+ if isinstance(children, list):
430
+ for child in children:
431
+ child_dict = self._expect_dict(child, context="comment.child")
432
+ child_data = self._expect_dict(child_dict.get("data"), context="comment.child.data")
433
+ summary = self._build_comment_summary(child_dict.get("kind"), child_data, submission_id=submission_id)
434
+ if summary:
435
+ summaries.append(summary)
436
+ if len(summaries) >= limit:
437
+ break
438
+ return summaries
439
+
440
+ async def fetch_user_activity(self, username: str, *, limit: int) -> RedditTimelineResponse:
441
+ posts = await self._fetch_listing(f"/user/{username}/submitted", limit=limit, entry_type="submission")
442
+ comments = await self._fetch_listing(f"/user/{username}/comments", limit=limit, entry_type="comment")
443
+ return RedditTimelineResponse(entries=posts + comments)
444
+
445
+ # ------------------------------------------------------------------
446
+ # Parsing helpers
447
+ # ------------------------------------------------------------------
448
+ async def _parse_submission_response(
449
+ self, response: httpx.Response, *, subreddit: str, title: str, body: str
450
+ ) -> Optional[RedditPostResult]:
451
+ if response.status_code >= 300:
452
+ raise RuntimeError(f"Submission failed ({response.status_code}): {response.text}")
453
+
454
+ payload = self._expect_dict(response.json(), context="submit")
455
+ json_data = payload.get("json")
456
+ if not isinstance(json_data, dict):
457
+ return None
458
+
459
+ errors = json_data.get("errors", [])
460
+ if isinstance(errors, list) and errors:
461
+ raise RuntimeError(f"Reddit returned errors: {errors}")
462
+
463
+ data_dict = self._expect_dict(json_data.get("data"), context="submit.data")
464
+ submission_id = self._strip_prefix(self._get_str(data_dict, "id"), prefix="t3_")
465
+ fullname = self._get_str(data_dict, "name") or f"t3_{submission_id}"
466
+ url = self._get_str(data_dict, "url")
467
+ return RedditPostResult(
468
+ submission=RedditSubmissionSummary(
469
+ submission_id=submission_id,
470
+ fullname=fullname,
471
+ title=title,
472
+ self_text=body,
473
+ url=url,
474
+ subreddit=subreddit,
475
+ author=self._credentials.username,
476
+ score=1,
477
+ num_comments=0,
478
+ created_at=self._now(),
479
+ permalink=url,
480
+ channel_reference=_build_channel_reference(subreddit, submission_id),
481
+ )
482
+ )
483
+
484
+ async def _parse_comment_response(self, response: httpx.Response) -> Optional[RedditCommentSummary]:
485
+ if response.status_code >= 300:
486
+ raise RuntimeError(f"Comment failed ({response.status_code}): {response.text}")
487
+
488
+ payload = self._expect_dict(response.json(), context="comment")
489
+ json_data = payload.get("json")
490
+ if not isinstance(json_data, dict):
491
+ logger.error(f"Comment response missing 'json' dict: {payload}")
492
+ return None
493
+
494
+ errors = json_data.get("errors", [])
495
+ if isinstance(errors, list) and errors:
496
+ raise RuntimeError(f"Reddit returned errors: {errors}")
497
+
498
+ data_dict = self._expect_dict(json_data.get("data"), context="comment.data")
499
+ things = data_dict.get("things", [])
500
+ if not isinstance(things, list) or not things:
501
+ logger.error(f"Comment response missing 'things' list: json_data={json_data}")
502
+ return None
503
+
504
+ first = self._expect_dict(things[0], context="comment.thing")
505
+ comment_data = self._expect_dict(first.get("data"), context="comment.thing.data")
506
+ subreddit = self._get_str(comment_data, "subreddit")
507
+ submission_id = self._strip_prefix(self._get_str(comment_data, "link_id"), prefix="t3_")
508
+ comment_id = self._get_str(comment_data, "id")
509
+ permalink = self._build_permalink(self._get_str(comment_data, "permalink"))
510
+ return RedditCommentSummary(
511
+ comment_id=comment_id,
512
+ fullname=self._get_str(comment_data, "name"),
513
+ submission_id=submission_id,
514
+ body=self._get_str(comment_data, "body"),
515
+ author=self._get_str(comment_data, "author"),
516
+ subreddit=subreddit,
517
+ permalink=permalink,
518
+ created_at=datetime.fromtimestamp(
519
+ float(self._get_str(comment_data, "created_utc", default="0")), tz=timezone.utc
520
+ ),
521
+ score=int(float(self._get_str(comment_data, "score", default="0"))),
522
+ channel_reference=_build_channel_reference(subreddit, submission_id, comment_id),
523
+ )
524
+
525
+ async def _fetch_item_metadata(self, fullname: str) -> Optional[JSONDict]:
526
+ response = await self._request("GET", "/api/info", params={"id": fullname})
527
+ if response.status_code >= 300:
528
+ raise RuntimeError(f"Failed to fetch metadata ({response.status_code}): {response.text}")
529
+
530
+ payload = self._expect_dict(response.json(), context="info")
531
+ data = self._expect_dict(payload.get("data"), context="info.data")
532
+ children = data.get("children", [])
533
+ if not isinstance(children, list) or not children:
534
+ return None
535
+
536
+ first = self._expect_dict(children[0], context="info.child")
537
+ return self._expect_dict(first.get("data"), context="info.child.data")
538
+
539
+ async def _build_submission_summary(
540
+ self,
541
+ data: JSONDict,
542
+ *,
543
+ include_comments: bool,
544
+ comment_limit: int,
545
+ ) -> RedditSubmissionSummary:
546
+ subreddit = self._get_str(data, "subreddit")
547
+ submission_id = self._get_str(data, "id")
548
+ permalink = self._build_permalink(self._get_str(data, "permalink"))
549
+ summary = RedditSubmissionSummary(
550
+ submission_id=submission_id,
551
+ fullname=self._get_str(data, "name"),
552
+ title=self._get_str(data, "title"),
553
+ self_text=self._get_str(data, "selftext"),
554
+ url=self._get_str(data, "url"),
555
+ subreddit=subreddit,
556
+ author=self._get_str(data, "author"),
557
+ score=int(float(self._get_str(data, "score", default="0"))),
558
+ num_comments=int(float(self._get_str(data, "num_comments", default="0"))),
559
+ created_at=datetime.fromtimestamp(float(self._get_str(data, "created_utc", default="0")), tz=timezone.utc),
560
+ permalink=permalink,
561
+ channel_reference=_build_channel_reference(subreddit, submission_id),
562
+ )
563
+
564
+ if include_comments:
565
+ summary.top_comments = await self.fetch_submission_comments(submission_id, limit=comment_limit)
566
+ return summary
567
+
568
+ async def _fetch_listing(self, path: str, *, limit: int, entry_type: str) -> List[RedditTimelineEntry]:
569
+ payload = await self._request_json("GET", path, params={"limit": str(limit)})
570
+ data = self._expect_dict(payload.get("data"), context="listing.data")
571
+ children_value = data.get("children", [])
572
+ entries: List[RedditTimelineEntry] = []
573
+
574
+ if isinstance(children_value, list):
575
+ for child in children_value:
576
+ child_dict = self._expect_dict(child, context="listing.child")
577
+ child_data = self._expect_dict(child_dict.get("data"), context="listing.child.data")
578
+ entry = self._build_timeline_entry(child_dict.get("kind"), child_data)
579
+ if entry and entry.entry_type == entry_type:
580
+ entries.append(entry)
581
+ if len(entries) >= limit:
582
+ break
583
+
584
+ return entries
585
+
586
+ def _build_timeline_entry(self, kind_value: JSONValue, child_data: JSONDict) -> Optional[RedditTimelineEntry]:
587
+ if not isinstance(kind_value, str):
588
+ return None
589
+
590
+ created = datetime.fromtimestamp(float(self._get_str(child_data, "created_utc", default="0")), tz=timezone.utc)
591
+ permalink = self._build_permalink(self._get_str(child_data, "permalink"))
592
+ subreddit = self._get_str(child_data, "subreddit")
593
+ fullname = self._get_str(child_data, "name")
594
+ item_id = self._strip_prefix(fullname, prefix="t3_" if kind_value == "t3" else "t1_")
595
+ score = int(float(self._get_str(child_data, "score", default="0")))
596
+
597
+ if kind_value == "t3":
598
+ return RedditTimelineEntry(
599
+ entry_type="submission",
600
+ item_id=item_id,
601
+ fullname=fullname,
602
+ subreddit=subreddit,
603
+ permalink=permalink,
604
+ score=score,
605
+ created_at=created,
606
+ channel_reference=_build_channel_reference(subreddit, item_id),
607
+ author=self._get_str(child_data, "author"),
608
+ title=self._get_str(child_data, "title"),
609
+ body=self._get_str(child_data, "selftext"),
610
+ url=self._get_str(child_data, "url"),
611
+ )
612
+ if kind_value == "t1":
613
+ submission_id = self._strip_prefix(self._get_str(child_data, "link_id"), prefix="t3_")
614
+ parent_id = self._get_str(child_data, "parent_id", default="") or None
615
+ return RedditTimelineEntry(
616
+ entry_type="comment",
617
+ item_id=item_id,
618
+ fullname=fullname,
619
+ subreddit=subreddit,
620
+ permalink=permalink,
621
+ score=score,
622
+ created_at=created,
623
+ channel_reference=_build_channel_reference(subreddit, submission_id, item_id),
624
+ author=self._get_str(child_data, "author"),
625
+ body=self._get_str(child_data, "body"),
626
+ parent_id=parent_id,
627
+ )
628
+ return None
629
+
630
+ def _build_comment_summary(
631
+ self,
632
+ kind_value: JSONValue,
633
+ comment_data: JSONDict,
634
+ *,
635
+ submission_id: str,
636
+ ) -> Optional[RedditCommentSummary]:
637
+ if kind_value != "t1":
638
+ return None
639
+ comment_id = self._get_str(comment_data, "id")
640
+ subreddit = self._get_str(comment_data, "subreddit")
641
+ permalink = self._build_permalink(self._get_str(comment_data, "permalink"))
642
+ parent_id = self._get_str(comment_data, "parent_id", default="") or None
643
+ return RedditCommentSummary(
644
+ comment_id=comment_id,
645
+ fullname=self._get_str(comment_data, "name"),
646
+ submission_id=submission_id,
647
+ body=self._get_str(comment_data, "body"),
648
+ author=self._get_str(comment_data, "author"),
649
+ subreddit=subreddit,
650
+ permalink=permalink,
651
+ created_at=datetime.fromtimestamp(
652
+ float(self._get_str(comment_data, "created_utc", default="0")), tz=timezone.utc
653
+ ),
654
+ score=int(float(self._get_str(comment_data, "score", default="0"))),
655
+ channel_reference=_build_channel_reference(subreddit, submission_id, comment_id),
656
+ parent_id=parent_id,
657
+ )
658
+
659
+
660
+ # ----------------------------------------------------------------------
661
+ # Base service shared by tool and communication implementations
662
+ # ----------------------------------------------------------------------
663
+
664
+
665
+ class RedditServiceBase(BaseService, RedditOAuthProtocol):
666
+ """Base class providing shared Reddit API functionality."""
667
+
668
+ def __init__(
669
+ self,
670
+ credentials: Optional[RedditCredentials] = None,
671
+ *,
672
+ time_service: Optional[TimeServiceProtocol] = None,
673
+ service_name: str,
674
+ ) -> None:
675
+ super().__init__(time_service=time_service, service_name=service_name, version="1.0.0")
676
+ resolved_credentials = credentials or RedditCredentials.from_env()
677
+ if not resolved_credentials:
678
+ raise RuntimeError("Reddit credentials are not configured")
679
+ self._credentials = resolved_credentials
680
+ self._client = RedditAPIClient(self._credentials, time_service=time_service)
681
+ self._subreddit = RedditChannelReference._normalize_subreddit(self._credentials.subreddit)
682
+
683
+ # BaseService overrides -------------------------------------------------
684
+ def _check_dependencies(self) -> bool:
685
+ return self._credentials is not None and self._credentials.is_complete()
686
+
687
+ async def _on_start(self) -> None:
688
+ await self._client.start()
689
+
690
+ async def _on_stop(self) -> None:
691
+ await self._client.stop()
692
+
693
+ def _collect_custom_metrics(self) -> Dict[str, float]:
694
+ metrics = self._client.metrics
695
+ metrics["token_active"] = 1.0 if self._client.token_active else 0.0
696
+ return metrics
697
+
698
+ def _register_dependencies(self) -> None:
699
+ super()._register_dependencies()
700
+ self._dependencies.add("httpx")
701
+
702
+ # RedditOAuthProtocol ---------------------------------------------------
703
+ async def update_credentials(self, credentials: RedditCredentials) -> None:
704
+ self._credentials = credentials
705
+ self._subreddit = RedditChannelReference._normalize_subreddit(credentials.subreddit)
706
+ await self._client.update_credentials(credentials)
707
+
708
+ async def refresh_token(self, force: bool = False) -> bool:
709
+ try:
710
+ return await self._client.refresh_token(force=force)
711
+ except Exception as exc: # pragma: no cover - defensive logging
712
+ self._track_error(exc)
713
+ return False
714
+
715
+
716
+ # ----------------------------------------------------------------------
717
+ # Tool service implementation
718
+ # ----------------------------------------------------------------------
719
+
720
+
721
+ class RedditToolService(RedditServiceBase):
722
+ """Tool service providing Reddit moderation and outreach utilities."""
723
+
724
+ def __init__(
725
+ self,
726
+ credentials: Optional[RedditCredentials] = None,
727
+ *,
728
+ time_service: Optional[TimeServiceProtocol] = None,
729
+ ) -> None:
730
+ super().__init__(credentials, time_service=time_service, service_name="RedditToolService")
731
+ self._results: Dict[str, ToolExecutionResult] = {}
732
+ self._tool_handlers = {
733
+ "reddit_get_user_context": self._tool_get_user_context,
734
+ "reddit_submit_post": self._tool_submit_post,
735
+ "reddit_submit_comment": self._tool_submit_comment,
736
+ "reddit_remove_content": self._tool_remove_content,
737
+ "reddit_get_submission": self._tool_get_submission,
738
+ "reddit_observe": self._tool_observe,
739
+ "reddit_delete_content": self._tool_delete_content,
740
+ "reddit_disclose_identity": self._tool_disclose_identity,
741
+ }
742
+ self._request_models: Dict[str, type[BaseModel]] = {
743
+ "reddit_get_user_context": RedditUserContextRequest,
744
+ "reddit_submit_post": RedditSubmitPostRequest,
745
+ "reddit_submit_comment": RedditSubmitCommentRequest,
746
+ "reddit_remove_content": RedditRemoveContentRequest,
747
+ "reddit_get_submission": RedditGetSubmissionRequest,
748
+ "reddit_delete_content": RedditDeleteContentRequest,
749
+ "reddit_disclose_identity": RedditDisclosureRequest,
750
+ }
751
+ # Deletion status tracking (DSAR pattern)
752
+ self._deletion_statuses: Dict[str, RedditDeletionStatus] = {}
753
+ self._tool_schemas = self._build_tool_schemas()
754
+ self._tool_info = self._build_tool_info()
755
+ self._executions = 0
756
+ self._failures = 0
757
+
758
+ # BaseService -----------------------------------------------------------
759
+ def get_service_type(self) -> ServiceType:
760
+ return ServiceType.TOOL
761
+
762
+ def _get_actions(self) -> List[str]:
763
+ return list(self._tool_handlers.keys())
764
+
765
+ # ToolServiceProtocol ---------------------------------------------------
766
+ async def execute_tool(self, tool_name: str, parameters: JSONDict) -> ToolExecutionResult:
767
+ self._track_request()
768
+ self._executions += 1
769
+
770
+ correlation_id_raw = parameters.get("correlation_id")
771
+ correlation_id = str(correlation_id_raw) if correlation_id_raw else str(uuid.uuid4())
772
+
773
+ handler = self._tool_handlers.get(tool_name)
774
+ if not handler:
775
+ self._failures += 1
776
+ result = ToolExecutionResult(
777
+ tool_name=tool_name,
778
+ status=ToolExecutionStatus.NOT_FOUND,
779
+ success=False,
780
+ data=None,
781
+ error=f"Unknown Reddit tool: {tool_name}",
782
+ correlation_id=correlation_id,
783
+ )
784
+ self._results[correlation_id] = result
785
+ return result
786
+
787
+ try:
788
+ result = await handler(parameters, correlation_id)
789
+ if not result.success:
790
+ self._failures += 1
791
+ self._results[correlation_id] = result
792
+ return result
793
+ except Exception as exc: # pragma: no cover - defensive logging
794
+ self._failures += 1
795
+ self._track_error(exc)
796
+ result = ToolExecutionResult(
797
+ tool_name=tool_name,
798
+ status=ToolExecutionStatus.FAILED,
799
+ success=False,
800
+ data=None,
801
+ error=str(exc),
802
+ correlation_id=correlation_id,
803
+ )
804
+ self._results[correlation_id] = result
805
+ return result
806
+
807
+ async def list_tools(self) -> List[str]:
808
+ return list(self._tool_handlers.keys())
809
+
810
+ async def get_tool_schema(self, tool_name: str) -> Optional[ToolParameterSchema]:
811
+ return self._tool_schemas.get(tool_name)
812
+
813
+ async def get_available_tools(self) -> List[str]:
814
+ return await self.list_tools()
815
+
816
+ async def get_tool_info(self, tool_name: str) -> Optional[ToolInfo]:
817
+ return self._tool_info.get(tool_name)
818
+
819
+ async def get_all_tool_info(self) -> List[ToolInfo]:
820
+ return list(self._tool_info.values())
821
+
822
+ async def validate_parameters(self, tool_name: str, parameters: JSONDict) -> bool:
823
+ model_cls = self._request_models.get(tool_name)
824
+ if not model_cls:
825
+ return False
826
+ try:
827
+ model_cls.model_validate(parameters)
828
+ return True
829
+ except ValidationError:
830
+ return False
831
+
832
+ async def get_tool_result(self, correlation_id: str, timeout: float = 30.0) -> Optional[ToolExecutionResult]:
833
+ return self._results.get(correlation_id)
834
+
835
+ def _collect_custom_metrics(self) -> Dict[str, float]:
836
+ metrics = super()._collect_custom_metrics()
837
+ metrics.update({"tool_executions": float(self._executions), "tool_failures": float(self._failures)})
838
+ return metrics
839
+
840
+ # Tool handlers ---------------------------------------------------------
841
+ async def _tool_get_user_context(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
842
+ try:
843
+ request = RedditUserContextRequest.model_validate(parameters)
844
+ except ValidationError as error:
845
+ return self._validation_error_result("reddit_get_user_context", correlation_id, error)
846
+
847
+ try:
848
+ context = await self._client.fetch_user_context(request)
849
+ except Exception as exc:
850
+ return self._api_error_result("reddit_get_user_context", correlation_id, str(exc))
851
+
852
+ return ToolExecutionResult(
853
+ tool_name="reddit_get_user_context",
854
+ status=ToolExecutionStatus.COMPLETED,
855
+ success=True,
856
+ data=context.model_dump(mode="json"),
857
+ error=None,
858
+ correlation_id=correlation_id,
859
+ )
860
+
861
+ async def _tool_submit_post(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
862
+ try:
863
+ request = RedditSubmitPostRequest.model_validate(parameters)
864
+ except ValidationError as error:
865
+ return self._validation_error_result("reddit_submit_post", correlation_id, error)
866
+
867
+ try:
868
+ summary = await self._client.submit_post(request)
869
+ except Exception as exc:
870
+ return self._api_error_result("reddit_submit_post", correlation_id, str(exc))
871
+
872
+ return ToolExecutionResult(
873
+ tool_name="reddit_submit_post",
874
+ status=ToolExecutionStatus.COMPLETED,
875
+ success=True,
876
+ data=summary.model_dump(mode="json"),
877
+ error=None,
878
+ correlation_id=correlation_id,
879
+ )
880
+
881
+ async def _tool_submit_comment(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
882
+ try:
883
+ request = RedditSubmitCommentRequest.model_validate(parameters)
884
+ except ValidationError as error:
885
+ return self._validation_error_result("reddit_submit_comment", correlation_id, error)
886
+
887
+ try:
888
+ comment = await self._client.submit_comment(request)
889
+ except Exception as exc:
890
+ return self._api_error_result("reddit_submit_comment", correlation_id, str(exc))
891
+
892
+ return ToolExecutionResult(
893
+ tool_name="reddit_submit_comment",
894
+ status=ToolExecutionStatus.COMPLETED,
895
+ success=True,
896
+ data=comment.model_dump(mode="json"),
897
+ error=None,
898
+ correlation_id=correlation_id,
899
+ )
900
+
901
+ async def _tool_remove_content(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
902
+ try:
903
+ request = RedditRemoveContentRequest.model_validate(parameters)
904
+ except ValidationError as error:
905
+ return self._validation_error_result("reddit_remove_content", correlation_id, error)
906
+
907
+ try:
908
+ removal_result = await self._client.remove_content(request)
909
+ except Exception as exc:
910
+ return self._api_error_result("reddit_remove_content", correlation_id, str(exc))
911
+
912
+ return ToolExecutionResult(
913
+ tool_name="reddit_remove_content",
914
+ status=ToolExecutionStatus.COMPLETED,
915
+ success=True,
916
+ data=removal_result.model_dump(mode="json"),
917
+ error=None,
918
+ correlation_id=correlation_id,
919
+ )
920
+
921
+ async def _tool_get_submission(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
922
+ try:
923
+ request = RedditGetSubmissionRequest.model_validate(parameters)
924
+ except ValidationError as error:
925
+ return self._validation_error_result("reddit_get_submission", correlation_id, error)
926
+
927
+ submission_id = request.submission_id
928
+ if not submission_id and request.permalink:
929
+ submission_id = self._extract_submission_id_from_permalink(request.permalink)
930
+ if not submission_id:
931
+ return self._api_error_result("reddit_get_submission", correlation_id, "Unable to determine submission ID")
932
+
933
+ try:
934
+ summary = await self._client.get_submission_summary(
935
+ submission_id,
936
+ include_comments=request.include_comments,
937
+ comment_limit=request.comment_limit,
938
+ )
939
+ except Exception as exc:
940
+ return self._api_error_result("reddit_get_submission", correlation_id, str(exc))
941
+
942
+ return ToolExecutionResult(
943
+ tool_name="reddit_get_submission",
944
+ status=ToolExecutionStatus.COMPLETED,
945
+ success=True,
946
+ data=summary.model_dump(mode="json"),
947
+ error=None,
948
+ correlation_id=correlation_id,
949
+ )
950
+
951
+ async def _tool_observe(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
952
+ target = parameters.get("channel_reference")
953
+ if not isinstance(target, str):
954
+ return self._api_error_result("reddit_observe", correlation_id, "channel_reference is required")
955
+
956
+ limit_value = parameters.get("limit", 25)
957
+ try:
958
+ # Handle various types that might come from parameters
959
+ if isinstance(limit_value, int):
960
+ limit = limit_value
961
+ elif isinstance(limit_value, (float, str)):
962
+ limit = int(float(limit_value))
963
+ else:
964
+ limit = 25
965
+ except (TypeError, ValueError):
966
+ limit = 25
967
+
968
+ try:
969
+ reference = RedditChannelReference.parse(target)
970
+ except ValueError as exc:
971
+ return self._api_error_result("reddit_observe", correlation_id, str(exc))
972
+
973
+ try:
974
+ payload = await self._active_observe(reference, limit=limit)
975
+ except Exception as exc:
976
+ return self._api_error_result("reddit_observe", correlation_id, str(exc))
977
+
978
+ return ToolExecutionResult(
979
+ tool_name="reddit_observe",
980
+ status=ToolExecutionStatus.COMPLETED,
981
+ success=True,
982
+ data=payload.model_dump(mode="json"),
983
+ error=None,
984
+ correlation_id=correlation_id,
985
+ )
986
+
987
+ async def _tool_delete_content(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
988
+ """
989
+ Permanently delete Reddit content (Reddit ToS compliance).
990
+
991
+ Reddit ToS Requirement: Zero retention of deleted content.
992
+ """
993
+ try:
994
+ request = RedditDeleteContentRequest.model_validate(parameters)
995
+ except ValidationError as error:
996
+ return self._validation_error_result("reddit_delete_content", correlation_id, error)
997
+
998
+ now = datetime.now(timezone.utc)
999
+ content_id = request.thing_fullname
1000
+ content_type = "submission" if content_id.startswith("t3_") else "comment"
1001
+
1002
+ try:
1003
+ # Phase 1: Delete from Reddit
1004
+ deletion_confirmed = await self._client.delete_content(content_id)
1005
+
1006
+ # Phase 2: Purge from local cache (Reddit ToS compliance)
1007
+ cache_purged = False
1008
+ if request.purge_cache and hasattr(self, "_client"):
1009
+ # NOTE: Cache purge logic would go here if cache exists
1010
+ # For now, we just mark as purged since there's no cache in base client
1011
+ cache_purged = True
1012
+
1013
+ # Phase 3: Create audit trail entry
1014
+ audit_entry_id = str(uuid.uuid4())
1015
+
1016
+ # Track deletion status (DSAR pattern)
1017
+ deletion_status = RedditDeletionStatus(
1018
+ content_id=content_id,
1019
+ initiated_at=now,
1020
+ completed_at=now if (deletion_confirmed and cache_purged) else None,
1021
+ deletion_confirmed=deletion_confirmed,
1022
+ cache_purged=cache_purged,
1023
+ audit_trail_updated=True,
1024
+ )
1025
+ self._deletion_statuses[content_id] = deletion_status
1026
+
1027
+ deletion_result = RedditDeletionResult(
1028
+ content_id=content_id,
1029
+ content_type=content_type,
1030
+ deleted_from_reddit=deletion_confirmed,
1031
+ purged_from_cache=cache_purged,
1032
+ audit_entry_id=audit_entry_id,
1033
+ deleted_at=now,
1034
+ )
1035
+
1036
+ logger.info(f"Deleted Reddit {content_type} {content_id} (ToS compliance)")
1037
+
1038
+ except Exception as exc:
1039
+ return self._api_error_result("reddit_delete_content", correlation_id, str(exc))
1040
+
1041
+ return ToolExecutionResult(
1042
+ tool_name="reddit_delete_content",
1043
+ status=ToolExecutionStatus.COMPLETED,
1044
+ success=True,
1045
+ data=deletion_result.model_dump(mode="json"),
1046
+ error=None,
1047
+ correlation_id=correlation_id,
1048
+ )
1049
+
1050
+ async def _tool_disclose_identity(self, parameters: JSONDict, correlation_id: str) -> ToolExecutionResult:
1051
+ """
1052
+ Post AI transparency disclosure (Reddit community guidelines compliance).
1053
+ """
1054
+ try:
1055
+ request = RedditDisclosureRequest.model_validate(parameters)
1056
+ except ValidationError as error:
1057
+ return self._validation_error_result("reddit_disclose_identity", correlation_id, error)
1058
+
1059
+ # Default disclosure message
1060
+ default_message = (
1061
+ "Hello! I'm CIRIS, an AI assistant helping moderate this community.\n\n"
1062
+ "I can help with content moderation, but all major decisions are reviewed "
1063
+ "by human moderators. If you have concerns, please contact the mod team."
1064
+ )
1065
+
1066
+ # Disclosure footer (always appended)
1067
+ disclosure_footer = (
1068
+ "\n\n---\n"
1069
+ "*I am CIRIS, an AI moderation assistant. "
1070
+ "[Learn more](https://ciris.ai) | [Report issues](https://ciris.ai/report)*"
1071
+ )
1072
+
1073
+ comment_text = (request.custom_message or default_message) + disclosure_footer
1074
+
1075
+ try:
1076
+ # Parse channel reference
1077
+ reference = RedditChannelReference.parse(request.channel_reference)
1078
+
1079
+ # Determine submission ID for comment
1080
+ submission_id = reference.submission_id
1081
+ if not submission_id:
1082
+ return self._api_error_result(
1083
+ "reddit_disclose_identity", correlation_id, "Disclosure requires submission ID in channel reference"
1084
+ )
1085
+
1086
+ # Post disclosure as comment
1087
+ comment_request = RedditSubmitCommentRequest(
1088
+ parent_fullname=f"t3_{submission_id}",
1089
+ text=comment_text,
1090
+ lock_thread=False,
1091
+ )
1092
+
1093
+ comment_result = await self._client.submit_comment(comment_request)
1094
+
1095
+ logger.info(f"Posted AI disclosure to {request.channel_reference}")
1096
+
1097
+ except Exception as exc:
1098
+ return self._api_error_result("reddit_disclose_identity", correlation_id, str(exc))
1099
+
1100
+ return ToolExecutionResult(
1101
+ tool_name="reddit_disclose_identity",
1102
+ status=ToolExecutionStatus.COMPLETED,
1103
+ success=True,
1104
+ data=comment_result.model_dump(mode="json"),
1105
+ error=None,
1106
+ correlation_id=correlation_id,
1107
+ )
1108
+
1109
+ def get_deletion_status(self, content_id: str) -> Optional[RedditDeletionStatus]:
1110
+ """
1111
+ Get deletion status for content (DSAR pattern).
1112
+
1113
+ Args:
1114
+ content_id: Reddit content ID (t3_xxxxx or t1_xxxxx)
1115
+
1116
+ Returns:
1117
+ Deletion status if tracked, None otherwise
1118
+ """
1119
+ return self._deletion_statuses.get(content_id)
1120
+
1121
+ # Observation helpers ---------------------------------------------------
1122
+ async def _active_observe(self, reference: RedditChannelReference, *, limit: int) -> RedditTimelineResponse:
1123
+ if reference.target is RedditChannelType.USER and reference.username:
1124
+ return await self._client.fetch_user_activity(reference.username, limit=limit)
1125
+
1126
+ if reference.target is RedditChannelType.SUBREDDIT and reference.subreddit:
1127
+ entries = await self._client.fetch_subreddit_new(reference.subreddit, limit=limit)
1128
+ return RedditTimelineResponse(entries=entries)
1129
+
1130
+ if reference.target in {RedditChannelType.SUBMISSION, RedditChannelType.COMMENT}:
1131
+ submission_id = reference.submission_id
1132
+ if not submission_id:
1133
+ raise RuntimeError("Submission ID required for observation")
1134
+ comments = await self._client.fetch_submission_comments(submission_id, limit=limit)
1135
+ entries = [
1136
+ RedditTimelineEntry(
1137
+ entry_type="comment",
1138
+ item_id=comment.comment_id,
1139
+ fullname=comment.fullname,
1140
+ subreddit=comment.subreddit,
1141
+ permalink=comment.permalink,
1142
+ score=comment.score,
1143
+ created_at=comment.created_at,
1144
+ channel_reference=comment.channel_reference,
1145
+ author=comment.author,
1146
+ body=comment.body,
1147
+ )
1148
+ for comment in comments
1149
+ ]
1150
+ return RedditTimelineResponse(entries=entries)
1151
+
1152
+ raise RuntimeError(f"Unsupported observation target: {reference.target.value}")
1153
+
1154
+ # Shared helpers --------------------------------------------------------
1155
+ def get_capabilities(self) -> ServiceCapabilities:
1156
+ return ServiceCapabilities(
1157
+ service_name=self.service_name,
1158
+ actions=self._get_actions(),
1159
+ version="1.0.0",
1160
+ dependencies=list(self._dependencies),
1161
+ metadata={"provider": "reddit", "channel_format": "reddit:r/<sub>:post/<id>:comment/<id>"},
1162
+ )
1163
+
1164
+ def _validation_error_result(
1165
+ self, tool_name: str, correlation_id: str, error: ValidationError
1166
+ ) -> ToolExecutionResult:
1167
+ return ToolExecutionResult(
1168
+ tool_name=tool_name,
1169
+ status=ToolExecutionStatus.FAILED,
1170
+ success=False,
1171
+ data=None,
1172
+ error=str(error),
1173
+ correlation_id=correlation_id,
1174
+ )
1175
+
1176
+ def _api_error_result(self, tool_name: str, correlation_id: str, message: str) -> ToolExecutionResult:
1177
+ return ToolExecutionResult(
1178
+ tool_name=tool_name,
1179
+ status=ToolExecutionStatus.FAILED,
1180
+ success=False,
1181
+ data=None,
1182
+ error=message,
1183
+ correlation_id=correlation_id,
1184
+ )
1185
+
1186
+ def _schema_to_param_schema(self, json_schema: JSONDict) -> ToolParameterSchema:
1187
+ """Convert a Pydantic JSON schema to ToolParameterSchema format."""
1188
+ return ToolParameterSchema(
1189
+ type=json_schema.get("type", "object"),
1190
+ properties=json_schema.get("properties", {}),
1191
+ required=json_schema.get("required", []),
1192
+ )
1193
+
1194
+ def _build_tool_schemas(self) -> Dict[str, ToolParameterSchema]:
1195
+ return {
1196
+ "reddit_get_user_context": self._schema_to_param_schema(RedditUserContextRequest.model_json_schema()),
1197
+ "reddit_submit_post": self._schema_to_param_schema(RedditSubmitPostRequest.model_json_schema()),
1198
+ "reddit_submit_comment": self._schema_to_param_schema(RedditSubmitCommentRequest.model_json_schema()),
1199
+ "reddit_remove_content": self._schema_to_param_schema(RedditRemoveContentRequest.model_json_schema()),
1200
+ "reddit_get_submission": self._schema_to_param_schema(RedditGetSubmissionRequest.model_json_schema()),
1201
+ "reddit_delete_content": self._schema_to_param_schema(RedditDeleteContentRequest.model_json_schema()),
1202
+ "reddit_disclose_identity": self._schema_to_param_schema(RedditDisclosureRequest.model_json_schema()),
1203
+ "reddit_observe": ToolParameterSchema(
1204
+ type="object",
1205
+ properties={
1206
+ "channel_reference": {"type": "string"},
1207
+ "limit": {"type": "integer", "minimum": 1, "maximum": 100},
1208
+ },
1209
+ required=["channel_reference"],
1210
+ ),
1211
+ }
1212
+
1213
+ def _build_tool_info(self) -> Dict[str, ToolInfo]:
1214
+ tool_descriptions = {
1215
+ "reddit_get_user_context": "Fetch metadata and recent activity for a Reddit user",
1216
+ "reddit_submit_post": "Submit a markdown self-post to the configured subreddit",
1217
+ "reddit_submit_comment": "Reply to a submission or comment",
1218
+ "reddit_remove_content": "Remove a submission or comment",
1219
+ "reddit_get_submission": "Fetch metadata for a submission",
1220
+ "reddit_delete_content": "Permanently delete content from Reddit (ToS compliance)",
1221
+ "reddit_disclose_identity": "Post AI transparency disclosure (community guidelines compliance)",
1222
+ "reddit_observe": "Fetch passive observation data for a subreddit, submission, comment, or user",
1223
+ }
1224
+ return {
1225
+ name: ToolInfo(name=name, description=tool_descriptions.get(name, ""), parameters=schema)
1226
+ for name, schema in self._tool_schemas.items()
1227
+ }
1228
+
1229
+ def _extract_submission_id_from_permalink(self, permalink: str) -> Optional[str]:
1230
+ from urllib.parse import urlparse
1231
+
1232
+ parsed = urlparse(permalink)
1233
+ path_parts = [part for part in parsed.path.split("/") if part]
1234
+
1235
+ if not path_parts:
1236
+ return None
1237
+
1238
+ lowered_parts = [part.lower() for part in path_parts]
1239
+
1240
+ # Canonical reddit URLs follow /r/<sub>/comments/<id>/slug
1241
+ if "comments" in lowered_parts:
1242
+ idx = lowered_parts.index("comments")
1243
+ if idx + 1 < len(path_parts):
1244
+ return path_parts[idx + 1]
1245
+
1246
+ # Shortlinks use redd.it/<id> or /comments/<id>
1247
+ if lowered_parts[0] == "comments" and len(path_parts) > 1:
1248
+ return path_parts[1]
1249
+
1250
+ # redd.it shortlinks surface the id as the only path component
1251
+ if len(path_parts) == 1:
1252
+ return path_parts[0]
1253
+ return None
1254
+
1255
+
1256
+ # ----------------------------------------------------------------------
1257
+ # Communication service implementation
1258
+ # ----------------------------------------------------------------------
1259
+
1260
+
1261
+ class RedditCommunicationService(RedditServiceBase):
1262
+ """Communication service that lets CIRIS speak and fetch on Reddit."""
1263
+
1264
+ def __init__(
1265
+ self,
1266
+ credentials: Optional[RedditCredentials] = None,
1267
+ *,
1268
+ time_service: Optional[TimeServiceProtocol] = None,
1269
+ bus_manager: Optional[object] = None,
1270
+ memory_service: Optional[object] = None,
1271
+ agent_id: Optional[str] = None,
1272
+ filter_service: Optional[object] = None,
1273
+ secrets_service: Optional[object] = None,
1274
+ agent_occurrence_id: str = "default",
1275
+ ) -> None:
1276
+ super().__init__(credentials, time_service=time_service, service_name="RedditCommunicationService")
1277
+ self._home_channel: Optional[str] = None
1278
+ self._wakeup_submission_id: Optional[str] = None
1279
+ # Store runtime dependencies for observer creation
1280
+ self._bus_manager = bus_manager
1281
+ self._memory_service = memory_service
1282
+ self._agent_id = agent_id
1283
+ self._filter_service = filter_service
1284
+ self._secrets_service = secrets_service
1285
+ self._agent_occurrence_id = agent_occurrence_id
1286
+ self._observer: Optional[object] = None # RedditObserver instance
1287
+
1288
+ async def _on_start(self) -> None:
1289
+ await super()._on_start()
1290
+ await self._resolve_home_channel()
1291
+
1292
+ # Create and start Reddit observer if runtime dependencies are available
1293
+ # Note: agent_id is optional, observer will use "ciris" as fallback
1294
+ if self._bus_manager and self._memory_service:
1295
+ from .observer import RedditObserver
1296
+
1297
+ logger.info("Creating RedditObserver with runtime dependencies")
1298
+ self._observer = RedditObserver(
1299
+ credentials=self._credentials,
1300
+ subreddit=self._credentials.subreddit if self._credentials else None,
1301
+ bus_manager=self._bus_manager if isinstance(self._bus_manager, BusManager) else None,
1302
+ memory_service=self._memory_service,
1303
+ agent_id=self._agent_id,
1304
+ filter_service=self._filter_service,
1305
+ secrets_service=self._secrets_service if isinstance(self._secrets_service, SecretsService) else None,
1306
+ time_service=self._time_service,
1307
+ agent_occurrence_id=self._agent_occurrence_id,
1308
+ )
1309
+ await self._observer.start()
1310
+ logger.info(
1311
+ f"RedditObserver started and monitoring r/{self._credentials.subreddit if self._credentials else 'unknown'}"
1312
+ )
1313
+ else:
1314
+ logger.warning("RedditCommunicationService: Runtime dependencies not available, observer not started")
1315
+
1316
+ async def _on_stop(self) -> None:
1317
+ # Stop observer if it was started
1318
+ if self._observer and hasattr(self._observer, "stop"):
1319
+ await self._observer.stop()
1320
+ logger.info("RedditObserver stopped")
1321
+ await super()._on_stop()
1322
+
1323
+ def get_service_type(self) -> ServiceType:
1324
+ return ServiceType.COMMUNICATION
1325
+
1326
+ def _get_actions(self) -> List[str]:
1327
+ return ["send_message", "fetch_messages"]
1328
+
1329
+ async def send_message(self, channel_id: str, content: str) -> bool:
1330
+ reference = RedditChannelReference.parse(channel_id)
1331
+ if reference.target == RedditChannelType.SUBREDDIT:
1332
+ raise RuntimeError(
1333
+ "Cannot send plain messages to a subreddit without a submission context. "
1334
+ "Provide a submission or comment reference (e.g., reddit:r/ciris:post/<id>)."
1335
+ )
1336
+
1337
+ if reference.target == RedditChannelType.SUBMISSION:
1338
+ parent_fullname = f"t3_{reference.submission_id}"
1339
+ elif reference.target == RedditChannelType.COMMENT:
1340
+ parent_fullname = f"t1_{reference.comment_id}"
1341
+ else:
1342
+ raise RuntimeError(f"Unsupported channel target for send_message: {reference.target.value}")
1343
+
1344
+ request = RedditSubmitCommentRequest(parent_fullname=parent_fullname, text=content)
1345
+ await self._client.submit_comment(request)
1346
+ return True
1347
+
1348
+ async def fetch_messages(
1349
+ self,
1350
+ channel_id: str,
1351
+ *,
1352
+ limit: int = 50,
1353
+ before: Optional[datetime] = None,
1354
+ ) -> List[FetchedMessage]:
1355
+ del before # Reddit does not support before filters in this implementation
1356
+ reference = RedditChannelReference.parse(channel_id)
1357
+ messages: List[FetchedMessage] = []
1358
+
1359
+ if reference.target == RedditChannelType.SUBREDDIT and reference.subreddit:
1360
+ entries = await self._client.fetch_subreddit_new(reference.subreddit, limit=limit)
1361
+ for entry in entries:
1362
+ messages.append(
1363
+ FetchedMessage(
1364
+ id=entry.item_id,
1365
+ content=entry.title or entry.body,
1366
+ author_name=entry.author,
1367
+ author_id=entry.author,
1368
+ timestamp=entry.created_at.isoformat(),
1369
+ channel_reference=entry.channel_reference,
1370
+ permalink=entry.permalink,
1371
+ )
1372
+ )
1373
+ return messages
1374
+
1375
+ if reference.target == RedditChannelType.SUBMISSION and reference.submission_id:
1376
+ comments = await self._client.fetch_submission_comments(reference.submission_id, limit=limit)
1377
+ for comment in comments:
1378
+ messages.append(
1379
+ FetchedMessage(
1380
+ id=comment.comment_id,
1381
+ content=comment.body,
1382
+ author_name=comment.author,
1383
+ author_id=comment.author,
1384
+ timestamp=comment.created_at.isoformat(),
1385
+ channel_reference=comment.channel_reference,
1386
+ permalink=comment.permalink,
1387
+ )
1388
+ )
1389
+ return messages
1390
+
1391
+ if reference.target == RedditChannelType.COMMENT and reference.comment_id:
1392
+ if not reference.submission_id:
1393
+ raise RuntimeError("Comment references must include a submission id")
1394
+ # Fetch the comment itself
1395
+ summary = await self._client.fetch_submission_comments(reference.submission_id, limit=limit)
1396
+ for comment in summary:
1397
+ if comment.comment_id == reference.comment_id:
1398
+ messages.append(
1399
+ FetchedMessage(
1400
+ id=comment.comment_id,
1401
+ content=comment.body,
1402
+ author_name=comment.author,
1403
+ author_id=comment.author,
1404
+ timestamp=comment.created_at.isoformat(),
1405
+ channel_reference=comment.channel_reference,
1406
+ permalink=comment.permalink,
1407
+ )
1408
+ )
1409
+ break
1410
+ return messages
1411
+
1412
+ if reference.target == RedditChannelType.USER and reference.username:
1413
+ timeline = await self._client.fetch_user_activity(reference.username, limit=limit)
1414
+ for entry in timeline.entries:
1415
+ messages.append(
1416
+ FetchedMessage(
1417
+ id=entry.item_id,
1418
+ content=entry.title or entry.body,
1419
+ author_name=entry.author,
1420
+ author_id=entry.author,
1421
+ timestamp=entry.created_at.isoformat(),
1422
+ channel_reference=entry.channel_reference,
1423
+ permalink=entry.permalink,
1424
+ )
1425
+ )
1426
+ return messages
1427
+
1428
+ raise RuntimeError(f"Unsupported channel reference for fetch_messages: {channel_id}")
1429
+
1430
+ def get_home_channel_id(self) -> Optional[str]:
1431
+ if self._home_channel:
1432
+ return self._home_channel
1433
+ return _build_channel_reference(self._subreddit)
1434
+
1435
+ async def _resolve_home_channel(self) -> None:
1436
+ """Resolve the WAKEUP submission used for default Reddit messaging."""
1437
+
1438
+ subreddit = self._subreddit
1439
+ try:
1440
+ entries = await self._client.fetch_subreddit_new(subreddit, limit=25)
1441
+ except Exception as exc: # pragma: no cover - defensive logging
1442
+ logger.warning(
1443
+ "RedditCommunicationService: failed to resolve WAKEUP submission for r/%s: %s",
1444
+ subreddit,
1445
+ exc,
1446
+ )
1447
+ self._home_channel = _build_channel_reference(subreddit)
1448
+ return
1449
+
1450
+ for entry in entries:
1451
+ if entry.entry_type != "submission":
1452
+ continue
1453
+ title = entry.title or ""
1454
+ body = entry.body or ""
1455
+ if "wakeup" in title.lower() or "wakeup" in body.lower():
1456
+ self._wakeup_submission_id = entry.item_id
1457
+ self._home_channel = _build_channel_reference(subreddit, submission_id=entry.item_id)
1458
+ logger.info(
1459
+ "RedditCommunicationService: resolved WAKEUP submission %s as default home channel",
1460
+ entry.item_id,
1461
+ )
1462
+ return
1463
+
1464
+ self._home_channel = _build_channel_reference(subreddit)
1465
+ logger.info(
1466
+ "RedditCommunicationService: no WAKEUP submission detected in r/%s; defaulting to subreddit channel",
1467
+ subreddit,
1468
+ )
1469
+
1470
+
1471
+ __all__ = ["RedditToolService", "RedditCommunicationService"]