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,449 @@
1
+ """ConversationSynthesisTask — periodically scan stored conversation memories
2
+ for implicit goals, commitments, and action items.
3
+
4
+ Architecture:
5
+ 1. Query recent episodic Memory nodes from Neo4j
6
+ 2. Parse turn summaries back into ConversationMessage objects
7
+ 3. Run GoalInferencePipeline.process_history() on each batch
8
+ 4. Feed candidates to GoalEngine for deduplication and creation
9
+ 5. Persist last-processed watermark to avoid re-processing
10
+
11
+ This complements live goal inference (GoalEngine.on_message) by catching
12
+ items that were missed in real-time or that only become clear in retrospect
13
+ across multiple turns.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ from apsimo.goals.inference import (
27
+ ConversationMessage,
28
+ GoalDeduplicator,
29
+ GoalInferencePipeline,
30
+ InferenceCandidate,
31
+ )
32
+ from apsimo.goals.models import GoalPriority, GoalSource, GoalStatus
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ # ── State persistence ───────────────────────────────────────────────────────
38
+
39
+ @dataclass
40
+ class SynthesisState:
41
+ """Persistent watermark for the synthesis task."""
42
+
43
+ last_processed_at: Optional[str] = None # ISO-8601
44
+ memories_processed: int = 0
45
+ goals_created: int = 0
46
+ last_run_at: Optional[str] = None
47
+
48
+ @classmethod
49
+ def load(cls, path: Path) -> "SynthesisState":
50
+ if path.exists():
51
+ try:
52
+ data = json.loads(path.read_text())
53
+ return cls(
54
+ last_processed_at=data.get("last_processed_at"),
55
+ memories_processed=data.get("memories_processed", 0),
56
+ goals_created=data.get("goals_created", 0),
57
+ last_run_at=data.get("last_run_at"),
58
+ )
59
+ except Exception:
60
+ logger.warning("Failed to load synthesis state from %s", path)
61
+ return cls()
62
+
63
+ def save(self, path: Path) -> None:
64
+ try:
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ path.write_text(json.dumps(self.__dict__, indent=2))
67
+ except Exception as exc:
68
+ logger.warning("Failed to save synthesis state: %s", exc)
69
+
70
+
71
+ # ── Memory parsing ────────────────────────────────────────────────────────
72
+
73
+ def _parse_turn_content(content: str) -> List[ConversationMessage]:
74
+ """Parse a turn summary back into ConversationMessage objects.
75
+
76
+ WARNING: This is tightly coupled to the format produced by
77
+ ``turns_sync`` in ``api/routers/host.py``. If that format changes
78
+ (e.g. prefix wording, line breaks), this parser will silently fall
79
+ back to treating the entire content as a single user message.
80
+
81
+ turns_sync currently stores combined summaries as:
82
+ User: {text}
83
+ Assistant: {text}
84
+
85
+ Returns a list of ConversationMessage objects (user first, then assistant).
86
+ """
87
+ messages: List[ConversationMessage] = []
88
+ if not content:
89
+ return messages
90
+
91
+ # Try to split on "User:" and "Assistant:" prefixes
92
+ lines = content.split("\n")
93
+ current_role: Optional[str] = None
94
+ current_parts: List[str] = []
95
+
96
+ for line in lines:
97
+ stripped = line.strip()
98
+ if stripped.startswith("User:"):
99
+ # Flush previous
100
+ if current_role and current_parts:
101
+ messages.append(ConversationMessage(
102
+ message_id=f"turn_{len(messages)}",
103
+ role=current_role,
104
+ content=" ".join(current_parts).strip(),
105
+ ))
106
+ current_role = "user"
107
+ current_parts = [stripped[5:].strip()]
108
+ elif stripped.startswith("Assistant:"):
109
+ if current_role and current_parts:
110
+ messages.append(ConversationMessage(
111
+ message_id=f"turn_{len(messages)}",
112
+ role=current_role,
113
+ content=" ".join(current_parts).strip(),
114
+ ))
115
+ current_role = "assistant"
116
+ current_parts = [stripped[10:].strip()]
117
+ else:
118
+ if current_role:
119
+ current_parts.append(stripped)
120
+
121
+ # Flush final
122
+ if current_role and current_parts:
123
+ messages.append(ConversationMessage(
124
+ message_id=f"turn_{len(messages)}",
125
+ role=current_role,
126
+ content=" ".join(current_parts).strip(),
127
+ ))
128
+
129
+ # Fallback: if no prefixes found, treat entire content as user message
130
+ if not messages and content.strip():
131
+ messages.append(ConversationMessage(
132
+ message_id="turn_0",
133
+ role="user",
134
+ content=content.strip(),
135
+ ))
136
+
137
+ return messages
138
+
139
+
140
+ # ── Main task ───────────────────────────────────────────────────────────
141
+
142
+ class ConversationSynthesisTask:
143
+ """Scheduler task that scans conversation memories for implicit goals.
144
+
145
+ Args:
146
+ registry: The AutonomyLoop's subsystem registry (provides graph, goals)
147
+ lookback_hours: How far back to look for memories (default 2.0)
148
+ min_confidence: Minimum inference confidence to create a goal (default 0.35)
149
+ max_memories_per_run: Safety cap on memories to process per tick (default 50)
150
+ max_goals_per_run: Safety cap on goals to create per tick (default 5)
151
+ state_file: Path to persist watermark state
152
+ telemetry: Optional TelemetryStore for health monitoring
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ registry: Any,
158
+ lookback_hours: float = 2.0,
159
+ min_confidence: float = 0.35,
160
+ max_memories_per_run: int = 50,
161
+ max_goals_per_run: int = 5,
162
+ state_file: Optional[Path] = None,
163
+ telemetry: Any = None,
164
+ ) -> None:
165
+ self.registry = registry
166
+ self.lookback_hours = lookback_hours
167
+ self.min_confidence = min_confidence
168
+ self.max_memories_per_run = max_memories_per_run
169
+ self.max_goals_per_run = max_goals_per_run
170
+ self.state_file = state_file or (Path.home() / ".colony" / "data" / "autonomy_synthesis.json")
171
+ self.telemetry = telemetry
172
+ self.state = SynthesisState.load(self.state_file)
173
+ self.pipeline = GoalInferencePipeline(min_confidence=min_confidence)
174
+ self._deduplicator = GoalDeduplicator()
175
+
176
+ # ------------------------------------------------------------------
177
+ # Scheduler interface
178
+ # ------------------------------------------------------------------
179
+
180
+ async def run(self) -> Dict[str, Any]:
181
+ """Execute one synthesis pass. Called by the scheduler.
182
+
183
+ Returns:
184
+ Dict with ``memories_scanned``, ``candidates_found``, ``goals_created``.
185
+ """
186
+ try:
187
+ from apsimo.cognition.goal_spine import cognition_spine_exclusive
188
+ if cognition_spine_exclusive():
189
+ return {
190
+ "memories_scanned": 0,
191
+ "candidates_found": 0,
192
+ "goals_created": 0,
193
+ "status": "legacy_goal_writer_read_only",
194
+ }
195
+ except Exception:
196
+ pass
197
+ start = time.monotonic()
198
+ graph = getattr(self.registry, "graph", None)
199
+ goals = getattr(self.registry, "goals", None)
200
+
201
+ if graph is None:
202
+ logger.warning("ConversationSynthesis: no graph available")
203
+ return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0, "error": "no_graph"}
204
+
205
+ if goals is None:
206
+ logger.warning("ConversationSynthesis: no goals engine available")
207
+ return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0, "error": "no_goals"}
208
+
209
+ # 1. Fetch recent episodic memories
210
+ memories = await self._fetch_recent_memories(graph)
211
+ if not memories:
212
+ logger.debug("ConversationSynthesis: no new memories in last %.1f hours", self.lookback_hours)
213
+ return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0}
214
+
215
+ # 2. Filter out already-processed memories (by parsed timestamp)
216
+ watermark = self.state.last_processed_at
217
+ watermark_dt = self._parse_dt(watermark) if watermark else None
218
+ new_memories = []
219
+ for mem in memories:
220
+ mem_time = mem.get("created_at")
221
+ mem_dt = self._parse_dt(mem_time) if mem_time else None
222
+ if watermark_dt and mem_dt and mem_dt <= watermark_dt:
223
+ continue
224
+ new_memories.append(mem)
225
+
226
+ if not new_memories:
227
+ logger.debug("ConversationSynthesis: all %d memories already processed", len(memories))
228
+ return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0}
229
+
230
+ # Safety cap
231
+ capped = False
232
+ if len(new_memories) > self.max_memories_per_run:
233
+ logger.info(
234
+ "ConversationSynthesis: capping %d memories to %d",
235
+ len(new_memories),
236
+ self.max_memories_per_run,
237
+ )
238
+ new_memories = new_memories[: self.max_memories_per_run]
239
+ capped = True
240
+
241
+ # 3. Process each memory
242
+ all_candidates: List[InferenceCandidate] = []
243
+ for mem in new_memories:
244
+ content = mem.get("content", "")
245
+ messages = _parse_turn_content(content)
246
+ if not messages:
247
+ continue
248
+
249
+ # Run inference pipeline on parsed messages.
250
+ # process_message already returns None for non-user roles,
251
+ # so candidates only come from user messages.
252
+ candidates = self.pipeline.process_history(messages)
253
+ all_candidates.extend(candidates)
254
+
255
+ # 4. Deduplicate candidates against each other (same tick)
256
+ seen_titles: set[str] = set()
257
+ unique_candidates: List[InferenceCandidate] = []
258
+ for cand in all_candidates:
259
+ normalized = cand.title.lower().strip()
260
+ if normalized not in seen_titles:
261
+ seen_titles.add(normalized)
262
+ unique_candidates.append(cand)
263
+
264
+ # 5. Load existing non-terminal goals for cross-run deduplication
265
+ existing_goals: List[Any] = []
266
+ try:
267
+ proposed = goals.list_goals(status=GoalStatus.PROPOSED, limit=100)
268
+ active = goals.list_goals(status=GoalStatus.ACTIVE, limit=100)
269
+ existing_goals = proposed + active
270
+ except Exception as exc:
271
+ logger.warning("ConversationSynthesis: failed to load existing goals: %s", exc)
272
+
273
+ # 6. Create goals via GoalEngine (with dedup, rate limit, and merge)
274
+ goals_created = 0
275
+ for cand in unique_candidates:
276
+ # Stop if we've hit the per-run cap
277
+ if goals_created >= self.max_goals_per_run:
278
+ logger.info(
279
+ "ConversationSynthesis: hit max_goals_per_run (%d), skipping remaining candidates",
280
+ self.max_goals_per_run,
281
+ )
282
+ break
283
+
284
+ # Check against existing goals
285
+ if existing_goals:
286
+ similarity = self._deduplicator.check(cand, existing_goals)
287
+ if similarity:
288
+ if similarity.recommendation == "duplicate":
289
+ logger.debug(
290
+ "ConversationSynthesis: skipping duplicate '%s' (sim=%.2f, goal=%s)",
291
+ cand.title,
292
+ similarity.similarity_score,
293
+ similarity.goal_id_b,
294
+ )
295
+ continue
296
+ elif similarity.recommendation == "merge":
297
+ # Find the existing goal, merge context, and persist
298
+ for goal in existing_goals:
299
+ if goal.goal_id == similarity.goal_id_b:
300
+ self._deduplicator.merge(goal, cand)
301
+ # Persist merged changes back to the store
302
+ try:
303
+ goals._store.save_goal(goal)
304
+ except Exception as save_exc:
305
+ logger.warning(
306
+ "ConversationSynthesis: failed to persist merge for goal %s: %s",
307
+ goal.goal_id,
308
+ save_exc,
309
+ )
310
+ logger.info(
311
+ "ConversationSynthesis: merged '%s' into existing goal %s (sim=%.2f)",
312
+ cand.title,
313
+ goal.goal_id,
314
+ similarity.similarity_score,
315
+ )
316
+ break
317
+ continue
318
+
319
+ # Root-cause guard: conversation memory content can carry raw
320
+ # messaging/skill markup (e.g. <<RCSCTX ...>> injected by the RCS
321
+ # bridge, or [IMPORTANT: ...skill...] system turns). Do not mint
322
+ # goals from system/skill-origin turns, and store only clean titles
323
+ # so downstream follow-ups are meaningful rather than junk.
324
+ from apsimo.delivery.reachout_policy import (
325
+ is_system_origin, sanitize_text,
326
+ )
327
+ if is_system_origin(cand.title) or is_system_origin(cand.description):
328
+ logger.debug(
329
+ "ConversationSynthesis: skipping system/skill-origin candidate %r",
330
+ str(cand.title)[:80],
331
+ )
332
+ continue
333
+ clean_title = sanitize_text(cand.title)
334
+ if not clean_title:
335
+ logger.debug("ConversationSynthesis: skipping empty-after-sanitise candidate")
336
+ continue
337
+ clean_description = sanitize_text(cand.description) or clean_title
338
+
339
+ try:
340
+ goal = goals.propose_goal(
341
+ title=clean_title,
342
+ description=clean_description,
343
+ priority=cand.priority,
344
+ source=GoalSource.INFERRED,
345
+ context={
346
+ "inferred_confidence": cand.confidence,
347
+ "inferred_signals": [s.value for s in cand.signals],
348
+ "source_messages": cand.source_messages,
349
+ },
350
+ )
351
+ if goal is not None:
352
+ goals_created += 1
353
+ existing_goals.append(goal) # prevent duplicates within same run
354
+ logger.info(
355
+ "ConversationSynthesis: created goal '%s' (confidence=%.2f, signals=%s)",
356
+ cand.title,
357
+ cand.confidence,
358
+ [s.value for s in cand.signals],
359
+ )
360
+ except Exception as exc:
361
+ logger.warning("ConversationSynthesis: failed to create goal '%s': %s", cand.title, exc)
362
+
363
+ # 7. Update watermark and state
364
+ newest_time = max(
365
+ (m.get("created_at") for m in new_memories if m.get("created_at")),
366
+ default=None,
367
+ )
368
+ if newest_time and not capped:
369
+ self.state.last_processed_at = newest_time
370
+ self.state.memories_processed += len(new_memories)
371
+ self.state.goals_created += goals_created
372
+ self.state.last_run_at = datetime.now(timezone.utc).isoformat()
373
+ self.state.save(self.state_file)
374
+
375
+ # 8. Telemetry
376
+ if self.telemetry is not None and hasattr(self.telemetry, "touch"):
377
+ try:
378
+ await self.telemetry.touch("last_synthesis_at")
379
+ except Exception:
380
+ pass
381
+
382
+ duration_ms = (time.monotonic() - start) * 1000
383
+ logger.info(
384
+ "ConversationSynthesis: scanned=%d candidates=%d goals_created=%d (%.1fms)",
385
+ len(new_memories),
386
+ len(unique_candidates),
387
+ goals_created,
388
+ duration_ms,
389
+ )
390
+
391
+ return {
392
+ "memories_scanned": len(new_memories),
393
+ "candidates_found": len(unique_candidates),
394
+ "goals_created": goals_created,
395
+ "duration_ms": round(duration_ms, 1),
396
+ }
397
+
398
+ # ------------------------------------------------------------------
399
+ # Private helpers
400
+ # ------------------------------------------------------------------
401
+
402
+ @staticmethod
403
+ def _parse_dt(iso_str: str) -> Optional[datetime]:
404
+ """Parse an ISO-8601 string to a timezone-aware datetime.
405
+
406
+ Handles both 'Z' suffix and '+00:00' offset.
407
+ """
408
+ if not iso_str:
409
+ return None
410
+ try:
411
+ normalized = iso_str.replace("Z", "+00:00")
412
+ return datetime.fromisoformat(normalized)
413
+ except (ValueError, TypeError):
414
+ logger.debug("ConversationSynthesis: failed to parse datetime '%s'", iso_str)
415
+ return None
416
+
417
+ async def _fetch_recent_memories(
418
+ self,
419
+ graph: Any,
420
+ ) -> List[Dict[str, Any]]:
421
+ """Return recent episodic Memory nodes from Neo4j."""
422
+ query = (
423
+ "MATCH (m:Memory) "
424
+ "WHERE m.type = 'episodic' "
425
+ " AND m.created_at >= datetime() - duration({hours: $hours}) "
426
+ "RETURN m.id AS id, m.content AS content, "
427
+ " toString(m.created_at) AS created_at, m.strength AS strength "
428
+ "ORDER BY m.created_at DESC "
429
+ "LIMIT $limit"
430
+ )
431
+
432
+ # Support both ColonyGraph (driver.session) and mock with execute()
433
+ if hasattr(graph, "driver"):
434
+ async with graph.driver.session(database=graph.database) as session:
435
+ result = await session.run(
436
+ query,
437
+ hours=self.lookback_hours,
438
+ limit=self.max_memories_per_run * 2, # fetch a bit more for filtering
439
+ )
440
+ rows = [dict(record) async for record in result]
441
+ return rows if rows else []
442
+
443
+ # Mock / test path
444
+ if hasattr(graph, "execute"):
445
+ rows = await graph.execute(query, hours=self.lookback_hours, limit=self.max_memories_per_run * 2)
446
+ return rows if rows else []
447
+
448
+ logger.warning("ConversationSynthesis: graph has no driver or execute method")
449
+ return []