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
apsimo/goals/engine.py ADDED
@@ -0,0 +1,617 @@
1
+ """GoalEngine — top-level coordinator for the Colony Goal Engine.
2
+
3
+ Wires together inference, decomposition, lifecycle management,
4
+ queue integration, and intelligence layer reporting.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from apsimo.goals.config import GoalEngineConfig
14
+ from apsimo.goals.decomposer import GoalDecomposer
15
+ from apsimo.goals.inference import (
16
+ ConversationMessage,
17
+ GoalDeduplicator,
18
+ GoalInferencePipeline,
19
+ InferenceCandidate,
20
+ )
21
+ from apsimo.goals.models import (
22
+ Goal,
23
+ GoalDAG,
24
+ GoalPriority,
25
+ GoalSource,
26
+ GoalStatus,
27
+ GoalSummary,
28
+ GoalTransitionRecord,
29
+ )
30
+ from apsimo.goals.priority import GoalProgressTracker, GoalPriorityScorer, UserPreferenceProfile
31
+ from apsimo.goals.queue_bridge import GoalQueueBridge
32
+ from apsimo.goals.replan import (
33
+ FailureAnalysis,
34
+ ReplanEngine,
35
+ ReplanResult,
36
+ ReplanStrategy,
37
+ )
38
+ from apsimo.goals.store import GoalNotFoundError, GoalStore
39
+ from apsimo.task_queue.models import JobResult
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class GoalEngine:
45
+ """Top-level coordinator for the Colony Goal Engine.
46
+
47
+ Wires together inference, decomposition, lifecycle management,
48
+ queue integration, and intelligence layer reporting.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ store: Optional[GoalStore] = None,
54
+ decomposer: Optional[GoalDecomposer] = None,
55
+ queue_bridge: Optional[GoalQueueBridge] = None,
56
+ meta_learner: Optional[Any] = None,
57
+ config: Optional[GoalEngineConfig] = None,
58
+ quorum_manager: Optional[Any] = None,
59
+ ) -> None:
60
+ self.config = config or GoalEngineConfig()
61
+ self._store = store or GoalStore(db_path=self.config.db_path)
62
+ self._decomposer = decomposer or GoalDecomposer()
63
+ self._queue_bridge = queue_bridge or GoalQueueBridge()
64
+ self._meta_learner = meta_learner
65
+ self._quorum = quorum_manager # Optional[QuorumManager]
66
+ self._inference = GoalInferencePipeline()
67
+ self._deduplicator = GoalDeduplicator()
68
+ self._scorer = GoalPriorityScorer()
69
+ self._tracker = GoalProgressTracker()
70
+ self._replan_engine = ReplanEngine(max_replans=self.config.max_replans)
71
+ self._preferences = UserPreferenceProfile()
72
+
73
+ # ── Quorum guard ───────────────────────────────────────────────────────────
74
+
75
+ def _require_quorum(self, operation: str) -> None:
76
+ """Raise QuorumError if the mesh quorum is unavailable.
77
+
78
+ No-ops when no quorum_manager was provided (e.g. single-node or tests).
79
+ """
80
+ if self._quorum is not None:
81
+ self._quorum.require_quorum(operation)
82
+
83
+ # ── Goal Lifecycle ─────────────────────────────────────────────────────────
84
+
85
+ def propose_goal(
86
+ self,
87
+ title: str,
88
+ description: str = "",
89
+ source: GoalSource = GoalSource.EXPLICIT,
90
+ priority: GoalPriority = GoalPriority.NORMAL,
91
+ deadline: Optional[datetime] = None,
92
+ context: Optional[Dict[str, Any]] = None,
93
+ tags: Optional[Dict[str, str]] = None,
94
+ parent_goal_id: Optional[str] = None,
95
+ ) -> Goal:
96
+ """Create a new goal in PROPOSED state."""
97
+ self._require_quorum("goal_propose")
98
+ goal = Goal(
99
+ title=title[:80],
100
+ description=description,
101
+ source=source,
102
+ status=GoalStatus.PROPOSED,
103
+ priority=priority,
104
+ deadline=deadline,
105
+ context=context or {},
106
+ tags=tags or {},
107
+ parent_goal_id=parent_goal_id,
108
+ )
109
+ self._store.save_goal(goal)
110
+ logger.info("Proposed goal %s: %r", goal.goal_id, goal.title)
111
+ return goal
112
+
113
+ def accept_goal(self, goal_id: str) -> Goal:
114
+ """Transition a PROPOSED goal to ACCEPTED and queue decomposition."""
115
+ self._require_quorum("goal_accept")
116
+ goal = self._store.get_goal(goal_id)
117
+ if goal.status != GoalStatus.PROPOSED:
118
+ raise ValueError(
119
+ f"Cannot accept goal {goal_id} in state {goal.status.value}"
120
+ )
121
+ old_status = goal.status
122
+ goal.status = GoalStatus.ACCEPTED
123
+ goal.accepted_at = datetime.now(timezone.utc)
124
+ self._store.save_goal(goal)
125
+ self._store.log_transition(goal_id, old_status, GoalStatus.ACCEPTED, "user_accepted")
126
+ logger.info("Accepted goal %s", goal_id)
127
+ return goal
128
+
129
+ def activate_goal(self, goal_id: str) -> Goal:
130
+ """Decompose an ACCEPTED goal and dispatch initial subtasks."""
131
+ self._require_quorum("goal_activate")
132
+ goal = self._store.get_goal(goal_id)
133
+ if goal.status != GoalStatus.ACCEPTED:
134
+ raise ValueError(
135
+ f"Cannot activate goal {goal_id} in state {goal.status.value}"
136
+ )
137
+
138
+ if not self._queue_bridge.available:
139
+ # Acceptance is durable. Without an executor, neither a generated
140
+ # DAG nor a throwaway job ID establishes that work has started.
141
+ if goal.context.get("dispatch_unavailable") != "queue_backend_unconfigured":
142
+ goal.context["dispatch_unavailable"] = "queue_backend_unconfigured"
143
+ self._store.save_goal(goal)
144
+ logger.warning("Goal %s remains accepted: queue backend unconfigured", goal_id)
145
+ return goal
146
+ goal.context.pop("dispatch_unavailable", None)
147
+
148
+ # Decompose
149
+ dag = self._decomposer.decompose(goal)
150
+ self._store.save_dag(dag)
151
+
152
+ # Dispatch ready subtasks
153
+ dispatched = self._queue_bridge.dispatch_ready_subtasks(goal, dag)
154
+ self._store.save_dag(dag) # Updated subtask statuses
155
+
156
+ old_status = goal.status
157
+ goal.status = GoalStatus.ACTIVE
158
+ goal.estimated_hours = self._estimate_total_hours(dag)
159
+ self._store.save_goal(goal)
160
+ self._store.log_transition(goal_id, old_status, GoalStatus.ACTIVE, "decomposition_complete")
161
+ logger.info(
162
+ "Activated goal %s: %d subtasks, %d dispatched",
163
+ goal_id, len(dag.subtasks), dispatched,
164
+ )
165
+ return goal
166
+
167
+ def abandon_goal(self, goal_id: str, reason: str) -> Goal:
168
+ """Transition any non-terminal goal to ABANDONED."""
169
+ self._require_quorum("goal_abandon")
170
+ goal = self._store.get_goal(goal_id)
171
+ if goal.is_terminal():
172
+ raise ValueError(
173
+ f"Cannot abandon terminal goal {goal_id} (status={goal.status.value})"
174
+ )
175
+
176
+ # Cancel running jobs
177
+ dag = self._store.get_dag(goal_id)
178
+ if dag:
179
+ self._queue_bridge.cancel_all_jobs(dag)
180
+ self._store.save_dag(dag)
181
+
182
+ old_status = goal.status
183
+ goal.status = GoalStatus.ABANDONED
184
+ goal.abandoned_at = datetime.now(timezone.utc)
185
+ goal.abandon_reason = reason
186
+ self._store.save_goal(goal)
187
+ self._store.log_transition(goal_id, old_status, GoalStatus.ABANDONED, "user_abandoned",
188
+ metadata={"reason": reason})
189
+ logger.info("Abandoned goal %s: %s", goal_id, reason)
190
+
191
+ # Emit telemetry
192
+ if self.config.telemetry_enabled:
193
+ self._emit_telemetry(goal, dag, success=False)
194
+
195
+ return goal
196
+
197
+ def block_goal(
198
+ self,
199
+ goal_id: str,
200
+ reason: str,
201
+ condition_type: Optional[str] = None,
202
+ condition_params: Optional[Dict[str, Any]] = None,
203
+ ) -> Goal:
204
+ """Transition an ACTIVE goal to BLOCKED.
205
+
206
+ With a ``condition_type`` (email_reply | deployment_health |
207
+ delivery_status | api_response | custom), the goal blocks on an
208
+ EXTERNAL condition: the autonomy loop's condition sweep polls it at
209
+ the type's cadence and unblocks the goal automatically when it's met.
210
+ Without one, the goal stays blocked until something explicitly
211
+ unblocks it."""
212
+ self._require_quorum("goal_block")
213
+ goal = self._store.get_goal(goal_id)
214
+ if goal.status != GoalStatus.ACTIVE:
215
+ raise ValueError(
216
+ f"Cannot block goal {goal_id} in state {goal.status.value}"
217
+ )
218
+ old_status = goal.status
219
+ goal.status = GoalStatus.BLOCKED
220
+ goal.context["block_reason"] = reason
221
+ if condition_type:
222
+ goal.context["condition_type"] = condition_type
223
+ goal.context["condition_params"] = condition_params or {}
224
+ goal.context.pop("condition_last_check", None)
225
+ self._store.save_goal(goal)
226
+ self._store.log_transition(goal_id, old_status, GoalStatus.BLOCKED, "blocked",
227
+ metadata={"reason": reason,
228
+ **({"condition_type": condition_type}
229
+ if condition_type else {})})
230
+ logger.warning("Goal %s blocked: %s%s", goal_id, reason,
231
+ f" (awaiting {condition_type})" if condition_type else "")
232
+ return goal
233
+
234
+ def unblock_goal(self, goal_id: str) -> Goal:
235
+ """Transition a BLOCKED goal back to ACTIVE."""
236
+ self._require_quorum("goal_unblock")
237
+ goal = self._store.get_goal(goal_id)
238
+ if goal.status != GoalStatus.BLOCKED:
239
+ raise ValueError(
240
+ f"Cannot unblock goal {goal_id} in state {goal.status.value}"
241
+ )
242
+ old_status = goal.status
243
+ goal.status = GoalStatus.ACTIVE
244
+ goal.context.pop("block_reason", None)
245
+ goal.context.pop("condition_type", None)
246
+ goal.context.pop("condition_params", None)
247
+ goal.context.pop("condition_last_check", None)
248
+ self._store.save_goal(goal)
249
+ self._store.log_transition(goal_id, old_status, GoalStatus.ACTIVE, "unblocked")
250
+
251
+ # Dispatch any newly ready subtasks
252
+ dag = self._store.get_dag(goal_id)
253
+ if dag:
254
+ self._queue_bridge.dispatch_ready_subtasks(goal, dag)
255
+ self._store.save_dag(dag)
256
+
257
+ logger.info("Unblocked goal %s", goal_id)
258
+ return goal
259
+
260
+ # ── Query ──────────────────────────────────────────────────────────────────
261
+
262
+ def get_goal(self, goal_id: str) -> Goal:
263
+ return self._store.get_goal(goal_id)
264
+
265
+ def list_goals(
266
+ self,
267
+ status: Optional[GoalStatus] = None,
268
+ limit: int = 50,
269
+ offset: int = 0,
270
+ ) -> List[Goal]:
271
+ return self._store.list_goals(status=status, limit=limit, offset=offset)
272
+
273
+ def get_dag(self, goal_id: str) -> Optional[GoalDAG]:
274
+ return self._store.get_dag(goal_id)
275
+
276
+ def get_audit_trail(self, goal_id: str) -> List[GoalTransitionRecord]:
277
+ return self._store.get_audit_trail(goal_id)
278
+
279
+ def get_active_summary(self) -> List[GoalSummary]:
280
+ """Return human-readable summaries of all active goals."""
281
+ active_goals = self._store.list_goals(status=GoalStatus.ACTIVE)
282
+ summaries = []
283
+ for goal in active_goals:
284
+ dag = self._store.get_dag(goal.goal_id)
285
+ subtask_count = len(dag.subtasks) if dag else 0
286
+ from apsimo.goals.models import SubtaskStatus
287
+ completed = (
288
+ sum(1 for s in dag.subtasks.values()
289
+ if s.status in {SubtaskStatus.COMPLETED, SubtaskStatus.SKIPPED})
290
+ if dag else 0
291
+ )
292
+ summaries.append(GoalSummary(
293
+ goal_id=goal.goal_id,
294
+ title=goal.title,
295
+ status=goal.status.value,
296
+ priority=goal.priority.value,
297
+ progress_pct=goal.progress_pct,
298
+ estimated_hours=goal.estimated_hours,
299
+ deadline=goal.deadline,
300
+ subtask_count=subtask_count,
301
+ completed_subtasks=completed,
302
+ is_overdue=goal.is_overdue(),
303
+ replan_count=goal.replan_count,
304
+ ))
305
+ return summaries
306
+
307
+ # ── Task Management (v0.7.10) ─────────────────────────────────────────────
308
+
309
+ def complete_task(self, goal_id: str) -> bool:
310
+ """Mark a goal/task as completed."""
311
+ return self._store.complete_task(goal_id)
312
+
313
+ def snooze_task(self, goal_id: str, hours: int, reason: str = "") -> bool:
314
+ """Snooze a goal/task for N hours."""
315
+ return self._store.snooze_task(goal_id, hours, reason)
316
+
317
+ def dismiss_task(self, goal_id: str, reason: str = "stale") -> bool:
318
+ """Dismiss a goal/task as no longer relevant."""
319
+ return self._store.dismiss_task(goal_id, reason)
320
+
321
+ def get_active_tasks(self, cooldown_hours: float = 12.0) -> List[Goal]:
322
+ """Get goals that should generate initiatives."""
323
+ return self._store.get_active_tasks(cooldown_hours)
324
+
325
+ def mark_initiative_generated(self, goal_id: str) -> bool:
326
+ """Mark that an initiative was just generated for this goal."""
327
+ return self._store.mark_initiative_generated(goal_id)
328
+
329
+ # ── Event Handlers ─────────────────────────────────────────────────────────
330
+
331
+ def on_job_completed(self, job_result: JobResult) -> None:
332
+ """Handle a completed task queue job."""
333
+ goal_id = job_result.output.get("goal_id")
334
+ subtask_id = job_result.output.get("subtask_id")
335
+
336
+ if not goal_id or not subtask_id:
337
+ logger.warning("Received job result without goal_id/subtask_id: %s", job_result.job_id)
338
+ return
339
+
340
+ try:
341
+ goal = self._store.get_goal(goal_id)
342
+ except GoalNotFoundError:
343
+ logger.warning("on_job_completed: goal %s not found", goal_id)
344
+ return
345
+
346
+ dag = self._store.get_dag(goal_id)
347
+ if dag is None:
348
+ logger.warning("on_job_completed: no DAG for goal %s", goal_id)
349
+ return
350
+
351
+ subtask = dag.subtasks.get(subtask_id)
352
+ if subtask is None:
353
+ logger.warning("on_job_completed: subtask %s not found in DAG", subtask_id)
354
+ return
355
+
356
+ if job_result.succeeded:
357
+ from apsimo.goals.models import SubtaskStatus
358
+ subtask.status = SubtaskStatus.COMPLETED
359
+ subtask.result = job_result.output
360
+ subtask.completed_at = job_result.completed_at or datetime.now(timezone.utc)
361
+ else:
362
+ from apsimo.goals.models import SubtaskStatus
363
+ subtask.status = SubtaskStatus.FAILED
364
+ subtask.error = job_result.error
365
+ self._trigger_replan(goal, dag, subtask)
366
+ self._store.save_dag(dag)
367
+ self._store.save_goal(goal)
368
+ return
369
+
370
+ # Update goal progress
371
+ goal.progress_pct = self._tracker.update_progress(goal_id, dag)
372
+
373
+ # Check for goal completion
374
+ if dag.is_complete():
375
+ self._complete_goal(goal, dag)
376
+ else:
377
+ # Dispatch any newly ready subtasks
378
+ self._queue_bridge.dispatch_ready_subtasks(goal, dag)
379
+
380
+ # Check if goal is now blocked
381
+ block_reason = self._tracker.check_blocked(goal, dag)
382
+ if block_reason and goal.status == GoalStatus.ACTIVE:
383
+ self.block_goal(goal_id, block_reason)
384
+
385
+ self._store.save_goal(goal)
386
+ self._store.save_dag(dag)
387
+
388
+ def on_message(
389
+ self,
390
+ message: ConversationMessage,
391
+ history: Optional[List[ConversationMessage]] = None,
392
+ ) -> Optional[InferenceCandidate]:
393
+ """Process a conversation message for goal inference.
394
+
395
+ Returns a candidate if a new goal was inferred, else None.
396
+ """
397
+ try:
398
+ from apsimo.cognition.goal_spine import cognition_spine_exclusive
399
+ if cognition_spine_exclusive():
400
+ return None
401
+ except Exception:
402
+ pass
403
+ if not self.config.inference_enabled:
404
+ return None
405
+
406
+ candidate = self._inference.process_message(message, history=history)
407
+ if candidate is None:
408
+ return None
409
+
410
+ # Deduplication check
411
+ existing = self._store.list_goals(limit=100)
412
+ similarity = self._deduplicator.check(candidate, existing)
413
+
414
+ if similarity and similarity.recommendation == "duplicate":
415
+ logger.debug(
416
+ "Inference candidate %r is duplicate of goal %s (score=%.2f)",
417
+ candidate.title, similarity.goal_id_b, similarity.similarity_score,
418
+ )
419
+ return None
420
+
421
+ if similarity and similarity.recommendation == "merge":
422
+ try:
423
+ base = self._store.get_goal(similarity.goal_id_b)
424
+ self._deduplicator.merge(base, candidate)
425
+ self._store.save_goal(base)
426
+ logger.info("Merged inference candidate into goal %s", base.goal_id)
427
+ except GoalNotFoundError:
428
+ pass
429
+ return candidate
430
+
431
+ # Preserve inference provenance and any detected deadline so the signal
432
+ # isn't silently lost. suggested_deadline is a matched phrase (e.g.
433
+ # "due by", "before the meeting"), not a parseable datetime, so it is
434
+ # carried as a context hint rather than the typed `deadline` field —
435
+ # downstream / the LLM can resolve it to a concrete date.
436
+ inferred_context: Dict[str, Any] = {
437
+ "inference_confidence": round(candidate.confidence, 3),
438
+ "inference_signals": [s.value for s in candidate.signals],
439
+ "source_messages": candidate.source_messages,
440
+ }
441
+ if candidate.suggested_deadline:
442
+ inferred_context["deadline_hint"] = candidate.suggested_deadline
443
+
444
+ goal = self.propose_goal(
445
+ title=candidate.title,
446
+ description=candidate.description,
447
+ source=GoalSource.INFERRED,
448
+ priority=candidate.priority,
449
+ context=inferred_context,
450
+ )
451
+
452
+ # Auto-accept if confidence is high enough; otherwise leave PROPOSED for
453
+ # the user to confirm.
454
+ if candidate.should_auto_accept(self.config.auto_accept_threshold):
455
+ self.accept_goal(goal.goal_id)
456
+ logger.info("Auto-accepted inferred goal %s: %r", goal.goal_id, goal.title)
457
+
458
+ return candidate
459
+
460
+ # ── Replan ─────────────────────────────────────────────────────────────────
461
+
462
+ def trigger_replan(
463
+ self,
464
+ goal_id: str,
465
+ reason: str,
466
+ strategy: Optional[ReplanStrategy] = None,
467
+ ) -> ReplanResult:
468
+ """Manually trigger a replan for a goal."""
469
+ goal = self._store.get_goal(goal_id)
470
+ dag = self._store.get_dag(goal_id)
471
+ if dag is None:
472
+ raise ValueError(f"No DAG found for goal {goal_id}")
473
+
474
+ # Find the first failed subtask to analyse
475
+ from apsimo.goals.models import SubtaskStatus
476
+ failed = next(
477
+ (s for s in dag.subtasks.values() if s.status == SubtaskStatus.FAILED),
478
+ None,
479
+ )
480
+
481
+ if failed is None:
482
+ # Create a synthetic analysis for scope change
483
+ from apsimo.goals.replan import FailureAnalysis, FailureClass
484
+ analysis = FailureAnalysis(
485
+ subtask_id=next(iter(dag.subtasks), ""),
486
+ failure_class=FailureClass.SCOPE_CHANGE,
487
+ error_message=reason,
488
+ retry_viable=False,
489
+ affected_subtasks=list(dag.subtasks.keys()),
490
+ suggested_strategy=(strategy or ReplanStrategy.FULL_REPLAN).value,
491
+ )
492
+ else:
493
+ analysis = self._replan_engine.analyze_failure(goal, dag, failed)
494
+ if strategy:
495
+ analysis.suggested_strategy = strategy.value
496
+
497
+ replan_result = self._replan_engine.generate_replan(goal, dag, analysis)
498
+
499
+ if not replan_result.requires_user_approval:
500
+ new_dag = self._replan_engine.apply_replan(goal, dag, replan_result)
501
+ self._store.save_goal(goal)
502
+ self._store.save_dag(new_dag)
503
+ self._queue_bridge.dispatch_ready_subtasks(goal, new_dag)
504
+ self._store.save_dag(new_dag)
505
+
506
+ return replan_result
507
+
508
+ # ── Private helpers ────────────────────────────────────────────────────────
509
+
510
+ def _trigger_replan(self, goal: Goal, dag: GoalDAG, failed_subtask) -> None:
511
+ """Internal: automatically replan after subtask failure."""
512
+ analysis = self._replan_engine.analyze_failure(goal, dag, failed_subtask)
513
+ replan_result = self._replan_engine.generate_replan(goal, dag, analysis)
514
+
515
+ if replan_result.requires_user_approval:
516
+ logger.warning(
517
+ "Goal %s replan requires user approval: %s",
518
+ goal.goal_id, replan_result.escalation_message,
519
+ )
520
+ # Block the goal pending user action
521
+ if goal.status == GoalStatus.ACTIVE:
522
+ old_status = goal.status
523
+ goal.status = GoalStatus.BLOCKED
524
+ goal.context["escalation"] = replan_result.escalation_message
525
+ self._store.log_transition(
526
+ goal.goal_id, old_status, GoalStatus.BLOCKED,
527
+ "replan_requires_approval",
528
+ metadata={"strategy": replan_result.strategy.value},
529
+ )
530
+ return
531
+
532
+ new_dag = self._replan_engine.apply_replan(goal, dag, replan_result)
533
+ # Mutate the dag dict in-place so the caller's reference is updated
534
+ dag.subtasks = new_dag.subtasks
535
+ dag.version = new_dag.version
536
+ dag.root_ids = new_dag.root_ids
537
+ dag.leaf_ids = new_dag.leaf_ids
538
+ dag.critical_path = new_dag.critical_path
539
+
540
+ def _complete_goal(self, goal: Goal, dag: GoalDAG) -> None:
541
+ """Finalise a goal after all subtasks complete."""
542
+ old_status = goal.status
543
+ goal.status = GoalStatus.COMPLETED
544
+ goal.completed_at = datetime.now(timezone.utc)
545
+ goal.progress_pct = 1.0
546
+ self._store.log_transition(
547
+ goal.goal_id, old_status, GoalStatus.COMPLETED, "all_subtasks_complete"
548
+ )
549
+ logger.info("Goal %s completed: %r", goal.goal_id, goal.title)
550
+
551
+ # Update preference profile
552
+ domain = goal.tags.get("domain", "")
553
+ if domain:
554
+ self._preferences.update_domain_completion(domain, success=True)
555
+
556
+ # Emit telemetry
557
+ if self.config.telemetry_enabled:
558
+ self._emit_telemetry(goal, dag, success=True)
559
+
560
+ def _emit_telemetry(self, goal: Goal, dag: Optional[GoalDAG], success: bool) -> None:
561
+ """Emit goal completion telemetry to MetaLearner (best-effort)."""
562
+ if self._meta_learner is None:
563
+ return
564
+ try:
565
+ from apsimo.goals.models import SubtaskStatus
566
+ failed_count = 0
567
+ subtask_types: Dict[str, int] = {}
568
+ if dag:
569
+ failed_count = sum(
570
+ 1 for s in dag.subtasks.values()
571
+ if s.status == SubtaskStatus.FAILED
572
+ )
573
+ for s in dag.subtasks.values():
574
+ subtask_types[s.job_type] = subtask_types.get(s.job_type, 0) + 1
575
+
576
+ actual_hours = 0.0
577
+ if goal.accepted_at and goal.completed_at:
578
+ actual_hours = (goal.completed_at - goal.accepted_at).total_seconds() / 3600.0
579
+ elif goal.accepted_at and goal.abandoned_at:
580
+ actual_hours = (goal.abandoned_at - goal.accepted_at).total_seconds() / 3600.0
581
+
582
+ estimation_error = None
583
+ if goal.estimated_hours and goal.estimated_hours > 0:
584
+ estimation_error = (actual_hours - goal.estimated_hours) / goal.estimated_hours
585
+
586
+ telemetry = {
587
+ "goal_id": goal.goal_id,
588
+ "goal_title": goal.title,
589
+ "goal_source": goal.source.value,
590
+ "priority": goal.priority.value,
591
+ "total_subtasks": len(dag.subtasks) if dag else 0,
592
+ "failed_subtasks": failed_count,
593
+ "replan_count": goal.replan_count,
594
+ "estimated_hours": goal.estimated_hours,
595
+ "actual_hours": actual_hours,
596
+ "estimation_error": estimation_error,
597
+ "subtask_type_breakdown": subtask_types,
598
+ "success": success,
599
+ "completed_at": (goal.completed_at or goal.abandoned_at or datetime.now(timezone.utc)).isoformat(),
600
+ }
601
+ # MetaLearner may expose record_metric or a custom hook
602
+ if hasattr(self._meta_learner, "_metric_hooks"):
603
+ for hook in self._meta_learner._metric_hooks:
604
+ hook(telemetry)
605
+ except Exception as exc:
606
+ logger.warning("Failed to emit goal telemetry: %s", exc)
607
+
608
+ def _estimate_total_hours(self, dag: GoalDAG) -> Optional[float]:
609
+ """Estimate total goal hours from the critical path."""
610
+ if not dag.critical_path:
611
+ return None
612
+ total = sum(
613
+ (dag.subtasks[sid].estimated_hours or 1.0)
614
+ for sid in dag.critical_path
615
+ if sid in dag.subtasks
616
+ )
617
+ return round(total, 2) if total > 0 else None