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
@@ -0,0 +1,131 @@
1
+ """Knowledge-asymmetry engine (tom2, H3.2) — daily, default OFF.
2
+
3
+ Compares the per-contact epistemic rows in SharedFactsStore and derives
4
+ second-order inferences:
5
+
6
+ * ``knows`` — a contact knows the facts recorded for them;
7
+ * ``unaware_of`` — a contact has no equivalent of a (confident) fact
8
+ another contact holds.
9
+
10
+ Fact TEXT is compared in memory only; what persists (live mode) are
11
+ Tom2Store rows holding fact IDS — the refs-not-content invariant is the
12
+ store's hard write-time pin, this engine simply never has a reason to
13
+ violate it.
14
+
15
+ Modes (COLONY_TOM2): off (default, engine is inert) | shadow (counts only,
16
+ writes nothing) | live (writes owner-only inference rows).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import re
24
+ from typing import Any, Dict, List
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # A cross-contact "unaware_of" is an inference, not an observation: born
29
+ # modest, and only derived from reasonably confident source facts.
30
+ _UNAWARE_CONFIDENCE = 0.4
31
+ _MIN_SOURCE_CONFIDENCE = 0.6
32
+
33
+
34
+ def tom2_mode() -> str:
35
+ from apsimo.util.autonomy_preset import resolve
36
+ return resolve("COLONY_TOM2", ("off", "shadow", "live"), "off")
37
+
38
+
39
+ def tom2_context_enabled() -> bool:
40
+ """COLONY_TOM2_CONTEXT (default 0, H3.3): may tom2 inferences be
41
+ injected into the OWNER'S assembled context? Injection is additionally
42
+ keyed to the owner's contact identity at assembly time — this flag can
43
+ never widen the audience, only turn the owner section on."""
44
+ return os.environ.get("COLONY_TOM2_CONTEXT", "0").strip().lower() in (
45
+ "1", "true", "yes", "on")
46
+
47
+
48
+ def _norm(text: Any) -> str:
49
+ return re.sub(r"\s+", " ", str(text or "").strip().lower())
50
+
51
+
52
+ class AsymmetryEngine:
53
+ def __init__(self, facts_store: Any, tom2_store: Any,
54
+ max_facts: int = 500,
55
+ max_rows_per_contact: int = 50) -> None:
56
+ self._facts = facts_store
57
+ self._tom2 = tom2_store
58
+ self._max_facts = max_facts
59
+ self._max_rows = max_rows_per_contact
60
+ self.last_report: Dict[str, Any] = {}
61
+
62
+ def run(self) -> Dict[str, Any]:
63
+ mode = tom2_mode()
64
+ report: Dict[str, Any] = {"mode": mode, "contacts": 0, "knows": 0,
65
+ "unaware_of": 0, "written": 0}
66
+ self.last_report = report
67
+ if mode == "off" or self._facts is None or self._tom2 is None:
68
+ return report
69
+
70
+ try:
71
+ facts: List[Dict[str, Any]] = self._facts.list_facts(
72
+ limit=self._max_facts).get("facts", [])
73
+ except Exception:
74
+ logger.warning("tom2 asymmetry: facts unavailable", exc_info=True)
75
+ return report
76
+
77
+ by_contact: Dict[str, List[Dict[str, Any]]] = {}
78
+ for f in facts:
79
+ cid = str(f.get("contact_id") or "").strip()
80
+ if cid and f.get("id"):
81
+ by_contact.setdefault(cid, []).append(f)
82
+ contacts = sorted(by_contact)
83
+ report["contacts"] = len(contacts)
84
+
85
+ for cid in contacts:
86
+ own = by_contact[cid]
87
+ own_texts = {_norm(f.get("fact")) for f in own}
88
+
89
+ # First-order grounding: a contact knows their own rows.
90
+ for f in own:
91
+ report["knows"] += 1
92
+ if mode == "live":
93
+ self._write(cid, "knows", f["id"],
94
+ float(f.get("confidence") or 0.5), report)
95
+
96
+ # Cross-contact asymmetry: confident facts held elsewhere with
97
+ # no textual equivalent in this contact's rows. The text match
98
+ # happens HERE, in memory; only ids are ever written.
99
+ written_rows = 0
100
+ for other in contacts:
101
+ if other == cid:
102
+ continue
103
+ for f in by_contact[other]:
104
+ if float(f.get("confidence") or 0.0) < _MIN_SOURCE_CONFIDENCE:
105
+ continue
106
+ if _norm(f.get("fact")) in own_texts:
107
+ continue
108
+ report["unaware_of"] += 1
109
+ if mode == "live" and written_rows < self._max_rows:
110
+ if self._write(cid, "unaware_of", f["id"],
111
+ _UNAWARE_CONFIDENCE, report):
112
+ written_rows += 1
113
+
114
+ logger.info(
115
+ "tom2 asymmetry[%s]: contacts=%d knows=%d unaware_of=%d written=%d",
116
+ mode, report["contacts"], report["knows"], report["unaware_of"],
117
+ report["written"])
118
+ return report
119
+
120
+ def _write(self, contact_id: str, kind: str, fact_ref: str,
121
+ confidence: float, report: Dict[str, Any]) -> bool:
122
+ try:
123
+ self._tom2.record_inference(
124
+ contact_id=contact_id, kind=kind, fact_ref=str(fact_ref),
125
+ confidence=confidence)
126
+ report["written"] += 1
127
+ return True
128
+ except Exception:
129
+ # A refused write (privacy pin) must never break the sweep.
130
+ logger.debug("tom2 inference write refused", exc_info=True)
131
+ return False
@@ -0,0 +1,248 @@
1
+ """Level-2 eligibility pipeline (L2.1) — ordered, first-fail-wins.
2
+
3
+ Decides whether ONE tom2 inference may be considered for level-2 rendering
4
+ to ONE reader in ONE conversation. Every check must pass, in order; the
5
+ first failure names itself (owner observability) and ends the evaluation.
6
+ Any internal error is a failure — never an exemption.
7
+
8
+ Order (each check exists for a specific threat):
9
+
10
+ 1. subject-scope — the subject is a real, resolvable contact and
11
+ not the reader themself.
12
+ 2. subject-owner — the subject is never the owner (T9: "the owner
13
+ hasn't heard X" must not be narrated to anyone).
14
+ 3. subject-present — the subject was NOT sighted in this conversation
15
+ window (T5: never model someone into the room
16
+ they are standing in).
17
+ 4. ref-visibility — the H3.5 double gate, delegated VERBATIM to
18
+ tom.tom2.render_inference_for_contact: every ref
19
+ must resolve to a fact row owned by the reader.
20
+ This is the structural guarantee that level 2
21
+ can never introduce new fact content.
22
+ 5. mutual-knowledge — M1: reader and subject demonstrably know of each
23
+ other (a shared conversation sighting within
24
+ COLONY_TOM2_MUTUAL_WINDOW_DAYS, default 30).
25
+ 6. tier-floors — M2: reader tier >= trusted, subject tier >=
26
+ regular.
27
+ 7. approval — M3: the owner has approved this (reader,
28
+ subject) pair (injected hook; required unless
29
+ COLONY_TOM2_L2_APPROVAL=off).
30
+ 8. budget — M7: the exposure ledger has budget for this
31
+ rendering (injected hook; no hook = no budget).
32
+
33
+ This module is dark: nothing live calls it until the wiring tranche.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ import os
40
+ from dataclasses import dataclass, field
41
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
42
+
43
+ from apsimo.gate.env_risk import _tier_rank, env_risk_window_hours
44
+ from apsimo.identity.participants import SYSTEM_CONTACT_ID
45
+ from apsimo.tom.tom2 import render_inference_for_contact
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ CHECKS = ("subject-scope", "subject-owner", "subject-present",
50
+ "ref-visibility", "mutual-knowledge", "tier-floors",
51
+ "approval", "budget")
52
+
53
+ #: Tier floors (M2): who may RECEIVE a level-2 line, who may be its SUBJECT.
54
+ READER_FLOOR = "trusted"
55
+ SUBJECT_FLOOR = "regular"
56
+
57
+ HookResult = Union[bool, Awaitable[bool]]
58
+
59
+
60
+ def l2_approval_mode() -> str:
61
+ """COLONY_TOM2_L2_APPROVAL: 'required' (default) or 'off'. Any other
62
+ value reads as 'required' (fail closed)."""
63
+ raw = os.environ.get("COLONY_TOM2_L2_APPROVAL", "required").strip().lower()
64
+ return "off" if raw == "off" else "required"
65
+
66
+
67
+ def mutual_window_days() -> float:
68
+ """COLONY_TOM2_MUTUAL_WINDOW_DAYS (default 30): how recent the reader/
69
+ subject co-sighting must be to count as mutual knowledge."""
70
+ try:
71
+ v = float(os.environ.get("COLONY_TOM2_MUTUAL_WINDOW_DAYS", "30"))
72
+ return v if v > 0 else 30.0
73
+ except (TypeError, ValueError):
74
+ return 30.0
75
+
76
+
77
+ @dataclass
78
+ class EligibilityDecision:
79
+ eligible: bool
80
+ failed_check: Optional[str] = None
81
+ detail: str = ""
82
+ checks_passed: List[str] = field(default_factory=list)
83
+
84
+ def to_dict(self) -> Dict[str, Any]:
85
+ return {"eligible": self.eligible, "failed_check": self.failed_check,
86
+ "detail": self.detail,
87
+ "checks_passed": list(self.checks_passed)}
88
+
89
+
90
+ def _fail(check: str, detail: str,
91
+ passed: List[str]) -> EligibilityDecision:
92
+ return EligibilityDecision(eligible=False, failed_check=check,
93
+ detail=detail, checks_passed=list(passed))
94
+
95
+
96
+ async def _hook_ok(hook: Any, *args: Any) -> bool:
97
+ """Run an injected hook; anything but a clean truthy answer is False."""
98
+ if hook is None:
99
+ return False
100
+ try:
101
+ r = hook(*args)
102
+ if hasattr(r, "__await__"):
103
+ r = await r
104
+ return bool(r)
105
+ except Exception:
106
+ logger.debug("eligibility hook failed (=> ineligible)", exc_info=True)
107
+ return False
108
+
109
+
110
+ async def evaluate_inference(
111
+ inference: Dict[str, Any],
112
+ *,
113
+ reader_contact_id: str,
114
+ conversation_key: str,
115
+ facts_store: Any,
116
+ contacts_store: Any,
117
+ presence_store: Any,
118
+ owner_id: Optional[str] = None,
119
+ approval_check: Optional[Callable[[str, str], HookResult]] = None,
120
+ budget_check: Optional[Callable[[str, str, str], HookResult]] = None,
121
+ ) -> EligibilityDecision:
122
+ """Run the full ordered pipeline for one inference. Never raises."""
123
+ try:
124
+ return await _evaluate(inference,
125
+ reader_contact_id=reader_contact_id,
126
+ conversation_key=conversation_key,
127
+ facts_store=facts_store,
128
+ contacts_store=contacts_store,
129
+ presence_store=presence_store,
130
+ owner_id=owner_id,
131
+ approval_check=approval_check,
132
+ budget_check=budget_check)
133
+ except Exception as exc:
134
+ logger.debug("eligibility evaluation failed (=> ineligible): %s",
135
+ exc, exc_info=True)
136
+ return _fail("error", f"{type(exc).__name__}", [])
137
+
138
+
139
+ async def _evaluate(inference, *, reader_contact_id, conversation_key,
140
+ facts_store, contacts_store, presence_store, owner_id,
141
+ approval_check, budget_check) -> EligibilityDecision:
142
+ passed: List[str] = []
143
+ reader = str(reader_contact_id or "").strip()
144
+ subject = str((inference or {}).get("contact_id") or "").strip()
145
+
146
+ # 1. subject-scope
147
+ if not isinstance(inference, dict) or not subject:
148
+ return _fail("subject-scope", "no subject", passed)
149
+ if not reader or reader == SYSTEM_CONTACT_ID:
150
+ return _fail("subject-scope", "no reader", passed)
151
+ if subject == reader or subject == SYSTEM_CONTACT_ID:
152
+ return _fail("subject-scope", "subject is reader/system", passed)
153
+ if contacts_store is None:
154
+ return _fail("subject-scope", "contacts store missing", passed)
155
+ subject_contact = await contacts_store.get(subject)
156
+ if subject_contact is None:
157
+ return _fail("subject-scope", "subject unresolvable", passed)
158
+ passed.append("subject-scope")
159
+
160
+ # 2. subject-owner (T9). Owner identity unknown fails closed: we cannot
161
+ # PROVE the subject is not the owner.
162
+ if owner_id is None:
163
+ from apsimo.identity.resolver import get_owner_contact_id
164
+ owner_id = get_owner_contact_id()
165
+ owner = str(owner_id or "").strip()
166
+ if not owner:
167
+ return _fail("subject-owner", "owner identity unset", passed)
168
+ if subject == owner:
169
+ return _fail("subject-owner", "subject is the owner", passed)
170
+ passed.append("subject-owner")
171
+
172
+ # 3. subject-present (T5). A broken/missing presence store cannot prove
173
+ # absence — fail closed.
174
+ if presence_store is None:
175
+ return _fail("subject-present", "presence store missing", passed)
176
+ try:
177
+ present = presence_store.is_present(
178
+ conversation_key, subject, window_hours=env_risk_window_hours())
179
+ except Exception:
180
+ return _fail("subject-present", "presence read failed", passed)
181
+ if present:
182
+ return _fail("subject-present", "subject sighted in conversation",
183
+ passed)
184
+ passed.append("subject-present")
185
+
186
+ # 4. ref-visibility — the H3.5 gate, verbatim. None = ineligible,
187
+ # whether from partial visibility, a missing fact, or the master
188
+ # COLONY_TOM2_CROSS_CONTEXT flag being off.
189
+ if render_inference_for_contact(inference, facts_store, reader) is None:
190
+ return _fail("ref-visibility", "H3.5 gate refused", passed)
191
+ passed.append("ref-visibility")
192
+
193
+ # 5. mutual-knowledge (M1)
194
+ try:
195
+ mutual = presence_store.cooccurred(reader, subject,
196
+ within_days=mutual_window_days())
197
+ except Exception:
198
+ return _fail("mutual-knowledge", "cooccurrence read failed", passed)
199
+ if not mutual:
200
+ return _fail("mutual-knowledge",
201
+ "no recent shared conversation", passed)
202
+ passed.append("mutual-knowledge")
203
+
204
+ # 6. tier-floors (M2)
205
+ reader_contact = await contacts_store.get(reader)
206
+ if reader_contact is None:
207
+ return _fail("tier-floors", "reader unresolvable", passed)
208
+ if _tier_rank(getattr(reader_contact, "trust_tier", "")) <\
209
+ _tier_rank(READER_FLOOR):
210
+ return _fail("tier-floors", "reader below trusted", passed)
211
+ if _tier_rank(getattr(subject_contact, "trust_tier", "")) <\
212
+ _tier_rank(SUBJECT_FLOOR):
213
+ return _fail("tier-floors", "subject below regular", passed)
214
+ passed.append("tier-floors")
215
+
216
+ # 7. approval (M3)
217
+ if l2_approval_mode() == "off":
218
+ passed.append("approval")
219
+ elif await _hook_ok(approval_check, reader, subject):
220
+ passed.append("approval")
221
+ else:
222
+ return _fail("approval", "pair not approved by owner", passed)
223
+
224
+ # 8. budget (M7): no ledger, no budget.
225
+ fact_ref = str((inference or {}).get("fact_ref") or "")
226
+ if not await _hook_ok(budget_check, reader, subject, fact_ref):
227
+ return _fail("budget", "no exposure budget", passed)
228
+ passed.append("budget")
229
+
230
+ return EligibilityDecision(eligible=True, checks_passed=passed)
231
+
232
+
233
+ async def eligible_inferences(rows: List[Dict[str, Any]], *,
234
+ limit: int = 5, **kwargs: Any
235
+ ) -> List[Dict[str, Any]]:
236
+ """Filter inference rows through the pipeline (order preserved), up to
237
+ ``limit`` eligible results. Errors inside any evaluation drop that row."""
238
+ out: List[Dict[str, Any]] = []
239
+ limit = max(0, int(limit))
240
+ if limit == 0:
241
+ return out
242
+ for row in rows or []:
243
+ decision = await evaluate_inference(row, **kwargs)
244
+ if decision.eligible:
245
+ out.append(row)
246
+ if len(out) >= limit:
247
+ break
248
+ return out
@@ -0,0 +1,214 @@
1
+ """Read current attributed engagement observations; retain numeric history only.
2
+
3
+ Canonical source claims own preferences; AppraisalStore projects them. Legacy
4
+ aggregates remain inspectable but no longer produce psychological certainty.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ import sqlite3
11
+ from datetime import datetime, timezone
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Numeric dimensions. OCEAN are signed (-1 low .. +1 high); style are 0..1.
17
+ OCEAN = ("openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism")
18
+ STYLE = ("formality", "directness", "warmth", "verbosity", "emoji_ok", "humor")
19
+ _ALL_DIMS = OCEAN + STYLE
20
+ _QUAL_KEYS = ("motivators", "topics", "avoid")
21
+ _QUAL_CAP = 8
22
+
23
+
24
+ def _now() -> str:
25
+ return datetime.now(timezone.utc).isoformat()
26
+
27
+
28
+ from .source_lineage import SourceLinkedStore
29
+
30
+
31
+ class EngagementStore(SourceLinkedStore):
32
+ """SQLite-backed evolving engagement profile per contact."""
33
+
34
+ def __init__(self, db_path: str, *, source_ledger=None) -> None:
35
+ self._db_path = str(db_path)
36
+ self._source_ledger = source_ledger
37
+ self._conn = sqlite3.connect(self._db_path)
38
+ self._conn.row_factory = sqlite3.Row
39
+ self._conn.execute("PRAGMA journal_mode=WAL")
40
+ self._conn.executescript(
41
+ """
42
+ CREATE TABLE IF NOT EXISTS engagement_profiles (
43
+ contact_id TEXT PRIMARY KEY,
44
+ dims_json TEXT NOT NULL DEFAULT '{}',
45
+ qual_json TEXT NOT NULL DEFAULT '{}',
46
+ observation_count INTEGER NOT NULL DEFAULT 0,
47
+ updated_at TEXT NOT NULL
48
+ );
49
+ """
50
+ )
51
+ # Capture old aggregate profiles once, without inventing source links.
52
+ # New observations are separate so deletion can replay their EMA exactly.
53
+ with self._conn:
54
+ self._conn.execute('BEGIN IMMEDIATE')
55
+ first = not self._conn.execute("SELECT 1 FROM sqlite_master WHERE name='engagement_baselines'").fetchone()
56
+ self._conn.execute("""CREATE TABLE IF NOT EXISTS engagement_baselines (
57
+ contact_id TEXT PRIMARY KEY, dims_json TEXT NOT NULL, qual_json TEXT NOT NULL,
58
+ observation_count INTEGER NOT NULL, updated_at TEXT NOT NULL,
59
+ evidence_basis TEXT NOT NULL DEFAULT 'legacy_unlinked')""")
60
+ self._conn.execute("""CREATE TABLE IF NOT EXISTS engagement_observations (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT, contact_id TEXT NOT NULL,
62
+ observation_json TEXT NOT NULL, source_lineage_json TEXT, created_at TEXT NOT NULL)""")
63
+ self._conn.execute('CREATE INDEX IF NOT EXISTS idx_engagement_observation_contact ON engagement_observations(contact_id)')
64
+ if first:
65
+ self._conn.execute("""INSERT INTO engagement_baselines
66
+ (contact_id,dims_json,qual_json,observation_count,updated_at)
67
+ SELECT contact_id,dims_json,qual_json,observation_count,updated_at FROM engagement_profiles""")
68
+
69
+ def purge_erased_sources(self, turn_ids=None, *, contact_id=None) -> int:
70
+ sql = 'SELECT id,contact_id,source_lineage_json FROM engagement_observations WHERE source_lineage_json IS NOT NULL'
71
+ rows = self._conn.execute(sql + (' AND contact_id=?' if contact_id else ''),
72
+ (contact_id,) if contact_id else ()).fetchall()
73
+ invalid = self._invalid_sources(rows, turn_ids)
74
+ with self._conn:
75
+ self._conn.executemany('DELETE FROM engagement_observations WHERE id=?', [(row['id'],) for row in invalid])
76
+ for person in {row['contact_id'] for row in invalid}:
77
+ self._recompute_profile(person)
78
+ return len(invalid)
79
+
80
+ # -- read ---------------------------------------------------------------
81
+ def _row(self, contact_id: str) -> Optional[sqlite3.Row]:
82
+ return self._conn.execute(
83
+ "SELECT * FROM engagement_profiles WHERE contact_id=?", (contact_id,)
84
+ ).fetchone()
85
+
86
+ def get_profile(self, contact_id: str) -> Dict[str, Any]:
87
+ self.purge_erased_sources(contact_id=contact_id)
88
+ row = self._row(contact_id)
89
+ baseline = self._conn.execute('SELECT observation_count FROM engagement_baselines WHERE contact_id=?', (contact_id,)).fetchone()
90
+ dims_raw = json.loads(row["dims_json"] or "{}") if row else {}
91
+ observations = []
92
+ if self._source_ledger is not None:
93
+ from apsimo.identity import get_owner_contact_id
94
+ from apsimo.self_model.appraisals import AppraisalStore
95
+ observations = AppraisalStore(self._source_ledger, owner_id=get_owner_contact_id()).view(
96
+ contact_id, viewer_contact_id=contact_id)['records']
97
+ return {
98
+ "contact_id": contact_id, "dims": {}, "qual": {},
99
+ "observations": observations, "observation_count": len(observations),
100
+ "legacy_profile": {"dims": dims_raw, "qual": json.loads(row["qual_json"] or "{}") if row else {},
101
+ "observation_count": row["observation_count"] if row else 0, "governing": False},
102
+ "updated_at": row["updated_at"] if row else None,
103
+ "legacy_unlinked_observations": baseline[0] if baseline else 0,
104
+ }
105
+
106
+ # -- write --------------------------------------------------------------
107
+ def update_from_observation(
108
+ self,
109
+ contact_id: str,
110
+ ocean: Optional[Dict[str, Any]] = None,
111
+ style: Optional[Dict[str, Any]] = None,
112
+ motivators: Optional[List[str]] = None,
113
+ topics: Optional[List[str]] = None,
114
+ avoid: Optional[List[str]] = None,
115
+ source_lineage: Optional[Dict[str, Any]] = None,
116
+ ) -> None:
117
+ """Fold one observation into the contact's evolving profile (EMA per dim)."""
118
+ if not contact_id:
119
+ return
120
+ if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
121
+ from apsimo.turns.idempotency import SourceErased
122
+ raise SourceErased('source_erased')
123
+ self.purge_erased_sources(contact_id=contact_id)
124
+ observation = dict(ocean=ocean, style=style, motivators=motivators, topics=topics, avoid=avoid)
125
+ with self._conn:
126
+ self._conn.execute("""INSERT INTO engagement_observations
127
+ (contact_id,observation_json,source_lineage_json,created_at) VALUES (?,?,?,?)""",
128
+ (contact_id, json.dumps(observation), json.dumps(source_lineage) if source_lineage is not None else None, _now()))
129
+ row = self._row(contact_id)
130
+ dims = json.loads(row['dims_json']) if row else {}
131
+ qual = json.loads(row['qual_json']) if row else {}
132
+ self._fold(dims, qual, **observation)
133
+ self._write_profile(contact_id, dims, qual, (row['observation_count'] if row else 0) + 1, _now())
134
+ # Erasure can commit while the projection database is being written.
135
+ if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
136
+ self.purge_erased_sources(contact_id=contact_id)
137
+ from apsimo.turns.idempotency import SourceErased
138
+ raise SourceErased('source_erased')
139
+
140
+ @staticmethod
141
+ def _fold(dims, qual, *, ocean=None, style=None, motivators=None, topics=None, avoid=None):
142
+ obs = dict(ocean or {})
143
+ obs.update(style or {})
144
+ for dim, val in obs.items():
145
+ if dim not in _ALL_DIMS or val is None:
146
+ continue
147
+ try:
148
+ val = float(val)
149
+ except (TypeError, ValueError):
150
+ continue
151
+ lo = -1.0 if dim in OCEAN else 0.0
152
+ val = max(lo, min(1.0, val))
153
+ cur = dims.get(dim)
154
+ if cur is None:
155
+ dims[dim] = {"v": round(val, 4), "n": 1}
156
+ else:
157
+ n = cur["n"] + 1
158
+ alpha = max(0.15, 1.0 / n) # responsive early, stabilises later
159
+ v = cur["v"] + alpha * (val - cur["v"])
160
+ dims[dim] = {"v": round(v, 4), "n": n}
161
+
162
+ for key, items in (("motivators", motivators), ("topics", topics), ("avoid", avoid)):
163
+ if not items:
164
+ continue
165
+ existing = list(qual.get(key, []))
166
+ seen = {s.lower() for s in existing}
167
+ for it in items:
168
+ it = (it or "").strip()
169
+ if it and it.lower() not in seen:
170
+ existing.append(it)
171
+ seen.add(it.lower())
172
+ qual[key] = existing[-_QUAL_CAP:] # keep most recent
173
+
174
+ def _recompute_profile(self, contact_id):
175
+ baseline = self._conn.execute('SELECT * FROM engagement_baselines WHERE contact_id=?', (contact_id,)).fetchone()
176
+ rows = self._conn.execute('SELECT * FROM engagement_observations WHERE contact_id=? ORDER BY id', (contact_id,)).fetchall()
177
+ if not baseline and not rows:
178
+ self._conn.execute('DELETE FROM engagement_profiles WHERE contact_id=?', (contact_id,))
179
+ return
180
+ dims = json.loads(baseline['dims_json']) if baseline else {}
181
+ qual = json.loads(baseline['qual_json']) if baseline else {}
182
+ count = baseline['observation_count'] if baseline else 0
183
+ updated = baseline['updated_at'] if baseline else None
184
+ for row in rows:
185
+ self._fold(dims, qual, **json.loads(row['observation_json']))
186
+ count += 1
187
+ updated = row['created_at']
188
+ self._write_profile(contact_id, dims, qual, count, updated)
189
+
190
+ def _write_profile(self, contact_id, dims, qual, count, updated):
191
+ self._conn.execute(
192
+ """INSERT INTO engagement_profiles (contact_id, dims_json, qual_json, observation_count, updated_at)
193
+ VALUES (?,?,?,?,?)
194
+ ON CONFLICT(contact_id) DO UPDATE SET
195
+ dims_json=excluded.dims_json, qual_json=excluded.qual_json,
196
+ observation_count=excluded.observation_count, updated_at=excluded.updated_at""",
197
+ (contact_id, json.dumps(dims), json.dumps(qual), count, updated),
198
+ )
199
+
200
+ def purge(self, contact_id: str) -> None:
201
+ with self._conn:
202
+ self._conn.execute("DELETE FROM engagement_profiles WHERE contact_id=?", (contact_id,))
203
+ self._conn.execute("DELETE FROM engagement_observations WHERE contact_id=?", (contact_id,))
204
+ self._conn.execute("DELETE FROM engagement_baselines WHERE contact_id=?", (contact_id,))
205
+
206
+
207
+ def build_guidance(profile: Dict[str, Any]) -> str:
208
+ """Only current attributed preferences; numeric legacy profiles never govern."""
209
+ lines = []
210
+ for item in profile.get('observations', [])[:4]:
211
+ if item.get('kind') != 'preference' or item.get('status') != 'current' or not item.get('sources'):
212
+ continue
213
+ lines.append('Stated preference (attributed, correctable): ' + str(item.get('text', '')))
214
+ return '\n'.join(lines)