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,357 @@
1
+ """Cross-channel communication ledger + outreach governance.
2
+
3
+ Gives the agent the WHOLE picture of its relationship traffic with a contact —
4
+ every inbound/outbound exchange across every channel (WhatsApp now, email/SMS
5
+ later) — and a principled decision on whether / how / when to (re)initiate, so it
6
+ never spams and always references the last discussion + open follow-ups before
7
+ reaching out. Proactive outreach to anyone but the owner is gated on owner
8
+ approval by policy.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ from pathlib import Path
15
+ import sqlite3
16
+ import uuid
17
+ from datetime import datetime, timezone
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _now() -> datetime:
24
+ return datetime.now(timezone.utc)
25
+
26
+
27
+ def _parse(ts: Optional[str]) -> Optional[datetime]:
28
+ if not ts:
29
+ return None
30
+ try:
31
+ dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
32
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ class CommsLog:
38
+ """SQLite ledger of communications with each contact, across all channels."""
39
+
40
+ def __init__(self, db_path: str) -> None:
41
+ self._db_path = str(db_path)
42
+ self._conn = sqlite3.connect(str(db_path))
43
+ self._conn.row_factory = sqlite3.Row
44
+ self._conn.execute("PRAGMA journal_mode=WAL")
45
+ self._conn.executescript(
46
+ """
47
+ CREATE TABLE IF NOT EXISTS communications (
48
+ id TEXT PRIMARY KEY,
49
+ contact_id TEXT NOT NULL,
50
+ channel TEXT NOT NULL DEFAULT 'unknown',
51
+ direction TEXT NOT NULL, -- 'in' (they->us) | 'out' (us->them)
52
+ summary TEXT,
53
+ session_id TEXT,
54
+ ts TEXT NOT NULL
55
+ );
56
+ CREATE INDEX IF NOT EXISTS idx_comms_contact ON communications(contact_id, ts);
57
+ """
58
+ )
59
+ existing = {str(row[1]) for row in self._conn.execute(
60
+ "PRAGMA table_info(communications)").fetchall()}
61
+ for name, sql_type in {
62
+ "external_ref": "TEXT",
63
+ "reply_to_ref": "TEXT",
64
+ "reaction": "TEXT",
65
+ "receipt_ref": "TEXT",
66
+ "outbound_ref": "TEXT",
67
+ }.items():
68
+ if name not in existing:
69
+ self._conn.execute(
70
+ f"ALTER TABLE communications ADD COLUMN {name} {sql_type}")
71
+ self._conn.execute(
72
+ "CREATE INDEX IF NOT EXISTS idx_comms_reply_ref "
73
+ "ON communications(contact_id,reply_to_ref,ts)")
74
+ self._conn.commit()
75
+ from .transport_ingress import ensure_schema
76
+ ensure_schema(self._conn)
77
+
78
+ def read_connection(self):
79
+ """An independent read connection for projections running on a worker thread.
80
+
81
+ The caller closes it. The transport writer's thread-bound connection
82
+ and transaction ownership remain unchanged.
83
+ """
84
+ db = sqlite3.connect(Path(self._db_path).resolve().as_uri()+'?mode=ro', uri=True)
85
+ db.row_factory = sqlite3.Row
86
+ return db
87
+
88
+ def log_receipt(self, *, event_id, contact_id, channel, direction, external_ref,
89
+ receipt_ref, occurred_at, status, reply_to_ref='', outbound_ref=''):
90
+ """Idempotent metadata from a trusted transport adapter, never prose."""
91
+ import hashlib
92
+ stamp = _parse(occurred_at)
93
+ if stamp is None or stamp > _now() or direction not in {'in', 'out'}:
94
+ raise ValueError('invalid_transport_observation')
95
+ identifier = 'transport:' + hashlib.sha256(event_id.encode()).hexdigest()
96
+ values = (identifier, contact_id, channel, direction, 'Transport '+status, '',
97
+ stamp.isoformat(), external_ref, reply_to_ref or None, None,
98
+ receipt_ref, outbound_ref or None)
99
+ columns = 'id,contact_id,channel,direction,summary,session_id,ts,external_ref,reply_to_ref,reaction,receipt_ref,outbound_ref'
100
+ with self._conn:
101
+ previous = self._conn.execute('SELECT '+columns+' FROM communications WHERE id=?', (identifier,)).fetchone()
102
+ if previous:
103
+ if tuple(previous) != values:
104
+ raise ValueError('transport_event_conflict')
105
+ return False
106
+ self._conn.execute('INSERT INTO communications ('+columns+') VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', values)
107
+ return True
108
+
109
+ def outbound_receipts(self, *, contact_id, outbound_ref):
110
+ rows = self._conn.execute('''SELECT * FROM communications WHERE contact_id=?
111
+ AND direction='out' AND (outbound_ref=? OR external_ref=?)
112
+ AND receipt_ref IS NOT NULL AND external_ref IS NOT NULL ORDER BY ts,id LIMIT 100''',
113
+ (contact_id, outbound_ref, outbound_ref)).fetchall()
114
+ return [dict(row) for row in rows]
115
+
116
+ def log(self, contact_id: str, *, channel: str = "unknown", direction: str = "in",
117
+ summary: str = "", session_id: str = "",
118
+ external_ref: str = "", reply_to_ref: str = "",
119
+ reaction: str = "", receipt_ref: str = "",
120
+ ts: Optional[str] = None) -> None:
121
+ if not contact_id or direction not in ("in", "out"):
122
+ return
123
+ normalized_reaction = (reaction or "").strip().lower()
124
+ if normalized_reaction not in {
125
+ "", "accepted", "acknowledged", "actioned", "negative",
126
+ "dismissed", "corrected", "rejected", "neutral"}:
127
+ normalized_reaction = "neutral"
128
+ self._conn.execute(
129
+ "INSERT INTO communications "
130
+ "(id,contact_id,channel,direction,summary,session_id,ts,"
131
+ "external_ref,reply_to_ref,reaction,receipt_ref) "
132
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
133
+ (uuid.uuid4().hex, contact_id, channel or "unknown", direction,
134
+ (summary or "")[:500], session_id or "", ts or _now().isoformat(),
135
+ (external_ref or "")[:512] or None,
136
+ (reply_to_ref or "")[:512] or None,
137
+ normalized_reaction or None,
138
+ (receipt_ref or "")[:512] or None),
139
+ )
140
+ self._conn.commit()
141
+
142
+ def history(self, contact_id: str, limit: int = 15) -> List[Dict[str, Any]]:
143
+ rows = self._conn.execute(
144
+ "SELECT channel, direction, summary, ts FROM communications WHERE contact_id=?"
145
+ " ORDER BY ts DESC LIMIT ?", (contact_id, limit)).fetchall()
146
+ return [dict(r) for r in rows]
147
+
148
+ def last_per_channel(self, contact_id: str) -> Dict[str, Dict[str, Any]]:
149
+ rows = self._conn.execute(
150
+ "SELECT channel, direction, summary, MAX(ts) AS ts FROM communications"
151
+ " WHERE contact_id=? GROUP BY channel", (contact_id,)).fetchall()
152
+ return {r["channel"]: {"direction": r["direction"], "summary": r["summary"], "ts": r["ts"]}
153
+ for r in rows}
154
+
155
+ def last_outbound(self, contact_id: str) -> Optional[Dict[str, Any]]:
156
+ r = self._conn.execute(
157
+ "SELECT channel, summary, ts FROM communications WHERE contact_id=? AND direction='out'"
158
+ " ORDER BY ts DESC LIMIT 1", (contact_id,)).fetchone()
159
+ return dict(r) if r else None
160
+
161
+ def inbound_since(self, contact_id: str, since_iso: str) -> List[str]:
162
+ """Timestamps of inbound rows from a contact since an ISO instant
163
+ (selfhood benchmark: did the owner respond after a delivery)."""
164
+ rows = self._conn.execute(
165
+ "SELECT ts FROM communications WHERE contact_id=? AND"
166
+ " direction='in' AND ts >= ? ORDER BY ts ASC LIMIT 5000",
167
+ (contact_id, since_iso)).fetchall()
168
+ return [r["ts"] for r in rows]
169
+
170
+ def reactions_for_refs(
171
+ self,
172
+ contact_id: str,
173
+ refs: List[str],
174
+ *,
175
+ since_iso: str,
176
+ until_iso: str,
177
+ ) -> List[Dict[str, Any]]:
178
+ """Explicit inbound reactions tied to exact outbound references."""
179
+
180
+ bounded = sorted({str(ref) for ref in refs if str(ref).strip()})[:5000]
181
+ if not bounded:
182
+ return []
183
+ placeholders = ",".join("?" for _ in bounded)
184
+ rows = self._conn.execute(
185
+ "SELECT id,channel,summary,ts,external_ref,reply_to_ref,reaction,"
186
+ "receipt_ref FROM communications WHERE contact_id=? "
187
+ "AND direction='in' AND ts>=? AND ts<? AND reply_to_ref IN ("
188
+ f"{placeholders}) AND reaction IS NOT NULL ORDER BY ts",
189
+ [contact_id, since_iso, until_iso, *bounded],
190
+ ).fetchall()
191
+ return [dict(row) for row in rows]
192
+
193
+ def match_reply(self, *, contact_id: str, outbound_ref: str, since_iso: str,
194
+ until_iso: Optional[str] = None, connection=None) -> Dict[str, Any]:
195
+ """Exact receipt-backed reply references for a previously resolved contact.
196
+
197
+ A reply link proves which message was addressed, not whether its requested
198
+ answer or artifact was supplied. The waiting-condition owner checks that.
199
+ Neither similar text nor an unrelated inbound message counts as a reply.
200
+ """
201
+ start, end = _parse(since_iso), _parse(until_iso) if until_iso else _now()
202
+ if not contact_id or not outbound_ref or start is None or end is None or end < start:
203
+ raise ValueError('invalid_reply_window')
204
+ rows = (connection or self._conn).execute('''SELECT channel,ts,external_ref,reply_to_ref,reaction,receipt_ref
205
+ FROM communications WHERE contact_id=? AND direction='in' AND reply_to_ref=?
206
+ AND external_ref IS NOT NULL AND external_ref!=''
207
+ AND receipt_ref IS NOT NULL AND receipt_ref!='' ORDER BY ts LIMIT 500''',
208
+ (contact_id, outbound_ref)).fetchall()
209
+ matches, seen = [], set()
210
+ for row in rows:
211
+ at = _parse(row['ts'])
212
+ key = (row['external_ref'], row['receipt_ref'])
213
+ if at is not None and start <= at <= end and key not in seen:
214
+ matches.append(dict(row))
215
+ seen.add(key)
216
+ return {'status': 'matched' if matches else 'unrelated', 'contact_id': contact_id,
217
+ 'outbound_ref': outbound_ref, 'matches': matches,
218
+ 'condition_satisfied': False, 'coverage_limited': len(rows) == 500}
219
+
220
+ def outbound_between(
221
+ self,
222
+ contact_id: str,
223
+ since_iso: str,
224
+ until_iso: str,
225
+ *,
226
+ require_receipt: bool = False,
227
+ ) -> List[Dict[str, Any]]:
228
+ """One auditable outbound cohort for benchmark denominators."""
229
+
230
+ query = (
231
+ "SELECT id,channel,summary,ts,external_ref,receipt_ref "
232
+ "FROM communications WHERE contact_id=? AND direction='out' "
233
+ "AND ts>=? AND ts<?")
234
+ if require_receipt:
235
+ query += " AND receipt_ref IS NOT NULL AND receipt_ref!=''"
236
+ query += " ORDER BY ts"
237
+ rows = self._conn.execute(
238
+ query, (contact_id, since_iso, until_iso)).fetchall()
239
+ return [dict(row) for row in rows]
240
+
241
+ def counts(self, contact_id: str) -> Dict[str, Any]:
242
+ r = self._conn.execute(
243
+ "SELECT SUM(direction='in') AS inbound, SUM(direction='out') AS outbound,"
244
+ " COUNT(DISTINCT channel) AS channels FROM communications WHERE contact_id=?",
245
+ (contact_id,)).fetchone()
246
+ return {"inbound": r["inbound"] or 0, "outbound": r["outbound"] or 0,
247
+ "channels": r["channels"] or 0}
248
+
249
+ def stats(self, contact_id: str, *, since_days: int = 90) -> Dict[str, Any]:
250
+ """Channel-usage counts and interaction-hour histogram (UTC hours;
251
+ the consumer shifts into the contact's timezone). Feeds the
252
+ relationship profiler's approach guidance (preferred channel,
253
+ best time to reach)."""
254
+ rows = self._conn.execute(
255
+ "SELECT channel, ts FROM communications"
256
+ " WHERE contact_id=? AND ts >= datetime('now', ?)",
257
+ (contact_id, f"-{int(since_days)} day")).fetchall()
258
+ channels: Dict[str, int] = {}
259
+ hours = [0] * 24
260
+ for r in rows:
261
+ channels[r["channel"]] = channels.get(r["channel"], 0) + 1
262
+ try:
263
+ hours[int(str(r["ts"])[11:13])] += 1
264
+ except (ValueError, IndexError):
265
+ pass
266
+ return {"total": len(rows), "channels": channels, "hours_utc": hours}
267
+
268
+ def recent(self, limit: int = 50) -> List[Dict[str, Any]]:
269
+ """The newest exchanges across ALL contacts and channels, most recent
270
+ first. The per-contact reads above answer 'how do I stand with X';
271
+ this answers 'what has been flowing lately' for an ops ledger view."""
272
+ limit = max(1, min(500, int(limit)))
273
+ rows = self._conn.execute(
274
+ "SELECT contact_id, channel, direction, summary, ts FROM communications"
275
+ " ORDER BY ts DESC LIMIT ?", (limit,)).fetchall()
276
+ return [dict(r) for r in rows]
277
+
278
+ def rollup(self, *, since_days: int = 30) -> Dict[str, Dict[str, int]]:
279
+ """Inbound/outbound counts per channel over a window: the ledger's
280
+ flow summary. ``{channel: {"in": n, "out": n}}``."""
281
+ rows = self._conn.execute(
282
+ "SELECT channel, direction, COUNT(*) AS n FROM communications"
283
+ " WHERE ts >= datetime('now', ?) GROUP BY channel, direction",
284
+ (f"-{int(since_days)} day",)).fetchall()
285
+ out: Dict[str, Dict[str, int]] = {}
286
+ for r in rows:
287
+ ch = out.setdefault(r["channel"], {"in": 0, "out": 0})
288
+ if r["direction"] in ("in", "out"):
289
+ ch[r["direction"]] = r["n"]
290
+ return out
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # Outreach governance
295
+ # ---------------------------------------------------------------------------
296
+ def evaluate_outreach(
297
+ contact: Any,
298
+ *,
299
+ is_owner: bool = False,
300
+ last_outbound_ts: Optional[str] = None,
301
+ cadence_days: Optional[float] = None,
302
+ overdue: bool = False,
303
+ open_followups: Optional[List[str]] = None,
304
+ suggested_channel: str = "",
305
+ now: Optional[datetime] = None,
306
+ ) -> Dict[str, Any]:
307
+ """Decide whether the agent should (re)initiate contact, and how.
308
+
309
+ Returns: should_contact, reason, requires_owner_approval, suggested_channel,
310
+ cooldown_active, talking_points. Policy: never contact a blocked contact;
311
+ respect a cooldown so we don't double-message; only reach out when there's a
312
+ real reason (overdue cadence or an open follow-up); and ANY proactive outreach
313
+ to someone other than the owner requires the owner's approval first.
314
+ """
315
+ now = now or _now()
316
+ followups = [f for f in (open_followups or []) if f]
317
+ allowed = getattr(contact, "interaction_allowed", True)
318
+
319
+ result = {
320
+ "should_contact": False,
321
+ "reason": "",
322
+ "requires_owner_approval": (not is_owner),
323
+ "suggested_channel": suggested_channel or "",
324
+ "cooldown_active": False,
325
+ "talking_points": followups[:5],
326
+ }
327
+
328
+ if not allowed:
329
+ result["reason"] = "contact is not authorized for outreach (interaction_allowed=false)"
330
+ result["requires_owner_approval"] = True
331
+ return result
332
+
333
+ # Cooldown: don't reach out again too soon after our last outbound.
334
+ last_out = _parse(last_outbound_ts)
335
+ if last_out is not None:
336
+ hrs = (now - last_out).total_seconds() / 3600.0
337
+ cooldown_hrs = max(24.0, (float(cadence_days) * 24.0 / 2.0) if cadence_days else 24.0)
338
+ if hrs < cooldown_hrs:
339
+ result["cooldown_active"] = True
340
+ result["reason"] = (f"reached out {round(hrs)}h ago; in cooldown "
341
+ f"(~{round(cooldown_hrs)}h) — hold off to avoid spamming")
342
+ return result
343
+
344
+ reasons = []
345
+ if overdue:
346
+ reasons.append("overdue vs their usual cadence"
347
+ + (f" (~{round(cadence_days)}d)" if cadence_days else ""))
348
+ if followups:
349
+ reasons.append(f"{len(followups)} open follow-up(s)")
350
+
351
+ if not reasons:
352
+ result["reason"] = "no current reason to reach out (not overdue, no open follow-ups)"
353
+ return result
354
+
355
+ result["should_contact"] = True
356
+ result["reason"] = "; ".join(reasons)
357
+ return result
@@ -0,0 +1,79 @@
1
+ """Colony Contacts — configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class ContactsEnrichmentConfig:
11
+ enabled: bool = True
12
+ min_update_confidence: float = 0.70
13
+ immutable_confidence_threshold: float = 0.95
14
+ respect_privacy_restricted: bool = True
15
+
16
+
17
+ @dataclass
18
+ class ContactsImportConfig:
19
+ normalize_phones: bool = True
20
+ default_phone_region: str = "US"
21
+ max_batch_size: int = 5000
22
+
23
+
24
+ @dataclass
25
+ class ContactsExportConfig:
26
+ vcard_include_tier: bool = False
27
+ vcard_include_tags: bool = True
28
+
29
+
30
+ @dataclass
31
+ class ContactsConfig:
32
+ enabled: bool = True
33
+ sqlite_path: str = ":memory:"
34
+
35
+ @classmethod
36
+ def from_env(cls) -> "ContactsConfig":
37
+ """Build config from the environment, persisting to the state dir.
38
+
39
+ The bare default of ":memory:" exists for tests; a production
40
+ sidecar must survive restarts (the IdentityResolver treats the
41
+ contact store as the source of truth for the owner), so the
42
+ server path resolves COLONY_CONTACTS_DB or falls back to
43
+ ``$COLONY_STATE_DIR/colony-contacts.db``.
44
+ """
45
+ path = os.environ.get("COLONY_CONTACTS_DB")
46
+ if not path:
47
+ # Anchor to the real state dir, never the process CWD: a
48
+ # cwd-relative default silently forks the contact store per
49
+ # working directory (the same failure class as the world-model
50
+ # store incident of 2026-07-04).
51
+ state_dir = os.environ.get("COLONY_STATE_DIR", "")
52
+ if not state_dir:
53
+ state_dir = os.path.join(os.path.expanduser("~"),
54
+ ".colony", "data")
55
+ path = os.path.join(state_dir, "colony-contacts.db")
56
+ auto_promote = os.environ.get("COLONY_AUTO_PROMOTE_GROUP_TO_1ON1", "").strip().lower() in (
57
+ "1", "true", "yes", "on")
58
+ try:
59
+ min_int = int(os.environ.get("COLONY_GROUP_PROMOTE_MIN_INTERACTIONS", "") or 5)
60
+ except ValueError:
61
+ min_int = 5
62
+ return cls(sqlite_path=path, auto_promote_group_to_1on1=auto_promote,
63
+ group_promote_min_interactions=min_int)
64
+ default_inbound_tier: str = "unknown"
65
+ unknown_tier_grace_hours: int = 48
66
+ auto_merge_confidence_threshold: float = 0.95
67
+ merge_proposal_threshold: float = 0.60
68
+ soft_delete_retention_days: int = 30
69
+ audit_retention_days: int = 365
70
+ brief_on_tier_change: bool = True
71
+ brief_on_merge_proposals: bool = True
72
+ enrichment: ContactsEnrichmentConfig = field(default_factory=ContactsEnrichmentConfig)
73
+ import_cfg: ContactsImportConfig = field(default_factory=ContactsImportConfig)
74
+ export_cfg: ContactsExportConfig = field(default_factory=ContactsExportConfig)
75
+ # When True, a scope (group) member with sustained contact is auto-promoted to a
76
+ # global ``regular`` 1:1 contact. When False (default), such members are surfaced as
77
+ # promotion candidates for the owner to approve — membership never auto-grants 1:1.
78
+ auto_promote_group_to_1on1: bool = False
79
+ group_promote_min_interactions: int = 5
@@ -0,0 +1 @@
1
+ """Colony Contacts exporters."""
@@ -0,0 +1,71 @@
1
+ """vCard 4.0 exporter (RFC 6350)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+ from ..models import Contact, ContactHandle
8
+
9
+
10
+ class VCardExporter:
11
+ """Exports contact records to vCard 4.0 format (RFC 6350).
12
+
13
+ Args:
14
+ include_tags: If True, export Colony tags as X-COLONY-TAGS property.
15
+ include_tier: If True, export trust tier as X-COLONY-TIER property.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ include_tags: bool = True,
21
+ include_tier: bool = False,
22
+ ) -> None:
23
+ self._include_tags = include_tags
24
+ self._include_tier = include_tier
25
+
26
+ def export_one(self, contact: Contact, handles: List[ContactHandle]) -> str:
27
+ """Render a single contact as a vCard 4.0 string.
28
+
29
+ Returns:
30
+ A vCard 4.0 string with CRLF line endings per RFC 6350.
31
+ """
32
+ lines = [
33
+ "BEGIN:VCARD",
34
+ "VERSION:4.0",
35
+ ]
36
+ fn = contact.display_name or "Unknown"
37
+ lines.append(f"FN:{fn}")
38
+
39
+ if contact.given_name or contact.family_name:
40
+ family = contact.family_name or ""
41
+ given = contact.given_name or ""
42
+ lines.append(f"N:{family};{given};;;")
43
+
44
+ if contact.organization:
45
+ lines.append(f"ORG:{contact.organization}")
46
+
47
+ for handle in handles:
48
+ if handle.gateway in ("imessage", "sms"):
49
+ lines.append(f"TEL;TYPE=CELL:{handle.address}")
50
+ elif handle.gateway == "email":
51
+ lines.append(f"EMAIL:{handle.address}")
52
+ elif handle.gateway == "telegram":
53
+ lines.append(f"X-TELEGRAM:{handle.address}")
54
+ elif handle.gateway == "signal":
55
+ lines.append(f"X-SIGNAL:{handle.address}")
56
+
57
+ if self._include_tags and contact.tags:
58
+ lines.append(f"X-COLONY-TAGS:{','.join(contact.tags)}")
59
+
60
+ if self._include_tier:
61
+ lines.append(f"X-COLONY-TIER:{contact.trust_tier}")
62
+
63
+ lines.append(f"UID:{contact.contact_id}")
64
+ lines.append("END:VCARD")
65
+ return "\r\n".join(lines) + "\r\n"
66
+
67
+ def export_many(self, contacts_with_handles) -> str:
68
+ """Export multiple contacts as concatenated vCard entries."""
69
+ return "".join(
70
+ self.export_one(c, h) for c, h in contacts_with_handles
71
+ )