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,21 @@
1
+ """Schedulable agent-side workers (v0.20.0+).
2
+
3
+ These are the agent-side halves of the Colony autonomy circuit, packaged
4
+ so pip installs ship them as console scripts:
5
+
6
+ - ``colony-agent-bridge`` (:mod:`apsimo.workers.agent_bridge`)
7
+ unified daemon that replaces the three cron scripts below with one
8
+ long-running process: initiative polling + job dispatch + skills sync
9
+ + circuit health monitoring. Preferred for new installs.
10
+ - ``colony-queue-worker`` (:mod:`apsimo.workers.queue_worker`)
11
+ claims approved ``agent_action`` jobs and hands them to the agent.
12
+ - ``colony-skills-sync`` (:mod:`apsimo.workers.skills_sync`)
13
+ reports the agent's installed skill index to Colony.
14
+
15
+ All modules are deliberately stdlib-only: they must run from cron on
16
+ machines where only the agent (not the full sidecar dependency stack)
17
+ is present. Do not import heavy dependencies here.
18
+
19
+ The historical loose scripts under ``plugins/hermes-plugin/poller/``
20
+ remain as thin back-compat wrappers around these modules.
21
+ """
@@ -0,0 +1,640 @@
1
+ """Colony Agent Bridge -- unified daemon for the agent-side autonomy circuit.
2
+
3
+ Replaces three separate cron scripts (initiative-poller, queue-worker,
4
+ skills-sync) with one long-running process that:
5
+
6
+ 1. Polls for pending initiatives and forwards them to the agent webhook
7
+ 2. Claims agent_action jobs from the task queue and dispatches them
8
+ 3. Periodically syncs the agent's skill index to Colony
9
+ 4. Monitors the full circuit health and surfaces silent failures
10
+
11
+ Silent failures this catches:
12
+ - Sidecar unreachable (connection refused / timeout)
13
+ - Autonomy loop not running or stuck (no ticks advancing)
14
+ - Initiatives generated but never executed (the exact bug that prompted this)
15
+ - Queue jobs stuck in QUEUED state with no worker claiming them
16
+ - Agent webhook unreachable (initiatives polled but can't be delivered)
17
+
18
+ Run as a daemon (``colony-agent-bridge``) or one-shot (``--once``).
19
+ All configuration via environment variables; no deployment-specific defaults.
20
+
21
+ Stdlib-only -- must run on machines where only the agent is installed.
22
+
23
+ Environment:
24
+ COLONY_URL sidecar URL (default http://127.0.0.1:7777)
25
+ COLONY_API_KEY API key (default dev-mode-no-key)
26
+ COLONY_INITIATIVE_WEBHOOK initiative webhook (default http://127.0.0.1:8644/webhooks/colony-initiatives)
27
+ COLONY_JOBS_WEBHOOK_URL jobs webhook (default http://127.0.0.1:8644/webhooks/colony-jobs)
28
+ COLONY_AGENT_NAME agent identity (default hermes)
29
+ COLONY_WORKER_MAX_JOBS jobs per cycle (default 1)
30
+ COLONY_BRIDGE_POLL_SECS main loop interval (default 60)
31
+ COLONY_BRIDGE_SKILLS_HOURS skills sync interval (default 24)
32
+ COLONY_BRIDGE_LOG_CHANNEL alert routing channel (optional)
33
+ COLONY_BRIDGE_PLATFORM platform for alerts (default unset; set to your messaging platform)
34
+ HERMES_SKILLS_DIR skills directory (default ~/.hermes/skills)
35
+ COLONY_BRIDGE_STATE_DIR state file directory (default ~/.colony/bridge)
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import argparse
41
+ import json
42
+ import logging
43
+ import os
44
+ import signal
45
+ import sys
46
+ import time
47
+ import urllib.error
48
+ import urllib.request
49
+ from datetime import datetime, timezone
50
+ from pathlib import Path
51
+ from typing import Any, Dict, Optional
52
+
53
+ from apsimo.workers.queue_worker import (
54
+ agent_action_capabilities,
55
+ encode_hermes_webhook,
56
+ work_order_webhook_fields,
57
+ )
58
+
59
+ logger = logging.getLogger("colony.bridge")
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Configuration
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def _cfg() -> Dict[str, Any]:
66
+ state_dir = Path(
67
+ os.environ.get("COLONY_BRIDGE_STATE_DIR", "~/.colony/bridge")
68
+ ).expanduser()
69
+ state_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ node_id = os.environ.get("COLONY_WORKER_NODE_ID") or (
72
+ os.environ.get("COLONY_AGENT_NAME", "hermes").strip().lower().replace(" ", "-")
73
+ + "-agent"
74
+ )
75
+ return {
76
+ "colony_url": os.environ.get("COLONY_URL", "http://127.0.0.1:7777"),
77
+ "api_key": os.environ.get("COLONY_API_KEY", "dev-mode-no-key"),
78
+ "initiative_webhook": os.environ.get(
79
+ "COLONY_INITIATIVE_WEBHOOK",
80
+ "http://127.0.0.1:8644/webhooks/colony-initiatives",
81
+ ),
82
+ "jobs_webhook": os.environ.get(
83
+ "COLONY_JOBS_WEBHOOK_URL",
84
+ "http://127.0.0.1:8644/webhooks/colony-jobs",
85
+ ),
86
+ "node_id": node_id,
87
+ "max_jobs": int(os.environ.get("COLONY_WORKER_MAX_JOBS", "1")),
88
+ "poll_secs": int(os.environ.get("COLONY_BRIDGE_POLL_SECS", "60")),
89
+ "skills_hours": float(os.environ.get("COLONY_BRIDGE_SKILLS_HOURS", "24")),
90
+ "log_channel": os.environ.get("COLONY_BRIDGE_LOG_CHANNEL", ""),
91
+ "platform": os.environ.get("COLONY_BRIDGE_PLATFORM", ""),
92
+ "state_dir": state_dir,
93
+ }
94
+
95
+
96
+ def _headers(cfg: Dict[str, Any]) -> Dict[str, str]:
97
+ return {"X-API-Key": cfg["api_key"], "Content-Type": "application/json"}
98
+
99
+
100
+ def _get(cfg: Dict[str, Any], path: str, timeout: int = 10) -> Optional[dict]:
101
+ req = urllib.request.Request(
102
+ f"{cfg['colony_url']}{path}",
103
+ headers=_headers(cfg),
104
+ method="GET",
105
+ )
106
+ try:
107
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
108
+ return json.loads(resp.read().decode("utf-8"))
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def _post(cfg: Dict[str, Any], url: str, body: dict, timeout: int = 15) -> Optional[dict]:
114
+ req = urllib.request.Request(
115
+ url,
116
+ data=json.dumps(body).encode("utf-8"),
117
+ headers=_headers(cfg),
118
+ method="POST",
119
+ )
120
+ try:
121
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
122
+ raw = resp.read().decode("utf-8")
123
+ return json.loads(raw) if raw else {}
124
+ except Exception:
125
+ return None
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # State persistence (survives restarts)
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def _load_set(path: Path) -> set:
133
+ try:
134
+ return set(line.strip() for line in path.read_text().splitlines() if line.strip())
135
+ except FileNotFoundError:
136
+ return set()
137
+
138
+
139
+ def _save_set(path: Path, items: set) -> None:
140
+ path.write_text("\n".join(sorted(items)) + "\n" if items else "")
141
+
142
+
143
+ def _load_json(path: Path) -> dict:
144
+ try:
145
+ return json.loads(path.read_text())
146
+ except (FileNotFoundError, json.JSONDecodeError):
147
+ return {}
148
+
149
+
150
+ def _save_json(path: Path, data: dict) -> None:
151
+ path.write_text(json.dumps(data, indent=2, default=str) + "\n")
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Health monitor
156
+ # ---------------------------------------------------------------------------
157
+
158
+ class HealthMonitor:
159
+ """Tracks circuit health across cycles and detects silent failures."""
160
+
161
+ def __init__(self, state_dir: Path):
162
+ self._path = state_dir / "health_state.json"
163
+ self._state = _load_json(self._path)
164
+ self._alerts: list = []
165
+
166
+ def check(self, cfg: Dict[str, Any]) -> dict:
167
+ self._alerts = []
168
+ now = datetime.now(timezone.utc).isoformat()
169
+
170
+ health = _get(cfg, "/v1/host/health")
171
+ if not health: # unreachable OR an empty 200 body — both are failures
172
+ consecutive = self._state.get("consecutive_failures", 0) + 1
173
+ self._state["consecutive_failures"] = consecutive
174
+ self._state["last_failure"] = now
175
+ self._alert("sidecar_unreachable",
176
+ f"Colony sidecar at {cfg['colony_url']} is unreachable "
177
+ "or returned an empty health body "
178
+ f"({consecutive} consecutive failure(s))",
179
+ severity="critical" if consecutive >= 3 else "warning")
180
+ self._save()
181
+ return {"ok": False, "alerts": self._alerts}
182
+
183
+ self._state["consecutive_failures"] = 0
184
+ self._state["last_success"] = now
185
+
186
+ status = health.get("status")
187
+ ok = status == "ok"
188
+ if not ok:
189
+ self._alert("sidecar_degraded",
190
+ f"Sidecar reports health status {status!r} (not 'ok')",
191
+ severity="warning")
192
+
193
+ autonomy = _get(cfg, "/v1/host/autonomy/status")
194
+ if autonomy is None:
195
+ # The probe did not run — a skipped check is not a passed check.
196
+ self._alert("autonomy_status_unavailable",
197
+ "Autonomy status endpoint is unreachable — the "
198
+ "autonomy probe was skipped, not passed",
199
+ severity="warning")
200
+ else:
201
+ self._check_autonomy(autonomy)
202
+ self._state["last_autonomy"] = autonomy
203
+
204
+ self._save()
205
+ return {"ok": ok, "health": health, "autonomy": autonomy,
206
+ "autonomy_probe": "skipped" if autonomy is None else "checked",
207
+ "alerts": self._alerts}
208
+
209
+ def _check_autonomy(self, status: dict) -> None:
210
+ if not status.get("running"):
211
+ self._alert("autonomy_stopped",
212
+ "Autonomy loop is not running",
213
+ severity="warning")
214
+ return
215
+
216
+ prev = self._state.get("last_autonomy", {})
217
+ prev_ticks = prev.get("ticks", 0)
218
+ curr_ticks = status.get("ticks", 0)
219
+
220
+ if prev_ticks > 0 and curr_ticks == prev_ticks:
221
+ self._alert("autonomy_stuck",
222
+ f"Autonomy loop stuck at tick {curr_ticks} (not advancing)",
223
+ severity="warning")
224
+
225
+ generated = status.get("initiatives_generated", 0)
226
+ executed = status.get("actions_executed", 0)
227
+ if generated > 100 and executed == 0:
228
+ self._alert("initiatives_never_executed",
229
+ f"Autonomy has generated {generated} initiatives but "
230
+ f"executed 0 actions. The delivery pipeline may be broken.",
231
+ severity="critical")
232
+
233
+ def _alert(self, alert_type: str, message: str, severity: str = "warning") -> None:
234
+ now_iso = datetime.now(timezone.utc).isoformat()
235
+ last_key = f"last_alert_{alert_type}"
236
+ last = self._state.get(last_key, "")
237
+ if last:
238
+ try:
239
+ elapsed = (
240
+ datetime.fromisoformat(now_iso)
241
+ - datetime.fromisoformat(last)
242
+ ).total_seconds()
243
+ if elapsed < 3600:
244
+ return
245
+ except (ValueError, TypeError):
246
+ pass
247
+ self._state[last_key] = now_iso
248
+ entry = {"type": alert_type, "message": message, "severity": severity, "at": now_iso}
249
+ self._alerts.append(entry)
250
+ level = logging.CRITICAL if severity == "critical" else logging.WARNING
251
+ logger.log(level, "[HEALTH] %s: %s", alert_type, message)
252
+
253
+ def _save(self) -> None:
254
+ _save_json(self._path, self._state)
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Initiative poller
259
+ # ---------------------------------------------------------------------------
260
+
261
+ class InitiativePoller:
262
+ """Polls pending initiatives and forwards them to the agent webhook."""
263
+
264
+ def __init__(self, state_dir: Path):
265
+ self._seen_path = state_dir / "seen_initiatives.txt"
266
+ self._dedup_path = state_dir / "seen_dedup_keys.txt"
267
+ self._seen_ids = _load_set(self._seen_path)
268
+ self._seen_dedup = _load_set(self._dedup_path)
269
+
270
+ def poll(self, cfg: Dict[str, Any]) -> int:
271
+ data = _get(cfg, "/v1/host/initiatives")
272
+ if data is None:
273
+ return 0
274
+
275
+ items = data if isinstance(data, list) else data.get("initiatives", [])
276
+ fired = 0
277
+
278
+ for initiative in items:
279
+ iid = initiative.get("id", "")
280
+ status = initiative.get("status", "")
281
+ dedup_key = initiative.get("dedup_key", "")
282
+
283
+ if status != "pending":
284
+ continue
285
+ if iid in self._seen_ids:
286
+ continue
287
+ if dedup_key and dedup_key in self._seen_dedup:
288
+ self._seen_ids.add(iid)
289
+ continue
290
+
291
+ payload = {
292
+ "type": "initiative",
293
+ "payload": initiative,
294
+ "occurred_at": initiative.get("created_at", ""),
295
+ "seq": 0,
296
+ "delivery_context": {
297
+ "log_channel": cfg.get("log_channel", ""),
298
+ "platform": cfg.get("platform", ""),
299
+ },
300
+ }
301
+ try:
302
+ body, headers = encode_hermes_webhook(payload)
303
+ req = urllib.request.Request(
304
+ cfg["initiative_webhook"],
305
+ data=body,
306
+ headers=headers,
307
+ method="POST",
308
+ )
309
+ urllib.request.urlopen(req, timeout=10)
310
+ self._seen_ids.add(iid)
311
+ if dedup_key:
312
+ self._seen_dedup.add(dedup_key)
313
+ fired += 1
314
+ logger.info("Fired initiative %s (%s)", iid,
315
+ initiative.get("initiative_type", "?"))
316
+ except Exception as exc:
317
+ logger.warning("Initiative webhook failed for %s: %s", iid, exc)
318
+
319
+ self._save()
320
+ if fired:
321
+ logger.info("Initiatives fired: %d", fired)
322
+ return fired
323
+
324
+ def _save(self) -> None:
325
+ if len(self._seen_ids) > 5000:
326
+ self._seen_ids = set(sorted(self._seen_ids)[-2000:])
327
+ if len(self._seen_dedup) > 5000:
328
+ self._seen_dedup = set(sorted(self._seen_dedup)[-2000:])
329
+ _save_set(self._seen_path, self._seen_ids)
330
+ _save_set(self._dedup_path, self._seen_dedup)
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Queue worker
335
+ # ---------------------------------------------------------------------------
336
+
337
+ class QueueWorker:
338
+ """Claims agent_action jobs and dispatches them to the agent webhook."""
339
+
340
+ def claim_and_dispatch(self, cfg: Dict[str, Any]) -> int:
341
+ if os.environ.get(
342
+ "COLONY_AGENT_JOB_CLAIMS_ENABLED", "true"
343
+ ).strip().lower() not in {"1", "true", "yes", "on"}:
344
+ return 0
345
+ try:
346
+ capabilities = agent_action_capabilities()
347
+ except ValueError as exc:
348
+ logger.error("Worker routes are invalid: %s", exc)
349
+ return 0
350
+ try:
351
+ _post(cfg, f"{cfg['colony_url']}/v1/host/queue/workers/register", {
352
+ "node_id": cfg["node_id"],
353
+ "capabilities": capabilities,
354
+ "job_types": ["agent_action"],
355
+ "max_concurrent": 1,
356
+ "available": True,
357
+ "load": 0.0,
358
+ })
359
+ except Exception as exc:
360
+ logger.warning("Worker registration failed: %s", exc)
361
+ return 0
362
+
363
+ dispatched = 0
364
+ for _ in range(cfg["max_jobs"]):
365
+ job = _post(cfg, f"{cfg['colony_url']}/v1/host/queue/jobs/claim", {
366
+ "node_id": cfg["node_id"],
367
+ "capabilities": capabilities,
368
+ "job_types": ["agent_action"],
369
+ "max_concurrent": 1,
370
+ })
371
+ if not job:
372
+ break
373
+
374
+ job_id = job.get("job_id") or job.get("id")
375
+ claim_attempt_id = job.get("claim_attempt_id")
376
+ params = job.get("payload") or job.get("params") or {}
377
+ colony_url = cfg["colony_url"]
378
+
379
+ try:
380
+ started = _post(
381
+ cfg,
382
+ f"{colony_url}/v1/host/queue/jobs/{job_id}/start",
383
+ {"claim_attempt_id": claim_attempt_id},
384
+ )
385
+ if not isinstance(started, dict) or started.get("success") is not True:
386
+ raise RuntimeError("start transition was not accepted")
387
+ except Exception as exc:
388
+ logger.warning("Job start failed for %s: %s", job_id, exc)
389
+ try:
390
+ _post(
391
+ cfg,
392
+ f"{colony_url}/v1/host/queue/jobs/{job_id}/release",
393
+ {"claim_attempt_id": claim_attempt_id},
394
+ )
395
+ except Exception:
396
+ pass
397
+ continue
398
+
399
+ payload = {
400
+ "type": "agent_job",
401
+ "occurred_at": datetime.now(timezone.utc).isoformat(),
402
+ "payload": {
403
+ "job_id": job_id,
404
+ "claim_attempt_id": claim_attempt_id,
405
+ "action_hint": params.get("action_hint", ""),
406
+ "domain": params.get("domain", ""),
407
+ "risk": params.get("risk", ""),
408
+ "description": params.get("description", ""),
409
+ "context": params.get("context", {}),
410
+ "report_example": params.get("report_example", {}),
411
+ **work_order_webhook_fields(params),
412
+ "colony_url": colony_url,
413
+ "observations_url": f"{colony_url}/v1/host/observations",
414
+ "heartbeat_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/heartbeat",
415
+ "complete_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/complete",
416
+ "fail_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/fail",
417
+ "api_key_header": "X-API-Key",
418
+ },
419
+ }
420
+ try:
421
+ body, headers = encode_hermes_webhook(payload)
422
+ req = urllib.request.Request(
423
+ cfg["jobs_webhook"],
424
+ data=body,
425
+ headers=headers,
426
+ method="POST",
427
+ )
428
+ urllib.request.urlopen(req, timeout=15)
429
+ dispatched += 1
430
+ hint = params.get("action_hint", "?")
431
+ logger.info("Dispatched job %s (%s)", job_id, hint)
432
+ except Exception as exc:
433
+ logger.warning("Job webhook failed for %s: %s", job_id, exc)
434
+ try:
435
+ _post(
436
+ cfg,
437
+ f"{colony_url}/v1/host/queue/jobs/{job_id}/release",
438
+ {"claim_attempt_id": claim_attempt_id},
439
+ )
440
+ except Exception:
441
+ pass
442
+
443
+ if dispatched:
444
+ logger.info("Jobs dispatched: %d", dispatched)
445
+ return dispatched
446
+
447
+
448
+ # ---------------------------------------------------------------------------
449
+ # Skills sync (periodic)
450
+ # ---------------------------------------------------------------------------
451
+
452
+ class SkillsSyncer:
453
+ """Periodically reports the agent's skill index to Colony."""
454
+
455
+ def __init__(self, interval_hours: float):
456
+ self._interval = interval_hours * 3600
457
+ self._last_sync = 0.0
458
+
459
+ def sync_if_due(self, cfg: Dict[str, Any]) -> bool:
460
+ now = time.monotonic()
461
+ if self._last_sync > 0 and (now - self._last_sync) < self._interval:
462
+ return False
463
+ self._last_sync = now
464
+
465
+ try:
466
+ from apsimo.workers.skills_sync import scan, report
467
+ observations = scan()
468
+ if observations:
469
+ status = report(observations)
470
+ logger.info("Skills sync: %d skills reported (HTTP %s)", len(observations), status)
471
+ return True
472
+ else:
473
+ logger.debug("Skills sync: no skills found")
474
+ return False
475
+ except ImportError:
476
+ logger.debug("Skills sync: apsimo.workers.skills_sync not importable, skipping")
477
+ return False
478
+ except Exception as exc:
479
+ logger.warning("Skills sync failed: %s", exc)
480
+ return False
481
+
482
+
483
+ # ---------------------------------------------------------------------------
484
+ # Alert delivery
485
+ # ---------------------------------------------------------------------------
486
+
487
+ def deliver_alerts(cfg: Dict[str, Any], alerts: list) -> None:
488
+ """Forward health alerts to the agent webhook for surfacing."""
489
+ if not alerts:
490
+ return
491
+ for alert in alerts:
492
+ payload = {
493
+ "type": "alert",
494
+ "payload": {
495
+ "alert_type": alert["type"],
496
+ "severity": alert["severity"],
497
+ "message": alert["message"],
498
+ },
499
+ "occurred_at": alert.get("at", datetime.now(timezone.utc).isoformat()),
500
+ "delivery_context": {
501
+ "log_channel": cfg.get("log_channel", ""),
502
+ "platform": cfg.get("platform", ""),
503
+ },
504
+ }
505
+ try:
506
+ body, headers = encode_hermes_webhook(payload)
507
+ req = urllib.request.Request(
508
+ cfg["initiative_webhook"],
509
+ data=body,
510
+ headers=headers,
511
+ method="POST",
512
+ )
513
+ urllib.request.urlopen(req, timeout=10)
514
+ logger.info("Alert delivered: %s", alert["type"])
515
+ except Exception as exc:
516
+ logger.warning("Alert delivery failed (%s): %s", alert["type"], exc)
517
+
518
+
519
+ # ---------------------------------------------------------------------------
520
+ # Main loop
521
+ # ---------------------------------------------------------------------------
522
+
523
+ class AgentBridge:
524
+ """Unified daemon that keeps the Colony-to-agent circuit alive."""
525
+
526
+ def __init__(self, cfg: Dict[str, Any]):
527
+ self.cfg = cfg
528
+ self._health = HealthMonitor(cfg["state_dir"])
529
+ self._poller = InitiativePoller(cfg["state_dir"])
530
+ self._worker = QueueWorker()
531
+ self._skills = SkillsSyncer(cfg["skills_hours"])
532
+ self._running = True
533
+
534
+ def stop(self, *_args: Any) -> None:
535
+ logger.info("Shutdown signal received")
536
+ self._running = False
537
+
538
+ def cycle(self) -> dict:
539
+ """Run one full cycle: health check, poll, dispatch, sync."""
540
+ result = {"ts": datetime.now(timezone.utc).isoformat()}
541
+
542
+ check = self._health.check(self.cfg)
543
+ result["health_ok"] = check.get("ok", False)
544
+
545
+ if check.get("alerts"):
546
+ deliver_alerts(self.cfg, check["alerts"])
547
+ result["alerts"] = len(check["alerts"])
548
+
549
+ if not check.get("ok"):
550
+ return result
551
+
552
+ result["initiatives_fired"] = self._poller.poll(self.cfg)
553
+ result["jobs_dispatched"] = self._worker.claim_and_dispatch(self.cfg)
554
+ result["skills_synced"] = self._skills.sync_if_due(self.cfg)
555
+
556
+ return result
557
+
558
+ def run(self, once: bool = False) -> None:
559
+ logger.info(
560
+ "Colony Agent Bridge starting (poll=%ds, skills_sync=%dh, url=%s)",
561
+ self.cfg["poll_secs"],
562
+ int(self.cfg["skills_hours"]),
563
+ self.cfg["colony_url"],
564
+ )
565
+
566
+ signal.signal(signal.SIGTERM, self.stop)
567
+ signal.signal(signal.SIGINT, self.stop)
568
+
569
+ while self._running:
570
+ try:
571
+ result = self.cycle()
572
+ level = logging.DEBUG
573
+ if result.get("initiatives_fired") or result.get("jobs_dispatched"):
574
+ level = logging.INFO
575
+ if result.get("alerts"):
576
+ level = logging.WARNING
577
+ logger.log(level, "Cycle: %s", json.dumps(result, default=str))
578
+ except Exception as exc:
579
+ logger.error("Cycle error: %s", exc, exc_info=True)
580
+
581
+ if once:
582
+ break
583
+
584
+ try:
585
+ time.sleep(self.cfg["poll_secs"])
586
+ except (KeyboardInterrupt, SystemExit):
587
+ break
588
+
589
+ logger.info("Colony Agent Bridge stopped")
590
+
591
+
592
+ # ---------------------------------------------------------------------------
593
+ # CLI
594
+ # ---------------------------------------------------------------------------
595
+
596
+ def main(argv: Optional[list] = None) -> int:
597
+ parser = argparse.ArgumentParser(
598
+ prog="colony-agent-bridge",
599
+ description=(
600
+ "Unified daemon that bridges Colony's autonomy loop to the agent. "
601
+ "Replaces the separate initiative-poller, queue-worker, and "
602
+ "skills-sync cron jobs with one process that also monitors "
603
+ "circuit health."
604
+ ),
605
+ )
606
+ parser.add_argument(
607
+ "--once", action="store_true",
608
+ help="run one cycle and exit (for cron or testing)",
609
+ )
610
+ parser.add_argument(
611
+ "--dry-run", action="store_true",
612
+ help="print configuration and exit without any network calls",
613
+ )
614
+ parser.add_argument(
615
+ "--verbose", "-v", action="store_true",
616
+ help="enable debug logging",
617
+ )
618
+ args = parser.parse_args(argv)
619
+
620
+ logging.basicConfig(
621
+ level=logging.DEBUG if args.verbose else logging.INFO,
622
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
623
+ datefmt="%H:%M:%S",
624
+ )
625
+
626
+ cfg = _cfg()
627
+
628
+ if args.dry_run:
629
+ print("colony-agent-bridge (dry run):")
630
+ for k, v in sorted(cfg.items()):
631
+ print(f" {k}: {v}")
632
+ return 0
633
+
634
+ bridge = AgentBridge(cfg)
635
+ bridge.run(once=args.once)
636
+ return 0
637
+
638
+
639
+ if __name__ == "__main__":
640
+ raise SystemExit(main())