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,148 @@
1
+ """Complexity scorer — heuristic prompt analysis for tier selection.
2
+
3
+ Scores range 0.0–1.0:
4
+ 0.00–0.30 → SMALL (simple Q&A, factual lookups, short summaries)
5
+ 0.30–0.65 → MEDIUM (multi-step reasoning, code generation, analysis)
6
+ 0.65–1.00 → LARGE (complex architecture, deep research, critical tasks)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+
14
+ from apsimo.router.tiers import ModelTier
15
+
16
+
17
+ @dataclass
18
+ class ComplexitySignals:
19
+ token_count: int
20
+ has_code: bool
21
+ has_math: bool
22
+ has_multi_step: bool # "first... then... finally..."
23
+ has_reasoning_required: bool # "why", "explain", "analyze"
24
+ has_tool_use: bool
25
+ conversation_depth: int # number of prior turns
26
+ user_tier: str # "standard" | "power" | "developer"
27
+
28
+
29
+ # Keyword sets for signal extraction
30
+ _MULTI_STEP_PATTERNS = re.compile(
31
+ r"\b(first|step \d|then|after that|finally|next|followed by|subsequently)\b",
32
+ re.IGNORECASE,
33
+ )
34
+ _REASONING_PATTERNS = re.compile(
35
+ r"\b(why|explain|analyze|analyse|compare|contrast|evaluate|assess|critique|"
36
+ r"debate|argue|justify|prove|derive|infer|reason|think through|walk me through)\b",
37
+ re.IGNORECASE,
38
+ )
39
+ _CODE_PATTERNS = re.compile(
40
+ r"(```|\bdef \b|\bclass \b|\bimport \b|\bfunction\b|\bvoid \b|\bpublic \b|"
41
+ r"\bprivate \b|<code>|<script)",
42
+ re.IGNORECASE,
43
+ )
44
+ _MATH_PATTERNS = re.compile(
45
+ r"(\$\$?|\\\(|\bintegral\b|\bderivative\b|\bmatrix\b|\bequation\b|"
46
+ r"\balgebra\b|\bcalculus\b|\bstatistics\b|\bprobability\b)",
47
+ re.IGNORECASE,
48
+ )
49
+
50
+ # Signal weights for the final score.
51
+ # Weights are intentionally skewed toward reasoning/multi-step signals
52
+ # because those are the strongest predictors of tasks that genuinely need
53
+ # a larger model (architecture analysis, complex debugging, etc.).
54
+ _WEIGHTS = {
55
+ "token_count": 0.12,
56
+ "has_code": 0.12,
57
+ "has_math": 0.08,
58
+ "has_multi_step": 0.18,
59
+ "has_reasoning_required": 0.22,
60
+ "has_tool_use": 0.08,
61
+ "conversation_depth": 0.06,
62
+ "user_tier": 0.05,
63
+ # Bonus applied when BOTH multi-step AND reasoning are detected.
64
+ # A prompt that requires step-by-step thinking AND analytical reasoning
65
+ # is reliably in the LARGE tier regardless of token count.
66
+ "combined_reasoning_bonus": 0.20,
67
+ }
68
+
69
+ _TOKEN_SCALE = 500 # tokens above which token_count score saturates at 1.0
70
+ _DEPTH_SCALE = 10 # turns above which depth score saturates at 1.0
71
+
72
+
73
+ class ComplexityScorer:
74
+ """Score prompt complexity to select an appropriate model tier."""
75
+
76
+ def score(self, prompt: str, context: dict | None = None) -> float:
77
+ """Return a complexity score in [0.0, 1.0]."""
78
+ signals = self._extract_signals(prompt, context or {})
79
+ return self._compute_score(signals)
80
+
81
+ def select_tier(self, prompt: str, context: dict | None = None) -> ModelTier:
82
+ """Return the cheapest model tier appropriate for this prompt."""
83
+ score = self.score(prompt, context)
84
+ if score < 0.3:
85
+ return ModelTier.SMALL
86
+ elif score < 0.65:
87
+ return ModelTier.MEDIUM
88
+ else:
89
+ return ModelTier.LARGE
90
+
91
+ # ------------------------------------------------------------------
92
+ # Internal helpers
93
+ # ------------------------------------------------------------------
94
+
95
+ def _extract_signals(self, prompt: str, context: dict) -> ComplexitySignals:
96
+ # Rough token estimate: ~4 chars per token
97
+ token_count = max(1, len(prompt) // 4)
98
+
99
+ messages = context.get("messages", [])
100
+ depth = len(messages) if isinstance(messages, list) else 0
101
+
102
+ tools = context.get("tools", [])
103
+ has_tool_use = bool(tools) or "tool" in prompt.lower() or "function" in prompt.lower()
104
+
105
+ user_tier = context.get("user_tier", "standard")
106
+
107
+ return ComplexitySignals(
108
+ token_count=token_count,
109
+ has_code=bool(_CODE_PATTERNS.search(prompt)),
110
+ has_math=bool(_MATH_PATTERNS.search(prompt)),
111
+ has_multi_step=bool(_MULTI_STEP_PATTERNS.search(prompt)),
112
+ has_reasoning_required=bool(_REASONING_PATTERNS.search(prompt)),
113
+ has_tool_use=has_tool_use,
114
+ conversation_depth=depth,
115
+ user_tier=user_tier,
116
+ )
117
+
118
+ def _compute_score(self, signals: ComplexitySignals) -> float:
119
+ w = _WEIGHTS
120
+
121
+ # Normalise continuous signals to [0, 1]
122
+ token_score = min(1.0, signals.token_count / _TOKEN_SCALE)
123
+ depth_score = min(1.0, signals.conversation_depth / _DEPTH_SCALE)
124
+ tier_score = {"standard": 0.0, "power": 0.5, "developer": 1.0}.get(
125
+ signals.user_tier, 0.0
126
+ )
127
+
128
+ # Bonus: a prompt that requires BOTH step-by-step structure AND analytical
129
+ # reasoning is reliably complex enough to warrant the LARGE tier.
130
+ # 0.25 ensures reasoning + multi_step clears the 0.65 LARGE threshold.
131
+ combined_bonus = (
132
+ 0.25
133
+ if signals.has_multi_step and signals.has_reasoning_required
134
+ else 0.0
135
+ )
136
+
137
+ raw = (
138
+ token_score * w["token_count"]
139
+ + float(signals.has_code) * w["has_code"]
140
+ + float(signals.has_math) * w["has_math"]
141
+ + float(signals.has_multi_step) * w["has_multi_step"]
142
+ + float(signals.has_reasoning_required) * w["has_reasoning_required"]
143
+ + float(signals.has_tool_use) * w["has_tool_use"]
144
+ + depth_score * w["conversation_depth"]
145
+ + tier_score * w["user_tier"]
146
+ + combined_bonus
147
+ )
148
+ return min(1.0, max(0.0, raw))
@@ -0,0 +1,153 @@
1
+ """Bounded observations of configured endpoints, separate from capabilities.
2
+
3
+ A model listing can omit a working request alias. Only completion outcomes
4
+ change routing availability; advertised metadata never grants tools or vision.
5
+ """
6
+ import asyncio
7
+ from collections import OrderedDict
8
+ import hashlib
9
+ import threading
10
+ import time
11
+
12
+
13
+ def models_url(base_url):
14
+ base = base_url.rstrip('/')
15
+ return base + ('/models' if base.endswith('/v1') else '/v1/models')
16
+
17
+
18
+ class EndpointRuntime:
19
+ def __init__(self, *, cooldown=15, ttl=30, clock=time.monotonic, wall=time.time):
20
+ self.cooldown, self.ttl, self.clock, self.wall = cooldown, ttl, clock, wall
21
+ self._calls, self._listings = OrderedDict(), OrderedDict()
22
+ self._probing = set()
23
+ self._lock = threading.RLock()
24
+
25
+ @staticmethod
26
+ def call_key(snapshot, binding):
27
+ return (snapshot.revision, binding.config.base_url, binding.config.model_id, binding.weight_revision)
28
+
29
+ @staticmethod
30
+ def listing_key(snapshot, binding):
31
+ credential = hashlib.sha256(binding.config.api_key.encode()).hexdigest()
32
+ return (snapshot.revision, binding.config.base_url, credential)
33
+
34
+ @staticmethod
35
+ def _put(cache, key, value):
36
+ cache[key] = value
37
+ cache.move_to_end(key)
38
+ while len(cache) > 256:
39
+ cache.popitem(last=False)
40
+
41
+ def acquire(self, snapshot, binding, request_id):
42
+ """Healthy calls are unrestricted; one call tests an expired cooldown."""
43
+ key = self.call_key(snapshot, binding)
44
+ with self._lock:
45
+ state = self._calls.get(key)
46
+ if not state or not state.get('failed'):
47
+ return True
48
+ if state['retry_at'] > self.clock() or state.get('recovering'):
49
+ return False
50
+ state['recovering'] = request_id
51
+ return True
52
+
53
+ def release(self, snapshot, binding, request_id):
54
+ # Cancellation must not leave the single recovery attempt occupied.
55
+ with self._lock:
56
+ state = self._calls.get(self.call_key(snapshot, binding))
57
+ if state and state.get('recovering') == request_id:
58
+ state['recovering'] = False
59
+
60
+ def failure(self, snapshot, binding, error):
61
+ with self._lock:
62
+ self._put(self._calls, self.call_key(snapshot, binding), {
63
+ 'failed': True, 'retry_at': self.clock() + self.cooldown,
64
+ 'checked': self.clock(),
65
+ 'observed_at': self.wall(), 'error_type': type(error).__name__,
66
+ 'status_code': getattr(error, 'status_code', None), 'recovering': False})
67
+
68
+ def success(self, snapshot, binding, response):
69
+ served = getattr(response.raw, 'model', None)
70
+ with self._lock:
71
+ self._put(self._calls, self.call_key(snapshot, binding), {
72
+ 'failed': False, 'observed_at': self.wall(),
73
+ 'checked': self.clock(),
74
+ 'served_model': served[:256] if isinstance(served, str) else None,
75
+ 'latency_ms': response.latency_ms})
76
+
77
+ async def refresh(self, snapshot, probe):
78
+ """At most four concurrent reads, only when /models is requested.
79
+
80
+ Later reads cover remaining endpoints. No queue, scan or background task
81
+ survives the request. Cached entries retain their actual observation age.
82
+ """
83
+ selected = []
84
+ with self._lock:
85
+ # Unknown and oldest observations go first, so infrequent reads
86
+ # still cover a pool larger than the four concurrent probe slots.
87
+ ordered = sorted(snapshot.bindings.values(), key=lambda binding:
88
+ self._listings.get(self.listing_key(snapshot, binding), {}).get('checked', float('-inf')))
89
+ for binding in ordered:
90
+ key = self.listing_key(snapshot, binding)
91
+ old = self._listings.get(key)
92
+ if key in self._probing or (old and self.clock() - old['checked'] < self.ttl):
93
+ continue
94
+ if len(self._probing) >= 4:
95
+ break
96
+ self._probing.add(key)
97
+ selected.append((key, binding))
98
+
99
+ async def observe(key, binding):
100
+ try:
101
+ models = await asyncio.wait_for(probe(snapshot, binding), timeout=2)
102
+ value = {'available': True, 'models': models, 'observed_at': self.wall()}
103
+ except Exception as error:
104
+ value = {'available': False, 'models': [], 'observed_at': self.wall(),
105
+ 'error_type': type(error).__name__}
106
+ else:
107
+ value['error_type'] = None
108
+ finally:
109
+ with self._lock:
110
+ self._probing.discard(key)
111
+ value['checked'] = self.clock()
112
+ with self._lock:
113
+ self._put(self._listings, key, value)
114
+
115
+ try:
116
+ await asyncio.gather(*(observe(key, binding) for key, binding in selected))
117
+ finally:
118
+ # Cancellation can precede a child's first instruction and its
119
+ # own finally block. Release every slot reserved by this request.
120
+ with self._lock:
121
+ self._probing.difference_update(key for key, _ in selected)
122
+
123
+ def status(self, snapshot):
124
+ now, wall = self.clock(), self.wall()
125
+ bindings, inventories = {}, {}
126
+ with self._lock:
127
+ for name, binding in snapshot.bindings.items():
128
+ state = self._calls.get(self.call_key(snapshot, binding))
129
+ if state:
130
+ failed = state.get('failed')
131
+ phase = ('recovering' if state.get('recovering') else
132
+ 'cooldown' if state.get('retry_at', 0) > now else 'retry_due') if failed else 'available'
133
+ bindings[name] = {k: v for k, v in state.items() if k not in {'failed', 'retry_at', 'recovering', 'checked'}}
134
+ bindings[name].update(state=phase,
135
+ stale=now-state['checked'] >= self.ttl,
136
+ age_seconds=round(max(0, wall-state['observed_at']), 1),
137
+ retry_after_seconds=round(max(0, state.get('retry_at', 0)-now), 2))
138
+ else:
139
+ bindings[name] = {'state': 'unknown', 'stale': True}
140
+ key = self.listing_key(snapshot, binding)
141
+ if key not in inventories:
142
+ listing = self._listings.get(key)
143
+ item = {'bindings': [], 'available': False, 'models': [], 'stale': True}
144
+ if listing:
145
+ item.update({k: v for k, v in listing.items() if k != 'checked'})
146
+ item['age_seconds'] = round(max(0, wall-listing['observed_at']), 1)
147
+ item['stale'] = now-listing['checked'] >= self.ttl
148
+ inventories[key] = item
149
+ inventories[key]['bindings'].append(name)
150
+ return {'completion_observations': bindings, 'model_inventory': list(inventories.values()),
151
+ 'inventory_complete': all(not row['stale'] for row in inventories.values()),
152
+ 'observation_ttl_seconds': self.ttl, 'retry_cooldown_seconds': self.cooldown,
153
+ 'observation_basis': 'endpoint advertisements and actual completion outcomes; declarations remain authoritative'}
@@ -0,0 +1,58 @@
1
+ """FallbackHandler — escalate to a higher tier when a lower tier fails.
2
+
3
+ When a model call raises a rate-limit, context-length, or capability error,
4
+ the handler promotes the request to the next tier and retries once.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING
11
+
12
+ from apsimo.router.tiers import ModelTier
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Errors that warrant a tier upgrade (substrings matched in error messages)
20
+ _UPGRADE_TRIGGERS = (
21
+ "context_length_exceeded",
22
+ "context window",
23
+ "maximum context",
24
+ "rate limit",
25
+ "rate_limit_exceeded",
26
+ "overloaded",
27
+ "too many tokens",
28
+ "insufficient capability",
29
+ )
30
+
31
+ _TIER_PROMOTION: dict[ModelTier, ModelTier] = {
32
+ ModelTier.SMALL: ModelTier.MEDIUM,
33
+ ModelTier.MEDIUM: ModelTier.LARGE,
34
+ # LARGE has no higher tier — callers must handle this
35
+ }
36
+
37
+
38
+ class FallbackHandler:
39
+ """Decide whether to escalate a failed request to the next model tier."""
40
+
41
+ def should_escalate(self, error: Exception, current_tier: ModelTier) -> bool:
42
+ """Return True if the error warrants a tier upgrade."""
43
+ if current_tier not in _TIER_PROMOTION:
44
+ return False # No escalation for the top tier or explicit roles.
45
+
46
+ msg = str(error).lower()
47
+ triggered = any(trigger in msg for trigger in _UPGRADE_TRIGGERS)
48
+ if triggered:
49
+ logger.warning(
50
+ "FallbackHandler: escalating from %s due to: %s",
51
+ current_tier.value,
52
+ type(error).__name__,
53
+ )
54
+ return triggered
55
+
56
+ def next_tier(self, current_tier: ModelTier) -> ModelTier | None:
57
+ """Return the next tier to try, or None if already at LARGE."""
58
+ return _TIER_PROMOTION.get(current_tier)
@@ -0,0 +1,243 @@
1
+ """Declared function bindings and immutable per-call routing snapshots.
2
+
3
+ No machine discovery or model-name capability inference. Hosts update the same
4
+ configuration when endpoints move; measurements remain labelled declarations.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from copy import deepcopy
9
+ from dataclasses import dataclass, replace
10
+ import hashlib
11
+ import ipaddress
12
+ import json
13
+ import re
14
+ from urllib.parse import urlsplit
15
+
16
+ from .tiers import ModelTier, TierConfig, _has_litellm_prefix
17
+
18
+ FUNCTIONS = {'chat', 'reasoning', 'planning', 'extraction', 'judging', 'vision', 'coding'}
19
+ DEFAULT_ROLES = {
20
+ 'chat': ['small'], 'reasoning': ['large', 'medium', 'small'],
21
+ 'planning': ['large', 'medium', 'small'], 'extraction': ['small', 'medium'],
22
+ 'judging': ['large', 'medium'], 'vision': ['vision'], 'coding': ['large', 'medium'],
23
+ }
24
+ DEFAULT_NETWORKS = ('127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '::1/128', 'fc00::/7')
25
+ TASK_ROLES = {
26
+ 'source_claim_extraction': 'extraction', 'source_image_description': 'vision',
27
+ 'source_appraisal': 'extraction', 'self_judgment': 'reasoning',
28
+ 'project_planning': 'planning', 'thought_job': 'reasoning',
29
+ 'tom_affect_extraction': 'extraction', 'tom_belief_extraction': 'extraction',
30
+ 'tom_intention_extraction': 'extraction',
31
+ 'tom_fact_extraction': 'extraction', 'tom_engagement_extraction': 'extraction',
32
+ 'context_compression': 'extraction',
33
+ 'workspace_thinking': 'reasoning', 'internal_thinking': 'reasoning',
34
+ 'toolsmith_draft': 'coding', 'skill_distillation': 'judging',
35
+ }
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Binding:
40
+ name: str
41
+ config: TierConfig
42
+ context_tokens: int = 0
43
+ supports_tools: bool | None = None
44
+ latency_ms: int = 0
45
+ tokens_per_second: float = 0
46
+ concurrency: int = 0
47
+ weight_revision: str = 'unknown'
48
+ legacy: bool = False
49
+ supports_json_schema: bool = False
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class FunctionRole:
54
+ candidates: tuple[str, ...]
55
+ timeout_seconds: float = 20
56
+ deadline_seconds: float = 40
57
+ min_context_tokens: int = 0
58
+ max_latency_ms: int = 0
59
+ min_tokens_per_second: float = 0
60
+ min_concurrency: int = 0
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class RoutingSnapshot:
65
+ revision: str
66
+ tiers: dict
67
+ bindings: dict[str, Binding]
68
+ roles: dict[str, FunctionRole]
69
+ networks: tuple
70
+ declared_hosts: frozenset[str]
71
+ task_roles: dict[str, str]
72
+ provider: str = ''
73
+ base_url: str = ''
74
+
75
+ def status(self):
76
+ return {'config_revision': self.revision, 'capability_basis': 'deployment declarations, not measured by this router',
77
+ 'roles': {key: list(value.candidates) for key, value in self.roles.items()},
78
+ 'task_roles': dict(self.task_roles),
79
+ 'models': {name: {'model_id': b.config.model_id, 'weight_revision': b.weight_revision,
80
+ 'context_tokens': b.context_tokens, 'supports_vision': b.config.supports_vision,
81
+ 'supports_tools': b.supports_tools, 'latency_ms': b.latency_ms,
82
+ 'supports_json_schema': b.supports_json_schema,
83
+ 'tokens_per_second': b.tokens_per_second, 'concurrency': b.concurrency,
84
+ 'legacy_unknown_tools_allowed': b.legacy and b.supports_tools is None}
85
+ for name, b in self.bindings.items()}}
86
+
87
+
88
+ def number(value, *, minimum=0, maximum=10**9):
89
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not minimum <= value <= maximum:
90
+ raise ValueError('Invalid routing capability or limit')
91
+ return value
92
+
93
+
94
+ def _binding(name, config, spec, *, legacy=False):
95
+ tools = spec.get('supportsTools')
96
+ if tools is not None and type(tools) is not bool:
97
+ raise ValueError('supportsTools must be an explicit boolean')
98
+ structured = spec.get('supportsJsonSchema', False)
99
+ if type(structured) is not bool:
100
+ raise ValueError('supportsJsonSchema must be an explicit boolean')
101
+ return Binding(name, config,
102
+ int(number(spec.get('contextTokens', config.useful_context_tokens))), tools,
103
+ int(number(spec.get('latencyMs', 0))), float(number(spec.get('tokensPerSecond', 0))),
104
+ int(number(spec.get('concurrency', 0))), str(spec.get('weightRevision') or 'unknown')[:160], legacy, structured)
105
+
106
+
107
+ def _transport(config, host, spec):
108
+ protocol = spec.get('protocol', host.get('protocol'))
109
+ if protocol not in {None, 'openai-chat'}:
110
+ raise ValueError('Function routing requires the openai-chat protocol')
111
+ if config.model_id.startswith('ollama/') and protocol == 'openai-chat':
112
+ if not config.base_url.rstrip('/').endswith('/v1'):
113
+ raise ValueError('Declared Ollama OpenAI compatibility requires an explicit /v1 base URL')
114
+ config = replace(config, model_id='openai/' + config.model_id.removeprefix('ollama/'))
115
+ if not config.model_id.startswith('openai/'):
116
+ raise ValueError('Function routing requires an explicit OpenAI-compatible endpoint; declare protocol openai-chat for compatible Ollama /v1 endpoints')
117
+ return config
118
+
119
+
120
+ def build_snapshot(host: dict, tiers: dict) -> RoutingSnapshot:
121
+ """Build privately, then replace one router reference after validation."""
122
+ bindings, materialized = {}, {}
123
+ for tier, original in tiers.items():
124
+ # Pin inherited endpoint/key into this snapshot. Later provider env
125
+ # changes cannot redirect a request that already selected this config.
126
+ spec = host.get('models', {}).get(tier.value, {})
127
+ spec = spec if isinstance(spec, dict) else {}
128
+ cfg = replace(deepcopy(original),
129
+ base_url=original.base_url or host.get('baseUrl', ''),
130
+ api_key=original.api_key or host.get('apiKey', ''))
131
+ cfg = _transport(cfg, host, spec)
132
+ materialized[tier] = cfg
133
+ bindings[tier.value] = _binding(tier.value, cfg, spec, legacy=True)
134
+ pool = host.get('modelPool', {})
135
+ if not isinstance(pool, dict) or len(pool) > 64:
136
+ raise ValueError('modelPool must contain at most 64 explicit bindings')
137
+ for name, spec in pool.items():
138
+ if not isinstance(name, str) or not re.fullmatch(r'[a-zA-Z0-9_.-]{1,80}', name) or name in bindings:
139
+ raise ValueError('Invalid or duplicate modelPool binding')
140
+ if not isinstance(spec, dict) or not isinstance(spec.get('model'), str) or not spec['model']:
141
+ raise ValueError('Every modelPool binding requires a model')
142
+ model = spec['model']
143
+ provider = str(spec.get('provider', host.get('provider', 'vllm')))
144
+ if not _has_litellm_prefix(model):
145
+ if provider == 'ollama': model = 'ollama/' + model
146
+ elif provider in {'local', 'custom', 'lmstudio', 'vllm', 'openai'}: model = 'openai/' + model
147
+ cfg = TierConfig(tier=ModelTier(spec.get('tier', 'medium')), model_id=model,
148
+ max_tokens=int(number(spec.get('maxTokens', 8192), minimum=1)),
149
+ cost_per_1k_input=0, cost_per_1k_output=0, latency_p50_ms=0,
150
+ base_url=spec.get('baseUrl', host.get('baseUrl', '')),
151
+ api_key=spec.get('apiKey', host.get('apiKey', '')),
152
+ extra_body=deepcopy(spec.get('extraBody')),
153
+ useful_context_tokens=int(number(spec.get('contextTokens', 0))),
154
+ supports_vision=spec.get('supportsVision') is True)
155
+ if not isinstance(cfg.base_url, str) or not isinstance(cfg.api_key, str):
156
+ raise ValueError('Invalid model endpoint configuration')
157
+ cfg = _transport(cfg, host, spec)
158
+ bindings[name] = _binding(name, cfg, spec)
159
+ if not bindings:
160
+ raise ValueError('At least one deployed model must be declared explicitly')
161
+ role_config = host.get('functionRoles', {})
162
+ if not isinstance(role_config, dict) or set(role_config) - FUNCTIONS:
163
+ raise ValueError('Unknown function role; speech/embedding/rerank use their own transports')
164
+ task_roles = host.get('taskRoles', {})
165
+ if (not isinstance(task_roles, dict) or set(task_roles) - TASK_ROLES.keys()
166
+ or any(not isinstance(role, str) or role not in FUNCTIONS for role in task_roles.values())):
167
+ raise ValueError('taskRoles must map supported task names to existing function roles')
168
+ roles = {}
169
+ for name, defaults in DEFAULT_ROLES.items():
170
+ raw = role_config.get(name, [key for key in defaults if key in bindings])
171
+ raw = {'candidates': raw} if isinstance(raw, list) else raw
172
+ if not isinstance(raw, dict): raise ValueError('Invalid function role')
173
+ if set(raw) - {'candidates', 'timeoutSeconds', 'deadlineSeconds', 'minContextTokens', 'maxLatencyMs', 'minTokensPerSecond', 'minConcurrency'}:
174
+ raise ValueError('Unknown function role constraint')
175
+ candidates = raw.get('candidates', [])
176
+ if not isinstance(candidates, list) or len(candidates) > 8 or any(not isinstance(key, str) or key not in bindings for key in candidates):
177
+ raise ValueError('Role candidates must name at most eight configured bindings')
178
+ slow = name in {'reasoning', 'planning', 'judging', 'coding'}
179
+ roles[name] = FunctionRole(tuple(dict.fromkeys(candidates)),
180
+ float(number(raw.get('timeoutSeconds', 120 if slow else 20), minimum=.05, maximum=300)),
181
+ float(number(raw.get('deadlineSeconds', 180 if slow else 40), minimum=.05, maximum=600)),
182
+ int(number(raw.get('minContextTokens', 0))), int(number(raw.get('maxLatencyMs', 0))),
183
+ float(number(raw.get('minTokensPerSecond', 0))), int(number(raw.get('minConcurrency', 0))))
184
+ raw_networks = host.get('localNetworks', DEFAULT_NETWORKS)
185
+ if not isinstance(raw_networks, (list, tuple)) or len(raw_networks) > 64:
186
+ raise ValueError('localNetworks must be a bounded list of CIDRs')
187
+ networks = tuple(ipaddress.ip_network(value, strict=False) for value in raw_networks)
188
+ if any(n.prefixlen == 0 for n in networks): raise ValueError('A default route cannot declare the whole internet local')
189
+ hosts = host.get('localHosts', ['localhost'])
190
+ if not isinstance(hosts, list) or any(not isinstance(h, str) or len(h) > 253 for h in hosts):
191
+ raise ValueError('localHosts must list deployment hostnames')
192
+ revision = hashlib.sha256(json.dumps(host, sort_keys=True, separators=(',', ':')).encode()).hexdigest()[:20]
193
+ return RoutingSnapshot(revision, materialized, bindings, roles, networks, frozenset(h.casefold() for h in hosts),
194
+ task_roles=dict(task_roles),
195
+ provider=str(host.get('provider') or ''), base_url=str(host.get('baseUrl') or ''))
196
+
197
+
198
+ def select_role(snapshot, context):
199
+ """The same selection governs capability hints, deadlines and dispatch."""
200
+ task = context.get('task')
201
+ return (context.get('function_role')
202
+ or (snapshot.task_roles.get(task) if snapshot else None)
203
+ or TASK_ROLES.get(task, 'reasoning'))
204
+
205
+
206
+ def endpoint_host(binding, snapshot):
207
+ """Return literal eligible IP or declared hostname needing DNS verification.
208
+
209
+ No suffix such as .local is trusted. Every resolved address must be inside
210
+ deployment networks; endpoints with URL credentials or query strings fail.
211
+ """
212
+ parsed = urlsplit(binding.config.base_url)
213
+ if parsed.scheme not in {'http', 'https'} or parsed.username or parsed.password or parsed.query or parsed.fragment:
214
+ return None
215
+ host = (parsed.hostname or '').casefold()
216
+ try:
217
+ address = ipaddress.ip_address(host)
218
+ return host if any(address in net for net in snapshot.networks) else None
219
+ except ValueError:
220
+ return host if host in snapshot.declared_hosts else None
221
+
222
+
223
+ def candidates(snapshot, role_name, context, *, has_images, has_tools):
224
+ role = snapshot.roles[role_name]
225
+ minimum = max(role.min_context_tokens, int(context.get('required_context_tokens', 0)))
226
+ output = []
227
+ for name in role.candidates:
228
+ b = snapshot.bindings[name]
229
+ if (has_images or role_name == 'vision') and not b.config.supports_vision: continue
230
+ if has_tools and b.supports_tools is not True and not (b.legacy and b.supports_tools is None): continue
231
+ if minimum and b.context_tokens < minimum: continue
232
+ # Reuse the existing text estimator to avoid an obviously undersized
233
+ # fallback. Image token accounting is model-specific and remains unknown.
234
+ requested_output = context.get('max_output_tokens', context.get('max_tokens', b.config.max_tokens))
235
+ input_tokens = context.get('estimated_input_tokens', 0)
236
+ if b.supports_json_schema:
237
+ input_tokens += context.get('response_schema_tokens', 0)
238
+ if b.context_tokens and input_tokens + min(b.config.max_tokens, int(requested_output)) > b.context_tokens: continue
239
+ if role.max_latency_ms and (not b.latency_ms or b.latency_ms > role.max_latency_ms): continue
240
+ if b.tokens_per_second < role.min_tokens_per_second or b.concurrency < role.min_concurrency: continue
241
+ if endpoint_host(b, snapshot) is None: continue
242
+ output.append(b)
243
+ return output
@@ -0,0 +1,52 @@
1
+ """Resolve a local function binding for a native Hermes caller.
2
+
3
+ This runs in the sidecar interpreter. The caller captures the private JSON pipe;
4
+ Hermes retains its own dependencies, agent loop, fallback and execution identity.
5
+ """
6
+ import argparse
7
+ import asyncio
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from .functions import candidates
12
+ from .router import LLMRouter
13
+
14
+
15
+ async def planning(configuration):
16
+ if 'planning' not in configuration.get('functionRoles', {}):
17
+ raise ValueError('An explicit planning role is required')
18
+ router = LLMRouter(tiers={}, self_learner=object())
19
+ router.configure(configuration)
20
+ snapshot = router._snapshot
21
+ selected = candidates(snapshot, 'planning', {}, has_images=False, has_tools=True)
22
+ if not selected:
23
+ raise ValueError('No tool-capable local planning binding is available')
24
+ for binding in selected:
25
+ if not await router._local_addresses(snapshot, binding):
26
+ raise ValueError('Planning endpoint has no eligible local address')
27
+ if any(binding.config.extra_body != selected[0].config.extra_body for binding in selected):
28
+ raise ValueError('Planning candidates require different native request overrides')
29
+ role = snapshot.roles['planning']
30
+ entries = [dict(provider='openai', model=b.config.model_id.removeprefix('openai/'),
31
+ base_url=b.config.base_url, api_key=b.config.api_key or 'local-no-key',
32
+ api_mode='chat_completions') for b in selected]
33
+ maximum = min(binding.config.max_tokens for binding in selected)
34
+ options = {**entries[0], 'requested_provider':'openai', 'fallback_model':entries[1:] or None,
35
+ 'max_tokens':maximum, 'request_overrides':{'extra_body':selected[0].config.extra_body or {}}}
36
+ policy = {'role':'planning', 'configuration_revision':snapshot.revision,
37
+ 'candidates':[{'binding':b.name, 'model':b.config.model_id, 'weight_revision':b.weight_revision} for b in selected],
38
+ 'max_output_tokens':maximum, 'request_timeout_seconds':role.timeout_seconds,
39
+ 'run_deadline_seconds':role.deadline_seconds,
40
+ 'fallback_owner':'Hermes native runtime'}
41
+ return options, policy
42
+
43
+
44
+ def main():
45
+ parser = argparse.ArgumentParser(description=__doc__)
46
+ parser.add_argument('--config', type=Path, required=True)
47
+ args = parser.parse_args()
48
+ print(json.dumps(asyncio.run(planning(json.loads(args.config.read_text())))))
49
+
50
+
51
+ if __name__ == '__main__':
52
+ main()