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/telemetry.py ADDED
@@ -0,0 +1,145 @@
1
+ """Sidecar telemetry store for temporal health monitoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import os
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # v0.21.0 — persisted so last_*_at survive a restart (previously in-memory only,
17
+ # which reset silence/last-outreach reasoning to null on every restart).
18
+ _PERSIST_KEYS = (
19
+ "started_at", "last_sync_at", "last_tick_at",
20
+ "last_initiative_at", "last_prefetch_at", "last_agent_outreach_at",
21
+ )
22
+
23
+
24
+ def _telemetry_path() -> Path:
25
+ return Path(os.environ.get("COLONY_STATE_DIR", os.path.expanduser("~/.colony"))) / "telemetry.json"
26
+
27
+
28
+ @dataclass
29
+ class TelemetryStore:
30
+ started_at: Optional[datetime] = None
31
+ last_sync_at: Optional[datetime] = None
32
+ last_tick_at: Optional[datetime] = None
33
+ last_initiative_at: Optional[datetime] = None
34
+ last_prefetch_at: Optional[datetime] = None
35
+ last_agent_outreach_at: Optional[datetime] = None
36
+ #: "ok" normally; "unknown" when persisted state could not be restored
37
+ #: (corrupt/unreadable file) — the reset must be visible, never a
38
+ #: silently fresh-looking store.
39
+ state: str = "ok"
40
+ _lock: asyncio.Lock = field(default_factory=asyncio.Lock)
41
+
42
+ def load(self) -> None:
43
+ """Restore persisted timestamps (except started_at) across restart."""
44
+ path = _telemetry_path()
45
+ if not path.exists():
46
+ return # first boot — nothing persisted yet
47
+ try:
48
+ data = json.loads(path.read_text())
49
+ except Exception as exc:
50
+ # A corrupt file silently resetting every timestamp to None used
51
+ # to produce a permanently unflaggable "ok". Make the reset loud.
52
+ logger.warning(
53
+ "telemetry state at %s is unreadable (%s) — timestamps reset; "
54
+ "temporal state is UNKNOWN until subsystems report again",
55
+ path, exc,
56
+ )
57
+ self.state = "unknown"
58
+ return
59
+ for key in _PERSIST_KEYS:
60
+ if key == "started_at":
61
+ continue # started_at is set fresh per process
62
+ val = data.get(key)
63
+ if val:
64
+ try:
65
+ setattr(self, key, datetime.fromisoformat(val))
66
+ except (ValueError, TypeError):
67
+ pass
68
+
69
+ def _persist(self) -> None:
70
+ try:
71
+ p = _telemetry_path()
72
+ p.parent.mkdir(parents=True, exist_ok=True)
73
+ data = {
74
+ k: (getattr(self, k).isoformat() if getattr(self, k) else None)
75
+ for k in _PERSIST_KEYS
76
+ }
77
+ tmp = p.with_suffix(".json.tmp")
78
+ tmp.write_text(json.dumps(data))
79
+ os.replace(tmp, p)
80
+ except Exception as exc: # pragma: no cover
81
+ logger.debug("telemetry persist failed: %s", exc)
82
+
83
+ async def touch(self, key: str) -> None:
84
+ async with self._lock:
85
+ setattr(self, key, datetime.now(timezone.utc))
86
+ self._persist()
87
+
88
+ async def silence_hours(self, key: str) -> Optional[float]:
89
+ """Return hours since the last event of the given type."""
90
+ attr_map = {
91
+ "sync": "last_sync_at",
92
+ "tick": "last_tick_at",
93
+ "initiative": "last_initiative_at",
94
+ "prefetch": "last_prefetch_at",
95
+ }
96
+ attr = attr_map.get(key, key)
97
+ async with self._lock:
98
+ ts = getattr(self, attr, None)
99
+ if ts is None:
100
+ return None
101
+ return (datetime.now(timezone.utc) - ts).total_seconds() / 3600
102
+
103
+ async def stale_flags(self, thresholds: Dict[str, float]) -> List[str]:
104
+ flags = []
105
+ for key, threshold in thresholds.items():
106
+ silence = await self.silence_hours(key)
107
+ if silence is None:
108
+ # Never recorded. A subsystem that has not run once since
109
+ # process start, past its own staleness threshold, is stale —
110
+ # "never ran" must flag, not read as silence/ok.
111
+ async with self._lock:
112
+ started = self.started_at
113
+ if started is not None:
114
+ uptime_h = (
115
+ datetime.now(timezone.utc) - started
116
+ ).total_seconds() / 3600
117
+ if uptime_h > threshold:
118
+ flags.append(f"{key}:never_ran")
119
+ continue
120
+ if silence > threshold:
121
+ flags.append(key)
122
+ return flags
123
+
124
+ async def to_dict(self, thresholds: Dict[str, float]) -> dict:
125
+ started = self.started_at.isoformat() if self.started_at else None
126
+ sync_at = self.last_sync_at.isoformat() if self.last_sync_at else None
127
+ tick_at = self.last_tick_at.isoformat() if self.last_tick_at else None
128
+ init_at = self.last_initiative_at.isoformat() if self.last_initiative_at else None
129
+ prefetch_at = self.last_prefetch_at.isoformat() if self.last_prefetch_at else None
130
+ outreach_at = self.last_agent_outreach_at.isoformat() if self.last_agent_outreach_at else None
131
+ silence = {}
132
+ for key in thresholds:
133
+ silence[key] = await self.silence_hours(key)
134
+ flags = await self.stale_flags(thresholds)
135
+ return {
136
+ "started_at": started,
137
+ "last_sync_at": sync_at,
138
+ "last_tick_at": tick_at,
139
+ "last_initiative_at": init_at,
140
+ "last_prefetch_at": prefetch_at,
141
+ "last_agent_outreach_at": outreach_at,
142
+ "silence_hours": silence,
143
+ "stale_flags": flags,
144
+ "state": self.state,
145
+ }
apsimo/tom/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Theory of Mind — social cognition for agents."""
2
+
3
+ from apsimo.tom.affect import AffectStore
4
+ from apsimo.tom.facts import SharedFactsStore
5
+
6
+ __all__ = ["AffectStore", "SharedFactsStore"]
apsimo/tom/affect.py ADDED
@@ -0,0 +1,387 @@
1
+ """Affect Tracker — per-contact emotional valence and arousal over time.
2
+
3
+ Stores discrete affect events and maintains a computed current state per
4
+ contact with exponential decay toward neutral.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import sqlite3
12
+ import uuid
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Default decay: 5% per hour toward neutral.
19
+ DEFAULT_DECAY_FACTOR = 0.95
20
+ DEFAULT_AROUSAL_BASELINE = 0.3
21
+ DEFAULT_SUSTAINED_DECLINE_MAGNITUDE = 0.3
22
+ _DECAY_ONLY_SOURCES = frozenset({"decay"})
23
+
24
+
25
+ from .source_lineage import SourceLinkedStore
26
+
27
+
28
+ class AffectStore(SourceLinkedStore):
29
+ """SQLite-backed affect event store with computed state."""
30
+
31
+ def __init__(self, db_path: str, *, decay_factor: float = DEFAULT_DECAY_FACTOR, source_ledger=None) -> None:
32
+ self._db_path = db_path
33
+ self._source_ledger = source_ledger
34
+ self._decay_factor = decay_factor
35
+ self._conn = sqlite3.connect(db_path)
36
+ self._conn.row_factory = sqlite3.Row
37
+ self._conn.execute("PRAGMA journal_mode=WAL")
38
+ self._conn.execute("PRAGMA foreign_keys=ON")
39
+ self._create_tables()
40
+
41
+ def _create_tables(self) -> None:
42
+ self._conn.executescript("""
43
+ CREATE TABLE IF NOT EXISTS affect_events (
44
+ id TEXT PRIMARY KEY,
45
+ contact_id TEXT NOT NULL,
46
+ valence REAL NOT NULL,
47
+ arousal REAL NOT NULL DEFAULT 0.5,
48
+ source TEXT NOT NULL,
49
+ trigger TEXT,
50
+ timestamp TEXT NOT NULL,
51
+ session_id TEXT
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_affect_contact
55
+ ON affect_events(contact_id);
56
+ CREATE INDEX IF NOT EXISTS idx_affect_timestamp
57
+ ON affect_events(timestamp);
58
+
59
+ CREATE TABLE IF NOT EXISTS affect_state (
60
+ contact_id TEXT PRIMARY KEY,
61
+ current_valence REAL NOT NULL DEFAULT 0.0,
62
+ current_arousal REAL NOT NULL DEFAULT 0.3,
63
+ trend TEXT NOT NULL DEFAULT 'stable',
64
+ last_event_id TEXT,
65
+ last_updated TEXT NOT NULL,
66
+ event_count INTEGER NOT NULL DEFAULT 0
67
+ );
68
+ """)
69
+ if 'source_lineage_json' not in {row[1] for row in self._conn.execute('PRAGMA table_info(affect_events)')}:
70
+ self._conn.execute('ALTER TABLE affect_events ADD COLUMN source_lineage_json TEXT')
71
+ self._conn.commit()
72
+
73
+ def purge_erased_sources(self, turn_ids=None, *, contact_id=None) -> int:
74
+ sql = 'SELECT id,contact_id,source_lineage_json FROM affect_events WHERE source_lineage_json IS NOT NULL'
75
+ rows = self._conn.execute(sql + (' AND contact_id=?' if contact_id else ''),
76
+ (contact_id,) if contact_id else ()).fetchall()
77
+ invalid = self._invalid_sources(rows, turn_ids)
78
+ with self._conn:
79
+ self._conn.executemany('DELETE FROM affect_events WHERE id=?', [(row['id'],) for row in invalid])
80
+ for person in {row['contact_id'] for row in invalid}:
81
+ self._recompute_state(person, commit=False)
82
+ return len(invalid)
83
+
84
+ @staticmethod
85
+ def _decode(row):
86
+ event = dict(row)
87
+ raw = event.pop('source_lineage_json', None)
88
+ event['source_lineage'] = json.loads(raw) if raw else None
89
+ event['evidence_basis'] = 'canonical_source' if raw else 'unlinked_observation'
90
+ return event
91
+
92
+ # ------------------------------------------------------------------
93
+ # Event CRUD
94
+ # ------------------------------------------------------------------
95
+
96
+ def create_event(
97
+ self,
98
+ *,
99
+ contact_id: str,
100
+ valence: float,
101
+ arousal: float = 0.5,
102
+ source: str = "explicit",
103
+ trigger: Optional[str] = None,
104
+ session_id: Optional[str] = None,
105
+ timestamp: Optional[str] = None,
106
+ source_lineage: Optional[Dict[str, Any]] = None,
107
+ ) -> Dict[str, Any]:
108
+ """Record an affect event and update the contact's computed state.
109
+
110
+ Returns the created event dict.
111
+ """
112
+ if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
113
+ from apsimo.turns.idempotency import SourceErased
114
+ raise SourceErased('source_erased')
115
+ # Clamp valence and arousal to valid ranges.
116
+ valence = max(-1.0, min(1.0, valence))
117
+ arousal = max(0.0, min(1.0, arousal))
118
+
119
+ event_id = str(uuid.uuid4())
120
+ ts = timestamp or datetime.now(timezone.utc).isoformat()
121
+
122
+ with self._conn:
123
+ self._conn.execute(
124
+ """INSERT INTO affect_events (id, contact_id, valence, arousal, source, trigger, timestamp, session_id, source_lineage_json)
125
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
126
+ (event_id, contact_id, valence, arousal, source, trigger, ts, session_id,
127
+ json.dumps(source_lineage) if source_lineage is not None else None),
128
+ )
129
+ self._recompute_state(contact_id, commit=False)
130
+
131
+ event = self.get_event(event_id)
132
+ if event is None:
133
+ from apsimo.turns.idempotency import SourceErased
134
+ raise SourceErased('source_erased')
135
+ return event
136
+
137
+ def get_event(self, event_id: str) -> Optional[Dict[str, Any]]:
138
+ owner = self._conn.execute('SELECT contact_id FROM affect_events WHERE id=?', (event_id,)).fetchone()
139
+ if owner is None:
140
+ return None
141
+ self.purge_erased_sources(contact_id=owner['contact_id'])
142
+ row = self._conn.execute(
143
+ "SELECT * FROM affect_events WHERE id = ?", (event_id,)
144
+ ).fetchone()
145
+ if row is None:
146
+ return None
147
+ return self._decode(row)
148
+
149
+ def list_events(
150
+ self,
151
+ *,
152
+ contact_id: Optional[str] = None,
153
+ source: Optional[str] = None,
154
+ limit: int = 50,
155
+ offset: int = 0,
156
+ ) -> List[Dict[str, Any]]:
157
+ """List affect events with optional filters."""
158
+ self.purge_erased_sources(contact_id=contact_id)
159
+ clauses: List[str] = []
160
+ params: List[Any] = []
161
+
162
+ if contact_id is not None:
163
+ clauses.append("contact_id = ?")
164
+ params.append(contact_id)
165
+ if source is not None:
166
+ clauses.append("source = ?")
167
+ params.append(source)
168
+
169
+ where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
170
+ rows = self._conn.execute(
171
+ f"SELECT * FROM affect_events{where} ORDER BY timestamp DESC LIMIT ? OFFSET ?",
172
+ params + [limit, offset],
173
+ ).fetchall()
174
+ return [self._decode(r) for r in rows]
175
+
176
+ def count_events(
177
+ self,
178
+ *,
179
+ contact_id: Optional[str] = None,
180
+ source: Optional[str] = None,
181
+ ) -> int:
182
+ """Total matching events, so paginated views can report a real total."""
183
+ self.purge_erased_sources(contact_id=contact_id)
184
+ clauses: List[str] = []
185
+ params: List[Any] = []
186
+ if contact_id is not None:
187
+ clauses.append("contact_id = ?")
188
+ params.append(contact_id)
189
+ if source is not None:
190
+ clauses.append("source = ?")
191
+ params.append(source)
192
+ where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
193
+ return int(self._conn.execute(
194
+ f"SELECT COUNT(*) FROM affect_events{where}", params
195
+ ).fetchone()[0])
196
+
197
+ def delete_event(self, event_id: str) -> bool:
198
+ """Delete an affect event and recompute state. Returns True if deleted."""
199
+ event = self.get_event(event_id)
200
+ if event is None:
201
+ return False
202
+ self._conn.execute("DELETE FROM affect_events WHERE id = ?", (event_id,))
203
+ self._conn.commit()
204
+ self._recompute_state(event["contact_id"])
205
+ return True
206
+
207
+ # ------------------------------------------------------------------
208
+ # Computed state
209
+ # ------------------------------------------------------------------
210
+
211
+ def get_state(self, contact_id: str) -> Dict[str, Any]:
212
+ """Get the current affect state for a contact.
213
+
214
+ Applies time-based decay before returning.
215
+ """
216
+ self.purge_erased_sources(contact_id=contact_id)
217
+ self._apply_decay(contact_id)
218
+ row = self._conn.execute(
219
+ "SELECT * FROM affect_state WHERE contact_id = ?", (contact_id,)
220
+ ).fetchone()
221
+ if row is None:
222
+ return {
223
+ "contact_id": contact_id,
224
+ "current_valence": 0.0,
225
+ "current_arousal": DEFAULT_AROUSAL_BASELINE,
226
+ "trend": "stable",
227
+ "last_event_id": None,
228
+ "last_updated": None,
229
+ "event_count": 0,
230
+ }
231
+ return dict(row)
232
+
233
+ def get_all_states(self) -> List[Dict[str, Any]]:
234
+ """Get affect states for all contacts with recorded events."""
235
+ self.purge_erased_sources()
236
+ rows = self._conn.execute("SELECT * FROM affect_state").fetchall()
237
+ results = []
238
+ for row in rows:
239
+ self._apply_decay(row["contact_id"])
240
+ results.append(self.get_state(row["contact_id"]))
241
+ return results
242
+
243
+ def detect_negative_spike(self, contact_id: str, threshold: float = -0.5) -> bool:
244
+ """Check if the most recent event is a negative spike."""
245
+ self.purge_erased_sources(contact_id=contact_id)
246
+ row = self._conn.execute(
247
+ "SELECT valence FROM affect_events WHERE contact_id = ? ORDER BY timestamp DESC LIMIT 1",
248
+ (contact_id,),
249
+ ).fetchone()
250
+ return row is not None and row["valence"] <= threshold
251
+
252
+ def detect_sustained_decline(
253
+ self,
254
+ contact_id: str,
255
+ min_events: int = 3,
256
+ min_magnitude: float = DEFAULT_SUSTAINED_DECLINE_MAGNITUDE,
257
+ ) -> bool:
258
+ """Check for a material decline supported by observed affect events.
259
+
260
+ Time decay changes the current state toward neutral but is not new
261
+ evidence about a contact. Recompute the trend from non-decay events
262
+ here so a stale/decayed state cannot trigger proactive initiative.
263
+ """
264
+ state = self.get_state(contact_id)
265
+ if state["current_valence"] > -abs(min_magnitude):
266
+ return False
267
+
268
+ rows = self._conn.execute(
269
+ "SELECT * FROM affect_events WHERE contact_id = ? ORDER BY timestamp ASC",
270
+ (contact_id,),
271
+ ).fetchall()
272
+ observed = [row for row in rows if not self._is_decay_only(row)]
273
+ return len(observed) >= min_events and self._event_trend(observed) == "declining"
274
+
275
+ # ------------------------------------------------------------------
276
+ # Internal
277
+ # ------------------------------------------------------------------
278
+
279
+ @staticmethod
280
+ def _is_decay_only(event: sqlite3.Row) -> bool:
281
+ return str(event["source"]).strip().lower() in _DECAY_ONLY_SOURCES
282
+
283
+ @staticmethod
284
+ def _event_trend(rows: List[sqlite3.Row]) -> str:
285
+ """Return the trend of the latest observed affect events."""
286
+ recent = rows[-5:] if len(rows) >= 5 else rows
287
+ if len(recent) < 2:
288
+ return "stable"
289
+
290
+ midpoint = len(recent) // 2
291
+ early_avg = sum(row["valence"] for row in recent[:midpoint]) / midpoint
292
+ late_avg = sum(row["valence"] for row in recent[midpoint:]) / (len(recent) - midpoint)
293
+ diff = late_avg - early_avg
294
+ if diff > 0.1:
295
+ return "improving"
296
+ if diff < -0.1:
297
+ return "declining"
298
+ return "stable"
299
+
300
+ def _apply_decay(self, contact_id: str) -> None:
301
+ """Apply exponential decay to a contact's affect state."""
302
+ row = self._conn.execute(
303
+ "SELECT * FROM affect_state WHERE contact_id = ?", (contact_id,)
304
+ ).fetchone()
305
+ if row is None or row["last_updated"] is None:
306
+ return
307
+
308
+ last_updated = datetime.fromisoformat(row["last_updated"])
309
+ now = datetime.now(timezone.utc)
310
+ hours_elapsed = (now - last_updated).total_seconds() / 3600.0
311
+
312
+ if hours_elapsed <= 0:
313
+ return
314
+
315
+ decayed_valence = row["current_valence"] * (self._decay_factor ** hours_elapsed)
316
+ decayed_arousal = DEFAULT_AROUSAL_BASELINE + (row["current_arousal"] - DEFAULT_AROUSAL_BASELINE) * (self._decay_factor ** hours_elapsed)
317
+
318
+ # Round to avoid floating-point drift.
319
+ decayed_valence = round(decayed_valence, 4)
320
+ decayed_arousal = round(decayed_arousal, 4)
321
+
322
+ self._conn.execute(
323
+ """UPDATE affect_state SET current_valence = ?, current_arousal = ?, last_updated = ?
324
+ WHERE contact_id = ?""",
325
+ (decayed_valence, decayed_arousal, now.isoformat(), contact_id),
326
+ )
327
+ self._conn.commit()
328
+
329
+ def _recompute_state(self, contact_id: str, *, commit=True) -> None:
330
+ """Recompute the affect state from recent events.
331
+
332
+ Uses a weighted average with recency bias: more recent events
333
+ contribute more to the current state.
334
+ """
335
+ rows = self._conn.execute(
336
+ "SELECT * FROM affect_events WHERE contact_id = ? ORDER BY timestamp ASC",
337
+ (contact_id,),
338
+ ).fetchall()
339
+
340
+ if not rows:
341
+ self._conn.execute(
342
+ "DELETE FROM affect_state WHERE contact_id = ?", (contact_id,)
343
+ )
344
+ if commit:
345
+ self._conn.commit()
346
+ return
347
+
348
+ # Weighted average with exponential recency bias.
349
+ total_weight = 0.0
350
+ weighted_valence = 0.0
351
+ weighted_arousal = 0.0
352
+ weight = 1.0
353
+
354
+ for row in rows:
355
+ weighted_valence += row["valence"] * weight
356
+ weighted_arousal += row["arousal"] * weight
357
+ total_weight += weight
358
+ weight *= 0.9 # decay weight for older events
359
+
360
+ current_valence = round(weighted_valence / total_weight, 4)
361
+ current_arousal = round(weighted_arousal / total_weight, 4)
362
+
363
+ # Decay is a state projection, not a new observation. Keep it out of
364
+ # the event trend even if an imported/legacy row labels it explicitly.
365
+ observed = [row for row in rows if not self._is_decay_only(row)]
366
+ trend = self._event_trend(observed)
367
+
368
+ last_event = rows[-1]
369
+ now = datetime.now(timezone.utc).isoformat()
370
+
371
+ self._conn.execute(
372
+ """INSERT INTO affect_state (contact_id, current_valence, current_arousal, trend, last_event_id, last_updated, event_count)
373
+ VALUES (?, ?, ?, ?, ?, ?, ?)
374
+ ON CONFLICT(contact_id) DO UPDATE SET
375
+ current_valence = excluded.current_valence,
376
+ current_arousal = excluded.current_arousal,
377
+ trend = excluded.trend,
378
+ last_event_id = excluded.last_event_id,
379
+ last_updated = excluded.last_updated,
380
+ event_count = excluded.event_count""",
381
+ (contact_id, current_valence, current_arousal, trend, last_event["id"], now, len(rows)),
382
+ )
383
+ if commit:
384
+ self._conn.commit()
385
+
386
+ def close(self) -> None:
387
+ self._conn.close()