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,560 @@
1
+ """Colony relationship scoring — evidence-based assessment of Colony's own social world.
2
+
3
+ Colony maintains its own relationship scores for every person it interacts with across
4
+ connected gateways (iMessage, Telegram, etc.). These scores represent Colony's independent
5
+ assessment of each relationship, built from interaction patterns Colony itself observes.
6
+
7
+ Two-layer scoring model
8
+ -----------------------
9
+ Owner layer (``owner_guidance`` weight = 0.18): the owner's explicit input informs
10
+ Colony's view but does not dominate it. Owner input is authoritative at onboarding
11
+ when Colony has no history; its relative weight naturally declines as Colony accumulates
12
+ its own evidence.
13
+
14
+ Colony observation layer (remaining 0.82): all signals Colony observes directly —
15
+ message frequency, sentiment, latency, initiative, recency — plus three Colony-specific
16
+ signals that capture the quality of Colony's own engagement: ``colony_initiative_response``
17
+ (do people respond to Colony's outreach?), ``colony_effectiveness`` (do Colony's actions
18
+ help?), and ``colony_style_fit`` (is Colony's adapted style working?).
19
+
20
+ A score of 80+ means Colony has observed strong, consistent engagement with this person
21
+ over time — not merely that the owner considers them important.
22
+ """
23
+ from dataclasses import dataclass, field
24
+ from typing import Optional, Dict, Any, List
25
+ from datetime import datetime, timezone
26
+ import logging
27
+ import math
28
+ import time
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class ScoreWeights:
35
+ """Weights for different scoring dimensions.
36
+
37
+ Total must sum to 1.0.
38
+
39
+ Interaction signals are Colony's direct observations of the exchange.
40
+ Owner guidance informs but does not dominate.
41
+ Colony-specific signals capture the quality of Colony's own behavior.
42
+ """
43
+ # Interaction signals (Colony's observations of the exchange)
44
+ message_frequency: float = 0.12
45
+ response_latency: float = 0.08
46
+ sentiment: float = 0.12
47
+ initiative_ratio: float = 0.08
48
+ meeting_coattendance: float = 0.08
49
+ task_involvement: float = 0.08
50
+ recency: float = 0.08
51
+
52
+ # Owner guidance (informs but does not dominate)
53
+ owner_guidance: float = 0.18
54
+
55
+ # Colony-specific signals (new)
56
+ colony_initiative_response: float = 0.08 # response rate to Colony's own outreach
57
+ colony_effectiveness: float = 0.06 # are Colony's suggestions/actions valued?
58
+ colony_style_fit: float = 0.04 # how well Colony's adapted style is received
59
+
60
+ def total(self) -> float:
61
+ return sum([
62
+ self.message_frequency,
63
+ self.response_latency,
64
+ self.sentiment,
65
+ self.initiative_ratio,
66
+ self.meeting_coattendance,
67
+ self.task_involvement,
68
+ self.recency,
69
+ self.owner_guidance,
70
+ self.colony_initiative_response,
71
+ self.colony_effectiveness,
72
+ self.colony_style_fit,
73
+ ])
74
+
75
+
76
+ @dataclass
77
+ class ScoreBreakdown:
78
+ """Breakdown of Colony's score into source layers.
79
+
80
+ Attributes:
81
+ person_id: The person this breakdown is for
82
+ total_score: Final 0-100 score
83
+ owner_layer_contribution: Points contributed by owner_guidance signal
84
+ colony_observation_contribution: Points from contact-behavior signals
85
+ colony_action_contribution: Points from Colony's own behavior signals
86
+ dominant_signal: Which single signal most influenced the score
87
+ """
88
+ person_id: str
89
+ total_score: float
90
+ owner_layer_contribution: float
91
+ colony_observation_contribution: float
92
+ colony_action_contribution: float
93
+ dominant_signal: str
94
+
95
+
96
+ @dataclass
97
+ class ScoreAuditEntry:
98
+ """Immutable audit record for score changes."""
99
+ person_id: str
100
+ old_score: float
101
+ new_score: float
102
+ old_tier: str
103
+ new_tier: str
104
+ delta: float
105
+ reason: str
106
+ timestamp: datetime
107
+
108
+
109
+ @dataclass
110
+ class TierChangeProposal:
111
+ """A Colony-generated proposal to change a person's tier.
112
+
113
+ Colony surfaces these via the briefing system. Owner approval is required
114
+ before any tier change takes effect.
115
+ """
116
+ person_id: str
117
+ current_tier: str
118
+ proposed_tier: str
119
+ direction: str # "upward" | "downward"
120
+ evidence_score: float
121
+ reasoning: str
122
+ created_at: datetime = field(default_factory=datetime.now)
123
+
124
+
125
+ @dataclass
126
+ class _SignalRecord:
127
+ """Internal container for signal data from Neo4j."""
128
+ signal_type: str
129
+ normalized_value: float
130
+ days_old: float
131
+ direction: str = "contact"
132
+
133
+
134
+ class RelationshipScorer:
135
+ """Colony's evidence-based scoring of its own relationships with each person.
136
+
137
+ Scores are Colony's independent assessment derived from interaction signals
138
+ (message patterns, sentiment, engagement, response cadence) observed across
139
+ all connected gateways. The owner's guidance is factored in via the
140
+ owner_guidance weight (0.18) but does not override Colony's evidence-based view.
141
+
142
+ Colony-specific signals (colony_initiative_response, colony_effectiveness,
143
+ colony_style_fit) capture the quality of Colony's own engagement behavior and
144
+ carry a combined 0.18 weight.
145
+
146
+ A score of 80+ means Colony has observed strong, consistent engagement with
147
+ this person over time — not merely that the owner considers them important.
148
+ """
149
+
150
+ TIER_THRESHOLDS = {
151
+ "inner_circle": 80,
152
+ "trusted": 60,
153
+ "regular": 30,
154
+ "peripheral": 10,
155
+ }
156
+ WINDOW_DAYS = 90
157
+
158
+ # Normalization ranges for each signal type
159
+ SIGNAL_RANGES = {
160
+ "message_frequency": (0, 20, False), # (min, max, inverted)
161
+ "response_latency": (0, 480, True), # Lower is better
162
+ "sentiment": (-1, 1, False),
163
+ "initiative_ratio": (0, 1, False),
164
+ "message_length": (-3, 3, False), # Z-score
165
+ # Colony-specific signals are already normalized 0-1
166
+ "colony_initiative_response": (0, 1, False),
167
+ "colony_effectiveness": (0, 1, False),
168
+ "colony_style_fit": (0, 1, False),
169
+ "contact_response_to_colony": (0, 1, False),
170
+ }
171
+
172
+ # Colony-specific signal types (direction="colony" or "bilateral")
173
+ COLONY_SIGNAL_TYPES = frozenset({
174
+ "colony_initiative",
175
+ "colony_initiative_response",
176
+ "colony_effectiveness",
177
+ "contact_response_to_colony",
178
+ "colony_style_fit",
179
+ "colony_style_signal",
180
+ })
181
+
182
+ def __init__(self, graph: "ColonyGraph", weights: Optional[ScoreWeights] = None, metrics=None):
183
+ self._metrics = metrics # Optional ColonyMetricsCollector
184
+ self.graph = graph
185
+ self.weights = weights or ScoreWeights()
186
+
187
+ async def compute_score(
188
+ self,
189
+ person_id: str,
190
+ ) -> tuple[float, str]:
191
+ """Compute relationship score from signals in window."""
192
+ cutoff = datetime.now(timezone.utc) - timedelta(days=self.WINDOW_DAYS)
193
+
194
+ # Fetch signals from graph
195
+ signals = await self._fetch_signals(person_id)
196
+
197
+ if not signals:
198
+ return 50.0, "regular" # Default
199
+
200
+ # Group by type, apply recency weighting
201
+ type_scores: Dict[str, float] = {}
202
+ type_counts: Dict[str, int] = {}
203
+
204
+ for sig in signals:
205
+ sig_type = sig.signal_type
206
+ value = sig.normalized_value
207
+
208
+ # Exponential recency decay
209
+ decay = math.exp(-sig.days_old / self.WINDOW_DAYS)
210
+
211
+ if sig_type not in type_scores:
212
+ type_scores[sig_type] = 0.0
213
+ type_counts[sig_type] = 0
214
+ type_scores[sig_type] += value * decay
215
+ type_counts[sig_type] += 1
216
+
217
+ # Normalize and weight
218
+ final_score = 0.0
219
+ weight_map = {
220
+ "message_frequency": self.weights.message_frequency,
221
+ "response_latency": self.weights.response_latency,
222
+ "sentiment": self.weights.sentiment,
223
+ "initiative_ratio": self.weights.initiative_ratio,
224
+ "message_length": self.weights.message_frequency * 0.5, # Split weight
225
+ # Colony-specific signals
226
+ "colony_initiative_response": self.weights.colony_initiative_response,
227
+ "contact_response_to_colony": self.weights.colony_initiative_response,
228
+ "colony_effectiveness": self.weights.colony_effectiveness,
229
+ "colony_style_fit": self.weights.colony_style_fit,
230
+ "colony_style_signal": self.weights.colony_style_fit,
231
+ }
232
+
233
+ for sig_type, total in type_scores.items():
234
+ if sig_type in weight_map and type_counts[sig_type] > 0:
235
+ avg = total / type_counts[sig_type]
236
+ normalized = self._normalize_to_100(sig_type, avg)
237
+ final_score += normalized * weight_map[sig_type]
238
+
239
+ # Owner's guidance contributes as one weighted signal.
240
+ # Informs Colony's assessment without overriding it.
241
+ final_score += 50.0 * self.weights.owner_guidance
242
+
243
+ # Clamp to 0-100
244
+ final_score = max(0.0, min(100.0, final_score))
245
+
246
+ # Determine tier
247
+ tier = self.tier_for(final_score)
248
+
249
+ if self._metrics is not None:
250
+ try:
251
+ self._metrics.record_score_computed(final_score)
252
+ except Exception:
253
+ pass
254
+
255
+ return final_score, tier
256
+
257
+ async def compute_score_with_breakdown(
258
+ self,
259
+ person_id: str,
260
+ ) -> tuple[float, str, ScoreBreakdown]:
261
+ """Compute score and return a ScoreBreakdown explaining source contributions."""
262
+ cutoff = datetime.now(timezone.utc) - timedelta(days=self.WINDOW_DAYS)
263
+ signals = await self._fetch_signals(person_id)
264
+
265
+ if not signals:
266
+ bd = ScoreBreakdown(
267
+ person_id=person_id,
268
+ total_score=50.0,
269
+ owner_layer_contribution=50.0 * self.weights.owner_guidance,
270
+ colony_observation_contribution=0.0,
271
+ colony_action_contribution=0.0,
272
+ dominant_signal="owner_guidance",
273
+ )
274
+ return 50.0, "regular", bd
275
+
276
+ type_scores: Dict[str, float] = {}
277
+ type_counts: Dict[str, int] = {}
278
+
279
+ for sig in signals:
280
+ decay = math.exp(-sig.days_old / self.WINDOW_DAYS)
281
+ if sig.signal_type not in type_scores:
282
+ type_scores[sig.signal_type] = 0.0
283
+ type_counts[sig.signal_type] = 0
284
+ type_scores[sig.signal_type] += sig.normalized_value * decay
285
+ type_counts[sig.signal_type] += 1
286
+
287
+ weight_map = {
288
+ "message_frequency": self.weights.message_frequency,
289
+ "response_latency": self.weights.response_latency,
290
+ "sentiment": self.weights.sentiment,
291
+ "initiative_ratio": self.weights.initiative_ratio,
292
+ "message_length": self.weights.message_frequency * 0.5,
293
+ "colony_initiative_response": self.weights.colony_initiative_response,
294
+ "contact_response_to_colony": self.weights.colony_initiative_response,
295
+ "colony_effectiveness": self.weights.colony_effectiveness,
296
+ "colony_style_fit": self.weights.colony_style_fit,
297
+ "colony_style_signal": self.weights.colony_style_fit,
298
+ }
299
+
300
+ colony_action_signals = {
301
+ "colony_initiative_response", "contact_response_to_colony",
302
+ "colony_effectiveness", "colony_style_fit", "colony_style_signal",
303
+ }
304
+
305
+ contributions: Dict[str, float] = {}
306
+ observation_total = 0.0
307
+ action_total = 0.0
308
+
309
+ for sig_type, total in type_scores.items():
310
+ if sig_type in weight_map and type_counts[sig_type] > 0:
311
+ avg = total / type_counts[sig_type]
312
+ normalized = self._normalize_to_100(sig_type, avg)
313
+ contribution = normalized * weight_map[sig_type]
314
+ contributions[sig_type] = contribution
315
+ if sig_type in colony_action_signals:
316
+ action_total += contribution
317
+ else:
318
+ observation_total += contribution
319
+
320
+ owner_contribution = 50.0 * self.weights.owner_guidance
321
+ total_score = max(0.0, min(100.0, observation_total + action_total + owner_contribution))
322
+ tier = self.tier_for(total_score)
323
+
324
+ dominant = max(contributions, key=contributions.get) if contributions else "owner_guidance"
325
+
326
+ bd = ScoreBreakdown(
327
+ person_id=person_id,
328
+ total_score=total_score,
329
+ owner_layer_contribution=owner_contribution,
330
+ colony_observation_contribution=observation_total,
331
+ colony_action_contribution=action_total,
332
+ dominant_signal=dominant,
333
+ )
334
+ return total_score, tier, bd
335
+
336
+ def tier_for(self, score: float) -> str:
337
+ """Map score to tier name."""
338
+ for tier_name, threshold in sorted(
339
+ self.TIER_THRESHOLDS.items(),
340
+ key=lambda x: -x[1]
341
+ ):
342
+ if score >= threshold:
343
+ return tier_name
344
+ return "peripheral"
345
+
346
+ async def record_score_change(
347
+ self,
348
+ person_id: str,
349
+ new_score: float,
350
+ new_tier: str,
351
+ old_score: float,
352
+ reason: str,
353
+ store=None,
354
+ ) -> ScoreAuditEntry:
355
+ """Atomic score update with audit trail.
356
+
357
+ ``store`` (optional SQLiteContactStore) is forwarded to the graph so the
358
+ behavioral score reverse-syncs onto the linked contact row. Without it the
359
+ computed score dead-ends in the graph and the contact's relationship_score
360
+ never reflects behavioral signals (the "scoring starved" symptom)."""
361
+ old_tier = self.tier_for(old_score)
362
+ entry = ScoreAuditEntry(
363
+ person_id=person_id,
364
+ old_score=old_score,
365
+ new_score=new_score,
366
+ old_tier=old_tier,
367
+ new_tier=new_tier,
368
+ delta=new_score - old_score,
369
+ reason=reason,
370
+ timestamp=datetime.now(timezone.utc),
371
+ )
372
+
373
+ # Persist to graph (and reverse-sync to SQLite when a store is supplied)
374
+ await self.graph.record_score_change(
375
+ person_id=person_id,
376
+ new_score=new_score,
377
+ new_tier=new_tier,
378
+ old_score=old_score,
379
+ reason=reason,
380
+ store=store,
381
+ )
382
+
383
+ return entry
384
+
385
+ async def refresh_all_scores(self, store=None) -> Dict[str, tuple]:
386
+ """Recompute scores for all active persons.
387
+
388
+ ``store`` is forwarded to each ``record_score_change`` so behavioral scores
389
+ reverse-sync onto the linked SQLite contact rows."""
390
+ t0 = time.monotonic()
391
+ results = {}
392
+ no_signal_count = 0
393
+ tier_changes = 0
394
+ persons = await self.graph.get_all_people()
395
+
396
+ for person in persons:
397
+ person_id = person.get("id")
398
+ if not person_id:
399
+ continue
400
+
401
+ old_score = person.get("score", 50.0)
402
+ old_tier = self.tier_for(old_score)
403
+ new_score, new_tier = await self.compute_score(person_id)
404
+
405
+ if new_score == 50.0 and new_tier == "regular":
406
+ no_signal_count += 1
407
+
408
+ if new_tier != old_tier:
409
+ tier_changes += 1
410
+ if self._metrics is not None:
411
+ try:
412
+ direction = "upward" if new_score > old_score else "downward"
413
+ self._metrics.record_tier_change(direction)
414
+ except Exception:
415
+ pass
416
+
417
+ # Skip the no-signal default (50/"regular") — persisting it would
418
+ # clobber the contact's interaction-derived score. Only record a
419
+ # genuine, signal-backed change.
420
+ is_no_signal_default = (new_score == 50.0 and new_tier == "regular")
421
+ if not is_no_signal_default and abs(new_score - old_score) > 0.5:
422
+ await self.record_score_change(
423
+ person_id, new_score, new_tier, old_score, "periodic_refresh", store=store
424
+ )
425
+
426
+ results[person_id] = (new_score, new_tier)
427
+
428
+ elapsed_ms = (time.monotonic() - t0) * 1000
429
+ logger.info(
430
+ "Refresh scores: %d persons, %d tier changes, %d no-signal in %.0fms",
431
+ len(results), tier_changes, no_signal_count, elapsed_ms,
432
+ )
433
+ if self._metrics is not None:
434
+ try:
435
+ self._metrics.record_scoring_run(
436
+ latency_ms=elapsed_ms,
437
+ persons_scored=len(results),
438
+ tier_changes=tier_changes,
439
+ )
440
+ self._metrics.set_persons_no_signals(no_signal_count)
441
+ except Exception:
442
+ pass
443
+
444
+ return results
445
+
446
+ def propose_tier_change(
447
+ self,
448
+ person_id: str,
449
+ current_tier: str,
450
+ evidence_score: float,
451
+ days_at_score: int = 0,
452
+ interaction_count: int = 0,
453
+ ) -> Optional[TierChangeProposal]:
454
+ """Generate a tier-change proposal based on Colony's evidence.
455
+
456
+ Colony MAY propose tier changes (upward or downward) based on its
457
+ own observations. Proposals are surfaced via the briefing system and
458
+ require owner approval before taking effect.
459
+
460
+ Upward proposals require the score to have been above the threshold
461
+ for 30+ days with ≥20 interactions.
462
+
463
+ Downward proposals are triggered when the score has been below the
464
+ threshold for 21+ days with no manual override in place.
465
+
466
+ Returns a TierChangeProposal or None if no proposal is warranted.
467
+ """
468
+ evidence_tier = self.tier_for(evidence_score)
469
+
470
+ if evidence_tier == current_tier:
471
+ return None
472
+
473
+ tier_order = ["peripheral", "regular", "trusted", "inner_circle"]
474
+
475
+ try:
476
+ current_idx = tier_order.index(current_tier)
477
+ evidence_idx = tier_order.index(evidence_tier)
478
+ except ValueError:
479
+ return None
480
+
481
+ if evidence_idx > current_idx:
482
+ # Upward proposal
483
+ if days_at_score < 30 or interaction_count < 20:
484
+ return None
485
+ direction = "upward"
486
+ reasoning = (
487
+ f"Colony has observed consistent engagement from this person "
488
+ f"(score {evidence_score:.0f}, {interaction_count} interactions "
489
+ f"over {days_at_score} days). Evidence suggests moving from "
490
+ f"{current_tier} to {evidence_tier}. Approve?"
491
+ )
492
+ else:
493
+ # Downward proposal
494
+ if days_at_score < 21:
495
+ return None
496
+ direction = "downward"
497
+ reasoning = (
498
+ f"Colony's engagement with this person has declined "
499
+ f"(score {evidence_score:.0f} for {days_at_score} days). "
500
+ f"Suggest moving from {current_tier} to {evidence_tier} "
501
+ f"unless this person should remain {current_tier}."
502
+ )
503
+
504
+ return TierChangeProposal(
505
+ person_id=person_id,
506
+ current_tier=current_tier,
507
+ proposed_tier=evidence_tier,
508
+ direction=direction,
509
+ evidence_score=evidence_score,
510
+ reasoning=reasoning,
511
+ )
512
+
513
+ async def _fetch_signals(
514
+ self,
515
+ person_id: str,
516
+ ) -> list:
517
+ """Fetch signals from Neo4j within window."""
518
+ try:
519
+ raw_signals = await self.graph.get_recent_signals(
520
+ person_id, hours=self.WINDOW_DAYS * 24
521
+ )
522
+
523
+ signals = []
524
+ now = datetime.now(timezone.utc)
525
+ for sig in raw_signals:
526
+ days_old = (now - sig.timestamp).total_seconds() / 86400
527
+ direction = getattr(sig, "direction", "contact")
528
+ signals.append(_SignalRecord(
529
+ signal_type=sig.signal_type,
530
+ normalized_value=sig.normalized_value,
531
+ days_old=days_old,
532
+ direction=direction,
533
+ ))
534
+ return signals
535
+ except Exception:
536
+ return []
537
+
538
+ def _normalize_to_100(self, signal_type: str, value: float) -> float:
539
+ """Normalize signal value to 0-100 range."""
540
+ if signal_type not in self.SIGNAL_RANGES:
541
+ return 50.0 # Neutral
542
+
543
+ min_val, max_val, inverted = self.SIGNAL_RANGES[signal_type]
544
+
545
+ # Clamp value to range
546
+ clamped = max(min_val, min(max_val, value))
547
+
548
+ # Normalize to 0-1
549
+ normalized = (clamped - min_val) / (max_val - min_val)
550
+
551
+ # Invert if needed
552
+ if inverted:
553
+ normalized = 1.0 - normalized
554
+
555
+ # Scale to 0-100
556
+ return normalized * 100.0
557
+
558
+
559
+ # Import timedelta for cutoff calculation
560
+ from datetime import timedelta
@@ -0,0 +1,66 @@
1
+ """Retain direct-exchange provenance without treating scalar closeness as evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Dict, List
7
+
8
+
9
+ def min_direct_exchanges() -> int:
10
+ try:
11
+ return max(1, int(os.environ.get("COLONY_RELATIONSHIP_MIN_EXCHANGES", "3")))
12
+ except (TypeError, ValueError):
13
+ return 3
14
+
15
+
16
+ def is_direct_interlocutor(candidate: Dict[str, Any]) -> bool:
17
+ """True only with recorded direct-exchange evidence at/above the floor.
18
+
19
+ Missing evidence means passively observed -- fail closed.
20
+ """
21
+ count = candidate.get("interaction_count")
22
+ try:
23
+ return count is not None and int(count) >= min_direct_exchanges()
24
+ except (TypeError, ValueError):
25
+ return False
26
+
27
+
28
+ def filter_relationship_candidates(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
29
+ """Direct-exchange provenance only; scalar closeness never proves usefulness.
30
+
31
+ The caller still needs an actual purpose and permission for any outreach.
32
+ Legacy scores are removed before candidates reach planning prompts.
33
+ """
34
+ return [{k: v for k, v in c.items() if k not in {'relationship_score', 'score', 'score_events'}}
35
+ for c in candidates if is_direct_interlocutor(c)]
36
+
37
+
38
+ async def enrich_interaction_counts(
39
+ candidates: List[Dict[str, Any]],
40
+ contacts_store: Any,
41
+ ) -> None:
42
+ """Fill ``interaction_count`` from the contact
43
+ record's direct-exchange data. Candidates that resolve to no contact keep
44
+ no count and will fail the provenance gate (correct: no direct-exchange
45
+ evidence means passively observed)."""
46
+ if contacts_store is None:
47
+ return
48
+ for c in candidates:
49
+ if c.get("interaction_count") is not None:
50
+ continue
51
+ entity_id = c.get("entity_id") or ""
52
+ contact = None
53
+ try:
54
+ if hasattr(contacts_store, "get"):
55
+ contact = await contacts_store.get(entity_id)
56
+ if contact is None and hasattr(contacts_store, "find_by_person_node_id"):
57
+ contact = await contacts_store.find_by_person_node_id(entity_id)
58
+ except Exception:
59
+ contact = None
60
+ if contact is None:
61
+ continue
62
+ count = getattr(contact, "interaction_count", None)
63
+ if count is None and isinstance(contact, dict):
64
+ count = contact.get("interaction_count")
65
+ if count is not None:
66
+ c["interaction_count"] = int(count)