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,77 @@
1
+ # Example feed spec — everything a feed instance needs, in one file.
2
+ # Create with: colony feeds create example-feed.yaml
3
+ # Only name/topic/destination/cadence + one source adapter are required.
4
+
5
+ name: rust-embedded
6
+ title: Rust Embedded Feed
7
+ topic: >-
8
+ Rust on microcontrollers and embedded targets: new HAL crates, RTIC and
9
+ Embassy releases, async-on-bare-metal techniques, and real hardware
10
+ benchmarks. The goal is catching tools and techniques worth adopting.
11
+
12
+ audience: personal # personal | group (group adds community-input handling)
13
+
14
+ destination:
15
+ kind: deliver # deliver | command | file
16
+ deliver: origin # harness delivery target: origin, platform:chat_id, ...
17
+ # kind: command # pipe the brief to a shell command instead:
18
+ # send_command: "python3 ~/.hermes/scripts/my-group-send.py 42"
19
+ # read_back: 'read_chat chat="My Group" limit=100' # group community input
20
+ # owner_deliver: origin # where discovery proposals go (defaults to deliver)
21
+
22
+ # llm: # optional but recommended: pin so global provider
23
+ # provider: my-provider # changes can never drift-skip these jobs
24
+ # model: my/model
25
+
26
+ cadence:
27
+ collect: "every 240m"
28
+ distill: "every 120m"
29
+ # digest: "0 8 * * *" # optional daily rollup
30
+ # alerts: "every 60m" # optional between-brief P0 pushes
31
+ # discovery: "every 480m" # optional new-source research agent
32
+
33
+ sources:
34
+ x_searches:
35
+ - '(embassy OR RTIC OR "embedded-hal") rust'
36
+ key_accounts: [rustembedded]
37
+ github_search: "embedded rust hal OR rtic OR embassy"
38
+ github_keywords: [embedded, hal, rtic, embassy, no_std]
39
+ hf_keywords: [] # empty = adapter disabled
40
+ forum_urls: [] # Discourse forums (fetched as <url>.json)
41
+ forum_title_keywords: []
42
+ arxiv:
43
+ categories: [cs.SE]
44
+ query_terms: [embedded, "real-time"]
45
+ keywords: [embedded, microcontroller]
46
+ rss:
47
+ - {name: rust-blog, url: "https://blog.rust-lang.org/feed.xml"}
48
+
49
+ registry: # tiered source registry used by promotion rules
50
+ - name: rust-embedded-wg
51
+ handle: "@rustembedded"
52
+ category: working_group
53
+ weight: P0
54
+ reason: Official embedded WG — every announcement matters.
55
+ platforms: [x]
56
+
57
+ scoring:
58
+ keyword_categories: # generic keyword->points buckets
59
+ - {keywords: [rtic, embassy, embedded-hal, no_std], points: 15}
60
+ - {keywords: [benchmark, latency, interrupt], points: 10}
61
+ thresholds: {p0: 40, p1: 25, p2: 15}
62
+
63
+ brief:
64
+ framing: "What changes for embedded Rust projects because of this?"
65
+ standard_cap: 5
66
+ # context_files: # extra files the distill agent reads if present
67
+ # - {path: ~/.hermes/data/my-extra-context.json, label: crate download stats}
68
+ # extra_instructions: "" # freeform text appended to the distill prompt
69
+
70
+ privacy:
71
+ forbidden:
72
+ - "private infrastructure details (hardware counts, cluster topology, hostnames, addresses)"
73
+ - "names of the operator, their contacts, or internal agent/system codenames"
74
+ # aliases: [DGX Spark] # allowed public product names
75
+ # note: "" # extra privacy instruction
76
+
77
+ learning_loop: true # log unknown-source promotions for registry evolution
@@ -0,0 +1,126 @@
1
+ """Harness (Hermes) cron integration for feed instances.
2
+
3
+ All scheduling goes through the ``hermes cron`` CLI; the one thing the CLI
4
+ cannot do — pinning a job to an explicit provider/model so global inference
5
+ config drift can never silently skip a feed job — is done by editing the
6
+ scheduler's jobs.json directly (with a timestamped backup first).
7
+
8
+ Paths/binaries are overridable via env for non-standard installs:
9
+ COLONY_HERMES_BIN hermes CLI (default: ``hermes`` on PATH, then the
10
+ conventional venv location)
11
+ COLONY_HERMES_HOME harness home (default ~/.hermes)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import time
21
+
22
+
23
+ def hermes_home() -> str:
24
+ return os.path.expanduser(os.environ.get("COLONY_HERMES_HOME", "~/.hermes"))
25
+
26
+
27
+ def hermes_bin() -> str:
28
+ env = os.environ.get("COLONY_HERMES_BIN")
29
+ if env:
30
+ return os.path.expanduser(env)
31
+ on_path = shutil.which("hermes")
32
+ if on_path:
33
+ return on_path
34
+ conventional = os.path.join(hermes_home(), "hermes-agent/venv/bin/hermes")
35
+ return conventional if os.path.exists(conventional) else "hermes"
36
+
37
+
38
+ def jobs_json_path() -> str:
39
+ return os.path.join(hermes_home(), "cron/jobs.json")
40
+
41
+
42
+ def scripts_dir() -> str:
43
+ return os.path.join(hermes_home(), "scripts")
44
+
45
+
46
+ def _run(args: list[str], timeout: int = 60) -> str:
47
+ proc = subprocess.run([hermes_bin(), *args], capture_output=True, text=True,
48
+ timeout=timeout)
49
+ if proc.returncode != 0:
50
+ raise RuntimeError(f"hermes {' '.join(args[:2])} failed: "
51
+ f"{proc.stderr.strip() or proc.stdout.strip()}")
52
+ return proc.stdout
53
+
54
+
55
+ def _load_jobs() -> list[dict]:
56
+ try:
57
+ with open(jobs_json_path(), encoding="utf-8") as f:
58
+ data = json.load(f)
59
+ except FileNotFoundError:
60
+ return []
61
+ return data.get("jobs", data) if isinstance(data, dict) else data
62
+
63
+
64
+ def find_jobs(name_prefix: str) -> list[dict]:
65
+ return [j for j in _load_jobs()
66
+ if isinstance(j, dict) and (j.get("name") or "").startswith(name_prefix)]
67
+
68
+
69
+ def create_job(name: str, schedule: str, *, prompt: str | None = None,
70
+ script: str | None = None, no_agent: bool = False,
71
+ deliver: str = "local") -> str:
72
+ """Create a job and return its id (resolved by unique name lookup)."""
73
+ if find_jobs(name):
74
+ raise RuntimeError(f"a cron job named {name!r} already exists")
75
+ args = ["cron", "create", schedule, "--name", name, "--deliver", deliver]
76
+ if script:
77
+ args += ["--script", script]
78
+ if no_agent:
79
+ args += ["--no-agent"]
80
+ if prompt is not None:
81
+ args.insert(3, prompt) # positional prompt right after the schedule
82
+ _run(args)
83
+ jobs = find_jobs(name)
84
+ if len(jobs) != 1:
85
+ raise RuntimeError(f"created job {name!r} but found {len(jobs)} matches in jobs.json")
86
+ return jobs[0]["id"]
87
+
88
+
89
+ def pin_model(job_ids: list[str], provider: str, model: str) -> None:
90
+ """Pin jobs to an explicit provider/model in jobs.json (backup first)."""
91
+ if not (provider and model):
92
+ return
93
+ path = jobs_json_path()
94
+ shutil.copy2(path, f"{path}.bak-feeds-{int(time.time())}")
95
+ with open(path, encoding="utf-8") as f:
96
+ data = json.load(f)
97
+ jobs = data.get("jobs", data) if isinstance(data, dict) else data
98
+ hit = 0
99
+ for j in jobs:
100
+ if isinstance(j, dict) and j.get("id") in job_ids:
101
+ j["provider"] = provider
102
+ j["model"] = model
103
+ hit += 1
104
+ with open(path, "w", encoding="utf-8") as f:
105
+ json.dump(data, f, indent=2)
106
+ if hit != len(job_ids):
107
+ raise RuntimeError(f"pinned {hit}/{len(job_ids)} jobs — jobs.json out of sync")
108
+
109
+
110
+ def remove_job(job_id: str) -> None:
111
+ _run(["cron", "remove", job_id])
112
+
113
+
114
+ def pause_job(job_id: str) -> None:
115
+ _run(["cron", "pause", job_id])
116
+
117
+
118
+ def resume_job(job_id: str) -> None:
119
+ _run(["cron", "resume", job_id])
120
+
121
+
122
+ def run_job_detached(job_id: str) -> None:
123
+ """Trigger a run without blocking: agent jobs can run for many minutes."""
124
+ subprocess.Popen([hermes_bin(), "cron", "run", job_id],
125
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
126
+ start_new_session=True)
@@ -0,0 +1,235 @@
1
+ """Feed instance lifecycle: create, list, status, pause/resume, run, delete.
2
+
3
+ Everything a feed needs is derived from its spec at create time:
4
+
5
+ data dir <data_root>/{spec.yaml,instance.json,config.json,queue.json,seen.db,briefs/}
6
+ engine one shared copy of engine.py in the harness scripts dir
7
+ shims tiny generated per-instance scripts that point the engine at
8
+ this instance's config (the scheduler's --script mechanism has
9
+ no argument passing, so the shim IS the argument)
10
+ cron jobs "[feeds:<name>] <stage>" jobs, agent jobs pinned to the spec's
11
+ provider/model so global config drift can never skip them
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import time
22
+
23
+ from . import hermes_cron as hc
24
+ from . import template
25
+ from .spec import FeedSpec
26
+
27
+ ENGINE_BASENAME = "colony-feed-engine.py"
28
+ JOB_PREFIX = "[feeds:{name}]"
29
+
30
+
31
+ def _prefix(name: str) -> str:
32
+ return JOB_PREFIX.format(name=name)
33
+
34
+
35
+ def _instance_path(name: str) -> str:
36
+ return os.path.expanduser(f"~/.hermes/data/feeds/{name}/instance.json")
37
+
38
+
39
+ def _load_instance(name: str) -> dict:
40
+ try:
41
+ with open(_instance_path(name), encoding="utf-8") as f:
42
+ return json.load(f)
43
+ except FileNotFoundError:
44
+ raise SystemExit(f"no feed instance named {name!r} (see: colony feeds list)")
45
+
46
+
47
+ def _engine_src() -> str:
48
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "engine.py")
49
+
50
+
51
+ def _install_engine() -> str:
52
+ dst = os.path.join(hc.scripts_dir(), ENGINE_BASENAME)
53
+ os.makedirs(hc.scripts_dir(), exist_ok=True)
54
+ shutil.copy2(_engine_src(), dst)
55
+ return dst
56
+
57
+
58
+ def _write_shim(name: str, stage: str, config_path: str) -> str:
59
+ shim = os.path.join(hc.scripts_dir(), f"feeds-{name}-{stage}.py")
60
+ with open(shim, "w", encoding="utf-8") as f:
61
+ f.write(f'''#!/usr/bin/env python3
62
+ # Auto-generated by `colony feeds` for instance {name!r} — do not edit.
63
+ import os, runpy
64
+ os.environ.setdefault("COLONY_FEED_CONFIG", {config_path!r})
65
+ os.environ.setdefault("COLONY_FEED_STAGE", {stage!r})
66
+ runpy.run_path(os.path.join(os.path.dirname(os.path.abspath(__file__)),
67
+ {ENGINE_BASENAME!r}), run_name="__main__")
68
+ ''')
69
+ return shim
70
+
71
+
72
+ # --------------------------------------------------------------------- create
73
+ def create(spec_path: str, dry_run: bool = False) -> dict:
74
+ spec = FeedSpec.load(spec_path)
75
+ root = spec.data_root()
76
+ if os.path.exists(_instance_path(spec.name)):
77
+ raise SystemExit(f"feed {spec.name!r} already exists — delete it first or pick a new name")
78
+
79
+ prompts = {"distill": template.render_distill(spec)}
80
+ if spec.cadence.get("digest"):
81
+ prompts["digest"] = template.render_digest(spec)
82
+ if spec.cadence.get("discovery"):
83
+ prompts["discovery"] = template.render_discovery(spec)
84
+ if dry_run:
85
+ return {"name": spec.name, "root": root, "prompts": prompts}
86
+
87
+ os.makedirs(spec.briefs_dir, exist_ok=True)
88
+ os.makedirs(root, exist_ok=True)
89
+ shutil.copy2(os.path.expanduser(spec_path), os.path.join(root, "spec" + os.path.splitext(spec_path)[1]))
90
+
91
+ # Engine config: an explicit storage.config_path means an external
92
+ # (possibly hand-maintained / mutated-by-other-tools) config owns the
93
+ # collector; otherwise render one from the spec.
94
+ config_path = spec.storage.get("config_path")
95
+ if config_path:
96
+ config_path = os.path.expanduser(config_path)
97
+ if not os.path.exists(config_path):
98
+ raise SystemExit(f"storage.config_path {config_path} does not exist")
99
+ else:
100
+ config_path = os.path.join(root, "config.json")
101
+ with open(config_path, "w", encoding="utf-8") as f:
102
+ json.dump(spec.engine_config(), f, indent=2)
103
+
104
+ _install_engine()
105
+ shims = {"collect": _write_shim(spec.name, "collect", config_path)}
106
+ if spec.cadence.get("alerts"):
107
+ shims["alert"] = _write_shim(spec.name, "alert", config_path)
108
+
109
+ deliver_agent = (spec.destination.get("deliver", "local")
110
+ if spec.destination.get("kind") == "deliver" else "local")
111
+ owner_deliver = spec.destination.get("owner_deliver", deliver_agent)
112
+
113
+ jobs: dict[str, str] = {}
114
+ try:
115
+ jobs["collect"] = hc.create_job(
116
+ f"{_prefix(spec.name)} collect", spec.cadence["collect"],
117
+ script=os.path.basename(shims["collect"]), no_agent=True)
118
+ jobs["distill"] = hc.create_job(
119
+ f"{_prefix(spec.name)} distill", spec.cadence["distill"],
120
+ prompt=prompts["distill"], deliver=deliver_agent)
121
+ if spec.cadence.get("digest"):
122
+ jobs["digest"] = hc.create_job(
123
+ f"{_prefix(spec.name)} digest", spec.cadence["digest"],
124
+ prompt=prompts["digest"], deliver=deliver_agent)
125
+ if spec.cadence.get("alerts"):
126
+ jobs["alert"] = hc.create_job(
127
+ f"{_prefix(spec.name)} alert", spec.cadence["alerts"],
128
+ script=os.path.basename(shims["alert"]), no_agent=True,
129
+ deliver=deliver_agent)
130
+ if spec.cadence.get("discovery"):
131
+ jobs["discovery"] = hc.create_job(
132
+ f"{_prefix(spec.name)} discovery", spec.cadence["discovery"],
133
+ prompt=prompts["discovery"], deliver=owner_deliver)
134
+ except Exception:
135
+ for jid in jobs.values(): # roll back partial registration
136
+ try:
137
+ hc.remove_job(jid)
138
+ except Exception:
139
+ pass
140
+ raise
141
+
142
+ agent_jobs = [jid for stage, jid in jobs.items()
143
+ if stage in ("distill", "digest", "discovery")]
144
+ hc.pin_model(agent_jobs, spec.llm.get("provider", ""), spec.llm.get("model", ""))
145
+
146
+ instance = {"name": spec.name, "title": spec.title, "created_at": int(time.time()),
147
+ "spec_path": os.path.abspath(os.path.expanduser(spec_path)),
148
+ "config_path": config_path, "jobs": jobs, "shims": shims,
149
+ "paused": False}
150
+ with open(_instance_path(spec.name), "w", encoding="utf-8") as f:
151
+ json.dump(instance, f, indent=2)
152
+ return instance
153
+
154
+
155
+ # ----------------------------------------------------------------- lifecycle
156
+ def list_instances() -> list[dict]:
157
+ base = os.path.expanduser("~/.hermes/data/feeds")
158
+ out = []
159
+ if not os.path.isdir(base):
160
+ return out
161
+ for name in sorted(os.listdir(base)):
162
+ p = _instance_path(name)
163
+ if os.path.exists(p):
164
+ with open(p, encoding="utf-8") as f:
165
+ out.append(json.load(f))
166
+ return out
167
+
168
+
169
+ def status(name: str) -> dict:
170
+ inst = _load_instance(name)
171
+ spec = FeedSpec.load(inst["spec_path"]) if os.path.exists(inst["spec_path"]) else None
172
+ info = dict(inst)
173
+ if spec and os.path.exists(spec.queue_path):
174
+ with open(spec.queue_path, encoding="utf-8") as f:
175
+ q = json.load(f)
176
+ info["queue"] = {"collected_at": q.get("collected_at"),
177
+ "total_items": q.get("total_items")}
178
+ if spec and os.path.isdir(spec.briefs_dir):
179
+ briefs = sorted(os.listdir(spec.briefs_dir))
180
+ info["latest_brief"] = briefs[-1] if briefs else None
181
+ live = {j["id"]: j for j in hc.find_jobs(_prefix(name))}
182
+ info["jobs_live"] = {stage: (jid in live) for stage, jid in inst["jobs"].items()}
183
+ return info
184
+
185
+
186
+ def pause(name: str) -> None:
187
+ inst = _load_instance(name)
188
+ for jid in inst["jobs"].values():
189
+ hc.pause_job(jid)
190
+ inst["paused"] = True
191
+ with open(_instance_path(name), "w", encoding="utf-8") as f:
192
+ json.dump(inst, f, indent=2)
193
+
194
+
195
+ def resume(name: str) -> None:
196
+ inst = _load_instance(name)
197
+ for jid in inst["jobs"].values():
198
+ hc.resume_job(jid)
199
+ inst["paused"] = False
200
+ with open(_instance_path(name), "w", encoding="utf-8") as f:
201
+ json.dump(inst, f, indent=2)
202
+
203
+
204
+ def run(name: str, stage: str) -> None:
205
+ """Run one stage now. collect/alert run synchronously (output streams);
206
+ agent stages are triggered detached because they can run for minutes."""
207
+ inst = _load_instance(name)
208
+ if stage in ("collect", "alert"):
209
+ shim = inst["shims"].get(stage) or inst["shims"]["collect"]
210
+ env = dict(os.environ, COLONY_FEED_STAGE=stage)
211
+ subprocess.run([sys.executable, shim], env=env, check=True)
212
+ return
213
+ jid = inst["jobs"].get(stage)
214
+ if not jid:
215
+ raise SystemExit(f"feed {name!r} has no {stage!r} job")
216
+ hc.run_job_detached(jid)
217
+ print(f"triggered {stage} (job {jid}) — check the scheduler's output dir for results")
218
+
219
+
220
+ def delete(name: str, purge: bool = False) -> None:
221
+ inst = _load_instance(name)
222
+ for jid in inst["jobs"].values():
223
+ try:
224
+ hc.remove_job(jid)
225
+ except Exception as e:
226
+ print(f"warning: could not remove job {jid}: {e}", file=sys.stderr)
227
+ for shim in inst.get("shims", {}).values():
228
+ try:
229
+ os.remove(shim)
230
+ except FileNotFoundError:
231
+ pass
232
+ if purge:
233
+ shutil.rmtree(os.path.dirname(_instance_path(name)), ignore_errors=True)
234
+ else:
235
+ os.remove(_instance_path(name))
apsimo/feeds/spec.py ADDED
@@ -0,0 +1,250 @@
1
+ """Feed instance spec: schema, defaults, validation.
2
+
3
+ A spec is a plain YAML (or JSON) mapping. Everything that distinguishes one
4
+ feed from another lives here — topic, sources, scoring, cadence, destination,
5
+ privacy — so an instance is fully reproducible from its spec file.
6
+
7
+ Minimal spec::
8
+
9
+ name: rust-embedded
10
+ title: Rust Embedded Feed
11
+ topic: Rust on microcontrollers — new crates, HALs, RTIC releases.
12
+ destination: {kind: deliver, deliver: origin}
13
+ cadence: {collect: "every 240m", distill: "every 120m"}
14
+ sources:
15
+ github_search: "embedded rust hal OR rtic"
16
+ arxiv: {categories: [cs.SE], keywords: [embedded]}
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import re
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+ _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,63}$")
28
+
29
+ DESTINATION_KINDS = ("command", "deliver", "file")
30
+
31
+ #: Adapter keys under ``sources:`` — at least one must be configured.
32
+ ADAPTER_KEYS = ("x_searches", "hf_keywords", "github_search", "forum_urls", "arxiv", "rss")
33
+
34
+ DEFAULT_FRAMING = "What changes for us because of this?"
35
+
36
+ DEFAULT_SCORING = {
37
+ "account_whitelist": 10,
38
+ "engagement": {"per_100_likes": 1, "per_1000_likes": 2, "max": 10},
39
+ "human_curated": 20,
40
+ "benchmark_numbers": 15,
41
+ "code_release": 10,
42
+ "open_weights": 10,
43
+ "thresholds": {"p0": 50, "p1": 35, "p2": 20},
44
+ }
45
+
46
+ DEFAULT_PRIVACY_FORBIDDEN = [
47
+ "private infrastructure details (hardware counts, cluster topology, hostnames, addresses)",
48
+ "names of the operator, their contacts, or internal agent/system codenames",
49
+ ]
50
+
51
+
52
+ class SpecError(ValueError):
53
+ """Raised when a spec fails validation; ``.errors`` lists every problem."""
54
+
55
+ def __init__(self, errors: list[str]):
56
+ self.errors = errors
57
+ super().__init__("invalid feed spec:\n - " + "\n - ".join(errors))
58
+
59
+
60
+ @dataclass
61
+ class FeedSpec:
62
+ """Validated, defaulted view over a raw spec mapping."""
63
+
64
+ raw: dict = field(repr=False)
65
+ name: str = ""
66
+ title: str = ""
67
+ topic: str = ""
68
+ audience: str = "personal" # personal | group
69
+ destination: dict = field(default_factory=dict)
70
+ llm: dict = field(default_factory=dict) # {provider, model} pin
71
+ cadence: dict = field(default_factory=dict)
72
+ storage: dict = field(default_factory=dict) # per-path overrides
73
+ sources: dict = field(default_factory=dict)
74
+ registry: list = field(default_factory=list) # tiered source registry
75
+ scoring: dict = field(default_factory=dict)
76
+ brief: dict = field(default_factory=dict)
77
+ privacy: dict = field(default_factory=dict)
78
+ learning_loop: bool = True
79
+ budget: dict = field(default_factory=dict)
80
+
81
+ # ------------------------------------------------------------------ load
82
+ @classmethod
83
+ def load(cls, path: str) -> "FeedSpec":
84
+ with open(os.path.expanduser(path), encoding="utf-8") as f:
85
+ text = f.read()
86
+ if path.endswith(".json"):
87
+ raw = json.loads(text)
88
+ else:
89
+ try:
90
+ import yaml # type: ignore
91
+ except ImportError as exc: # pragma: no cover
92
+ raise SpecError([f"PyYAML unavailable ({exc}); use a .json spec"]) from exc
93
+ raw = yaml.safe_load(text)
94
+ if not isinstance(raw, dict):
95
+ raise SpecError(["spec root must be a mapping"])
96
+ return cls.from_dict(raw)
97
+
98
+ @classmethod
99
+ def from_dict(cls, raw: dict) -> "FeedSpec":
100
+ spec = cls(raw=raw)
101
+ errors: list[str] = []
102
+
103
+ spec.name = str(raw.get("name", "")).strip()
104
+ if not _SLUG_RE.match(spec.name):
105
+ errors.append("name: required kebab-case slug, e.g. 'ai-inference'")
106
+ spec.title = str(raw.get("title") or spec.name.replace("-", " ").title())
107
+ spec.topic = str(raw.get("topic", "")).strip()
108
+ if not spec.topic:
109
+ errors.append("topic: required — one paragraph describing what this feed tracks and why")
110
+
111
+ spec.audience = raw.get("audience", "personal")
112
+ if spec.audience not in ("personal", "group"):
113
+ errors.append("audience: must be 'personal' or 'group'")
114
+
115
+ dest = raw.get("destination") or {}
116
+ kind = dest.get("kind")
117
+ if kind not in DESTINATION_KINDS:
118
+ errors.append(f"destination.kind: required, one of {DESTINATION_KINDS}")
119
+ elif kind == "command" and not dest.get("send_command"):
120
+ errors.append("destination.send_command: required for kind=command "
121
+ "(shell command the brief is piped to)")
122
+ elif kind == "deliver" and not dest.get("deliver"):
123
+ errors.append("destination.deliver: required for kind=deliver "
124
+ "(harness delivery target, e.g. 'origin' or 'platform:chat_id')")
125
+ spec.destination = dest
126
+
127
+ cadence = raw.get("cadence") or {}
128
+ for req in ("collect", "distill"):
129
+ if not cadence.get(req):
130
+ errors.append(f"cadence.{req}: required (harness schedule string, e.g. 'every 120m')")
131
+ spec.cadence = cadence
132
+
133
+ sources = raw.get("sources") or {}
134
+ if not any(sources.get(k) for k in ADAPTER_KEYS):
135
+ errors.append(f"sources: at least one adapter of {ADAPTER_KEYS} must be configured")
136
+ spec.sources = sources
137
+
138
+ spec.registry = raw.get("registry") or []
139
+ for i, entry in enumerate(spec.registry):
140
+ if not isinstance(entry, dict) or not entry.get("name"):
141
+ errors.append(f"registry[{i}]: each entry needs at least a 'name'")
142
+ elif entry.get("weight", "P1") not in ("P0", "P1", "P2"):
143
+ errors.append(f"registry[{i}] ({entry.get('name')}): weight must be P0/P1/P2")
144
+
145
+ spec.scoring = _merged(DEFAULT_SCORING, raw.get("scoring") or {})
146
+ spec.llm = raw.get("llm") or {}
147
+ spec.storage = raw.get("storage") or {}
148
+ spec.brief = raw.get("brief") or {}
149
+ spec.brief.setdefault("framing", DEFAULT_FRAMING)
150
+ spec.brief.setdefault("standard_cap", 5)
151
+ spec.privacy = raw.get("privacy") or {}
152
+ spec.privacy.setdefault("forbidden", list(DEFAULT_PRIVACY_FORBIDDEN))
153
+ spec.learning_loop = bool(raw.get("learning_loop", True))
154
+ spec.budget = raw.get("budget") or {}
155
+
156
+ if errors:
157
+ raise SpecError(errors)
158
+ return spec
159
+
160
+ # ------------------------------------------------------------- storage
161
+ def data_root(self, base: str = "~/.hermes/data/feeds") -> str:
162
+ root = self.storage.get("root") or f"{base}/{self.name}"
163
+ return os.path.expanduser(root)
164
+
165
+ def path(self, key: str, default_name: str) -> str:
166
+ """Resolve a storage path, honouring per-key spec overrides."""
167
+ override = self.storage.get(key)
168
+ if override:
169
+ return os.path.expanduser(override)
170
+ return os.path.join(self.data_root(), default_name)
171
+
172
+ @property
173
+ def queue_path(self) -> str:
174
+ return self.path("queue_path", "queue.json")
175
+
176
+ @property
177
+ def seen_db(self) -> str:
178
+ return self.path("seen_db", "seen.db")
179
+
180
+ @property
181
+ def briefs_dir(self) -> str:
182
+ return self.path("briefs_dir", "briefs")
183
+
184
+ @property
185
+ def alert_path(self) -> str:
186
+ return self.path("alert_path", "alert.json")
187
+
188
+ @property
189
+ def deltas_log(self) -> str:
190
+ return self.path("deltas_log", "briefs/_registry_deltas.log")
191
+
192
+ # -------------------------------------------------------- engine config
193
+ def engine_config(self) -> dict[str, Any]:
194
+ """Render the self-contained collector-engine config for this feed."""
195
+ s = self.sources
196
+ arxiv = s.get("arxiv") or {}
197
+ sc = self.scoring
198
+ cfg: dict[str, Any] = {
199
+ "feed_name": self.name,
200
+ "queue_path": self.queue_path,
201
+ "seen_db": self.seen_db,
202
+ "alert_path": self.alert_path,
203
+ # adapters (empty list / missing = adapter disabled)
204
+ "x_searches": s.get("x_searches") or [],
205
+ "x_results_per_query": s.get("x_results_per_query", 15),
206
+ "key_accounts": s.get("key_accounts") or [],
207
+ "hf_keywords": s.get("hf_keywords") or [],
208
+ "github_search": s.get("github_search") or "",
209
+ "github_keywords": s.get("github_keywords") or [],
210
+ "forum_urls": s.get("forum_urls") or [],
211
+ "forum_title_keywords": s.get("forum_title_keywords") or [],
212
+ "arxiv_categories": arxiv.get("categories") or [],
213
+ "arxiv_query_terms": arxiv.get("query_terms") or [],
214
+ "arxiv_keywords": arxiv.get("keywords") or [],
215
+ "rss": s.get("rss") or [],
216
+ # scoring
217
+ "keyword_categories": sc.get("keyword_categories") or [],
218
+ "scoring": {
219
+ "account_whitelist": sc.get("account_whitelist", 10),
220
+ "engagement_per_100_likes": sc.get("engagement", {}).get("per_100_likes", 1),
221
+ "engagement_per_1000_likes": sc.get("engagement", {}).get("per_1000_likes", 2),
222
+ "engagement_max": sc.get("engagement", {}).get("max", 10),
223
+ "human_curated": sc.get("human_curated", 20),
224
+ "benchmark_numbers": sc.get("benchmark_numbers", 15),
225
+ "code_release": sc.get("code_release", 10),
226
+ "open_weights": sc.get("open_weights", 10),
227
+ },
228
+ "benchmark_regex": sc.get(
229
+ "benchmark_regex",
230
+ r"\d+\s*(tok/s|tokens?/?s|t/s|tflops?|ms|latency|throughput)",
231
+ ),
232
+ "priority_thresholds": sc.get("thresholds", DEFAULT_SCORING["thresholds"]),
233
+ # alert delivery for the alert stage (kind=command only)
234
+ "send_command": (self.destination.get("send_command")
235
+ if self.destination.get("kind") == "command" else ""),
236
+ "alert_min_priority": (self.brief.get("alert_min_priority", "P0")),
237
+ }
238
+ # Legacy compatibility: a spec may point config_path at a hand-written
239
+ # engine config; then that file wins entirely (see manager.create).
240
+ return cfg
241
+
242
+
243
+ def _merged(base: dict, override: dict) -> dict:
244
+ out = json.loads(json.dumps(base))
245
+ for k, v in override.items():
246
+ if isinstance(v, dict) and isinstance(out.get(k), dict):
247
+ out[k].update(v)
248
+ else:
249
+ out[k] = v
250
+ return out