apsimo 1.3.0__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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
apsimo/autonomy/cli.py ADDED
@@ -0,0 +1,110 @@
1
+ """CLI entry point for Colony autonomy loop management.
2
+
3
+ Usage:
4
+ colony autonomy status # live loop stats from the running sidecar
5
+ colony autonomy cycle # wake the loop / run one cycle now
6
+
7
+ The loop itself lives inside the sidecar process and starts with it — there
8
+ is no standalone "start" mode (running a second loop against the same state
9
+ would double-fire every phase). `status` and `cycle` talk to the running
10
+ sidecar over HTTP, resolved the same way the doctor does (COLONY_URL /
11
+ COLONY_SIDECAR_HOST/PORT, COLONY_API_KEY).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import sys
19
+ import urllib.error
20
+ import urllib.request
21
+
22
+
23
+ def _request(path: str, method: str = "GET") -> dict:
24
+ from apsimo.doctor import default_colony_url
25
+
26
+ url = default_colony_url().rstrip("/") + path
27
+ key = os.environ.get("COLONY_API_KEY", "")
28
+ req = urllib.request.Request(
29
+ url,
30
+ method=method,
31
+ data=b"{}" if method == "POST" else None,
32
+ headers={
33
+ "Content-Type": "application/json",
34
+ **({"Authorization": f"Bearer {key}"} if key else {}),
35
+ },
36
+ )
37
+ with urllib.request.urlopen(req, timeout=10) as r:
38
+ return json.loads(r.read().decode())
39
+
40
+
41
+ def _print_config_defaults() -> None:
42
+ from apsimo.autonomy.config import AutonomyConfig
43
+
44
+ cfg = AutonomyConfig.from_env()
45
+ print(f" tick_interval_secs: {cfg.tick_interval_secs}")
46
+ print(f" initiative_confidence_threshold: {cfg.initiative_confidence_threshold}")
47
+ print(f" max_actions_per_hour: {cfg.max_actions_per_hour}")
48
+ print(f" quiet_hours: {cfg.quiet_hours_start} – {cfg.quiet_hours_end}")
49
+ print(f" anomaly_severity_threshold: {cfg.anomaly_severity_threshold}")
50
+ print(f" prediction_confidence_threshold: {cfg.prediction_confidence_threshold}")
51
+ print(f" goal_stale_threshold_hours: {cfg.goal_stale_threshold_hours}")
52
+
53
+
54
+ def run_autonomy_status(loop_instance=None) -> int:
55
+ """Print live autonomy status.
56
+
57
+ A provided loop_instance (in-process embedding) is used directly;
58
+ otherwise the running sidecar is queried over HTTP. Only when no sidecar
59
+ is reachable do we fall back to printing config defaults, clearly
60
+ labelled as such.
61
+ """
62
+ if loop_instance is not None:
63
+ print(json.dumps(loop_instance.status(), indent=2))
64
+ return 0
65
+
66
+ try:
67
+ status = _request("/v1/host/autonomy/status")
68
+ print(json.dumps(status, indent=2))
69
+ return 0
70
+ except (urllib.error.URLError, OSError, ValueError) as exc:
71
+ print(f"Autonomy Loop — sidecar unreachable ({exc}); env config defaults:")
72
+ _print_config_defaults()
73
+ return 1
74
+
75
+
76
+ def run_autonomy_cycle() -> int:
77
+ """Wake the running loop (proactive mode) or run one cycle (reactive)."""
78
+ try:
79
+ out = _request("/v1/host/autonomy/cycle", method="POST")
80
+ print(json.dumps(out, indent=2))
81
+ return 0
82
+ except (urllib.error.URLError, OSError, ValueError) as exc:
83
+ print(f"cycle failed: sidecar unreachable ({exc})", file=sys.stderr)
84
+ return 1
85
+
86
+
87
+ def run_autonomy_command(args: list) -> int:
88
+ """Dispatch 'colony autonomy <subcommand>'."""
89
+ if not args:
90
+ print("Usage: colony autonomy <status|cycle>", file=sys.stderr)
91
+ return 1
92
+
93
+ sub = args[0]
94
+
95
+ if sub == "status":
96
+ return run_autonomy_status()
97
+
98
+ if sub == "cycle":
99
+ return run_autonomy_cycle()
100
+
101
+ if sub == "start":
102
+ # The loop starts with the sidecar; a second standalone loop against
103
+ # the same state would double-fire every phase.
104
+ print("The autonomy loop runs inside the sidecar and starts with it "
105
+ "(colony serve). Use 'colony autonomy status' to inspect it or "
106
+ "'colony autonomy cycle' to wake it.", file=sys.stderr)
107
+ return 1
108
+
109
+ print(f"Unknown autonomy subcommand: {sub!r}", file=sys.stderr)
110
+ return 1
@@ -0,0 +1,437 @@
1
+ """ConditionWorker — poll external conditions on behalf of blocked goals.
2
+
3
+ Two consumption paths:
4
+ - The system-level checkers (commitment overdue flip, affect decline,
5
+ surprise accumulation) run hourly from the autonomy loop's
6
+ _phase_condition_checks — that is the LIVE path.
7
+ - handle_check_condition remains available as a task-queue handler for
8
+ per-goal external conditions (email reply, deployment health, ...), but no
9
+ producer schedules those jobs yet (see docs/KNOWN-GAPS.md); a goal blocked
10
+ on such a condition is only unblocked when something enqueues the check.
11
+
12
+ When a per-goal condition is met, the handler calls
13
+ goal_engine.unblock_goal(); when not yet met it returns condition_met=False
14
+ and the caller may reschedule.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from datetime import datetime, timezone, timedelta
21
+ from typing import Any, Dict, Optional
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Default polling intervals (seconds) by condition type
26
+ DEFAULT_INTERVALS: Dict[str, int] = {
27
+ "email_reply": 5 * 60, # 5 minutes
28
+ "deployment_health": 30, # 30 seconds
29
+ "delivery_status": 60 * 60, # 1 hour
30
+ "api_response": 60, # 1 minute
31
+ "custom": 5 * 60, # 5 minutes (fallback)
32
+ "commitment_overdue": 30 * 60, # 30 minutes
33
+ "affect_decline": 30 * 60, # 30 minutes
34
+ "surprise_accumulation": 30 * 60, # 30 minutes
35
+ }
36
+
37
+
38
+ async def handle_check_condition(
39
+ payload: Dict[str, Any],
40
+ goal_engine: Any,
41
+ ) -> Dict[str, Any]:
42
+ """Execute a single condition check and report to the goal engine.
43
+
44
+ Returns a result dict with:
45
+ - condition_met: bool
46
+ - goal_id: str
47
+ - details: dict (condition-specific)
48
+
49
+ If condition is not yet met, returns condition_met=False.
50
+ The loop's _phase_goals() picks up any newly unblocked goals on the
51
+ next tick.
52
+ """
53
+ goal_id = payload["goal_id"]
54
+ condition_type = payload["condition_type"]
55
+ condition_params = payload.get("condition_params", {})
56
+
57
+ condition_checkers = {
58
+ "email_reply": _check_email_reply,
59
+ "deployment_health": _check_deployment_health,
60
+ "delivery_status": _check_delivery_status,
61
+ "api_response": _check_api_response,
62
+ "commitment_overdue": _check_commitment_overdue,
63
+ "affect_decline": _check_affect_decline,
64
+ "surprise_accumulation": _check_surprise_accumulation,
65
+ }
66
+
67
+ checker = condition_checkers.get(condition_type)
68
+ if not checker:
69
+ logger.warning("Unknown condition type %r for goal %s", condition_type, goal_id)
70
+ return {"condition_met": False, "goal_id": goal_id, "details": {}}
71
+
72
+ try:
73
+ result = await checker(condition_params)
74
+ except NotImplementedError:
75
+ logger.debug("Condition checker %r not yet implemented", condition_type)
76
+ return {"condition_met": False, "goal_id": goal_id, "details": {}}
77
+ except Exception as exc:
78
+ logger.error(
79
+ "Condition check %r failed for goal %s: %s",
80
+ condition_type,
81
+ goal_id,
82
+ exc,
83
+ )
84
+ return {"condition_met": False, "goal_id": goal_id, "details": {"error": str(exc)}}
85
+
86
+ if result.get("condition_met"):
87
+ try:
88
+ goal_engine.unblock_goal(goal_id)
89
+ logger.info("Condition met for goal %s — unblocked", goal_id)
90
+ except Exception as exc:
91
+ logger.warning("Could not unblock goal %s: %s", goal_id, exc)
92
+
93
+ return {
94
+ "condition_met": result.get("condition_met", False),
95
+ "goal_id": goal_id,
96
+ "details": result,
97
+ }
98
+
99
+
100
+ def get_check_interval(condition_type: str, deadline: Optional[datetime] = None) -> int:
101
+ """Return appropriate polling interval in seconds.
102
+
103
+ If deadline is within 1 hour, returns 60s regardless of type.
104
+ """
105
+ if deadline is not None:
106
+ remaining = (deadline - datetime.now(timezone.utc)).total_seconds()
107
+ if remaining < 3600:
108
+ return 60
109
+
110
+ return DEFAULT_INTERVALS.get(condition_type, DEFAULT_INTERVALS["custom"])
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Condition checker implementations
115
+ # ---------------------------------------------------------------------------
116
+
117
+ async def _check_deployment_health(params: dict) -> dict:
118
+ """Check if a deployment endpoint returns the expected HTTP status.
119
+
120
+ params:
121
+ endpoint (str): HTTP URL to GET.
122
+ expected_status (int, default 200): Expected HTTP status code.
123
+ timeout_secs (float, default 10): Per-request timeout.
124
+
125
+ Returns:
126
+ {"condition_met": bool, "status_code": int, "latency_ms": float}
127
+ """
128
+ try:
129
+ import aiohttp
130
+ except ImportError as exc:
131
+ raise RuntimeError(
132
+ "_check_deployment_health requires 'aiohttp'. "
133
+ "Install with: pip install aiohttp"
134
+ ) from exc
135
+
136
+ import time
137
+
138
+ endpoint = params["endpoint"]
139
+ expected = params.get("expected_status", 200)
140
+ timeout = params.get("timeout_secs", 10.0)
141
+
142
+ t0 = time.monotonic()
143
+ async with aiohttp.ClientSession() as session:
144
+ async with session.get(
145
+ endpoint, timeout=aiohttp.ClientTimeout(total=timeout)
146
+ ) as resp:
147
+ latency_ms = (time.monotonic() - t0) * 1000
148
+ return {
149
+ "condition_met": resp.status == expected,
150
+ "status_code": resp.status,
151
+ "latency_ms": round(latency_ms, 2),
152
+ }
153
+
154
+
155
+ async def _check_api_response(params: dict) -> dict:
156
+ """Check if an API endpoint returns an expected field value.
157
+
158
+ params:
159
+ url (str): URL to request.
160
+ method (str, default "GET"): HTTP method.
161
+ headers (dict, optional): Request headers.
162
+ body (dict, optional): JSON body (for POST/PUT).
163
+ expected_field (str, optional): Dot-separated JSON path, e.g. "data.status".
164
+ expected_value (Any, optional): Value to compare against.
165
+ timeout_secs (float, default 30): Per-request timeout.
166
+
167
+ Returns:
168
+ {"condition_met": bool, "field_value": Any}
169
+ """
170
+ try:
171
+ import aiohttp
172
+ except ImportError as exc:
173
+ raise RuntimeError(
174
+ "_check_api_response requires 'aiohttp'. "
175
+ "Install with: pip install aiohttp"
176
+ ) from exc
177
+
178
+ method = params.get("method", "GET").upper()
179
+ url = params["url"]
180
+ headers = params.get("headers", {})
181
+ body = params.get("body")
182
+ expected_field = params.get("expected_field")
183
+ expected_value = params.get("expected_value")
184
+ timeout = params.get("timeout_secs", 30.0)
185
+
186
+ async with aiohttp.ClientSession() as session:
187
+ async with session.request(
188
+ method,
189
+ url,
190
+ headers=headers,
191
+ json=body,
192
+ timeout=aiohttp.ClientTimeout(total=timeout),
193
+ ) as resp:
194
+ data = await resp.json()
195
+
196
+ # Traverse dot-separated field path
197
+ field_value = data
198
+ if expected_field:
199
+ for key in expected_field.split("."):
200
+ if isinstance(field_value, dict):
201
+ field_value = field_value.get(key)
202
+ else:
203
+ field_value = None
204
+ break
205
+
206
+ return {
207
+ "condition_met": field_value == expected_value,
208
+ "field_value": field_value,
209
+ }
210
+
211
+
212
+ async def _check_email_reply(params: dict) -> dict:
213
+ """Check if a reply has arrived to a specific email thread via IMAP.
214
+
215
+ params:
216
+ thread_id (str, optional): Message-ID to match in In-Reply-To/References.
217
+ expected_sender (str, optional): Match on from address (substring).
218
+ subject_pattern (str, optional): Regex to match subject line.
219
+ since_iso (str, optional): ISO timestamp — only consider messages after this.
220
+
221
+ Credentials are loaded from SecretsManager:
222
+ COLONY_IMAP_HOST, COLONY_IMAP_PORT, COLONY_IMAP_USERNAME, COLONY_IMAP_PASSWORD
223
+
224
+ Returns:
225
+ {"condition_met": bool, "message_id": str|None, "from": str|None}
226
+ """
227
+ import re
228
+ from datetime import datetime
229
+
230
+ try:
231
+ from apsimo.secrets.manager import SecretsManager
232
+ from apsimo.email.providers import IMAPProvider
233
+ except ImportError:
234
+ logger.debug("email_reply check skipped — IMAP provider not installed")
235
+ return {
236
+ "condition_met": False,
237
+ "message_id": None,
238
+ "from": None,
239
+ "details": {"unavailable": "imap_provider_not_installed"},
240
+ }
241
+
242
+ sm = SecretsManager()
243
+ host = sm.get_required("COLONY_IMAP_HOST")
244
+ port = int(sm.get("COLONY_IMAP_PORT") or "993")
245
+ username = sm.get_required("COLONY_IMAP_USERNAME")
246
+ password = sm.get_required("COLONY_IMAP_PASSWORD")
247
+
248
+ since_str = params.get("since_iso")
249
+ since = datetime.fromisoformat(since_str) if since_str else None
250
+
251
+ provider = IMAPProvider(
252
+ host=host, port=port, username=username, password=password
253
+ )
254
+ entries = provider.fetch_recent(limit=50, since=since)
255
+
256
+ thread_id = params.get("thread_id", "")
257
+ expected_sender = params.get("expected_sender", "")
258
+ subject_pattern = params.get("subject_pattern")
259
+
260
+ for entry in entries:
261
+ if expected_sender and expected_sender.lower() not in entry.from_address.lower():
262
+ continue
263
+ if subject_pattern and not re.search(
264
+ subject_pattern, entry.subject or "", re.IGNORECASE
265
+ ):
266
+ continue
267
+ # thread_id match: check external_id (Message-ID) as a best-effort proxy
268
+ if thread_id and thread_id not in (entry.external_id or ""):
269
+ continue
270
+ return {
271
+ "condition_met": True,
272
+ "message_id": entry.external_id,
273
+ "from": entry.from_address,
274
+ }
275
+
276
+ return {"condition_met": False, "message_id": None, "from": None}
277
+
278
+
279
+ async def _check_delivery_status(params: dict) -> dict:
280
+ """Check carrier delivery status for a tracking number.
281
+
282
+ params:
283
+ tracking_number (str): Carrier tracking number.
284
+ carrier (str): "ups" | "fedex"
285
+
286
+ Carrier API keys are loaded from SecretsManager:
287
+ COLONY_UPS_API_KEY, COLONY_FEDEX_API_KEY
288
+
289
+ Returns:
290
+ {"condition_met": bool, "status": str, "last_update": str|None}
291
+ """
292
+ try:
293
+ from apsimo.secrets.manager import SecretsManager
294
+ except ImportError as exc:
295
+ raise RuntimeError(
296
+ "_check_delivery_status requires colony.secrets"
297
+ ) from exc
298
+
299
+ sm = SecretsManager()
300
+ carrier = params.get("carrier", "").lower()
301
+ tracking_number = params["tracking_number"]
302
+
303
+ CARRIER_CONFIGS: dict = {
304
+ "ups": {
305
+ "url": f"https://onlinetools.ups.com/track/v1/details/{tracking_number}",
306
+ "key_secret": "COLONY_UPS_API_KEY",
307
+ "delivered_statuses": {"D"},
308
+ "status_path": "trackResponse.shipment.0.activity.0.status.type",
309
+ },
310
+ "fedex": {
311
+ "url": "https://apis.fedex.com/track/v1/trackingnumbers",
312
+ "key_secret": "COLONY_FEDEX_API_KEY",
313
+ "delivered_statuses": {"DL"},
314
+ "status_path": (
315
+ "output.completeTrackResults.0.trackResults"
316
+ ".0.latestStatusDetail.code"
317
+ ),
318
+ },
319
+ }
320
+
321
+ config = CARRIER_CONFIGS.get(carrier)
322
+ if not config:
323
+ raise ValueError(
324
+ f"Unsupported carrier: {carrier!r}. "
325
+ f"Supported: {sorted(CARRIER_CONFIGS)}"
326
+ )
327
+
328
+ api_key = sm.get(config["key_secret"])
329
+ if not api_key:
330
+ raise KeyError(
331
+ f"Missing carrier API key secret: {config['key_secret']}"
332
+ )
333
+
334
+ result = await _check_api_response({
335
+ "url": config["url"],
336
+ "method": "GET",
337
+ "headers": {"Authorization": f"Bearer {api_key}"},
338
+ "expected_field": config["status_path"],
339
+ "expected_value": None,
340
+ "timeout_secs": 15.0,
341
+ })
342
+
343
+ status_code = result.get("field_value")
344
+ delivered = status_code in config["delivered_statuses"]
345
+ return {
346
+ "condition_met": delivered,
347
+ "status": str(status_code),
348
+ "last_update": None,
349
+ }
350
+
351
+
352
+ async def _check_commitment_overdue(params: dict) -> dict:
353
+ """Check for commitments that are past their due_at and still pending."""
354
+ from apsimo.api.routers.host import _commitment_store, emit
355
+
356
+ if _commitment_store is None:
357
+ return {"condition_met": False, "overdue_count": 0}
358
+
359
+ overdue = _commitment_store.get_overdue()
360
+ if not overdue:
361
+ return {"condition_met": False, "overdue_count": 0}
362
+
363
+ # Mark pending → overdue. get_overdue() also returns items already
364
+ # flipped, so guard on status or the overdue event re-fires every check.
365
+ for c in overdue:
366
+ if c.get("status") != "pending":
367
+ continue
368
+ try:
369
+ _commitment_store.update(c["id"], status="overdue")
370
+ emit("commitment.overdue", {
371
+ "commitment_id": c["id"],
372
+ "person_id": c["person_id"],
373
+ "description": c["description"],
374
+ })
375
+ except Exception:
376
+ logger.debug("commitment overdue update failed for %s", c["id"], exc_info=True)
377
+
378
+ return {
379
+ "condition_met": True,
380
+ "overdue_count": len(overdue),
381
+ "overdue_commitments": [
382
+ {"id": c["id"], "description": c["description"], "person_id": c["person_id"]}
383
+ for c in overdue[:5]
384
+ ],
385
+ }
386
+
387
+
388
+ async def _check_affect_decline(params: dict) -> dict:
389
+ """Check for sustained affect decline across contacts."""
390
+ from apsimo.api.routers.host import _affect_store
391
+
392
+ if _affect_store is None:
393
+ return {"condition_met": False, "declining_contacts": 0}
394
+
395
+ states = _affect_store.get_all_states()
396
+ declining = [s for s in states if _affect_store.detect_sustained_decline(s["contact_id"])]
397
+ if not declining:
398
+ return {"condition_met": False, "declining_contacts": 0}
399
+
400
+ try:
401
+ from apsimo.events.broadcaster import emit as _emit
402
+ for s in declining:
403
+ _emit("affect.sustained_decline", {
404
+ "contact_id": s["contact_id"],
405
+ "current_valence": s["current_valence"],
406
+ })
407
+ except Exception:
408
+ logger.debug("affect decline emit failed", exc_info=True)
409
+
410
+ return {
411
+ "condition_met": True,
412
+ "declining_contacts": len(declining),
413
+ "contacts": [{"contact_id": s["contact_id"], "valence": s["current_valence"]} for s in declining],
414
+ }
415
+
416
+
417
+ async def _check_surprise_accumulation(params: dict) -> dict:
418
+ """Check for accumulated unresolved surprises."""
419
+ from apsimo.api.routers.host import _surprise_store
420
+
421
+ if _surprise_store is None:
422
+ return {"condition_met": False, "unresolved_count": 0}
423
+
424
+ count = _surprise_store.count_unresolved(since_hours=1)
425
+ if count < 5:
426
+ return {"condition_met": False, "unresolved_count": count}
427
+
428
+ try:
429
+ from apsimo.events.broadcaster import emit as _emit
430
+ _emit("surprise.accumulation", {"unresolved_count": count})
431
+ except Exception:
432
+ logger.debug("surprise accumulation emit failed", exc_info=True)
433
+
434
+ return {
435
+ "condition_met": True,
436
+ "unresolved_count": count,
437
+ }