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,855 @@
1
+ """SQLite storage backend for the Colony World Model."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import time
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import aiosqlite
12
+
13
+
14
+ def _fts_escape(q: str, max_len: int = 200) -> str:
15
+ """Strip FTS5 operators and special chars; keep only word chars and spaces.
16
+
17
+ Caps input at max_len before processing to prevent regex DoS on huge
18
+ inputs. Specials are replaced with SPACES (not deleted): deleting fused
19
+ hyphenated names into single tokens ("huggingface-hub" ->
20
+ "huggingfacehub") that the FTS tokenizer (which splits on the hyphen)
21
+ could never match, so exact-name resolution silently failed and every
22
+ mention minted a duplicate entity.
23
+ """
24
+ safe = re.sub(r'[^\w\s]', ' ', q[:max_len], flags=re.UNICODE)
25
+ return re.sub(r'\s+', ' ', safe).strip()
26
+
27
+ from ..constants import (
28
+ CAUSAL_RELATIONSHIP_TYPES,
29
+ ENTITY_ID_PREFIX,
30
+ RELATIONSHIP_ID_PREFIX,
31
+ OBSERVATION_ID_PREFIX,
32
+ MERGE_PROPOSAL_ID_PREFIX,
33
+ MERGE_AUDIT_ID_PREFIX,
34
+ )
35
+ from ..entities import BaseEntity, ENTITY_CLASS_MAP, entity_from_dict
36
+ from ..relationships import WorldRelationship
37
+
38
+ # Query-only causal guard (H2.5): untyped (generic) reads NEVER return
39
+ # causal edges; only /world/causal/* and explicitly typed queries do.
40
+ _CAUSAL_EXCLUSION = tuple(sorted(CAUSAL_RELATIONSHIP_TYPES))
41
+
42
+
43
+ def _generate_id(prefix: str) -> str:
44
+ """Generate a world model ID: prefix-<unix_ms>-<rand12>."""
45
+ import secrets
46
+ ts = int(time.time() * 1000)
47
+ rand = secrets.token_hex(6) # 12 hex chars, 48 bits of CSPRNG entropy
48
+ return f"{prefix}-{ts}-{rand}"
49
+
50
+
51
+ def _now_iso() -> str:
52
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
53
+
54
+
55
+ def _entity_to_row(entity: BaseEntity) -> Dict[str, Any]:
56
+ """Flatten a BaseEntity to a dict suitable for SQLite insertion."""
57
+ import dataclasses
58
+
59
+ row: Dict[str, Any] = {
60
+ "id": entity.id,
61
+ "name": entity.name,
62
+ "entity_type": entity.entity_type,
63
+ "aliases": json.dumps(entity.aliases),
64
+ "external_ids": json.dumps(entity.external_ids),
65
+ "confidence": entity.confidence,
66
+ "properties": json.dumps(_collect_type_properties(entity)),
67
+ "first_seen": (
68
+ entity.first_seen.strftime("%Y-%m-%dT%H:%M:%SZ")
69
+ if entity.first_seen
70
+ else _now_iso()
71
+ ),
72
+ "last_seen": (
73
+ entity.last_seen.strftime("%Y-%m-%dT%H:%M:%SZ")
74
+ if entity.last_seen
75
+ else _now_iso()
76
+ ),
77
+ "created_at": (
78
+ entity.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
79
+ if entity.created_at
80
+ else _now_iso()
81
+ ),
82
+ "updated_at": _now_iso(),
83
+ }
84
+ return row
85
+
86
+
87
+ def _collect_type_properties(entity: BaseEntity) -> Dict[str, Any]:
88
+ """Merge entity.properties with type-specific fields into a single dict."""
89
+ import dataclasses
90
+ from ..entities import BaseEntity
91
+
92
+ base_fields = {f.name for f in dataclasses.fields(BaseEntity)}
93
+ extra: Dict[str, Any] = dict(entity.properties)
94
+ for f in dataclasses.fields(entity):
95
+ if f.name not in base_fields:
96
+ v = getattr(entity, f.name)
97
+ if v is not None:
98
+ extra[f.name] = v
99
+ return extra
100
+
101
+
102
+ def _row_to_entity(row: aiosqlite.Row) -> BaseEntity:
103
+ """Reconstruct an entity from a SQLite row."""
104
+ d = dict(row)
105
+ d["aliases"] = json.loads(d.get("aliases") or "[]")
106
+ d["external_ids"] = json.loads(d.get("external_ids") or "{}")
107
+ props = json.loads(d.get("properties") or "{}")
108
+ d["properties"] = {}
109
+
110
+ entity_type = d.get("entity_type", "")
111
+ cls = ENTITY_CLASS_MAP.get(entity_type, BaseEntity)
112
+ import dataclasses
113
+ valid_fields = {f.name for f in dataclasses.fields(cls)}
114
+
115
+ # Pull type-specific fields out of properties into top-level keys
116
+ for k, v in props.items():
117
+ if k in valid_fields:
118
+ d[k] = v
119
+ else:
120
+ d["properties"][k] = v
121
+
122
+ # Parse datetime fields
123
+ for dt_field in ("first_seen", "last_seen", "created_at", "updated_at"):
124
+ val = d.get(dt_field)
125
+ if val and isinstance(val, str):
126
+ try:
127
+ d[dt_field] = datetime.fromisoformat(val.replace("Z", "+00:00"))
128
+ except ValueError:
129
+ d[dt_field] = None
130
+
131
+ filtered = {k: v for k, v in d.items() if k in valid_fields}
132
+ return cls(**filtered)
133
+
134
+
135
+ def _row_to_relationship(row: aiosqlite.Row) -> WorldRelationship:
136
+ """Reconstruct a WorldRelationship from a SQLite row."""
137
+ d = dict(row)
138
+ d["properties"] = json.loads(d.get("properties") or "{}")
139
+ return WorldRelationship(
140
+ id=d["id"],
141
+ source_id=d["source_id"],
142
+ target_id=d["target_id"],
143
+ relationship_type=d["relationship_type"],
144
+ confidence=d.get("confidence", 0.5),
145
+ valid_from=d.get("valid_from"),
146
+ valid_to=d.get("valid_to"),
147
+ properties=d["properties"],
148
+ source_observation_id=d.get("source_observation_id"),
149
+ created_at=d.get("created_at"),
150
+ updated_at=d.get("updated_at"),
151
+ )
152
+
153
+
154
+ _SCHEMA_DIR = Path(__file__).parent
155
+
156
+
157
+ class SQLiteBackend:
158
+ """Async SQLite backend for the World Model.
159
+
160
+ All methods are async and safe for concurrent use within a single process.
161
+ """
162
+
163
+ MIGRATIONS = [
164
+ ("wm-001", "Create base entity tables and FTS index"),
165
+ ("wm-002", "Create relationship table"),
166
+ ("wm-003", "Create observations table"),
167
+ ("wm-004", "Create merge proposals and audit log"),
168
+ ("wm-005", "Create schema migrations table"),
169
+ ("wm-006", "Add mention_count reinforcement column to wm_entities"),
170
+ ]
171
+
172
+ def __init__(self, db_path: str = ":memory:") -> None:
173
+ self._db_path = db_path
174
+ self._db: Optional[aiosqlite.Connection] = None
175
+
176
+ async def connect(self) -> None:
177
+ """Open database connection and apply schema."""
178
+ self._db = await aiosqlite.connect(self._db_path)
179
+ self._db.row_factory = aiosqlite.Row
180
+ await self._db.execute("PRAGMA journal_mode=WAL")
181
+ await self._db.execute("PRAGMA foreign_keys=ON")
182
+ await self._apply_schema()
183
+
184
+ async def close(self) -> None:
185
+ if self._db:
186
+ await self._db.close()
187
+ self._db = None
188
+
189
+ async def _apply_schema(self) -> None:
190
+ schema_path = _SCHEMA_DIR / "schema.sql"
191
+ sql = schema_path.read_text()
192
+ await self._db.executescript(sql)
193
+ await self._db.commit()
194
+ # wm-006: additive column for repeat-mention reinforcement. Guarded so
195
+ # pre-existing databases migrate in place ("duplicate column name" on
196
+ # every later connect is the expected no-op). DEFAULT 1: every stored
197
+ # entity has, by definition, been mentioned at least once.
198
+ try:
199
+ await self._db.execute(
200
+ "ALTER TABLE wm_entities ADD COLUMN "
201
+ "mention_count INTEGER NOT NULL DEFAULT 1")
202
+ await self._db.commit()
203
+ except Exception:
204
+ pass # column already exists
205
+ await self._record_migrations()
206
+
207
+ async def _record_migrations(self) -> None:
208
+ for version, description in self.MIGRATIONS:
209
+ await self._db.execute(
210
+ """
211
+ INSERT OR IGNORE INTO wm_schema_migrations(version, description)
212
+ VALUES (?, ?)
213
+ """,
214
+ (version, description),
215
+ )
216
+ await self._db.commit()
217
+
218
+ # ── Entity CRUD ───────────────────────────────────────────────────────────
219
+
220
+ async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
221
+ async with self._db.execute(
222
+ "SELECT * FROM wm_entities WHERE id = ?", (entity_id,)
223
+ ) as cur:
224
+ row = await cur.fetchone()
225
+ return _row_to_entity(row) if row else None
226
+
227
+ async def get_entity_by_external_id(
228
+ self, key: str, value: str
229
+ ) -> Optional[BaseEntity]:
230
+ """Find entity where external_ids JSON contains {key: value}."""
231
+ # SQLite JSON1 function to extract the key
232
+ async with self._db.execute(
233
+ """
234
+ SELECT * FROM wm_entities
235
+ WHERE json_extract(external_ids, '$.' || ?) = ?
236
+ LIMIT 1
237
+ """,
238
+ (key, value),
239
+ ) as cur:
240
+ row = await cur.fetchone()
241
+ return _row_to_entity(row) if row else None
242
+
243
+ async def find_entities(
244
+ self,
245
+ query: str,
246
+ entity_type: Optional[str] = None,
247
+ min_confidence: float = 0.30,
248
+ limit: int = 20,
249
+ ) -> List[BaseEntity]:
250
+ """Full-text search on name + aliases (plus exact-name fallback).
251
+
252
+ Exact canonical-name matching must never depend on FTS tokenizer
253
+ quirks, so a direct case-insensitive name lookup is unioned in; the
254
+ EntityResolver's dedup depends on it.
255
+ """
256
+ rows = []
257
+ if query:
258
+ sql = """
259
+ SELECT e.* FROM wm_entities e
260
+ JOIN wm_entities_fts fts ON fts.id = e.id
261
+ WHERE wm_entities_fts MATCH ?
262
+ AND e.confidence >= ?
263
+ """
264
+ params: list = [_fts_escape(query) + "*", min_confidence]
265
+ if entity_type:
266
+ sql += " AND e.entity_type = ?"
267
+ params.append(entity_type)
268
+ sql += " LIMIT ?"
269
+ params.append(limit)
270
+ try:
271
+ async with self._db.execute(sql, params) as cur:
272
+ rows = list(await cur.fetchall())
273
+ except Exception:
274
+ rows = []
275
+ # Exact-name fallback/union.
276
+ sql2 = ("SELECT * FROM wm_entities WHERE lower(name) = lower(?) "
277
+ "AND confidence >= ?")
278
+ params2: list = [query.strip(), min_confidence]
279
+ if entity_type:
280
+ sql2 += " AND entity_type = ?"
281
+ params2.append(entity_type)
282
+ sql2 += " LIMIT ?"
283
+ params2.append(limit)
284
+ async with self._db.execute(sql2, params2) as cur:
285
+ exact = await cur.fetchall()
286
+ seen = {r["id"] for r in rows} if rows else set()
287
+ rows.extend(r for r in exact if r["id"] not in seen)
288
+ else:
289
+ sql = "SELECT * FROM wm_entities WHERE confidence >= ?"
290
+ params = [min_confidence]
291
+ if entity_type:
292
+ sql += " AND entity_type = ?"
293
+ params.append(entity_type)
294
+ sql += " ORDER BY confidence DESC LIMIT ?"
295
+ params.append(limit)
296
+ async with self._db.execute(sql, params) as cur:
297
+ rows = await cur.fetchall()
298
+ return [_row_to_entity(r) for r in rows]
299
+
300
+ async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
301
+ """Insert or replace an entity row."""
302
+ if not entity.id:
303
+ entity.id = _generate_id(ENTITY_ID_PREFIX)
304
+ row = _entity_to_row(entity)
305
+ await self._db.execute(
306
+ """
307
+ INSERT INTO wm_entities
308
+ (id, name, entity_type, aliases, external_ids, properties,
309
+ confidence, first_seen, last_seen, created_at, updated_at)
310
+ VALUES
311
+ (:id, :name, :entity_type, :aliases, :external_ids, :properties,
312
+ :confidence, :first_seen, :last_seen, :created_at, :updated_at)
313
+ ON CONFLICT(id) DO UPDATE SET
314
+ name = excluded.name,
315
+ aliases = excluded.aliases,
316
+ external_ids = excluded.external_ids,
317
+ properties = excluded.properties,
318
+ confidence = excluded.confidence,
319
+ last_seen = excluded.last_seen,
320
+ updated_at = excluded.updated_at
321
+ """,
322
+ row,
323
+ )
324
+ await self._db.commit()
325
+ return entity
326
+
327
+ async def update_entity_property(
328
+ self, entity_id: str, property_key: str, property_value: Any, confidence: float
329
+ ) -> None:
330
+ """Update a property only if new confidence is higher than existing."""
331
+ entity = await self.get_entity(entity_id)
332
+ if not entity:
333
+ return
334
+ import dataclasses
335
+ existing_fields = {f.name for f in dataclasses.fields(type(entity))}
336
+ if property_key in existing_fields:
337
+ # Update the field directly
338
+ setattr(entity, property_key, property_value)
339
+ if confidence > entity.confidence:
340
+ entity.confidence = confidence
341
+ await self.upsert_entity(entity)
342
+ else:
343
+ # Store in properties JSON
344
+ if confidence >= entity.properties.get(f"_conf_{property_key}", 0.0):
345
+ entity.properties[property_key] = property_value
346
+ entity.properties[f"_conf_{property_key}"] = confidence
347
+ await self.upsert_entity(entity)
348
+
349
+ async def add_entity_alias(self, entity_id: str, alias: str) -> None:
350
+ entity = await self.get_entity(entity_id)
351
+ if entity and alias not in entity.aliases:
352
+ entity.aliases.append(alias)
353
+ await self._db.execute(
354
+ "UPDATE wm_entities SET aliases = ?, updated_at = ? WHERE id = ?",
355
+ (json.dumps(entity.aliases), _now_iso(), entity_id),
356
+ )
357
+ await self._db.commit()
358
+
359
+ async def reinforce_entity(self, entity_id: str) -> None:
360
+ """Record familiarity without treating repetition as corroboration."""
361
+ now = _now_iso()
362
+ await self._db.execute(
363
+ """
364
+ UPDATE wm_entities
365
+ SET last_seen = MAX(last_seen, ?),
366
+ updated_at = ?,
367
+ mention_count = COALESCE(mention_count, 1) + 1
368
+ WHERE id = ?
369
+ """,
370
+ (now, now, entity_id),
371
+ )
372
+ await self._db.commit()
373
+
374
+ async def delete_entity(self, entity_id: str) -> None:
375
+ await self._db.execute("DELETE FROM wm_entities WHERE id = ?", (entity_id,))
376
+ await self._db.commit()
377
+
378
+ async def prune_entities(self, last_seen_before: str,
379
+ max_confidence: float) -> int:
380
+ """Delete entities not seen since ``last_seen_before`` (ISO, the
381
+ schema's Z-suffixed format) whose confidence is below
382
+ ``max_confidence``. Relationships and observations cascade
383
+ (foreign_keys=ON); the FTS mirror follows via triggers. Returns the
384
+ number of entities removed."""
385
+ cur = await self._db.execute(
386
+ "DELETE FROM wm_entities WHERE last_seen < ? AND confidence < ?",
387
+ (last_seen_before, max_confidence),
388
+ )
389
+ count = cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
390
+ await self._db.commit()
391
+ return count
392
+
393
+ # ── Relationship CRUD ─────────────────────────────────────────────────────
394
+
395
+ async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
396
+ async with self._db.execute(
397
+ "SELECT * FROM wm_relationships WHERE id = ?", (rel_id,)
398
+ ) as cur:
399
+ row = await cur.fetchone()
400
+ return _row_to_relationship(row) if row else None
401
+
402
+ async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
403
+ if not rel.id:
404
+ rel.id = _generate_id(RELATIONSHIP_ID_PREFIX)
405
+ now = _now_iso()
406
+ await self._db.execute(
407
+ """
408
+ INSERT INTO wm_relationships
409
+ (id, source_id, target_id, relationship_type, confidence,
410
+ valid_from, valid_to, properties, source_observation_id,
411
+ created_at, updated_at)
412
+ VALUES
413
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
414
+ ON CONFLICT(id) DO UPDATE SET
415
+ confidence = excluded.confidence,
416
+ valid_from = excluded.valid_from,
417
+ valid_to = excluded.valid_to,
418
+ properties = excluded.properties,
419
+ updated_at = excluded.updated_at
420
+ """,
421
+ (
422
+ rel.id,
423
+ rel.source_id,
424
+ rel.target_id,
425
+ rel.relationship_type,
426
+ rel.confidence,
427
+ rel.valid_from,
428
+ rel.valid_to,
429
+ json.dumps(rel.properties),
430
+ rel.source_observation_id,
431
+ rel.created_at or now,
432
+ now,
433
+ ),
434
+ )
435
+ await self._db.commit()
436
+ return rel
437
+
438
+ async def close_relationship(self, rel_id: str, valid_to: str) -> None:
439
+ await self._db.execute(
440
+ "UPDATE wm_relationships SET valid_to = ?, updated_at = ? WHERE id = ?",
441
+ (valid_to, _now_iso(), rel_id),
442
+ )
443
+ await self._db.commit()
444
+
445
+ async def query_relationships(
446
+ self,
447
+ source_id: Optional[str] = None,
448
+ target_id: Optional[str] = None,
449
+ relationship_type: Optional[str] = None,
450
+ target_types: Optional[List[str]] = None,
451
+ active_only: bool = False,
452
+ min_confidence: float = 0.30,
453
+ limit: int = 100,
454
+ ) -> List[WorldRelationship]:
455
+ sql = "SELECT r.* FROM wm_relationships r"
456
+ conditions = ["r.confidence >= ?"]
457
+ params: list = [min_confidence]
458
+
459
+ if target_types:
460
+ sql += " JOIN wm_entities te ON te.id = r.target_id"
461
+ placeholders = ",".join("?" * len(target_types))
462
+ conditions.append(f"te.entity_type IN ({placeholders})")
463
+ params.extend(target_types)
464
+
465
+ if source_id:
466
+ conditions.append("r.source_id = ?")
467
+ params.append(source_id)
468
+ if target_id:
469
+ conditions.append("r.target_id = ?")
470
+ params.append(target_id)
471
+ if relationship_type:
472
+ conditions.append("r.relationship_type = ?")
473
+ params.append(relationship_type)
474
+ else:
475
+ # Untyped read: causal edges are query-only, exclude them.
476
+ ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
477
+ conditions.append(f"r.relationship_type NOT IN ({ph})")
478
+ params.extend(_CAUSAL_EXCLUSION)
479
+ if active_only:
480
+ conditions.append("r.valid_to IS NULL")
481
+
482
+ sql += " WHERE " + " AND ".join(conditions)
483
+ sql += " ORDER BY r.confidence DESC LIMIT ?"
484
+ params.append(limit)
485
+
486
+ async with self._db.execute(sql, params) as cur:
487
+ rows = await cur.fetchall()
488
+ return [_row_to_relationship(r) for r in rows]
489
+
490
+ async def query_at_time(
491
+ self,
492
+ entity_id: str,
493
+ as_of: str,
494
+ relationship_types: Optional[List[str]] = None,
495
+ ) -> List[WorldRelationship]:
496
+ """Relationships for entity active at as_of datetime."""
497
+ sql = """
498
+ SELECT * FROM wm_relationships
499
+ WHERE (source_id = ? OR target_id = ?)
500
+ AND (valid_from IS NULL OR valid_from <= ?)
501
+ AND (valid_to IS NULL OR valid_to > ?)
502
+ """
503
+ params: list = [entity_id, entity_id, as_of, as_of]
504
+ if relationship_types:
505
+ placeholders = ",".join("?" * len(relationship_types))
506
+ sql += f" AND relationship_type IN ({placeholders})"
507
+ params.extend(relationship_types)
508
+ else:
509
+ # Untyped read: causal edges are query-only, exclude them.
510
+ ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
511
+ sql += f" AND relationship_type NOT IN ({ph})"
512
+ params.extend(_CAUSAL_EXCLUSION)
513
+ async with self._db.execute(sql, params) as cur:
514
+ rows = await cur.fetchall()
515
+ return [_row_to_relationship(r) for r in rows]
516
+
517
+ # ── Graph Traversal ───────────────────────────────────────────────────────
518
+
519
+ async def get_neighbors(
520
+ self,
521
+ entity_id: str,
522
+ min_confidence: float = 0.30,
523
+ relationship_types: Optional[List[str]] = None,
524
+ ) -> List[tuple]:
525
+ """Return (neighbor_entity, relationship) pairs for direct neighbors."""
526
+ sql = """
527
+ SELECT e.*, r.id as rel_id, r.source_id, r.target_id,
528
+ r.relationship_type, r.confidence as rel_confidence,
529
+ r.valid_from, r.valid_to, r.properties as rel_props,
530
+ r.source_observation_id, r.created_at as rel_created_at,
531
+ r.updated_at as rel_updated_at
532
+ FROM wm_relationships r
533
+ JOIN wm_entities e ON (
534
+ (r.source_id = ? AND e.id = r.target_id) OR
535
+ (r.target_id = ? AND e.id = r.source_id)
536
+ )
537
+ WHERE r.confidence >= ? AND e.confidence >= ?
538
+ """
539
+ params: list = [entity_id, entity_id, min_confidence, min_confidence]
540
+ if relationship_types:
541
+ placeholders = ",".join("?" * len(relationship_types))
542
+ sql += f" AND r.relationship_type IN ({placeholders})"
543
+ params.extend(relationship_types)
544
+ else:
545
+ # Untyped read: causal edges are query-only, exclude them.
546
+ ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
547
+ sql += f" AND r.relationship_type NOT IN ({ph})"
548
+ params.extend(_CAUSAL_EXCLUSION)
549
+ async with self._db.execute(sql, params) as cur:
550
+ rows = await cur.fetchall()
551
+
552
+ results = []
553
+ for row in rows:
554
+ d = dict(row)
555
+ entity = _row_to_entity(row)
556
+ rel = WorldRelationship(
557
+ id=d["rel_id"],
558
+ source_id=d["source_id"],
559
+ target_id=d["target_id"],
560
+ relationship_type=d["relationship_type"],
561
+ confidence=d["rel_confidence"],
562
+ valid_from=d.get("valid_from"),
563
+ valid_to=d.get("valid_to"),
564
+ properties=json.loads(d.get("rel_props") or "{}"),
565
+ source_observation_id=d.get("source_observation_id"),
566
+ created_at=d.get("rel_created_at"),
567
+ updated_at=d.get("rel_updated_at"),
568
+ )
569
+ results.append((entity, rel))
570
+ return results
571
+
572
+ # ── Observations ──────────────────────────────────────────────────────────
573
+
574
+ async def put_property_observation(self, data):
575
+ from ..observations import canonical, SOURCE_PREFIX
576
+ encoded, marker = canonical(data), SOURCE_PREFIX + data['property_key']
577
+ await self._db.execute('''INSERT INTO wm_observations
578
+ (id,entity_id,observation,source) VALUES (?,?,?,?) ON CONFLICT(id) DO NOTHING''',
579
+ (data['observation_id'], data['entity_id'], encoded, marker))
580
+ await self._db.commit()
581
+ async with self._db.execute('SELECT observation,source FROM wm_observations WHERE id=?',
582
+ (data['observation_id'],)) as cur:
583
+ row = await cur.fetchone()
584
+ if row['observation'] != encoded or row['source'] != marker:
585
+ raise ValueError('world_observation_id_conflict')
586
+ return data
587
+
588
+ async def current_property_observations(self, entity_id, property_key, *, subject_person_id,
589
+ viewer_scope, shareability, as_of, limit=2001):
590
+ """Reduce in SQL so regular telemetry does not fill a context window.
591
+
592
+ Deduplicate one evidence packet before selecting the latest report per
593
+ producer. Equal-time contradictory values remain separate candidates.
594
+ Raw observation history stays in the existing journal.
595
+ """
596
+ from ..observations import SOURCE_PREFIX
597
+ async with self._db.execute('''WITH scoped AS (
598
+ SELECT id,observation,
599
+ json_extract(observation,'$.producer') AS producer,
600
+ json_extract(observation,'$.kind') AS kind,
601
+ json_extract(observation,'$.observed_at') AS observed_at,
602
+ json_extract(observation,'$.evidence_refs') AS evidence_refs,
603
+ json_type(observation,'$.value') AS value_type,
604
+ json_extract(observation,'$.value') AS value
605
+ FROM wm_observations WHERE entity_id=? AND source=?
606
+ AND json_extract(observation,'$.subject_person_id')=?
607
+ AND json_extract(observation,'$.viewer_scope')=?
608
+ AND json_extract(observation,'$.shareability')=?
609
+ AND json_extract(observation,'$.observed_at')<=?
610
+ AND json_extract(observation,'$.valid_from')<=?
611
+ ), dedup AS (
612
+ SELECT *,ROW_NUMBER() OVER (PARTITION BY producer,kind,evidence_refs,value_type,value
613
+ ORDER BY observed_at,id) AS copy_rank FROM scoped
614
+ ), latest AS (
615
+ SELECT *,RANK() OVER (PARTITION BY producer,kind ORDER BY observed_at DESC) AS recency
616
+ FROM dedup WHERE copy_rank=1
617
+ ) SELECT observation FROM latest WHERE recency=1 ORDER BY producer,kind,id LIMIT ?''',
618
+ (entity_id, SOURCE_PREFIX + property_key, subject_person_id, viewer_scope, shareability,
619
+ as_of, as_of, limit)) as cur:
620
+ return [json.loads(row['observation']) for row in await cur.fetchall()]
621
+
622
+ async def property_keys(self, entity_id, *, subject_person_id, viewer_scope, shareability, limit=16):
623
+ from ..observations import SOURCE_PREFIX
624
+ async with self._db.execute('''SELECT DISTINCT substr(source,?) AS property_key
625
+ FROM wm_observations WHERE entity_id=? AND source LIKE ?
626
+ AND json_extract(observation,'$.subject_person_id')=?
627
+ AND json_extract(observation,'$.viewer_scope')=?
628
+ AND json_extract(observation,'$.shareability')=? ORDER BY property_key LIMIT ?''',
629
+ (len(SOURCE_PREFIX)+1, entity_id, SOURCE_PREFIX+'%', subject_person_id,
630
+ viewer_scope, shareability, limit)) as cur:
631
+ return [row['property_key'] for row in await cur.fetchall()]
632
+
633
+ async def erase_property_evidence(self, evidence_refs, *, subject_person_id):
634
+ from ..observations import SOURCE_PREFIX
635
+ # Canonical erasure/correction has already selected these exact refs.
636
+ # Redact derived values, retaining the latest invalidated head so an
637
+ # older report cannot silently become current after a correction.
638
+ removed = []
639
+ for ref in sorted(set(evidence_refs)):
640
+ async with self._db.execute('''UPDATE wm_observations
641
+ SET observation=json_set(observation,'$.value',NULL,'$.invalidated',json('true'))
642
+ WHERE id IN (
643
+ SELECT o.id FROM wm_observations o,json_each(
644
+ CASE WHEN o.source LIKE ? THEN o.observation ELSE '{}' END,'$.evidence_refs') e
645
+ WHERE o.source LIKE ? AND json_extract(o.observation,'$.subject_person_id')=?
646
+ AND e.value=?) RETURNING id''', (SOURCE_PREFIX + '%', SOURCE_PREFIX + '%', subject_person_id, ref)) as cur:
647
+ removed.extend(row['id'] for row in await cur.fetchall())
648
+ await self._db.commit()
649
+ return sorted(set(removed))
650
+
651
+ async def add_observation(
652
+ self,
653
+ entity_id: Optional[str],
654
+ relationship_id: Optional[str],
655
+ observation: str,
656
+ source: str,
657
+ ) -> str:
658
+ obs_id = _generate_id(OBSERVATION_ID_PREFIX)
659
+ await self._db.execute(
660
+ """
661
+ INSERT INTO wm_observations(id, entity_id, relationship_id, observation, source)
662
+ VALUES (?, ?, ?, ?, ?)
663
+ """,
664
+ (obs_id, entity_id, relationship_id, observation, source),
665
+ )
666
+ await self._db.commit()
667
+ return obs_id
668
+
669
+ # ── Merge Proposals ───────────────────────────────────────────────────────
670
+
671
+ async def create_merge_proposal(
672
+ self,
673
+ candidate_id: str,
674
+ existing_id: str,
675
+ confidence: float,
676
+ reason: str,
677
+ evidence: Optional[Dict[str, Any]] = None,
678
+ ) -> str:
679
+ proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
680
+ await self._db.execute(
681
+ """
682
+ INSERT INTO wm_merge_proposals
683
+ (id, candidate_id, existing_id, match_confidence, match_reason, evidence)
684
+ VALUES (?, ?, ?, ?, ?, ?)
685
+ """,
686
+ (
687
+ proposal_id,
688
+ candidate_id,
689
+ existing_id,
690
+ confidence,
691
+ reason,
692
+ json.dumps(evidence or {}),
693
+ ),
694
+ )
695
+ await self._db.commit()
696
+ return proposal_id
697
+
698
+ async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
699
+ async with self._db.execute(
700
+ "SELECT * FROM wm_merge_proposals WHERE id = ?", (proposal_id,)
701
+ ) as cur:
702
+ row = await cur.fetchone()
703
+ if not row:
704
+ return None
705
+ d = dict(row)
706
+ d["evidence"] = json.loads(d.get("evidence") or "{}")
707
+ return d
708
+
709
+ async def update_merge_proposal_status(
710
+ self, proposal_id: str, status: str
711
+ ) -> None:
712
+ await self._db.execute(
713
+ """
714
+ UPDATE wm_merge_proposals
715
+ SET status = ?, resolved_at = ?
716
+ WHERE id = ?
717
+ """,
718
+ (status, _now_iso(), proposal_id),
719
+ )
720
+ await self._db.commit()
721
+
722
+ async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
723
+ async with self._db.execute(
724
+ "SELECT * FROM wm_merge_proposals WHERE status = 'pending'"
725
+ ) as cur:
726
+ rows = await cur.fetchall()
727
+ result = []
728
+ for row in rows:
729
+ d = dict(row)
730
+ d["evidence"] = json.loads(d.get("evidence") or "{}")
731
+ result.append(d)
732
+ return result
733
+
734
+ # ── Merge Execution ───────────────────────────────────────────────────────
735
+
736
+ async def execute_merge(
737
+ self,
738
+ surviving_id: str,
739
+ retired_id: str,
740
+ executed_by: str,
741
+ proposal_id: Optional[str] = None,
742
+ ) -> str:
743
+ """Repoint all relationships from retired_id to surviving_id.
744
+
745
+ All writes are wrapped in a SAVEPOINT so a failure midway through
746
+ (e.g. the DELETE or audit INSERT raising) rolls back the relationship
747
+ repoint and alias merge as a unit.
748
+ """
749
+ await self._db.execute("SAVEPOINT execute_merge")
750
+ try:
751
+ # Read both entities up-front; bail out before mutating if either
752
+ # was deleted by a concurrent merge.
753
+ surviving = await self.get_entity(surviving_id)
754
+ retired = await self.get_entity(retired_id)
755
+
756
+ async with self._db.execute(
757
+ "SELECT COUNT(*) FROM wm_relationships WHERE source_id = ? OR target_id = ?",
758
+ (retired_id, retired_id),
759
+ ) as cur:
760
+ row = await cur.fetchone()
761
+ rel_count = row[0] if row else 0
762
+
763
+ await self._db.execute(
764
+ "UPDATE wm_relationships SET source_id = ? WHERE source_id = ?",
765
+ (surviving_id, retired_id),
766
+ )
767
+ await self._db.execute(
768
+ "UPDATE wm_relationships SET target_id = ? WHERE target_id = ?",
769
+ (surviving_id, retired_id),
770
+ )
771
+
772
+ props_updated = 0
773
+ if surviving and retired:
774
+ new_aliases = surviving.aliases.copy()
775
+ if retired_id not in new_aliases:
776
+ new_aliases.append(retired_id)
777
+ for alias in retired.aliases:
778
+ if alias not in new_aliases:
779
+ new_aliases.append(alias)
780
+ await self._db.execute(
781
+ "UPDATE wm_entities SET aliases = ?, updated_at = ? WHERE id = ?",
782
+ (json.dumps(new_aliases), _now_iso(), surviving_id),
783
+ )
784
+ props_updated = len(new_aliases) - len(surviving.aliases)
785
+
786
+ await self._db.execute(
787
+ "DELETE FROM wm_entities WHERE id = ?", (retired_id,)
788
+ )
789
+
790
+ audit_id = _generate_id(MERGE_AUDIT_ID_PREFIX)
791
+ await self._db.execute(
792
+ """
793
+ INSERT INTO wm_merge_log
794
+ (id, surviving_id, retired_id, relationships_repointed,
795
+ properties_updated, executed_by, merge_proposal_id)
796
+ VALUES (?, ?, ?, ?, ?, ?, ?)
797
+ """,
798
+ (
799
+ audit_id,
800
+ surviving_id,
801
+ retired_id,
802
+ rel_count,
803
+ props_updated,
804
+ executed_by,
805
+ proposal_id,
806
+ ),
807
+ )
808
+ await self._db.execute("RELEASE SAVEPOINT execute_merge")
809
+ await self._db.commit()
810
+ return audit_id
811
+ except Exception:
812
+ await self._db.execute("ROLLBACK TO SAVEPOINT execute_merge")
813
+ await self._db.execute("RELEASE SAVEPOINT execute_merge")
814
+ raise
815
+
816
+ # ── Stats ─────────────────────────────────────────────────────────────────
817
+
818
+ async def get_stats(self) -> Dict[str, Any]:
819
+ async with self._db.execute(
820
+ "SELECT entity_type, COUNT(*) as cnt FROM wm_entities GROUP BY entity_type"
821
+ ) as cur:
822
+ type_rows = await cur.fetchall()
823
+
824
+ async with self._db.execute("SELECT COUNT(*) FROM wm_entities") as cur:
825
+ row = await cur.fetchone()
826
+ total_entities = row[0] if row else 0
827
+
828
+ async with self._db.execute("SELECT COUNT(*) FROM wm_relationships") as cur:
829
+ row = await cur.fetchone()
830
+ total_rels = row[0] if row else 0
831
+
832
+ async with self._db.execute(
833
+ "SELECT COUNT(*) FROM wm_relationships WHERE valid_to IS NULL"
834
+ ) as cur:
835
+ row = await cur.fetchone()
836
+ active_rels = row[0] if row else 0
837
+
838
+ async with self._db.execute("SELECT COUNT(*) FROM wm_observations") as cur:
839
+ row = await cur.fetchone()
840
+ total_obs = row[0] if row else 0
841
+
842
+ async with self._db.execute(
843
+ "SELECT COUNT(*) FROM wm_merge_proposals WHERE status = 'pending'"
844
+ ) as cur:
845
+ row = await cur.fetchone()
846
+ pending_proposals = row[0] if row else 0
847
+
848
+ return {
849
+ "total_entities": total_entities,
850
+ "entities_by_type": {r["entity_type"]: r["cnt"] for r in type_rows},
851
+ "total_relationships": total_rels,
852
+ "active_relationships": active_rels,
853
+ "total_observations": total_obs,
854
+ "merge_proposals_pending": pending_proposals,
855
+ }