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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- 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)
|