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,993 @@
1
+ """Distributed Task Queue — Worker Node.
2
+
3
+ A WorkerNode polls the queue for eligible jobs, executes them via
4
+ registered handlers, and sends periodic heartbeats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import platform
13
+ import shutil
14
+ import subprocess
15
+ from datetime import datetime, timezone
16
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
17
+
18
+ if TYPE_CHECKING:
19
+ from apsimo.skills.learning.triggers import SkillLearningService
20
+
21
+ from apsimo.task_queue.models import (
22
+ Job,
23
+ JobResult,
24
+ JobStatus,
25
+ JobType,
26
+ WorkerCapabilities,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def _prepare_embedded_report(
33
+ job: Job,
34
+ output: Any,
35
+ ) -> tuple[Dict[str, Any], bool]:
36
+ """Normalize a trusted handler result and decide server attestation.
37
+
38
+ Local execution is not itself proof of success. The handler must return
39
+ an unambiguous completed contract with no failure/skip markers. WorkOrder
40
+ success always remains transport-only until its independent verifier runs.
41
+ """
42
+
43
+ if not isinstance(output, dict):
44
+ raise TypeError("embedded job handler must return a dictionary")
45
+ report = dict(output)
46
+ action_plane = (
47
+ dict(report.get("action_plane"))
48
+ if isinstance(report.get("action_plane"), dict) else {}
49
+ )
50
+ semantic = str(report.get("status") or "").strip().lower()
51
+ action_state = str(action_plane.get("state") or "").strip().lower()
52
+ failure = bool(
53
+ report.get("success") is False
54
+ or report.get("errors")
55
+ or report.get("alerts")
56
+ or semantic in {"failed", "failure", "error"}
57
+ or action_state in {"failed", "failure", "error"}
58
+ )
59
+ skipped = bool(
60
+ report.get("gate_blocked") is True
61
+ or semantic in {"skipped", "cancelled", "canceled"}
62
+ or action_state in {"skipped", "cancelled", "canceled"}
63
+ )
64
+ if failure:
65
+ report["status"] = "failed"
66
+ report["action_plane"] = {**action_plane, "state": "failed"}
67
+ report.setdefault(
68
+ "summary",
69
+ str(report.get("error") or "embedded handler reported failure"),
70
+ )
71
+ return report, False
72
+ if skipped:
73
+ report["status"] = "skipped"
74
+ report["action_plane"] = {**action_plane, "state": "skipped"}
75
+ return report, False
76
+ attested = bool(
77
+ semantic in {"completed", "verified"}
78
+ and action_state in {"completed", "verified"}
79
+ and not (
80
+ isinstance(job.payload, dict)
81
+ and job.payload.get("schema") == "WorkOrderV1"
82
+ )
83
+ )
84
+ return report, attested
85
+
86
+
87
+ def detect_local_capabilities() -> Dict[str, Any]:
88
+ """Probe the local host and return a capabilities dict.
89
+
90
+ Detects:
91
+ - GPU presence and VRAM (nvidia-smi / Metal)
92
+ - CPU core count and architecture
93
+ - Available RAM (psutil if available)
94
+ - macOS/Linux platform
95
+ - Installed tools (docker, ollama, ffmpeg, git, etc.)
96
+ - Apple Silicon identifier
97
+ """
98
+ caps: Dict[str, Any] = {}
99
+ capacity: Dict[str, float] = {}
100
+
101
+ # Platform
102
+ system = platform.system().lower()
103
+ machine = platform.machine().lower()
104
+ caps["os_" + system] = True
105
+ if "arm" in machine or "aarch64" in machine:
106
+ caps["arm64"] = True
107
+ if system == "darwin" and ("arm" in machine or "aarch64" in machine):
108
+ caps["apple_silicon"] = True
109
+ caps["metal"] = True
110
+
111
+ # CPU
112
+ cpu_count = os.cpu_count() or 1
113
+ capacity["cpu_cores"] = float(cpu_count)
114
+
115
+ # RAM
116
+ try:
117
+ import psutil
118
+ ram_bytes = psutil.virtual_memory().total
119
+ capacity["ram_gb"] = round(ram_bytes / (1024 ** 3), 1)
120
+ except ImportError:
121
+ pass
122
+
123
+ # GPU (NVIDIA)
124
+ if shutil.which("nvidia-smi"):
125
+ try:
126
+ result = subprocess.run(
127
+ ["nvidia-smi", "--query-gpu=name,memory.total",
128
+ "--format=csv,noheader,nounits"],
129
+ capture_output=True, text=True, timeout=5,
130
+ )
131
+ if result.returncode == 0:
132
+ gpus = result.stdout.strip().splitlines()
133
+ caps["gpu"] = True
134
+ caps["cuda"] = True
135
+ capacity["gpu_count"] = float(len(gpus))
136
+ total_vram = 0.0
137
+ for line in gpus:
138
+ if "," not in line:
139
+ continue
140
+ raw = line.split(",")[1].strip()
141
+ try:
142
+ total_vram += float(raw)
143
+ except ValueError:
144
+ pass # nvidia-smi returned "[N/A]" or similar non-numeric value
145
+ capacity["gpu_vram_gb"] = round(total_vram / 1024, 1)
146
+ except Exception:
147
+ logger.warning("GPU detection failed; treating node as CPU-only", exc_info=True)
148
+
149
+ # Installed tools
150
+ for tool in ["docker", "ollama", "ffmpeg", "git", "python3", "node"]:
151
+ if shutil.which(tool):
152
+ caps[tool] = True
153
+
154
+ return {"capabilities": set(caps.keys()), "capacity": capacity}
155
+
156
+
157
+ class JobHandler:
158
+ """Base class for job type handlers registered on a worker node.
159
+
160
+ Subclass and implement ``execute`` to handle a specific JobType.
161
+ """
162
+
163
+ # Opt in only when cancelling the owning asyncio task really stops all
164
+ # work. Handlers that spawn subprocesses/background effects need their own
165
+ # wrapper and must leave this false.
166
+ work_control_interrupt_safe = False
167
+ # Steering can cross a response-loss/restart boundary. An override is not
168
+ # advertised unless the handler guarantees that ``operation_id`` is a
169
+ # durable idempotency key, so replay after the tiny handler/ledger crash
170
+ # window cannot apply guidance twice.
171
+ work_control_steer_idempotent = False
172
+
173
+ async def execute(self, job: Job) -> Dict[str, Any]:
174
+ """Execute the job and return an output dict.
175
+
176
+ Raise any exception on failure. WorkerNode catches all exceptions
177
+ and transitions the job to FAILED.
178
+ """
179
+ raise NotImplementedError
180
+
181
+ async def apply_steer(
182
+ self,
183
+ job: Job,
184
+ *,
185
+ directive: str,
186
+ context_refs: list[str],
187
+ operation_id: str,
188
+ authority_digest: str,
189
+ ) -> Dict[str, Any]:
190
+ """Cooperatively apply a directive to an in-flight handler.
191
+
192
+ Handlers that can truthfully change an ongoing execution override
193
+ this method. Worker registration advertises steer support per job
194
+ type only for an override, so this default is never presented to a
195
+ controller as usable capability. Steering is subordinate guidance,
196
+ never new authority: it may not expand the durable payload,
197
+ capabilities, recipient, effect/risk class, approval, deadline, or
198
+ attempt budget. An expansion requires a new governed queue job.
199
+
200
+ An implementation must return the exact ``authority_digest`` and
201
+ ``authority_expanded=False``. The worker rejects any other report.
202
+ """
203
+
204
+ raise NotImplementedError("handler does not support live steering")
205
+
206
+
207
+ class WorkerNode:
208
+ """A mesh node that polls and executes jobs from the distributed queue.
209
+
210
+ Usage::
211
+
212
+ worker = WorkerNode(
213
+ node_id="node-1",
214
+ queue=queue_manager,
215
+ handlers={JobType.INFERENCE: MyInferenceHandler()},
216
+ )
217
+ await worker.start()
218
+ """
219
+
220
+ def __init__(
221
+ self,
222
+ node_id: str,
223
+ queue: Any, # QueueManager — avoid circular import
224
+ handlers: Optional[Dict[JobType, JobHandler]] = None,
225
+ capabilities: Optional[WorkerCapabilities] = None,
226
+ poll_interval_secs: float = 5.0,
227
+ heartbeat_interval_secs: float = 15.0,
228
+ skill_learning_service: "Optional[SkillLearningService]" = None,
229
+ ) -> None:
230
+ self.node_id = node_id
231
+ self._queue = queue
232
+ self._handlers: Dict[JobType, JobHandler] = handlers or {}
233
+ self._capabilities = capabilities or self._build_capabilities()
234
+ # Advertise ONLY the job types we can actually run. Handlers passed via the
235
+ # constructor (build_default_handlers) never touched job_types — only
236
+ # register_handler did — so an embedded worker left job_types empty, which
237
+ # WorkerCapabilities.can_accept treats as "accept ALL types". It then claimed
238
+ # AGENT_ACTION jobs it has no handler for and failed every one ("No handler
239
+ # registered for job_type=..."). AGENT_ACTION is executed by a separate
240
+ # agent-backed worker; keep this node scoped to the types it handles.
241
+ if self._handlers and not self._capabilities.job_types:
242
+ self._capabilities.job_types = set(self._handlers.keys())
243
+ if JobType.THOUGHT in self._handlers:
244
+ # Only a node that deliberately loaded the strict thought handler
245
+ # may claim owner-private cognition jobs. Job-type matching alone
246
+ # is insufficient because an empty advertised set means "all".
247
+ self._configure_thought_handler_capabilities()
248
+ self._rebuild_work_control_capabilities()
249
+ self._poll_interval = poll_interval_secs
250
+ self._heartbeat_interval = heartbeat_interval_secs
251
+ self._running_jobs: Dict[str, asyncio.Task] = {}
252
+ self._skill_tasks: set[asyncio.Task] = set()
253
+ self._job_attempt_ids: Dict[str, str] = {}
254
+ self._job_start_times: Dict[str, datetime] = {}
255
+ self._running = False
256
+ self._registered = False
257
+ self._loop_tasks: set[asyncio.Task] = set()
258
+ self._stop_in_progress = False
259
+ self._stop_cleanup_done = asyncio.Event()
260
+ self._stop_cleanup_done.set()
261
+ self._skill_learning: Optional[Any] = skill_learning_service
262
+
263
+ def _build_capabilities(self) -> WorkerCapabilities:
264
+ detected = detect_local_capabilities()
265
+ return WorkerCapabilities(
266
+ node_id=self.node_id,
267
+ capabilities=detected["capabilities"],
268
+ capacity=detected["capacity"],
269
+ )
270
+
271
+ def register_handler(self, job_type: JobType, handler: JobHandler) -> None:
272
+ """Register a handler for a specific job type."""
273
+ if self._running or self._registered:
274
+ raise RuntimeError(
275
+ "handlers must be registered before the worker starts"
276
+ )
277
+ self._handlers[job_type] = handler
278
+ self._capabilities.job_types.add(job_type)
279
+ if job_type is JobType.THOUGHT:
280
+ self._configure_thought_handler_capabilities()
281
+ self._rebuild_work_control_capabilities()
282
+
283
+ @staticmethod
284
+ def _handler_supports_steer(handler: JobHandler) -> bool:
285
+ implementation = getattr(type(handler), "apply_steer", None)
286
+ return bool(
287
+ implementation is not None
288
+ and implementation is not JobHandler.apply_steer
289
+ and getattr(
290
+ handler, "work_control_steer_idempotent", False,
291
+ ) is True
292
+ )
293
+
294
+ def _rebuild_work_control_capabilities(self) -> None:
295
+ """Derive every WorkControl capability from the final handler map.
296
+
297
+ Caller-preseeded values and capabilities from a replaced handler are
298
+ removed first. This makes registration an attestation of current code,
299
+ never a sticky string supplied by configuration.
300
+ """
301
+
302
+ from apsimo.task_queue.work_control import (
303
+ interrupt_capability,
304
+ steer_capability,
305
+ work_control_mode,
306
+ )
307
+
308
+ capabilities = {
309
+ str(value)
310
+ for value in (self._capabilities.capabilities or ())
311
+ if not str(value).startswith("work_control:")
312
+ }
313
+ if work_control_mode() != "off":
314
+ for job_type, handler in self._handlers.items():
315
+ if getattr(
316
+ handler, "work_control_interrupt_safe", False,
317
+ ) is True:
318
+ capabilities.add(interrupt_capability(job_type))
319
+ if self._handler_supports_steer(handler):
320
+ capabilities.add(steer_capability(job_type))
321
+ self._capabilities.capabilities = capabilities
322
+
323
+ def _configure_thought_handler_capabilities(self) -> None:
324
+ handler = self._handlers.get(JobType.THOUGHT)
325
+ if not bool(getattr(handler, "thought_only", False)):
326
+ raise ValueError(
327
+ "ThoughtJobV1 requires the strict thought-only handler"
328
+ )
329
+ self._capabilities.capabilities = set(
330
+ self._capabilities.capabilities or (),
331
+ )
332
+ self._capabilities.capabilities.update({
333
+ "cognition_scoped", "thought_engine:v1",
334
+ })
335
+
336
+ async def start(self) -> None:
337
+ """Register with the queue and start poll + heartbeat loops."""
338
+ if self._running or self._registered:
339
+ raise RuntimeError("worker is already started")
340
+ self._running = True
341
+ registration_attempted = False
342
+ thought_attested = False
343
+ try:
344
+ registration_attempted = True
345
+ # Handler attributes may have been configured after construction;
346
+ # register only the final, locally re-derived capability truth.
347
+ self._rebuild_work_control_capabilities()
348
+ await self._queue.register_worker(self._capabilities)
349
+ self._registered = True
350
+ if not self._running:
351
+ return
352
+ if JobType.THOUGHT in self._handlers:
353
+ thought_attested = self._queue.set_thought_runtime_ready(
354
+ True, node_id=self.node_id,
355
+ )
356
+ if not thought_attested:
357
+ raise RuntimeError(
358
+ "ThoughtJobV1 handler node does not match "
359
+ "COLONY_THOUGHT_WORKER_NODE_ID"
360
+ )
361
+ logger.info(
362
+ "Worker %s started (caps=%s)",
363
+ self.node_id, self._capabilities.capabilities,
364
+ )
365
+ await self._run_worker_loops()
366
+ finally:
367
+ if JobType.THOUGHT in self._handlers:
368
+ try:
369
+ self._queue.set_thought_runtime_ready(
370
+ False,
371
+ node_id=self.node_id,
372
+ reason="thought_worker_loop_exited",
373
+ )
374
+ except Exception:
375
+ logger.exception(
376
+ "Worker %s failed to clear Thought readiness",
377
+ self.node_id,
378
+ )
379
+ self._running = False
380
+ if self._stop_in_progress:
381
+ await self._stop_cleanup_done.wait()
382
+ elif self._registered or registration_attempted:
383
+ self._registered = False
384
+ try:
385
+ await self._queue.deregister_worker(self.node_id)
386
+ except Exception:
387
+ logger.exception(
388
+ "Worker %s failed to deregister after loop exit",
389
+ self.node_id,
390
+ )
391
+
392
+ async def _run_worker_loops(self) -> None:
393
+ """Run both owned loops with sibling cancellation on any failure."""
394
+
395
+ tasks = {
396
+ asyncio.create_task(
397
+ self._poll_loop(), name=f"{self.node_id}:poll",
398
+ ),
399
+ asyncio.create_task(
400
+ self._heartbeat_loop(), name=f"{self.node_id}:heartbeat",
401
+ ),
402
+ }
403
+ from apsimo.task_queue.work_control import work_control_mode
404
+
405
+ if work_control_mode() != "off":
406
+ tasks.add(asyncio.create_task(
407
+ self._work_control_loop(), name=f"{self.node_id}:control",
408
+ ))
409
+ self._loop_tasks = tasks
410
+ try:
411
+ done, pending = await asyncio.wait(
412
+ tasks,
413
+ return_when=asyncio.FIRST_COMPLETED,
414
+ )
415
+ failure: Optional[BaseException] = None
416
+ for task in done:
417
+ if task.cancelled():
418
+ if self._running:
419
+ failure = asyncio.CancelledError()
420
+ break
421
+ continue
422
+ exc = task.exception()
423
+ if exc is not None:
424
+ failure = exc
425
+ break
426
+ if failure is None and self._running:
427
+ failure = RuntimeError("worker lifecycle loop exited unexpectedly")
428
+ if failure is not None:
429
+ for task in pending:
430
+ task.cancel()
431
+ await asyncio.gather(*pending, return_exceptions=True)
432
+ raise failure
433
+ if pending:
434
+ await asyncio.gather(*pending)
435
+ finally:
436
+ for task in tasks:
437
+ if not task.done():
438
+ task.cancel()
439
+ await asyncio.gather(*tasks, return_exceptions=True)
440
+ if self._loop_tasks is tasks:
441
+ self._loop_tasks = set()
442
+
443
+ async def stop(self, drain_timeout: float = 30.0) -> None:
444
+ """Gracefully stop: wait for in-flight jobs, then deregister."""
445
+ if self._stop_in_progress:
446
+ await self._stop_cleanup_done.wait()
447
+ return
448
+ self._stop_in_progress = True
449
+ self._stop_cleanup_done.clear()
450
+ try:
451
+ self._running = False
452
+ if JobType.THOUGHT in self._handlers:
453
+ self._queue.set_thought_runtime_ready(
454
+ False,
455
+ node_id=self.node_id,
456
+ reason="thought_worker_stopped",
457
+ )
458
+ loop_tasks = list(self._loop_tasks)
459
+ for task in loop_tasks:
460
+ task.cancel()
461
+ if loop_tasks:
462
+ await asyncio.gather(*loop_tasks, return_exceptions=True)
463
+ if self._running_jobs:
464
+ logger.info("Worker %s draining %d jobs...", self.node_id, len(self._running_jobs))
465
+ attempts = dict(self._job_attempt_ids)
466
+ task_jobs = {
467
+ task: job_id for job_id, task in self._running_jobs.items()
468
+ }
469
+ _done, pending = await asyncio.wait(
470
+ list(task_jobs),
471
+ timeout=drain_timeout,
472
+ )
473
+ if pending:
474
+ logger.warning(
475
+ "Worker %s cancelling %d jobs after drain timeout",
476
+ self.node_id,
477
+ len(pending),
478
+ )
479
+ for task in pending:
480
+ task.cancel()
481
+ await asyncio.gather(*pending, return_exceptions=True)
482
+ for task in pending:
483
+ job_id = task_jobs[task]
484
+ attempt = attempts.get(job_id)
485
+ if not attempt:
486
+ continue
487
+ try:
488
+ await self._queue.fail_job(
489
+ job_id,
490
+ self.node_id,
491
+ "worker_shutdown_drain_timeout",
492
+ claim_attempt_id=attempt,
493
+ )
494
+ except Exception:
495
+ logger.exception(
496
+ "Worker %s could not close shutdown job %s",
497
+ self.node_id,
498
+ job_id,
499
+ )
500
+ if self._skill_tasks:
501
+ for task in list(self._skill_tasks):
502
+ task.cancel()
503
+ await asyncio.gather(*self._skill_tasks, return_exceptions=True)
504
+ self._skill_tasks.clear()
505
+ if self._registered:
506
+ self._registered = False
507
+ await self._queue.deregister_worker(self.node_id)
508
+ logger.info("Worker %s stopped", self.node_id)
509
+ finally:
510
+ self._stop_cleanup_done.set()
511
+ self._stop_in_progress = False
512
+
513
+ async def _poll_loop(self) -> None:
514
+ while self._running:
515
+ if len(self._running_jobs) < self._capabilities.max_concurrent:
516
+ try:
517
+ job = await self._queue.claim_job(
518
+ self.node_id, self._capabilities
519
+ )
520
+ if job is not None:
521
+ if not self._running:
522
+ attempt = str(job.claim_attempt_id or "").strip()
523
+ if attempt:
524
+ try:
525
+ await self._queue.release_job(
526
+ job.job_id,
527
+ self.node_id,
528
+ claim_attempt_id=attempt,
529
+ )
530
+ except Exception:
531
+ logger.exception(
532
+ "Worker %s could not release post-stop "
533
+ "claim %s",
534
+ self.node_id,
535
+ job.job_id,
536
+ )
537
+ else:
538
+ logger.error(
539
+ "Worker %s received a post-stop claim with "
540
+ "no exact attempt: %s",
541
+ self.node_id,
542
+ job.job_id,
543
+ )
544
+ break
545
+ task = asyncio.create_task(self._execute_job(job))
546
+ self._running_jobs[job.job_id] = task
547
+ if job.claim_attempt_id:
548
+ self._job_attempt_ids[job.job_id] = job.claim_attempt_id
549
+ task.add_done_callback(
550
+ lambda t, jid=job.job_id: (
551
+ self._running_jobs.pop(jid, None),
552
+ self._job_attempt_ids.pop(jid, None),
553
+ )
554
+ )
555
+ except Exception:
556
+ logger.exception("Worker %s: error claiming job", self.node_id)
557
+ await asyncio.sleep(self._poll_interval)
558
+
559
+ async def _heartbeat_loop(self) -> None:
560
+ while self._running:
561
+ await asyncio.sleep(self._heartbeat_interval)
562
+ attempts = dict(self._job_attempt_ids)
563
+ running_job_ids = list(attempts)
564
+ if running_job_ids:
565
+ try:
566
+ await self._queue.send_heartbeat(
567
+ self.node_id,
568
+ running_job_ids,
569
+ claim_attempt_ids=attempts,
570
+ )
571
+ except Exception:
572
+ logger.exception("Worker %s: heartbeat failed", self.node_id)
573
+ # Update load
574
+ load = len(self._running_jobs) / max(self._capabilities.max_concurrent, 1)
575
+ self._capabilities.load = load
576
+ try:
577
+ await self._queue.update_worker_load(self.node_id, load)
578
+ except Exception:
579
+ logger.warning("Worker %s: failed to update load metric", self.node_id, exc_info=True)
580
+
581
+ async def _work_control_loop(self) -> None:
582
+ """Deliver and acknowledge controls for exact owned attempts."""
583
+
584
+ from apsimo.task_queue.work_control import work_control_mode
585
+
586
+ while self._running:
587
+ await asyncio.sleep(min(max(self._poll_interval / 4.0, 0.1), 1.0))
588
+ if work_control_mode() != "live":
589
+ reconcile = getattr(
590
+ self._queue,
591
+ "reconcile_work_control_delivery_state",
592
+ None,
593
+ )
594
+ if callable(reconcile):
595
+ try:
596
+ await reconcile()
597
+ except Exception:
598
+ logger.exception(
599
+ "Worker %s: inactive WorkControl reconciliation "
600
+ "failed",
601
+ self.node_id,
602
+ )
603
+ continue
604
+ pending_controls = getattr(
605
+ self._queue, "pending_work_control_operations", None,
606
+ )
607
+ if not callable(pending_controls):
608
+ continue
609
+ try:
610
+ controls = await pending_controls(self.node_id)
611
+ except Exception:
612
+ logger.exception(
613
+ "Worker %s: WorkControl poll failed", self.node_id,
614
+ )
615
+ continue
616
+ if not isinstance(controls, list):
617
+ continue
618
+ for control in controls:
619
+ try:
620
+ await self._apply_work_control(control)
621
+ except asyncio.CancelledError:
622
+ raise
623
+ except Exception:
624
+ logger.exception(
625
+ "Worker %s: WorkControl delivery failed for %s",
626
+ self.node_id, control.get("operation_id"),
627
+ )
628
+
629
+ async def _apply_work_control(self, control: Dict[str, Any]) -> None:
630
+ """Cooperate with one durable control, acknowledging only truth."""
631
+
632
+ operation = str(control.get("operation") or "")
633
+ operation_id = str(control.get("operation_id") or "")
634
+ job_id = str(control.get("target_id") or "")
635
+ attempt_id = str(control.get("attempt_id") or "")
636
+
637
+ def _control_live() -> bool:
638
+ from apsimo.task_queue.work_control import (
639
+ work_control_mode,
640
+ )
641
+
642
+ return work_control_mode() == "live"
643
+
644
+ async def _retire_if_inactive() -> bool:
645
+ if _control_live():
646
+ return False
647
+ reconcile = getattr(
648
+ self._queue, "reconcile_work_control_delivery_state", None,
649
+ )
650
+ if callable(reconcile):
651
+ await reconcile()
652
+ return True
653
+
654
+ def _deadline(value: Any) -> Optional[datetime]:
655
+ try:
656
+ parsed = datetime.fromisoformat(str(value))
657
+ except (TypeError, ValueError):
658
+ return None
659
+ if parsed.tzinfo is None:
660
+ parsed = parsed.replace(tzinfo=timezone.utc)
661
+ return parsed.astimezone(timezone.utc)
662
+
663
+ def _lease_valid(job: Optional[Job] = None) -> bool:
664
+ now = datetime.now(timezone.utc)
665
+ lease = _deadline(control.get("ack_deadline"))
666
+ if lease is None or now >= lease:
667
+ return False
668
+ if operation == "steer" and job is not None:
669
+ deadline = job.deadline
670
+ if deadline is not None:
671
+ if deadline.tzinfo is None:
672
+ deadline = deadline.replace(tzinfo=timezone.utc)
673
+ if now >= deadline.astimezone(timezone.utc):
674
+ return False
675
+ return True
676
+
677
+ def _exact_active(job: Optional[Job]) -> bool:
678
+ return bool(
679
+ job is not None
680
+ and job.claimed_by == self.node_id
681
+ and job.claim_attempt_id == attempt_id
682
+ and job.status in {JobStatus.CLAIMED, JobStatus.RUNNING}
683
+ )
684
+
685
+ async def _ack(outcome: str, details: Dict[str, Any]) -> Any:
686
+ if await _retire_if_inactive():
687
+ return None
688
+ return await self._queue.acknowledge_work_control_operation(
689
+ worker_id=self.node_id,
690
+ operation_id=operation_id,
691
+ attempt_id=attempt_id,
692
+ outcome=outcome,
693
+ details=details,
694
+ ack_authority={
695
+ "authority_kind": "in_process_worker",
696
+ "worker_id": self.node_id,
697
+ },
698
+ )
699
+
700
+ if await _retire_if_inactive():
701
+ return
702
+
703
+ if not _lease_valid():
704
+ # The queue acknowledgement path independently enforces the hard
705
+ # deadline and records expiry; this call only accelerates closure.
706
+ await _ack("rejected", {"reason": "control_lease_elapsed"})
707
+ return
708
+
709
+ job = await self._queue.get_job(job_id)
710
+
711
+ if operation == "steer":
712
+ handler = self._handlers.get(job.job_type) if job else None
713
+ stored_outcome = getattr(
714
+ self._queue, "get_work_control_worker_outcome", None,
715
+ )
716
+ record_outcome = getattr(
717
+ self._queue, "record_work_control_worker_outcome", None,
718
+ )
719
+ if not callable(stored_outcome) or not callable(record_outcome):
720
+ await _ack("rejected", {
721
+ "reason": "durable_steer_outcome_ledger_unavailable",
722
+ })
723
+ return
724
+ persisted = await stored_outcome(
725
+ worker_id=self.node_id,
726
+ operation_id=operation_id,
727
+ attempt_id=attempt_id,
728
+ )
729
+ if persisted is not None:
730
+ # Response loss after durable outcome recording must retry the
731
+ # acknowledgement before the handler can be invoked again.
732
+ await _ack(
733
+ str(persisted["outcome"]),
734
+ dict(persisted.get("details") or {}),
735
+ )
736
+ return
737
+ task = self._running_jobs.get(job_id)
738
+ exact_running = bool(
739
+ _exact_active(job)
740
+ and _lease_valid(job)
741
+ and task is not None
742
+ and not task.done()
743
+ and self._job_attempt_ids.get(job_id) == attempt_id
744
+ )
745
+ if (
746
+ not exact_running
747
+ or handler is None
748
+ or not self._handler_supports_steer(handler)
749
+ ):
750
+ await _ack("rejected", {
751
+ "reason": "attempt_or_steer_handler_unavailable",
752
+ })
753
+ return
754
+ parameters = control.get("parameters") or {}
755
+ # Re-read after every preceding await and immediately before the
756
+ # local apply. A delayed attempt-A delivery cannot steer B.
757
+ current = await self._queue.get_job(job_id)
758
+ current_task = self._running_jobs.get(job_id)
759
+ if await _retire_if_inactive():
760
+ return
761
+ if not (
762
+ _exact_active(current)
763
+ and _lease_valid(current)
764
+ and current_task is task
765
+ and current_task is not None
766
+ and not current_task.done()
767
+ and self._job_attempt_ids.get(job_id) == attempt_id
768
+ ):
769
+ await _ack("rejected", {
770
+ "reason": "attempt_changed_before_steer_apply",
771
+ })
772
+ return
773
+ try:
774
+ report = await asyncio.wait_for(
775
+ handler.apply_steer(
776
+ job,
777
+ directive=str(parameters.get("directive") or ""),
778
+ context_refs=[
779
+ str(item)
780
+ for item in parameters.get("context_refs", [])
781
+ ],
782
+ operation_id=operation_id,
783
+ authority_digest=str(
784
+ control.get("authority_digest") or ""
785
+ ),
786
+ ),
787
+ timeout=5.0,
788
+ )
789
+ except (NotImplementedError, asyncio.TimeoutError) as exc:
790
+ rejected = {"reason": type(exc).__name__}
791
+ await record_outcome(
792
+ worker_id=self.node_id,
793
+ operation_id=operation_id,
794
+ attempt_id=attempt_id,
795
+ outcome="rejected",
796
+ details=rejected,
797
+ )
798
+ await _ack("rejected", rejected)
799
+ return
800
+ if not isinstance(report, dict):
801
+ report = {"handler_report": str(report)[:500]}
802
+ if (
803
+ report.get("authority_digest")
804
+ != control.get("authority_digest")
805
+ or report.get("authority_expanded") is not False
806
+ ):
807
+ rejected = {
808
+ "reason": "steer_authority_expansion_or_unattested",
809
+ }
810
+ await record_outcome(
811
+ worker_id=self.node_id,
812
+ operation_id=operation_id,
813
+ attempt_id=attempt_id,
814
+ outcome="rejected",
815
+ details=rejected,
816
+ )
817
+ await _ack("rejected", rejected)
818
+ return
819
+ await record_outcome(
820
+ worker_id=self.node_id,
821
+ operation_id=operation_id,
822
+ attempt_id=attempt_id,
823
+ outcome="applied",
824
+ details=report,
825
+ )
826
+ # The handler outcome is durable now. Re-read immediately before
827
+ # acknowledging; a winning lifecycle transition owns the receipt.
828
+ current = await self._queue.get_job(job_id)
829
+ if await _retire_if_inactive():
830
+ return
831
+ if not (_exact_active(current) and _lease_valid(current)):
832
+ await _ack("rejected", {
833
+ "reason": "attempt_changed_before_steer_ack",
834
+ })
835
+ return
836
+ await _ack("applied", report)
837
+ return
838
+
839
+ if operation not in {"interrupt", "cancel"}:
840
+ await _ack("rejected", {"reason": "unsupported_worker_operation"})
841
+ return
842
+
843
+ handler = self._handlers.get(job.job_type) if job else None
844
+ task = self._running_jobs.get(job_id)
845
+ if not (
846
+ _exact_active(job)
847
+ and _lease_valid(job)
848
+ and handler is not None
849
+ and getattr(
850
+ handler, "work_control_interrupt_safe", False,
851
+ ) is True
852
+ and task is not None
853
+ and not task.done()
854
+ and self._job_attempt_ids.get(job_id) == attempt_id
855
+ ):
856
+ await _ack("rejected", {
857
+ "reason": "attempt_or_interrupt_handler_unavailable",
858
+ })
859
+ return
860
+
861
+ # Re-read immediately before cancel and bind the exact local Task
862
+ # object as well as the durable attempt. There is no await between the
863
+ # final checks and task.cancel(), so a newer task cannot be selected.
864
+ current = await self._queue.get_job(job_id)
865
+ current_task = self._running_jobs.get(job_id)
866
+ if await _retire_if_inactive():
867
+ return
868
+ if not (
869
+ _exact_active(current)
870
+ and _lease_valid(current)
871
+ and current_task is task
872
+ and current_task is not None
873
+ and not current_task.done()
874
+ and self._job_attempt_ids.get(job_id) == attempt_id
875
+ ):
876
+ await _ack("rejected", {
877
+ "reason": "attempt_changed_before_interrupt",
878
+ })
879
+ return
880
+ task.cancel()
881
+ _done, pending = await asyncio.wait({task}, timeout=5.0)
882
+ if pending:
883
+ await _ack("rejected", {"reason": "handler_did_not_stop"})
884
+ return
885
+
886
+ current = await self._queue.get_job(job_id)
887
+ if await _retire_if_inactive():
888
+ return
889
+ if not (_exact_active(current) and _lease_valid(current)):
890
+ await _ack("rejected", {
891
+ "reason": "attempt_changed_before_stop_ack",
892
+ })
893
+ return
894
+ await _ack("stopped", {"cooperative_stop": True})
895
+
896
+ async def _execute_job(self, job: Job) -> None:
897
+ handler = self._handlers.get(job.job_type)
898
+ started_at = datetime.now(timezone.utc)
899
+ self._job_start_times[job.job_id] = started_at
900
+
901
+ if handler is None:
902
+ await self._queue.fail_job(
903
+ job.job_id, self.node_id,
904
+ error=f"No handler registered for job_type={job.job_type}",
905
+ started_at=started_at,
906
+ claim_attempt_id=job.claim_attempt_id,
907
+ )
908
+ self._job_start_times.pop(job.job_id, None)
909
+ return
910
+
911
+ if not await self._queue.start_job(
912
+ job.job_id,
913
+ self.node_id,
914
+ claim_attempt_id=job.claim_attempt_id,
915
+ ):
916
+ # The claim may have expired, been released, or moved to another
917
+ # worker between polling and execution. Never run a handler after
918
+ # the central queue rejects the claimant/state transition.
919
+ logger.warning(
920
+ "Worker %s: start authority rejected for job %s",
921
+ self.node_id,
922
+ job.job_id,
923
+ )
924
+ self._job_start_times.pop(job.job_id, None)
925
+ return
926
+ try:
927
+ output = await asyncio.wait_for(
928
+ handler.execute(job),
929
+ timeout=job.timeout_secs,
930
+ )
931
+ output, server_attested = _prepare_embedded_report(job, output)
932
+ completion = await self._queue.complete_job(
933
+ job.job_id,
934
+ self.node_id,
935
+ output or {},
936
+ started_at=started_at,
937
+ claim_attempt_id=job.claim_attempt_id,
938
+ server_attested=server_attested,
939
+ )
940
+ # ThoughtJobV1 is strictly read-only. The generic post-task
941
+ # learner writes skill state, so it must never run for this type;
942
+ # validated MemoryWriteProposal remains only a proposal.
943
+ if (completion.get("transitioned") is True
944
+ and completion.get("job_status") == JobStatus.COMPLETED.value
945
+ and completion.get("governor_outcome") == "success"
946
+ and self._skill_learning is not None
947
+ and job.job_type != JobType.THOUGHT):
948
+ _hook_task = asyncio.create_task(self._fire_skill_hook(job, output or {}))
949
+ self._skill_tasks.add(_hook_task)
950
+ _hook_task.add_done_callback(
951
+ self._skill_tasks.discard
952
+ )
953
+ except asyncio.TimeoutError:
954
+ await self._queue.fail_job(
955
+ job.job_id, self.node_id,
956
+ error=f"Timed out after {job.timeout_secs}s",
957
+ started_at=started_at,
958
+ claim_attempt_id=job.claim_attempt_id,
959
+ )
960
+ except Exception as exc:
961
+ await self._queue.fail_job(
962
+ job.job_id, self.node_id,
963
+ error=str(exc),
964
+ started_at=started_at,
965
+ claim_attempt_id=job.claim_attempt_id,
966
+ )
967
+ finally:
968
+ self._job_start_times.pop(job.job_id, None)
969
+
970
+ async def _fire_skill_hook(self, job: Job, output: Dict[str, Any]) -> None:
971
+ """Asynchronously fire the skill learning post-task hook.
972
+
973
+ Builds a minimal TaskSolution from the completed job and delegates
974
+ to SkillLearningService.post_task_hook. Errors are logged and
975
+ swallowed so they never surface to the caller.
976
+ """
977
+ try:
978
+ from apsimo.skills.models import TaskSolution
979
+ solution = TaskSolution(
980
+ task_id=job.job_id,
981
+ task_description=str(job.payload.get("description", job.job_type.value)),
982
+ inputs=dict(job.payload),
983
+ output=output,
984
+ trace=[],
985
+ dependencies=list(job.payload.get("dependencies", [])),
986
+ embedding=None,
987
+ step_fingerprint=[str(k) for k in output.keys()],
988
+ duration_secs=0.0,
989
+ completed_at=datetime.now(timezone.utc),
990
+ )
991
+ await self._skill_learning.post_task_hook(solution)
992
+ except Exception:
993
+ logger.debug("Skill learning hook failed for job %s", job.job_id, exc_info=True)