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,12 @@
1
+ """Colony Skills — security layer (AST scanner and capability guards)."""
2
+
3
+ from apsimo.skills.security.scanner import ASTScanner, ASTScanResult, ScanFinding
4
+ from apsimo.skills.security.guards import CapabilityGuard, GuardResult
5
+
6
+ __all__ = [
7
+ "ASTScanner",
8
+ "ASTScanResult",
9
+ "ScanFinding",
10
+ "CapabilityGuard",
11
+ "GuardResult",
12
+ ]
@@ -0,0 +1,53 @@
1
+ """Colony Skills — capability guards for pre-execution checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List
7
+
8
+ from apsimo.skills.models import SkillCapability, SkillManifest
9
+
10
+
11
+ @dataclass
12
+ class GuardResult:
13
+ allowed: bool
14
+ reason: str
15
+ violations: List[str] = field(default_factory=list)
16
+
17
+
18
+ class CapabilityGuard:
19
+ """Pre-execution checks that enforce declared capability constraints.
20
+
21
+ Checks performed:
22
+ - Input parameters do not include path traversal sequences
23
+ - Network capability required if inputs reference external URLs
24
+ - Execution blocked entirely if skill is quarantined or inactive
25
+ """
26
+
27
+ async def check(
28
+ self, manifest: SkillManifest, inputs: Dict[str, Any]
29
+ ) -> GuardResult:
30
+ violations: List[str] = []
31
+
32
+ # Path traversal in string inputs
33
+ for key, value in inputs.items():
34
+ if isinstance(value, str) and ".." in value:
35
+ violations.append(f"Path traversal in input '{key}': {value[:80]}")
36
+
37
+ # Network capability required for URL inputs
38
+ has_url_input = any(
39
+ isinstance(v, str) and (v.startswith("http://") or v.startswith("https://"))
40
+ for v in inputs.values()
41
+ )
42
+ if has_url_input and SkillCapability.NETWORK not in manifest.permissions.capabilities:
43
+ violations.append(
44
+ "Input contains URL but skill does not declare NETWORK capability."
45
+ )
46
+
47
+ if violations:
48
+ return GuardResult(
49
+ allowed=False,
50
+ reason="Pre-execution guard failed.",
51
+ violations=violations,
52
+ )
53
+ return GuardResult(allowed=True, reason="All guards passed.")
@@ -0,0 +1,223 @@
1
+ """Colony Skills — Python AST-based static analysis for skill source code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import hashlib
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Optional
10
+
11
+
12
+ BLOCKED_BUILTINS = frozenset({
13
+ "exec", "eval", "compile", "__import__", "open",
14
+ "breakpoint", "input",
15
+ })
16
+
17
+ BLOCKED_MODULES = frozenset({
18
+ "subprocess", "os.system", "pty", "ctypes", "pickle",
19
+ "marshal", "importlib.util",
20
+ })
21
+
22
+ # Dunder attributes that reach the runtime's object graph and let a skill
23
+ # escape the builtins allowlist via chains like
24
+ # ``().__class__.__bases__[0].__subclasses__()`` or
25
+ # ``getattr(__builtins__, "eval")``. This is a *defense-in-depth* check —
26
+ # real isolation requires a subprocess+seccomp sandbox, which is tracked
27
+ # separately; this scanner just makes the obvious escape patterns loud.
28
+ BLOCKED_DUNDERS = frozenset({
29
+ "__class__", "__bases__", "__mro__", "__subclasses__",
30
+ "__globals__", "__builtins__", "__import__",
31
+ "__getattribute__", "__dict__", "__code__",
32
+ })
33
+
34
+ _DYNAMIC_ATTR_FUNCS = frozenset({"getattr", "setattr", "delattr"})
35
+
36
+
37
+ @dataclass
38
+ class ScanFinding:
39
+ rule_id: str
40
+ severity: str # "critical" | "warning" | "info"
41
+ line: int
42
+ message: str
43
+ evidence: str
44
+
45
+
46
+ @dataclass
47
+ class ASTScanResult:
48
+ skill_id: str
49
+ status: str # "clean" | "warning" | "critical"
50
+ findings: List[ScanFinding] = field(default_factory=list)
51
+ scanned_lines: int = 0
52
+ duration_ms: int = 0
53
+
54
+ @property
55
+ def has_critical(self) -> bool:
56
+ return any(f.severity == "critical" for f in self.findings)
57
+
58
+ @property
59
+ def has_warning(self) -> bool:
60
+ return any(f.severity == "warning" for f in self.findings)
61
+
62
+
63
+ class ASTScanner:
64
+ """Python AST-based static analysis for skill source code.
65
+
66
+ Checks for:
67
+ - Direct calls to blocked builtins (exec, eval, etc.) [BLT001]
68
+ - Imports of blocked modules [IMP001]
69
+ - base64.decode + exec/eval combinations [OBF001]
70
+ - os.environ access to undeclared variables [ENV001]
71
+ - Dunder access used to escape the builtins allowlist [ESC001]
72
+ - getattr/setattr/delattr with dynamic/dunder targets [ESC002]
73
+ - socket.connect to non-declared hosts [NET001]
74
+
75
+ Defense-in-depth only: a determined attacker can still compose escapes
76
+ this static scanner misses. Real isolation needs a subprocess + seccomp
77
+ sandbox, which is out of scope here.
78
+ """
79
+
80
+ def scan(self, source: str, skill_id: str) -> ASTScanResult:
81
+ """Scan Python source for security findings.
82
+
83
+ Args:
84
+ source: Python source code string.
85
+ skill_id: Identifier used in the result.
86
+
87
+ Returns:
88
+ ASTScanResult with status and list of findings.
89
+ """
90
+ lines = source.splitlines()
91
+ findings: List[ScanFinding] = []
92
+ try:
93
+ tree = ast.parse(source)
94
+ except SyntaxError as exc:
95
+ return ASTScanResult(
96
+ skill_id=skill_id,
97
+ status="critical",
98
+ findings=[ScanFinding(
99
+ rule_id="SYN001",
100
+ severity="critical",
101
+ line=exc.lineno or 0,
102
+ message="Syntax error prevents AST analysis.",
103
+ evidence=str(exc),
104
+ )],
105
+ scanned_lines=len(lines),
106
+ )
107
+
108
+ # Track imports to detect obfuscation combos
109
+ imported_modules: set[str] = set()
110
+
111
+ for node in ast.walk(tree):
112
+ # BLT001: blocked builtins
113
+ if isinstance(node, ast.Call):
114
+ func_name = self._call_name(node)
115
+ if func_name in BLOCKED_BUILTINS:
116
+ findings.append(ScanFinding(
117
+ rule_id="BLT001",
118
+ severity="critical",
119
+ line=node.lineno,
120
+ message=f"Blocked builtin call: {func_name}()",
121
+ evidence=ast.unparse(node)[:200],
122
+ ))
123
+
124
+ # IMP001: blocked module imports
125
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
126
+ mods = self._import_names(node)
127
+ for mod in mods:
128
+ imported_modules.add(mod.split(".")[0])
129
+ if any(mod == b or mod.startswith(b + ".") for b in BLOCKED_MODULES):
130
+ findings.append(ScanFinding(
131
+ rule_id="IMP001",
132
+ severity="critical",
133
+ line=node.lineno,
134
+ message=f"Blocked module import: {mod}",
135
+ evidence=ast.unparse(node)[:200],
136
+ ))
137
+
138
+ # ENV001: os.environ access
139
+ if isinstance(node, ast.Attribute):
140
+ if (
141
+ isinstance(node.value, ast.Name)
142
+ and node.value.id == "os"
143
+ and node.attr == "environ"
144
+ ):
145
+ findings.append(ScanFinding(
146
+ rule_id="ENV001",
147
+ severity="warning",
148
+ line=node.lineno,
149
+ message="os.environ access detected; verify env vars are declared.",
150
+ evidence=ast.unparse(node)[:200],
151
+ ))
152
+
153
+ # ESC001: dunder access used to escape the builtins allowlist
154
+ if node.attr in BLOCKED_DUNDERS:
155
+ findings.append(ScanFinding(
156
+ rule_id="ESC001",
157
+ severity="critical",
158
+ line=node.lineno,
159
+ message=f"Blocked dunder access: .{node.attr}",
160
+ evidence=ast.unparse(node)[:200],
161
+ ))
162
+
163
+ # ESC002: getattr/setattr/delattr with a dynamic or dunder target
164
+ if isinstance(node, ast.Call):
165
+ func_name = self._call_name(node)
166
+ if func_name in _DYNAMIC_ATTR_FUNCS and len(node.args) >= 2:
167
+ second = node.args[1]
168
+ flagged = False
169
+ # getattr(x, "__class__") or similar constant dunder
170
+ if isinstance(second, ast.Constant) and isinstance(second.value, str):
171
+ if second.value in BLOCKED_DUNDERS:
172
+ flagged = True
173
+ # getattr(x, some_variable) — dynamic attribute name
174
+ elif not isinstance(second, ast.Constant):
175
+ flagged = True
176
+ if flagged:
177
+ findings.append(ScanFinding(
178
+ rule_id="ESC002",
179
+ severity="critical",
180
+ line=node.lineno,
181
+ message=f"{func_name}() with dynamic or dunder attribute",
182
+ evidence=ast.unparse(node)[:200],
183
+ ))
184
+
185
+ # OBF001: base64 + exec combo (check if both in imports or calls)
186
+ has_base64 = "base64" in imported_modules
187
+ has_exec_call = any(f.rule_id == "BLT001" and "exec" in f.message for f in findings)
188
+ if has_base64 and has_exec_call:
189
+ findings.append(ScanFinding(
190
+ rule_id="OBF001",
191
+ severity="critical",
192
+ line=0,
193
+ message="Potential obfuscation: base64 import combined with exec() call.",
194
+ evidence="base64 + exec combination detected.",
195
+ ))
196
+
197
+ has_critical = any(f.severity == "critical" for f in findings)
198
+ has_warning = any(f.severity == "warning" for f in findings)
199
+ status = "critical" if has_critical else ("warning" if has_warning else "clean")
200
+ return ASTScanResult(
201
+ skill_id=skill_id,
202
+ status=status,
203
+ findings=findings,
204
+ scanned_lines=len(lines),
205
+ )
206
+
207
+ @staticmethod
208
+ def _call_name(node: ast.Call) -> str:
209
+ if isinstance(node.func, ast.Name):
210
+ return node.func.id
211
+ if isinstance(node.func, ast.Attribute):
212
+ return node.func.attr
213
+ return ""
214
+
215
+ @staticmethod
216
+ def _import_names(node: ast.stmt) -> List[str]:
217
+ names: List[str] = []
218
+ if isinstance(node, ast.Import):
219
+ names = [alias.name for alias in node.names]
220
+ elif isinstance(node, ast.ImportFrom):
221
+ mod = node.module or ""
222
+ names = [f"{mod}.{alias.name}" for alias in node.names] + [mod]
223
+ return names
@@ -0,0 +1,26 @@
1
+ """Compounding learning: reusable procedure memory (item 3).
2
+
3
+ Distills a compact Skill (situation, steps, gotchas) from non-trivial
4
+ successes -- success after a retry, or a novel diagnosis -- and retrieves the
5
+ relevant ones into executor/planner prompts so the second encounter with a
6
+ problem class starts from the first one's solution. Failures update a short
7
+ per-domain strategy note (post-mortem memory).
8
+
9
+ Named ``skills_memory`` to avoid clashing with the existing ``skills/``
10
+ executor-skill registry (packaged executable skills). These are prompt-level
11
+ procedure memories for the sidecar's own reasoning loops; they inform
12
+ reasoning and never act, so retrieval is safe to run live
13
+ (COLONY_SKILLS_ENABLED, default true). Distillation costs one LLM call per
14
+ qualifying completion and starts in shadow (COLONY_SKILLS_DISTILL).
15
+ """
16
+
17
+ from apsimo.skills_memory.models import Skill, signature_overlap, situation_signature
18
+ from apsimo.skills_memory.store import SkillStore, skills_enabled, skills_distill_mode
19
+ from apsimo.skills_memory.distill import should_distill, distill_from_completion
20
+ from apsimo.skills_memory.retrieve import relevant_skills, format_block
21
+
22
+ __all__ = [
23
+ "Skill", "SkillStore", "signature_overlap", "situation_signature",
24
+ "should_distill", "distill_from_completion", "relevant_skills",
25
+ "format_block", "skills_enabled", "skills_distill_mode",
26
+ ]
@@ -0,0 +1,159 @@
1
+ """Skill distillation: one bounded LLM pass on qualifying completions.
2
+
3
+ Trigger discipline keeps the LLM cost modest and the library honest:
4
+ only success AFTER at least one retry, or a completion whose result carries a
5
+ resolution not already covered by an existing skill, qualifies. The LLM
6
+ proposes {title, situation, steps, gotchas} as STRICT JSON; code validates,
7
+ dedups by situation-signature overlap (>0.8 bumps the existing skill instead
8
+ of storing a near-duplicate), and enforces the library cap.
9
+
10
+ Modes (COLONY_SKILLS_DISTILL): off | shadow (log the would-be skill, store
11
+ nothing) | live. Default shadow.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import re
19
+ from typing import Any, List, Optional
20
+
21
+ from apsimo.skills_memory.models import (
22
+ Skill, signature_overlap, situation_signature,
23
+ )
24
+ from apsimo.skills_memory.store import SkillStore, skills_distill_mode, skills_max
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Result text that indicates a diagnosis/resolution worth remembering.
29
+ _RESOLUTION_RE = re.compile(
30
+ r"\b(?:root cause|fixed by|resolved by|the fix|turned out|workaround|"
31
+ r"solution was|caused by|diagnos)\w*\b", re.IGNORECASE)
32
+
33
+ _MAX_STEPS = 10
34
+ _MAX_GOTCHAS = 5
35
+
36
+ _SYSTEM_PROMPT = """\
37
+ You distill reusable procedures from completed work. Given a task and how it
38
+ was completed, extract ONE compact, generic procedure another agent could
39
+ follow when a similar situation recurs.
40
+
41
+ Respond with ONLY a JSON object (no prose, no markdown fences):
42
+ {"title": str (<80 chars, imperative), "situation": str (when this applies,
43
+ <200 chars), "steps": [str, ...] (2-8 concrete steps), "gotchas": [str, ...]
44
+ (0-4 pitfalls, may be empty)}.
45
+ Generalize: replace deployment-specific names (hosts, people, repos, ids)
46
+ with role placeholders like <host>, <repo>, <person> so the procedure
47
+ transfers to similar situations. Every step must be grounded in what
48
+ actually happened in the work log; never invent steps.
49
+ If the work contains no reusable procedure, respond with exactly: null"""
50
+
51
+
52
+ def should_distill(attempt_count: int, result_text: str,
53
+ store: Optional[SkillStore]) -> bool:
54
+ """Trigger conditions: retry-success, or a novel diagnosis in the result."""
55
+ if attempt_count and int(attempt_count) >= 1:
56
+ return True
57
+ text = (result_text or "").strip()
58
+ if not text or not _RESOLUTION_RE.search(text):
59
+ return False
60
+ # Novel: the resolution's signature is not already covered by a skill.
61
+ if store is not None:
62
+ sig = situation_signature(text[:400])
63
+ for s in store.list(limit=1000):
64
+ if signature_overlap(sig, s.situation_signature) > 0.5:
65
+ return False
66
+ return True
67
+
68
+
69
+ def _parse_skill_json(content: str) -> Optional[dict]:
70
+ text = (content or "").strip()
71
+ if "```" in text:
72
+ m = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
73
+ if m:
74
+ text = m.group(1).strip()
75
+ if text.lower() in ("null", "none", ""):
76
+ return None
77
+ if not text.startswith("{"):
78
+ m = re.search(r"\{.*\}", text, re.DOTALL)
79
+ if not m:
80
+ return None
81
+ text = m.group(0)
82
+ try:
83
+ data = json.loads(text)
84
+ except json.JSONDecodeError:
85
+ logger.debug("skill distill output was not valid JSON; dropping")
86
+ return None
87
+ return data if isinstance(data, dict) else None
88
+
89
+
90
+ def _validate(data: dict, domain: str, source_ref: str) -> Optional[Skill]:
91
+ title = str(data.get("title", "")).strip()[:120]
92
+ situation = str(data.get("situation", "")).strip()[:400]
93
+ steps = [str(s).strip()[:300] for s in (data.get("steps") or [])
94
+ if str(s).strip()][:_MAX_STEPS]
95
+ gotchas = [str(g).strip()[:300] for g in (data.get("gotchas") or [])
96
+ if str(g).strip()][:_MAX_GOTCHAS]
97
+ if not title or not situation or not steps:
98
+ return None
99
+ return Skill(title=title, situation=situation, steps=steps,
100
+ gotchas=gotchas, domain=(domain or "").lower(),
101
+ source_ref=source_ref)
102
+
103
+
104
+ async def distill_from_completion(
105
+ router: Any,
106
+ store: Optional[SkillStore],
107
+ *,
108
+ domain: str,
109
+ task_text: str,
110
+ result_text: str,
111
+ source_ref: str = "",
112
+ mode: Optional[str] = None,
113
+ ) -> Optional[Skill]:
114
+ """One reasoning pass -> validated, deduped Skill (or None).
115
+
116
+ Returns the stored (or shadow-logged) Skill. Dedup: an existing skill with
117
+ >0.8 signature overlap gets its use count bumped instead.
118
+ """
119
+ mode = mode or skills_distill_mode()
120
+ if mode == "off" or router is None:
121
+ return None
122
+ prompt = (f"TASK ({domain}):\n{(task_text or '')[:1500]}\n\n"
123
+ f"HOW IT WAS COMPLETED:\n{(result_text or '')[:2500]}")
124
+ try:
125
+ response = await router.complete(
126
+ [{"role": "system", "content": _SYSTEM_PROMPT},
127
+ {"role": "user", "content": prompt}],
128
+ context={"task": "skill_distillation"})
129
+ content = getattr(response, "content", "") or ""
130
+ except Exception as exc:
131
+ logger.debug("skill distillation LLM call failed: %s", exc)
132
+ return None
133
+
134
+ data = _parse_skill_json(content)
135
+ if not data:
136
+ return None
137
+ skill = _validate(data, domain, source_ref)
138
+ if skill is None:
139
+ return None
140
+
141
+ if store is not None:
142
+ existing = store.find_similar(skill.situation_signature, threshold=0.8)
143
+ if existing is not None:
144
+ store.bump_use(existing.id)
145
+ logger.info("skills_memory: near-duplicate of %s -- bumped use "
146
+ "instead of storing %r", existing.id, skill.title)
147
+ return existing
148
+
149
+ if mode == "shadow":
150
+ logger.info("SHADOW-SKILL distilled (not stored): %s",
151
+ json.dumps(skill.to_row(), default=str)[:600])
152
+ return skill
153
+
154
+ if store is not None:
155
+ store.add(skill)
156
+ store.evict_to_cap(skills_max())
157
+ logger.info("skills_memory: stored skill %s %r (domain=%s)",
158
+ skill.id, skill.title, skill.domain)
159
+ return skill
@@ -0,0 +1,85 @@
1
+ """Skill data model + situation-signature matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from apsimo.directives.models import normalize_terms
12
+
13
+
14
+ def situation_signature(text: str) -> str:
15
+ """Normalized, order-stable term signature for a situation description."""
16
+ return " ".join(sorted(set(normalize_terms(text))))
17
+
18
+
19
+ def signature_overlap(sig_a: str, sig_b: str) -> float:
20
+ """Jaccard overlap between two signatures (0..1)."""
21
+ a, b = set((sig_a or "").split()), set((sig_b or "").split())
22
+ if not a or not b:
23
+ return 0.0
24
+ return len(a & b) / len(a | b)
25
+
26
+
27
+ @dataclass
28
+ class Skill:
29
+ title: str
30
+ situation: str = "" # when does this apply
31
+ steps: List[str] = field(default_factory=list)
32
+ gotchas: List[str] = field(default_factory=list)
33
+ domain: str = "" # initiative type | project | directed
34
+ source_ref: str = "" # initiative/project/job id it came from
35
+ situation_signature: str = "" # derived from situation when empty
36
+ uses: int = 0
37
+ wins: int = 0
38
+ losses: int = 0
39
+ confidence: float = 0.6
40
+ id: str = field(default_factory=lambda: f"skl-{uuid.uuid4().hex[:12]}")
41
+ created_at: float = field(default_factory=time.time)
42
+ last_used_at: Optional[float] = None
43
+
44
+ def __post_init__(self) -> None:
45
+ if not self.situation_signature:
46
+ self.situation_signature = situation_signature(
47
+ f"{self.title} {self.situation}")
48
+
49
+ def score(self, now: Optional[float] = None) -> float:
50
+ """Eviction score: confidence + track record - staleness."""
51
+ now = now or time.time()
52
+ age_days = max(0.0, (now - (self.last_used_at or self.created_at)) / 86400.0)
53
+ return self.confidence + 0.2 * (self.wins - self.losses) - age_days / 180.0
54
+
55
+ def to_row(self) -> Dict[str, Any]:
56
+ return {
57
+ "id": self.id, "title": self.title, "situation": self.situation,
58
+ "situation_signature": self.situation_signature,
59
+ "steps": json.dumps(self.steps), "gotchas": json.dumps(self.gotchas),
60
+ "domain": self.domain, "source_ref": self.source_ref,
61
+ "uses": self.uses, "wins": self.wins, "losses": self.losses,
62
+ "confidence": self.confidence, "created_at": self.created_at,
63
+ "last_used_at": self.last_used_at,
64
+ }
65
+
66
+ @classmethod
67
+ def from_row(cls, r: Dict[str, Any]) -> "Skill":
68
+ def _j(key):
69
+ try:
70
+ v = json.loads(r.get(key) or "[]")
71
+ return [str(x) for x in v] if isinstance(v, list) else []
72
+ except Exception:
73
+ return []
74
+ return cls(
75
+ id=r["id"], title=r.get("title", "") or "",
76
+ situation=r.get("situation", "") or "",
77
+ situation_signature=r.get("situation_signature", "") or "",
78
+ steps=_j("steps"), gotchas=_j("gotchas"),
79
+ domain=r.get("domain", "") or "", source_ref=r.get("source_ref", "") or "",
80
+ uses=int(r.get("uses") or 0), wins=int(r.get("wins") or 0),
81
+ losses=int(r.get("losses") or 0),
82
+ confidence=float(r.get("confidence", 0.6) or 0.6),
83
+ created_at=float(r.get("created_at") or time.time()),
84
+ last_used_at=(float(r["last_used_at"]) if r.get("last_used_at") else None),
85
+ )
@@ -0,0 +1,62 @@
1
+ """Skill retrieval: situation -> the k most relevant procedures, as a compact
2
+ prompt block. Pure read; keyword-signature scoring (embedding optional later)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import List, Optional
7
+
8
+ from apsimo.skills_memory.models import (
9
+ Skill, signature_overlap, situation_signature,
10
+ )
11
+ from apsimo.skills_memory.store import SkillStore
12
+
13
+ _MIN_OVERLAP = 0.08
14
+
15
+
16
+ def relevant_skills(store: Optional[SkillStore], situation: str,
17
+ k: int = 3, domain: Optional[str] = None) -> List[Skill]:
18
+ """Top-k skills by signature overlap with the situation (domain-boosted)."""
19
+ if store is None or not (situation or "").strip():
20
+ return []
21
+ sig = situation_signature(situation)
22
+ scored = []
23
+ for s in store.list(limit=100000):
24
+ ov = signature_overlap(sig, s.situation_signature)
25
+ if domain and s.domain == (domain or "").lower():
26
+ ov += 0.05
27
+ # Track-record weighting: skills that historically informed winning
28
+ # runs rank up, losers rank down. Laplace prior keeps a fresh skill
29
+ # neutral (factor 1.0); the executor's outcome attribution feeds
30
+ # wins/losses, so use changes future retrieval.
31
+ wins = int(getattr(s, "wins", 0) or 0)
32
+ losses = int(getattr(s, "losses", 0) or 0)
33
+ win_rate = (wins + 1.0) / (wins + losses + 2.0)
34
+ ov *= 0.75 + 0.5 * win_rate
35
+ if ov >= _MIN_OVERLAP:
36
+ scored.append((ov, s))
37
+ scored.sort(key=lambda t: t[0], reverse=True)
38
+ top = [s for _, s in scored[:k]]
39
+ for s in top:
40
+ try:
41
+ store.bump_use(s.id)
42
+ except Exception:
43
+ pass
44
+ return top
45
+
46
+
47
+ def format_block(skills: List[Skill], strategy_note: str = "") -> str:
48
+ """Render skills (+ optional per-domain strategy note) for a system prompt.
49
+ Empty string when there is nothing to say."""
50
+ lines: List[str] = []
51
+ if skills:
52
+ lines.append("## Relevant past procedures (learned from your own prior work)")
53
+ for s in skills:
54
+ lines.append(f"- {s.title} -- applies when: {s.situation}")
55
+ for i, step in enumerate(s.steps, 1):
56
+ lines.append(f" {i}. {step}")
57
+ for g in s.gotchas:
58
+ lines.append(f" ! gotcha: {g}")
59
+ if (strategy_note or "").strip():
60
+ lines.append("## Lessons from past failures in this domain")
61
+ lines.append(strategy_note.strip())
62
+ return "\n".join(lines)