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
apsimo/server.py
ADDED
|
@@ -0,0 +1,4197 @@
|
|
|
1
|
+
"""Colony sidecar FastAPI server.
|
|
2
|
+
|
|
3
|
+
Intelligence sidecar server mounted by agent frameworks (OpenClaw, Hermes,
|
|
4
|
+
etc.) as a plugin via the ``/v1/host`` API surface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# CLI and direct service starts use the same selected private instance.
|
|
20
|
+
from apsimo.util.instance import load_environment
|
|
21
|
+
load_environment()
|
|
22
|
+
from apsimo.environment import apply_environment_aliases
|
|
23
|
+
apply_environment_aliases()
|
|
24
|
+
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
|
|
27
|
+
from apsimo.api.routers.host import (
|
|
28
|
+
router as host_router,
|
|
29
|
+
v2_router as host_v2_router,
|
|
30
|
+
set_llm_router,
|
|
31
|
+
set_autonomy_loop,
|
|
32
|
+
set_scheduler,
|
|
33
|
+
set_chain_manager,
|
|
34
|
+
set_reasoning_loop,
|
|
35
|
+
set_tool_executor,
|
|
36
|
+
set_graph,
|
|
37
|
+
set_consolidator,
|
|
38
|
+
set_response_gate,
|
|
39
|
+
set_signal_collector,
|
|
40
|
+
set_embedder,
|
|
41
|
+
set_reranker,
|
|
42
|
+
set_goals_engine,
|
|
43
|
+
set_contacts_store,
|
|
44
|
+
set_briefings_engine,
|
|
45
|
+
set_world_store,
|
|
46
|
+
set_extraction_pipeline,
|
|
47
|
+
set_metalearner,
|
|
48
|
+
set_research_pipeline,
|
|
49
|
+
set_search_orchestrator,
|
|
50
|
+
set_delivery_bridge,
|
|
51
|
+
set_connection_discoverer,
|
|
52
|
+
set_insight_store,
|
|
53
|
+
set_learner,
|
|
54
|
+
set_skills_registry,
|
|
55
|
+
set_skill_executor,
|
|
56
|
+
set_secrets_manager,
|
|
57
|
+
set_session_store,
|
|
58
|
+
set_task_queue,
|
|
59
|
+
set_commitment_store,
|
|
60
|
+
set_affect_store,
|
|
61
|
+
set_facts_store,
|
|
62
|
+
set_p8_runtime,
|
|
63
|
+
set_context_provenance_store,
|
|
64
|
+
set_response_guard,
|
|
65
|
+
set_engagement_store,
|
|
66
|
+
set_comms_log,
|
|
67
|
+
set_preference_learner,
|
|
68
|
+
set_pattern_store,
|
|
69
|
+
set_surprise_store,
|
|
70
|
+
set_tom_extractor,
|
|
71
|
+
# Multi-Agent v0.7.0
|
|
72
|
+
set_agent_store,
|
|
73
|
+
set_invite_store,
|
|
74
|
+
set_initiative_store,
|
|
75
|
+
set_assignment_engine,
|
|
76
|
+
set_websocket_manager,
|
|
77
|
+
set_telemetry,
|
|
78
|
+
set_session_report_store,
|
|
79
|
+
set_agent_bridge,
|
|
80
|
+
set_initiative_executor,
|
|
81
|
+
set_situation_spine,
|
|
82
|
+
set_cognition_evidence,
|
|
83
|
+
set_drive_governance,
|
|
84
|
+
set_cognition_spine,
|
|
85
|
+
set_cognition_attachment_status,
|
|
86
|
+
set_external_event_intake,
|
|
87
|
+
set_worker_governor,
|
|
88
|
+
supported_capabilities,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
from apsimo import get_state_dir
|
|
92
|
+
|
|
93
|
+
logger = logging.getLogger(__name__)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _state_dir() -> Path:
|
|
97
|
+
"""Resolve the Colony state directory (wrapper for get_state_dir)."""
|
|
98
|
+
return get_state_dir()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _embedded_worker_enabled() -> bool:
|
|
102
|
+
"""Whether this process may construct its in-process execution worker.
|
|
103
|
+
|
|
104
|
+
Generic installs retain the historical default (enabled). Deployment
|
|
105
|
+
cutovers can pin ``COLONY_EMBEDDED_WORKER_ENABLED=false`` while keeping
|
|
106
|
+
queue maintenance, health, and durable outcome reconciliation online.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
raw = os.environ.get(
|
|
110
|
+
"COLONY_EMBEDDED_WORKER_ENABLED", "true"
|
|
111
|
+
).strip().lower()
|
|
112
|
+
if raw in {"1", "true", "yes", "on"}:
|
|
113
|
+
return True
|
|
114
|
+
if raw in {"0", "false", "no", "off"}:
|
|
115
|
+
return False
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
"COLONY_EMBEDDED_WORKER_ENABLED must be true or false"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _configured_embedded_worker_enabled() -> bool:
|
|
122
|
+
"""Read the same posture for startup wiring outside worker construction.
|
|
123
|
+
|
|
124
|
+
The host's release attestation intentionally reserves
|
|
125
|
+
``_embedded_worker_enabled`` for the exact lifespan branch that constructs
|
|
126
|
+
``WorkerNode``. Cognition readiness still needs the same strict setting
|
|
127
|
+
before that branch, so keep this non-construction reader behaviorally
|
|
128
|
+
identical and lock both parsers together in lifecycle tests.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
raw = os.environ.get(
|
|
132
|
+
"COLONY_EMBEDDED_WORKER_ENABLED", "true"
|
|
133
|
+
).strip().lower()
|
|
134
|
+
if raw in {"1", "true", "yes", "on"}:
|
|
135
|
+
return True
|
|
136
|
+
if raw in {"0", "false", "no", "off"}:
|
|
137
|
+
return False
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
"COLONY_EMBEDDED_WORKER_ENABLED must be true or false"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _resolve_guard_mode(raw: Optional[str]):
|
|
144
|
+
"""Strict COLONY_GUARD_MODE parser: 'enforce', 'shadow', or unset only.
|
|
145
|
+
|
|
146
|
+
An unrecognised value (e.g. a typo of 'enforce') must never silently
|
|
147
|
+
fall back to shadow — that would quietly disable the enforcement the
|
|
148
|
+
operator asked for. Log loudly and raise; the ResponseGuard then stays
|
|
149
|
+
uninitialized and the gate endpoints report unavailable (fail closed).
|
|
150
|
+
"""
|
|
151
|
+
from apsimo.gate.response_guard import GuardMode
|
|
152
|
+
value = (raw or "").strip().lower()
|
|
153
|
+
if value in ("", "shadow"):
|
|
154
|
+
return GuardMode.SHADOW
|
|
155
|
+
if value == "enforce":
|
|
156
|
+
return GuardMode.ENFORCE
|
|
157
|
+
logger.error(
|
|
158
|
+
"COLONY_GUARD_MODE=%r is not a recognised guard mode "
|
|
159
|
+
"(expected 'enforce' or 'shadow') — refusing to silently "
|
|
160
|
+
"fall back to shadow", raw,
|
|
161
|
+
)
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
f"COLONY_GUARD_MODE must be 'enforce' or 'shadow', got {raw!r}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cognition_owner_spec(*, router, node_id: str):
|
|
168
|
+
"""Build the mechanically isolated embedded ThoughtJobV1 owner."""
|
|
169
|
+
|
|
170
|
+
from apsimo.task_queue.handlers.inference import (
|
|
171
|
+
ThoughtOnlyInferenceHandler,
|
|
172
|
+
)
|
|
173
|
+
from apsimo.task_queue.models import JobType, WorkerCapabilities
|
|
174
|
+
from apsimo.task_queue.routing import THOUGHT_ROUTE
|
|
175
|
+
|
|
176
|
+
handlers = {
|
|
177
|
+
JobType.THOUGHT: ThoughtOnlyInferenceHandler(router),
|
|
178
|
+
}
|
|
179
|
+
capabilities = WorkerCapabilities(
|
|
180
|
+
node_id=node_id,
|
|
181
|
+
capabilities={"cognition_scoped", THOUGHT_ROUTE},
|
|
182
|
+
job_types={JobType.THOUGHT},
|
|
183
|
+
max_concurrent=1,
|
|
184
|
+
)
|
|
185
|
+
return handlers, capabilities
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cognition_worker_profile(*, configured_mode: str, attached: bool) -> str:
|
|
189
|
+
"""Select the embedded worker profile without inspecting partial objects.
|
|
190
|
+
|
|
191
|
+
A configured cognition owner is never allowed to become a generic action
|
|
192
|
+
worker merely because P3 attachment failed. The caller must hold worker
|
|
193
|
+
startup when this returns ``held``.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
mode = str(configured_mode or "off").strip().lower()
|
|
197
|
+
if mode in {"shadow", "live"}:
|
|
198
|
+
return "thought_only" if attached else "held"
|
|
199
|
+
return "generic"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _queue_seconds_env(name: str, default: float) -> float:
|
|
203
|
+
try:
|
|
204
|
+
value = float(os.environ.get(name, str(default)))
|
|
205
|
+
except ValueError as exc:
|
|
206
|
+
raise RuntimeError(f"{name} must be numeric") from exc
|
|
207
|
+
if not math.isfinite(value) or value <= 0:
|
|
208
|
+
raise RuntimeError(f"{name} must be positive")
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _attach_p8_runtime(*, state_dir: Path, facts_store, graph=None):
|
|
213
|
+
"""Atomically attach the reviewed P8 stores in explicit shadow mode.
|
|
214
|
+
|
|
215
|
+
Unset, off, unknown, and live all stay dark and create no P8 artifacts.
|
|
216
|
+
Live is intentionally excluded from this integration slice: recipient
|
|
217
|
+
simulation remains an ignored non-real-time shadow observation.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
from apsimo.tom.integration import P8Runtime, p8_integration_mode
|
|
221
|
+
|
|
222
|
+
set_p8_runtime(None)
|
|
223
|
+
graph_policy = getattr(graph, "set_recall_source_exclusions", None)
|
|
224
|
+
if callable(graph_policy):
|
|
225
|
+
# Clear any policy left on a reused graph before resolving this mode.
|
|
226
|
+
graph_policy(())
|
|
227
|
+
if p8_integration_mode() != "shadow":
|
|
228
|
+
return None
|
|
229
|
+
if facts_store is None:
|
|
230
|
+
raise RuntimeError("P8 shadow requires the canonical SharedFactsStore")
|
|
231
|
+
|
|
232
|
+
visibility = None
|
|
233
|
+
arcs = None
|
|
234
|
+
audit = None
|
|
235
|
+
try:
|
|
236
|
+
from apsimo.tom.arcs import ArcStore
|
|
237
|
+
from apsimo.tom.recipient_audit import (
|
|
238
|
+
open_recipient_simulation_audit_store,
|
|
239
|
+
)
|
|
240
|
+
from apsimo.tom.visibility_store import (
|
|
241
|
+
open_visibility_envelope_store,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
visibility = open_visibility_envelope_store(
|
|
245
|
+
state_dir / "colony-p8-visibility.db", enabled=True)
|
|
246
|
+
arcs = ArcStore(str(state_dir / "colony-p8-arcs.db"))
|
|
247
|
+
audit = open_recipient_simulation_audit_store(
|
|
248
|
+
state_dir / "colony-p8-recipient-audit.db", mode="shadow")
|
|
249
|
+
if visibility is None or audit is None:
|
|
250
|
+
raise RuntimeError("P8 shadow stores failed to open")
|
|
251
|
+
runtime = P8Runtime(
|
|
252
|
+
visibility_store=visibility,
|
|
253
|
+
arc_store=arcs,
|
|
254
|
+
audit_store=audit,
|
|
255
|
+
facts_store=facts_store,
|
|
256
|
+
mode="shadow",
|
|
257
|
+
)
|
|
258
|
+
if graph is not None:
|
|
259
|
+
if not callable(graph_policy):
|
|
260
|
+
raise RuntimeError(
|
|
261
|
+
"P8 shadow requires graph-wide recall source exclusions")
|
|
262
|
+
# SharedFacts graph rows are compatibility mirrors, not an
|
|
263
|
+
# authorized content path. Typed projection is their only reader.
|
|
264
|
+
graph_policy(
|
|
265
|
+
("tom:shared_fact",),
|
|
266
|
+
legacy_metadata_markers=("shared_fact",),
|
|
267
|
+
)
|
|
268
|
+
set_p8_runtime(runtime)
|
|
269
|
+
return runtime
|
|
270
|
+
except Exception:
|
|
271
|
+
if callable(graph_policy):
|
|
272
|
+
try:
|
|
273
|
+
graph_policy(())
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
for store in (audit, arcs, visibility):
|
|
277
|
+
if store is not None:
|
|
278
|
+
try:
|
|
279
|
+
store.close()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
set_p8_runtime(None)
|
|
283
|
+
raise
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_research_pipeline(*, graph, p8_runtime):
|
|
287
|
+
"""Preserve legacy research ownership unless P8 needs the governed graph."""
|
|
288
|
+
|
|
289
|
+
from apsimo.research.pipeline import ResearchPipeline
|
|
290
|
+
|
|
291
|
+
if p8_runtime is None:
|
|
292
|
+
return ResearchPipeline()
|
|
293
|
+
return ResearchPipeline(
|
|
294
|
+
graph=graph,
|
|
295
|
+
allow_fallback_graph=False,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def _governed_autonomy_stop_signal(autonomy_loop) -> None:
|
|
300
|
+
"""Request a prompt stop without awaiting the host route's task join."""
|
|
301
|
+
|
|
302
|
+
if autonomy_loop is not None:
|
|
303
|
+
await autonomy_loop.stop()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _work_order_runtime_hold_reason(job, project_store, concern_store) -> str:
|
|
307
|
+
"""Resolve one canonical cognition WorkOrder to its current source hold."""
|
|
308
|
+
|
|
309
|
+
from collections.abc import Mapping
|
|
310
|
+
from apsimo.self_model.event_concerns import (
|
|
311
|
+
project_turn_concern_hold_reason,
|
|
312
|
+
)
|
|
313
|
+
from apsimo.task_queue.models import JobType
|
|
314
|
+
from apsimo.work_orders import WorkOrderV1
|
|
315
|
+
|
|
316
|
+
payload = getattr(job, "payload", None)
|
|
317
|
+
if not isinstance(payload, Mapping) or payload.get("schema") != "WorkOrderV1":
|
|
318
|
+
return ""
|
|
319
|
+
try:
|
|
320
|
+
order = WorkOrderV1.from_payload(payload)
|
|
321
|
+
except Exception:
|
|
322
|
+
# A malformed row is not a canonical WorkOrder. Fail closed only
|
|
323
|
+
# when it asserts the cognition-spine source this fence owns; generic
|
|
324
|
+
# and external queue jobs retain their existing lifecycle.
|
|
325
|
+
if str(payload.get("source") or "") == "cognition_spine":
|
|
326
|
+
raise
|
|
327
|
+
return ""
|
|
328
|
+
if order.source != "cognition_spine":
|
|
329
|
+
return ""
|
|
330
|
+
if (
|
|
331
|
+
getattr(job, "job_type", None) is not JobType.AGENT_ACTION
|
|
332
|
+
or str(getattr(job, "job_id", "") or "") != order.work_order_id
|
|
333
|
+
):
|
|
334
|
+
raise RuntimeError("canonical cognition WorkOrder queue identity mismatch")
|
|
335
|
+
ledger = project_store.get_work_order(order.work_order_id)
|
|
336
|
+
if ledger is None:
|
|
337
|
+
raise RuntimeError("canonical cognition WorkOrder ledger row unavailable")
|
|
338
|
+
ledger_order = WorkOrderV1.from_payload(ledger.get("payload") or {})
|
|
339
|
+
if (
|
|
340
|
+
ledger_order.work_order_digest != order.work_order_digest
|
|
341
|
+
or str(ledger.get("work_order_digest") or "")
|
|
342
|
+
!= order.work_order_digest
|
|
343
|
+
or str(ledger.get("project_id") or "") != order.project_id
|
|
344
|
+
or str(ledger.get("step_id") or "") != order.step_id
|
|
345
|
+
):
|
|
346
|
+
raise RuntimeError("canonical cognition WorkOrder ledger binding mismatch")
|
|
347
|
+
project = project_store.get_project(order.project_id)
|
|
348
|
+
if project is None or str(project.source or "") != order.source:
|
|
349
|
+
raise RuntimeError("canonical cognition WorkOrder project binding mismatch")
|
|
350
|
+
return project_turn_concern_hold_reason(project, concern_store)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
_COGNITION_RUNTIME_INITIALIZATION_HOLD = (
|
|
354
|
+
"cognition_runtime_initialization_pending"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cognition_work_order_startup_hold_reason(job) -> str:
|
|
359
|
+
"""Hold only cognition WorkOrders until their durable stores are wired.
|
|
360
|
+
|
|
361
|
+
This deliberately relies only on the queue payload declaration. The
|
|
362
|
+
digest-bound verifier cannot run until ProjectStore initialization has
|
|
363
|
+
succeeded, so a payload asserting the owned source must remain held even
|
|
364
|
+
when the rest of that payload is malformed.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
from collections.abc import Mapping
|
|
368
|
+
|
|
369
|
+
payload = getattr(job, "payload", None)
|
|
370
|
+
if not isinstance(payload, Mapping):
|
|
371
|
+
return ""
|
|
372
|
+
if (
|
|
373
|
+
payload.get("schema") == "WorkOrderV1"
|
|
374
|
+
and str(payload.get("source") or "") == "cognition_spine"
|
|
375
|
+
):
|
|
376
|
+
return _COGNITION_RUNTIME_INITIALIZATION_HOLD
|
|
377
|
+
return ""
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _install_cognition_work_order_startup_fence(task_queue) -> None:
|
|
381
|
+
"""Install the pre-initialization queue fence when a queue is present."""
|
|
382
|
+
|
|
383
|
+
if task_queue is not None:
|
|
384
|
+
task_queue.queue.configure_runtime_claim_hold(
|
|
385
|
+
_cognition_work_order_startup_hold_reason,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _install_cognition_work_order_runtime_fence(
|
|
390
|
+
task_queue, project_store, concern_store,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Replace the startup fence with the complete digest-bound verifier."""
|
|
393
|
+
|
|
394
|
+
if task_queue is None:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
def _runtime_hold(job) -> str:
|
|
398
|
+
return _work_order_runtime_hold_reason(
|
|
399
|
+
job, project_store, concern_store,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
task_queue.queue.configure_runtime_claim_hold(_runtime_hold)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _attach_cognition_spine(
|
|
406
|
+
*,
|
|
407
|
+
state_dir: Path,
|
|
408
|
+
task_queue,
|
|
409
|
+
workspace,
|
|
410
|
+
concern_store,
|
|
411
|
+
project_store,
|
|
412
|
+
project_engine,
|
|
413
|
+
directive_manager,
|
|
414
|
+
llm_router=None,
|
|
415
|
+
embedded_worker_enabled: Optional[bool] = None,
|
|
416
|
+
proposal_store=None,
|
|
417
|
+
):
|
|
418
|
+
"""Attach the typed P3 cognition spine when its migration flag is on.
|
|
419
|
+
|
|
420
|
+
This is kept as one small, directly testable startup seam. In particular,
|
|
421
|
+
the default-off path must not create a database, and an enabled spine must
|
|
422
|
+
never fall back to a partial attachment when a durable dependency is
|
|
423
|
+
absent.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
from apsimo.cognition.goal_spine import (
|
|
427
|
+
CognitionSpine,
|
|
428
|
+
CognitionSpineStore,
|
|
429
|
+
ThoughtQueueAdapter,
|
|
430
|
+
ThoughtProposalPresentationSink,
|
|
431
|
+
cognition_spine_enabled,
|
|
432
|
+
cognition_spine_mode,
|
|
433
|
+
)
|
|
434
|
+
from apsimo.cognition.runtime import CognitionRuntimeContractV1
|
|
435
|
+
|
|
436
|
+
configured_mode = cognition_spine_mode()
|
|
437
|
+
configured_catalog = (
|
|
438
|
+
["thought"] if configured_mode in {"shadow", "live"} else []
|
|
439
|
+
)
|
|
440
|
+
set_cognition_spine(None, attachment_status={
|
|
441
|
+
"configured_mode": configured_mode,
|
|
442
|
+
"state": "attaching" if cognition_spine_enabled() else "off",
|
|
443
|
+
"reason": (
|
|
444
|
+
"attachment_in_progress" if cognition_spine_enabled()
|
|
445
|
+
else "cognition_not_configured"
|
|
446
|
+
),
|
|
447
|
+
"configured_handler_catalog": configured_catalog,
|
|
448
|
+
"effective_handler_catalog": [],
|
|
449
|
+
})
|
|
450
|
+
if not cognition_spine_enabled():
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
required = {
|
|
454
|
+
"task_queue": task_queue,
|
|
455
|
+
"workspace": workspace,
|
|
456
|
+
"concern_store": concern_store,
|
|
457
|
+
"project_store": project_store,
|
|
458
|
+
"project_engine": project_engine,
|
|
459
|
+
"directive_manager": directive_manager,
|
|
460
|
+
}
|
|
461
|
+
missing = sorted(name for name, value in required.items() if value is None)
|
|
462
|
+
if missing:
|
|
463
|
+
raise RuntimeError(
|
|
464
|
+
"P3 cognition spine missing durable dependencies: "
|
|
465
|
+
+ ", ".join(missing)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if cognition_spine_mode() == "live":
|
|
469
|
+
from apsimo.directives import Action
|
|
470
|
+
from apsimo.projects.models import projects_mode
|
|
471
|
+
|
|
472
|
+
if projects_mode() != "live":
|
|
473
|
+
raise RuntimeError(
|
|
474
|
+
"P3 live cognition requires ProjectEngine live mode"
|
|
475
|
+
)
|
|
476
|
+
if getattr(project_engine, "_work_orders", None) is None:
|
|
477
|
+
raise RuntimeError(
|
|
478
|
+
"P3 live cognition requires the canonical WorkOrder adapter"
|
|
479
|
+
)
|
|
480
|
+
try:
|
|
481
|
+
# This is a read-only health probe. An active boundary (including
|
|
482
|
+
# an observation blackout) may deny it; either verdict proves the
|
|
483
|
+
# store was readable. Only an exception or malformed result is
|
|
484
|
+
# unhealthy. A read avoids manufacturing a global-pause action
|
|
485
|
+
# block during startup.
|
|
486
|
+
boundary_probe = directive_manager.check(Action(
|
|
487
|
+
kind="read",
|
|
488
|
+
text="colony cognition startup boundary health probe",
|
|
489
|
+
target="cognition startup",
|
|
490
|
+
high_risk=False,
|
|
491
|
+
))
|
|
492
|
+
except Exception as exc:
|
|
493
|
+
raise RuntimeError(
|
|
494
|
+
"P3 live cognition requires a readable DirectiveGuard"
|
|
495
|
+
) from exc
|
|
496
|
+
if not isinstance(getattr(boundary_probe, "allowed", None), bool):
|
|
497
|
+
raise RuntimeError(
|
|
498
|
+
"P3 live cognition received a malformed DirectiveGuard verdict"
|
|
499
|
+
)
|
|
500
|
+
if llm_router is None:
|
|
501
|
+
raise RuntimeError(
|
|
502
|
+
"P3 live cognition requires the ThoughtJobV1 LLM router"
|
|
503
|
+
)
|
|
504
|
+
enabled = (
|
|
505
|
+
_configured_embedded_worker_enabled()
|
|
506
|
+
if embedded_worker_enabled is None
|
|
507
|
+
else bool(embedded_worker_enabled)
|
|
508
|
+
)
|
|
509
|
+
if not enabled:
|
|
510
|
+
raise RuntimeError(
|
|
511
|
+
"P3 live cognition requires the embedded strict ThoughtJobV1 "
|
|
512
|
+
"handler"
|
|
513
|
+
)
|
|
514
|
+
configured_owner = os.environ.get(
|
|
515
|
+
"COLONY_THOUGHT_WORKER_NODE_ID", ""
|
|
516
|
+
).strip()
|
|
517
|
+
if not configured_owner:
|
|
518
|
+
raise RuntimeError(
|
|
519
|
+
"P3 live cognition requires COLONY_THOUGHT_WORKER_NODE_ID"
|
|
520
|
+
)
|
|
521
|
+
from apsimo.chain.node import get_or_create_node_id
|
|
522
|
+
|
|
523
|
+
actual_owner = get_or_create_node_id(state_dir)
|
|
524
|
+
if configured_owner != actual_owner:
|
|
525
|
+
raise RuntimeError(
|
|
526
|
+
"COLONY_THOUGHT_WORKER_NODE_ID does not match the local "
|
|
527
|
+
"cognition worker node"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
project_limit = int(
|
|
532
|
+
os.environ.get("COLONY_PROJECTS_MAX_CONCURRENT", "3")
|
|
533
|
+
)
|
|
534
|
+
except ValueError as exc:
|
|
535
|
+
raise RuntimeError(
|
|
536
|
+
"COLONY_PROJECTS_MAX_CONCURRENT must be an integer"
|
|
537
|
+
) from exc
|
|
538
|
+
if not 1 <= project_limit <= 100:
|
|
539
|
+
raise RuntimeError(
|
|
540
|
+
"COLONY_PROJECTS_MAX_CONCURRENT must be between 1 and 100"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
available_capabilities = {
|
|
544
|
+
value.strip()
|
|
545
|
+
for value in os.environ.get(
|
|
546
|
+
"COLONY_COGNITION_AVAILABLE_CAPABILITIES",
|
|
547
|
+
"memory:read,reasoning,web:read",
|
|
548
|
+
).split(",")
|
|
549
|
+
if value.strip()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
def _charter(proposal, concern):
|
|
553
|
+
# Parsing already enforces the typed schema. This deployment seam
|
|
554
|
+
# adds a deterministic minimum: a goal needs an objective and cited
|
|
555
|
+
# evidence from its originating concern.
|
|
556
|
+
allowed = bool(proposal.objective and proposal.evidence_refs)
|
|
557
|
+
return allowed, (
|
|
558
|
+
"typed_goal_with_source_evidence"
|
|
559
|
+
if allowed
|
|
560
|
+
else "typed_goal_missing_objective_or_source_evidence"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
def _situation(proposal, concern):
|
|
564
|
+
active = project_engine.open_capacity_used()
|
|
565
|
+
allowed = active < project_limit
|
|
566
|
+
return allowed, (
|
|
567
|
+
"capacity_available" if allowed else "project_capacity_exhausted"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
runtime_bindings = {
|
|
571
|
+
"charter_store": None,
|
|
572
|
+
"situation_status": None,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
def _runtime_contract():
|
|
576
|
+
from apsimo.cognition.drive_governance import (
|
|
577
|
+
drive_governance_mode,
|
|
578
|
+
)
|
|
579
|
+
from apsimo.self_model.event_concerns import event_concern_mode
|
|
580
|
+
from apsimo.self_model.workspace import workspace_mode
|
|
581
|
+
|
|
582
|
+
active_id = None
|
|
583
|
+
blockers = []
|
|
584
|
+
charter_store = runtime_bindings["charter_store"]
|
|
585
|
+
if drive_governance_mode() == "live" and charter_store is not None:
|
|
586
|
+
try:
|
|
587
|
+
active = charter_store.active_revision("default")
|
|
588
|
+
active_id = active.revision_id if active is not None else None
|
|
589
|
+
except Exception:
|
|
590
|
+
blockers.append("active_charter_read_failed")
|
|
591
|
+
return CognitionRuntimeContractV1.compose(
|
|
592
|
+
requested_mode=cognition_spine_mode(),
|
|
593
|
+
workspace_mode=workspace_mode(),
|
|
594
|
+
event_concern_mode=event_concern_mode(),
|
|
595
|
+
drive_governance_mode=drive_governance_mode(),
|
|
596
|
+
charter_revision_id=active_id,
|
|
597
|
+
charter_store_attached=charter_store is not None,
|
|
598
|
+
attachment_blockers=tuple(blockers),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def _revision_snapshot():
|
|
602
|
+
try:
|
|
603
|
+
boundary_payload = directive_manager.context_brief() or ""
|
|
604
|
+
except Exception:
|
|
605
|
+
boundary_payload = "directive-store-unavailable"
|
|
606
|
+
policy_payload = {
|
|
607
|
+
"goal_admission_policy": "v1",
|
|
608
|
+
"available_capabilities": sorted(available_capabilities),
|
|
609
|
+
}
|
|
610
|
+
try:
|
|
611
|
+
capacity_payload = {
|
|
612
|
+
"planning": project_store.count("planning"),
|
|
613
|
+
"active": project_store.count("active"),
|
|
614
|
+
"used": project_engine.open_capacity_used(),
|
|
615
|
+
"limit": project_limit,
|
|
616
|
+
}
|
|
617
|
+
except Exception:
|
|
618
|
+
capacity_payload = {"status": "capacity-store-unavailable"}
|
|
619
|
+
situation_provider = runtime_bindings["situation_status"]
|
|
620
|
+
if callable(situation_provider):
|
|
621
|
+
try:
|
|
622
|
+
governed_situation = situation_provider()
|
|
623
|
+
except Exception:
|
|
624
|
+
governed_situation = {"status": "unavailable"}
|
|
625
|
+
else:
|
|
626
|
+
governed_situation = {"status": "p6-not-attached"}
|
|
627
|
+
situation_payload = {
|
|
628
|
+
"capacity": capacity_payload,
|
|
629
|
+
"governed_situation": governed_situation,
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
def _revision(label, payload):
|
|
633
|
+
encoded = json.dumps(
|
|
634
|
+
payload, sort_keys=True, separators=(",", ":"), default=str,
|
|
635
|
+
).encode("utf-8")
|
|
636
|
+
return f"{label}:{hashlib.sha256(encoded).hexdigest()[:24]}"
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
"policy_revision": _revision("policy", policy_payload),
|
|
640
|
+
"situation_revision": _revision("situation", situation_payload),
|
|
641
|
+
"boundary_revision": _revision("boundary", boundary_payload),
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
cognition_store = CognitionSpineStore(
|
|
645
|
+
str(state_dir / "colony-cognition.db")
|
|
646
|
+
)
|
|
647
|
+
spine = CognitionSpine(
|
|
648
|
+
concern_store=concern_store,
|
|
649
|
+
cognition_store=cognition_store,
|
|
650
|
+
project_engine=project_engine,
|
|
651
|
+
thought_queue=ThoughtQueueAdapter(
|
|
652
|
+
task_queue, cognition_store=cognition_store
|
|
653
|
+
),
|
|
654
|
+
directive_manager=directive_manager,
|
|
655
|
+
charter_validator=_charter,
|
|
656
|
+
situation_validator=_situation,
|
|
657
|
+
available_capabilities=available_capabilities,
|
|
658
|
+
enforce_runtime_contract=True,
|
|
659
|
+
runtime_contract_provider=_runtime_contract,
|
|
660
|
+
revision_provider=_revision_snapshot,
|
|
661
|
+
worker_health_provider=(
|
|
662
|
+
task_queue.queue.execution_readiness
|
|
663
|
+
if callable(getattr(task_queue.queue, "execution_readiness", None))
|
|
664
|
+
else None
|
|
665
|
+
),
|
|
666
|
+
proposal_presentation_sink=(
|
|
667
|
+
ThoughtProposalPresentationSink(proposal_store)
|
|
668
|
+
if proposal_store is not None else None
|
|
669
|
+
),
|
|
670
|
+
owner_person_id=(
|
|
671
|
+
os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
|
|
672
|
+
or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
|
|
673
|
+
or "owner"
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
spine._runtime_bindings = runtime_bindings
|
|
677
|
+
workspace.cognition_spine = spine
|
|
678
|
+
set_cognition_spine(spine, attachment_status={
|
|
679
|
+
"configured_mode": configured_mode,
|
|
680
|
+
"state": "attached",
|
|
681
|
+
"reason": "cognition_spine_attached_worker_pending",
|
|
682
|
+
"configured_handler_catalog": configured_catalog,
|
|
683
|
+
"effective_handler_catalog": [],
|
|
684
|
+
})
|
|
685
|
+
return spine
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _validator_allowed(result) -> bool:
|
|
689
|
+
"""Interpret P3's existing validator shapes without changing its verdict."""
|
|
690
|
+
|
|
691
|
+
if isinstance(result, dict):
|
|
692
|
+
return result.get("allowed") is True
|
|
693
|
+
if isinstance(result, (tuple, list)):
|
|
694
|
+
return bool(result and result[0] is True)
|
|
695
|
+
return result is True
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _situation_failure(reason: str) -> dict:
|
|
699
|
+
"""Stable fail-closed result consumed by P3's policy normalizer."""
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
"allowed": False,
|
|
703
|
+
"reason": reason,
|
|
704
|
+
"evidence_refs": [],
|
|
705
|
+
"does_not_grant_authority": True,
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _capacity_plus_attachment_failure(original_validator, reason: str):
|
|
710
|
+
"""Preserve an existing P3 denial but never allow past failed P6 startup."""
|
|
711
|
+
|
|
712
|
+
def _validator(proposal, concern):
|
|
713
|
+
try:
|
|
714
|
+
capacity = original_validator(proposal, concern)
|
|
715
|
+
except Exception:
|
|
716
|
+
logger.exception("P3 capacity validator failed during P6 failure")
|
|
717
|
+
return _situation_failure("capacity_validator_failed_closed")
|
|
718
|
+
if not _validator_allowed(capacity):
|
|
719
|
+
return capacity
|
|
720
|
+
return _situation_failure(reason)
|
|
721
|
+
|
|
722
|
+
return _validator
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _attach_situation_spine(
|
|
726
|
+
*,
|
|
727
|
+
state_dir: Path,
|
|
728
|
+
cognition_spine,
|
|
729
|
+
scheduler=None,
|
|
730
|
+
task_queue=None,
|
|
731
|
+
):
|
|
732
|
+
"""Attach P6 as an observer in shadow and a composed P3 gate in live.
|
|
733
|
+
|
|
734
|
+
The off path clears stale in-process router handles before returning and
|
|
735
|
+
constructs nothing. Shadow deliberately does not replace P3's situation
|
|
736
|
+
validator. Live preserves the exact capacity denial and consults P6 only
|
|
737
|
+
after capacity allows; any reducer, snapshot, or gate failure is a denial.
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
from apsimo.self_model.situation import (
|
|
741
|
+
AppropriatenessGate,
|
|
742
|
+
SituationReducer,
|
|
743
|
+
SituationStore,
|
|
744
|
+
situation_spine_enabled,
|
|
745
|
+
situation_spine_mode,
|
|
746
|
+
task_queue_resource_observation,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
set_situation_spine(None, None)
|
|
750
|
+
if not situation_spine_enabled():
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
mode = situation_spine_mode()
|
|
754
|
+
if mode == "live" and cognition_spine is None:
|
|
755
|
+
raise RuntimeError("P6 live situation spine requires the P3 cognition spine")
|
|
756
|
+
original_validator = (
|
|
757
|
+
getattr(cognition_spine, "_situation", None)
|
|
758
|
+
if mode == "live" else None
|
|
759
|
+
)
|
|
760
|
+
if mode == "live" and not callable(original_validator):
|
|
761
|
+
raise RuntimeError(
|
|
762
|
+
"P6 live situation spine requires P3's capacity validator"
|
|
763
|
+
)
|
|
764
|
+
queue_resource = getattr(task_queue, "queue", None)
|
|
765
|
+
if queue_resource is not None and (
|
|
766
|
+
not callable(getattr(queue_resource, "execution_readiness", None))
|
|
767
|
+
or not callable(getattr(queue_resource, "get_queue_stats", None))
|
|
768
|
+
):
|
|
769
|
+
raise RuntimeError(
|
|
770
|
+
"P6 task queue observation requires readiness and worker truth"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
store = SituationStore(str(state_dir / "colony-situation.db"))
|
|
774
|
+
try:
|
|
775
|
+
reducer = SituationReducer(store)
|
|
776
|
+
gate = AppropriatenessGate()
|
|
777
|
+
try:
|
|
778
|
+
initial_status = reducer.run_once(limit=100)
|
|
779
|
+
except Exception as exc: # the live wrapper remains explicitly fail closed
|
|
780
|
+
initial_status = {
|
|
781
|
+
"enabled": True,
|
|
782
|
+
"mode": mode,
|
|
783
|
+
"processed": 0,
|
|
784
|
+
"error": f"initial_reduce_failed:{type(exc).__name__}",
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if scheduler is not None:
|
|
788
|
+
try:
|
|
789
|
+
interval = int(
|
|
790
|
+
os.environ.get(
|
|
791
|
+
"COLONY_SITUATION_REDUCE_INTERVAL_SECONDS", "30"
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
except ValueError:
|
|
795
|
+
interval = 30
|
|
796
|
+
interval = max(5, min(3600, interval))
|
|
797
|
+
|
|
798
|
+
if queue_resource is None:
|
|
799
|
+
|
|
800
|
+
def _reduce_situation():
|
|
801
|
+
return reducer.run_once(limit=100)
|
|
802
|
+
|
|
803
|
+
else:
|
|
804
|
+
|
|
805
|
+
async def _reduce_situation():
|
|
806
|
+
observation = await task_queue_resource_observation(
|
|
807
|
+
queue_resource,
|
|
808
|
+
)
|
|
809
|
+
observed = store.ingest(observation)
|
|
810
|
+
result = dict(reducer.run_once(limit=100))
|
|
811
|
+
result["resource_observation"] = {
|
|
812
|
+
"disposition": observed["disposition"],
|
|
813
|
+
"observation_id": observation.observation_id,
|
|
814
|
+
"state": observation.state,
|
|
815
|
+
}
|
|
816
|
+
return result
|
|
817
|
+
|
|
818
|
+
scheduler.register(
|
|
819
|
+
"situation_reduce",
|
|
820
|
+
_reduce_situation,
|
|
821
|
+
interval_seconds=interval,
|
|
822
|
+
metadata={
|
|
823
|
+
"description": (
|
|
824
|
+
"Reduce durable evidence into the scoped P6 situation spine"
|
|
825
|
+
),
|
|
826
|
+
"mode": mode,
|
|
827
|
+
},
|
|
828
|
+
)
|
|
829
|
+
except Exception:
|
|
830
|
+
store.close()
|
|
831
|
+
raise
|
|
832
|
+
|
|
833
|
+
if mode == "live":
|
|
834
|
+
|
|
835
|
+
def _capacity_plus_situation(proposal, concern):
|
|
836
|
+
try:
|
|
837
|
+
capacity = original_validator(proposal, concern)
|
|
838
|
+
except Exception:
|
|
839
|
+
logger.exception("P3 capacity validator failed before P6")
|
|
840
|
+
return _situation_failure("capacity_validator_failed_closed")
|
|
841
|
+
if not _validator_allowed(capacity):
|
|
842
|
+
return capacity
|
|
843
|
+
try:
|
|
844
|
+
reduced = reducer.run_once(limit=100)
|
|
845
|
+
if not isinstance(reduced, dict) or reduced.get("error"):
|
|
846
|
+
return _situation_failure("situation_reducer_unhealthy")
|
|
847
|
+
snapshot = store.snapshot(
|
|
848
|
+
subject_person_id=proposal.subject_person_id,
|
|
849
|
+
viewer_scope=proposal.viewer_scope,
|
|
850
|
+
)
|
|
851
|
+
verdict = gate.for_goal_proposal(proposal, concern, snapshot)
|
|
852
|
+
if not isinstance(verdict, dict):
|
|
853
|
+
return _situation_failure("situation_gate_invalid_result")
|
|
854
|
+
return verdict
|
|
855
|
+
except Exception:
|
|
856
|
+
logger.exception("P6 live gate failed closed")
|
|
857
|
+
return _situation_failure("situation_gate_failed_closed")
|
|
858
|
+
|
|
859
|
+
cognition_spine._situation = _capacity_plus_situation
|
|
860
|
+
|
|
861
|
+
runtime_bindings = getattr(cognition_spine, "_runtime_bindings", None)
|
|
862
|
+
if isinstance(runtime_bindings, dict):
|
|
863
|
+
runtime_bindings["situation_status"] = reducer.status
|
|
864
|
+
|
|
865
|
+
set_situation_spine(store, reducer)
|
|
866
|
+
return {
|
|
867
|
+
"mode": mode,
|
|
868
|
+
"store": store,
|
|
869
|
+
"reducer": reducer,
|
|
870
|
+
"gate": gate,
|
|
871
|
+
"initial_status": initial_status,
|
|
872
|
+
"original_validator": original_validator,
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _attach_cognition_evidence(
|
|
877
|
+
*,
|
|
878
|
+
state_dir: Path,
|
|
879
|
+
project_store,
|
|
880
|
+
self_model=None,
|
|
881
|
+
expectations=None,
|
|
882
|
+
scheduler=None,
|
|
883
|
+
):
|
|
884
|
+
"""Attach the project outbox and optional receipt-derived learning loop.
|
|
885
|
+
|
|
886
|
+
Project events are operational truth, so their journal projector remains
|
|
887
|
+
active even when learning is off. Off mode keeps only a minimal durable
|
|
888
|
+
passthrough cursor so outcomes already counted by the legacy writer cannot
|
|
889
|
+
be replayed as new learning after live is re-enabled. Live mode requires
|
|
890
|
+
the canonical SelfModel and fails closed on an unhealthy initial
|
|
891
|
+
projection/reduction instead of falling back to self-reported success.
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
from apsimo.cognition.evidence_pipeline import (
|
|
895
|
+
CognitionEvidenceReducer,
|
|
896
|
+
CognitionEvidenceStore,
|
|
897
|
+
cognition_evidence_mode,
|
|
898
|
+
)
|
|
899
|
+
from apsimo.projects.event_outbox import ProjectEventProjector
|
|
900
|
+
|
|
901
|
+
mode = cognition_evidence_mode()
|
|
902
|
+
set_cognition_evidence(None, None, None, {
|
|
903
|
+
"configured_mode": mode,
|
|
904
|
+
"state": "attaching" if project_store is not None else "off",
|
|
905
|
+
"reason": (
|
|
906
|
+
"attachment_in_progress" if project_store is not None
|
|
907
|
+
else "project_store_unavailable"
|
|
908
|
+
),
|
|
909
|
+
})
|
|
910
|
+
if project_store is None:
|
|
911
|
+
if mode == "live":
|
|
912
|
+
raise RuntimeError(
|
|
913
|
+
"live cognition evidence requires the canonical ProjectStore"
|
|
914
|
+
)
|
|
915
|
+
return None
|
|
916
|
+
if mode == "live" and self_model is None:
|
|
917
|
+
raise RuntimeError(
|
|
918
|
+
"live cognition evidence requires the canonical SelfModel"
|
|
919
|
+
)
|
|
920
|
+
if mode == "live" and scheduler is None:
|
|
921
|
+
raise RuntimeError(
|
|
922
|
+
"live cognition evidence requires the autonomy scheduler"
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
projector = ProjectEventProjector(project_store)
|
|
926
|
+
evidence_store = None
|
|
927
|
+
reducer = None
|
|
928
|
+
try:
|
|
929
|
+
initial_outbox = projector.run_once(limit=100)
|
|
930
|
+
if mode == "live" and (
|
|
931
|
+
initial_outbox.get("failed")
|
|
932
|
+
or initial_outbox.get("acknowledgement_failures")
|
|
933
|
+
or initial_outbox.get("outbox", {}).get("last_error")
|
|
934
|
+
):
|
|
935
|
+
raise RuntimeError(
|
|
936
|
+
"live cognition evidence project outbox is unhealthy"
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
try:
|
|
940
|
+
interval = int(os.environ.get(
|
|
941
|
+
"COLONY_COGNITION_EVIDENCE_INTERVAL_SECONDS", "30",
|
|
942
|
+
))
|
|
943
|
+
except ValueError as exc:
|
|
944
|
+
raise RuntimeError(
|
|
945
|
+
"COLONY_COGNITION_EVIDENCE_INTERVAL_SECONDS must be an integer"
|
|
946
|
+
) from exc
|
|
947
|
+
interval = max(5, min(3600, interval))
|
|
948
|
+
|
|
949
|
+
evidence_store = CognitionEvidenceStore(
|
|
950
|
+
str(state_dir / "colony-cognition-evidence.db")
|
|
951
|
+
)
|
|
952
|
+
reducer = CognitionEvidenceReducer(
|
|
953
|
+
evidence_store,
|
|
954
|
+
project_store=project_store,
|
|
955
|
+
self_model=self_model,
|
|
956
|
+
expectations=expectations,
|
|
957
|
+
project_event_projector=projector,
|
|
958
|
+
)
|
|
959
|
+
attachment_armed = {"value": False}
|
|
960
|
+
if scheduler is not None:
|
|
961
|
+
|
|
962
|
+
def _reduce_cognition_evidence():
|
|
963
|
+
if not attachment_armed["value"]:
|
|
964
|
+
return {
|
|
965
|
+
"enabled": mode != "off", "mode": mode,
|
|
966
|
+
"processed": 0, "error": "attachment_not_armed",
|
|
967
|
+
}
|
|
968
|
+
return reducer.run_once(limit=100)
|
|
969
|
+
|
|
970
|
+
# Register before any live sink can mutate. A registration failure
|
|
971
|
+
# therefore leaves competence, expectations, and the evidence
|
|
972
|
+
# cursor untouched. If a scheduler ticks concurrently during
|
|
973
|
+
# attachment, the captured callback remains inert until armed.
|
|
974
|
+
scheduler.register(
|
|
975
|
+
"cognition_evidence_reduce",
|
|
976
|
+
_reduce_cognition_evidence,
|
|
977
|
+
interval_seconds=interval,
|
|
978
|
+
metadata={
|
|
979
|
+
"description": (
|
|
980
|
+
"Checkpoint or reduce receipt-bound project evidence"
|
|
981
|
+
),
|
|
982
|
+
"mode": mode,
|
|
983
|
+
},
|
|
984
|
+
)
|
|
985
|
+
initial_status = reducer.run_once(limit=100)
|
|
986
|
+
if mode == "live" and initial_status.get("error"):
|
|
987
|
+
raise RuntimeError(
|
|
988
|
+
"live cognition evidence initial reduction failed: "
|
|
989
|
+
+ str(initial_status["error"])[:300]
|
|
990
|
+
)
|
|
991
|
+
attachment = {
|
|
992
|
+
"configured_mode": mode,
|
|
993
|
+
"state": "attached" if scheduler is not None else "degraded",
|
|
994
|
+
"reason": (
|
|
995
|
+
"evidence_passthrough_cursor_attached_learning_off"
|
|
996
|
+
if mode == "off" and scheduler is not None else
|
|
997
|
+
"evidence_passthrough_cursor_restart_only"
|
|
998
|
+
if mode == "off" else
|
|
999
|
+
"receipt_derived_evidence_attached"
|
|
1000
|
+
if scheduler is not None else
|
|
1001
|
+
"receipt_derived_evidence_restart_replay_only"
|
|
1002
|
+
),
|
|
1003
|
+
}
|
|
1004
|
+
set_cognition_evidence(
|
|
1005
|
+
evidence_store, reducer, projector, attachment,
|
|
1006
|
+
)
|
|
1007
|
+
attachment_armed["value"] = True
|
|
1008
|
+
return {
|
|
1009
|
+
"mode": mode,
|
|
1010
|
+
"store": evidence_store,
|
|
1011
|
+
"reducer": reducer,
|
|
1012
|
+
"projector": projector,
|
|
1013
|
+
"initial_status": initial_status,
|
|
1014
|
+
"attachment": attachment,
|
|
1015
|
+
}
|
|
1016
|
+
except Exception:
|
|
1017
|
+
if evidence_store is not None:
|
|
1018
|
+
evidence_store.close()
|
|
1019
|
+
set_cognition_evidence(None, None, None, {
|
|
1020
|
+
"configured_mode": mode,
|
|
1021
|
+
"state": "failed",
|
|
1022
|
+
"reason": "cognition_evidence_attachment_failed",
|
|
1023
|
+
})
|
|
1024
|
+
raise
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _compose_p7_charter_admission(
|
|
1028
|
+
original_validator, charter_store, directive_manager=None,
|
|
1029
|
+
):
|
|
1030
|
+
"""Narrow P3 admission to the active owner-ratified P7 charter.
|
|
1031
|
+
|
|
1032
|
+
P7 lifecycle activation is already bound to the canonical approval
|
|
1033
|
+
authority. This adapter consumes that durable fact; it never creates a
|
|
1034
|
+
second approval path and never treats charter prose as executable policy.
|
|
1035
|
+
"""
|
|
1036
|
+
|
|
1037
|
+
from apsimo.cognition.drive_governance import (
|
|
1038
|
+
CharterAdmissionConstraintsV1,
|
|
1039
|
+
ScopeV1,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
def _parts(result):
|
|
1043
|
+
if isinstance(result, dict):
|
|
1044
|
+
return (
|
|
1045
|
+
result.get("allowed") is True,
|
|
1046
|
+
str(result.get("reason") or "charter_validator_denied")[:500],
|
|
1047
|
+
[str(ref) for ref in result.get("evidence_refs") or ()],
|
|
1048
|
+
)
|
|
1049
|
+
if isinstance(result, (tuple, list)):
|
|
1050
|
+
return (
|
|
1051
|
+
bool(result and result[0] is True),
|
|
1052
|
+
str(result[1] if len(result) > 1 else "charter_validator_denied")[:500],
|
|
1053
|
+
[str(ref) for ref in (result[2] if len(result) > 2 else ())],
|
|
1054
|
+
)
|
|
1055
|
+
return result is True, "charter_validator_denied", []
|
|
1056
|
+
|
|
1057
|
+
def _validator(proposal, concern):
|
|
1058
|
+
try:
|
|
1059
|
+
base = original_validator(proposal, concern)
|
|
1060
|
+
except Exception:
|
|
1061
|
+
return {
|
|
1062
|
+
"allowed": False,
|
|
1063
|
+
"reason": "base_charter_validator_failed",
|
|
1064
|
+
"evidence_refs": [],
|
|
1065
|
+
}
|
|
1066
|
+
allowed, _reason, evidence = _parts(base)
|
|
1067
|
+
if not allowed:
|
|
1068
|
+
return base
|
|
1069
|
+
try:
|
|
1070
|
+
active = charter_store.active_revision("default")
|
|
1071
|
+
except Exception:
|
|
1072
|
+
return {
|
|
1073
|
+
"allowed": False,
|
|
1074
|
+
"reason": "active_charter_read_failed",
|
|
1075
|
+
"evidence_refs": list(dict.fromkeys(evidence)),
|
|
1076
|
+
}
|
|
1077
|
+
if active is None:
|
|
1078
|
+
return {
|
|
1079
|
+
"allowed": False,
|
|
1080
|
+
"reason": "active_owner_ratified_charter_required",
|
|
1081
|
+
"evidence_refs": list(dict.fromkeys(evidence)),
|
|
1082
|
+
}
|
|
1083
|
+
try:
|
|
1084
|
+
proposal_scope = ScopeV1(
|
|
1085
|
+
proposal.subject_person_id,
|
|
1086
|
+
proposal.viewer_scope,
|
|
1087
|
+
proposal.shareability,
|
|
1088
|
+
)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
return {
|
|
1091
|
+
"allowed": False,
|
|
1092
|
+
"reason": "goal_scope_invalid_for_active_charter",
|
|
1093
|
+
"evidence_refs": list(dict.fromkeys(evidence)),
|
|
1094
|
+
}
|
|
1095
|
+
charter_evidence = [
|
|
1096
|
+
active.revision_id,
|
|
1097
|
+
f"charter-active:{active.revision_id}",
|
|
1098
|
+
*active.evidence_refs,
|
|
1099
|
+
]
|
|
1100
|
+
combined = list(dict.fromkeys([*charter_evidence, *evidence]))[:30]
|
|
1101
|
+
if not active.scope.permits_child(proposal_scope):
|
|
1102
|
+
return {
|
|
1103
|
+
"allowed": False,
|
|
1104
|
+
"reason": "active_charter_scope_holds_goal",
|
|
1105
|
+
"evidence_refs": combined,
|
|
1106
|
+
}
|
|
1107
|
+
constraints = (
|
|
1108
|
+
active.admission_constraints
|
|
1109
|
+
if getattr(active, "admission_constraints", None) is not None
|
|
1110
|
+
else CharterAdmissionConstraintsV1()
|
|
1111
|
+
)
|
|
1112
|
+
required_boundaries = set(constraints.required_boundary_refs)
|
|
1113
|
+
if required_boundaries:
|
|
1114
|
+
if directive_manager is None:
|
|
1115
|
+
return {
|
|
1116
|
+
"allowed": False,
|
|
1117
|
+
"reason": "charter_boundary_reader_unavailable",
|
|
1118
|
+
"evidence_refs": combined,
|
|
1119
|
+
}
|
|
1120
|
+
try:
|
|
1121
|
+
active_boundaries = {
|
|
1122
|
+
str(item.id) for item in directive_manager.active()
|
|
1123
|
+
}
|
|
1124
|
+
except Exception:
|
|
1125
|
+
return {
|
|
1126
|
+
"allowed": False,
|
|
1127
|
+
"reason": "charter_boundary_read_failed",
|
|
1128
|
+
"evidence_refs": combined,
|
|
1129
|
+
}
|
|
1130
|
+
if not required_boundaries.issubset(active_boundaries):
|
|
1131
|
+
return {
|
|
1132
|
+
"allowed": False,
|
|
1133
|
+
"reason": "charter_required_boundary_missing",
|
|
1134
|
+
"evidence_refs": combined,
|
|
1135
|
+
}
|
|
1136
|
+
objective = (
|
|
1137
|
+
f"{getattr(proposal, 'title', '')} "
|
|
1138
|
+
f"{getattr(proposal, 'objective', '')}"
|
|
1139
|
+
).casefold()
|
|
1140
|
+
if any(term in objective for term in constraints.objective_deny_terms):
|
|
1141
|
+
return {
|
|
1142
|
+
"allowed": False,
|
|
1143
|
+
"reason": "charter_objective_explicitly_denied",
|
|
1144
|
+
"evidence_refs": combined,
|
|
1145
|
+
}
|
|
1146
|
+
if constraints.objective_allow_terms and not any(
|
|
1147
|
+
term in objective for term in constraints.objective_allow_terms
|
|
1148
|
+
):
|
|
1149
|
+
return {
|
|
1150
|
+
"allowed": False,
|
|
1151
|
+
"reason": "charter_objective_not_explicitly_allowed",
|
|
1152
|
+
"evidence_refs": combined,
|
|
1153
|
+
}
|
|
1154
|
+
destructive_terms = {
|
|
1155
|
+
"delete", "destroy", "drop", "format", "overwrite", "wipe",
|
|
1156
|
+
}
|
|
1157
|
+
if (
|
|
1158
|
+
any(term in objective for term in destructive_terms)
|
|
1159
|
+
and not constraints.allow_destructive
|
|
1160
|
+
):
|
|
1161
|
+
return {
|
|
1162
|
+
"allowed": False,
|
|
1163
|
+
"reason": "charter_destructive_objective_not_allowed",
|
|
1164
|
+
"evidence_refs": combined,
|
|
1165
|
+
}
|
|
1166
|
+
requested = set(getattr(proposal, "required_capabilities", ()) or ())
|
|
1167
|
+
denied = sorted(requested.intersection(constraints.capability_deny))
|
|
1168
|
+
if denied:
|
|
1169
|
+
return {
|
|
1170
|
+
"allowed": False,
|
|
1171
|
+
"reason": "charter_capability_explicitly_denied:" + ",".join(denied),
|
|
1172
|
+
"evidence_refs": combined,
|
|
1173
|
+
}
|
|
1174
|
+
if not requested.issubset(set(constraints.capability_ceiling)):
|
|
1175
|
+
return {
|
|
1176
|
+
"allowed": False,
|
|
1177
|
+
"reason": "charter_capability_ceiling_exceeded",
|
|
1178
|
+
"evidence_refs": combined,
|
|
1179
|
+
}
|
|
1180
|
+
if "root:shell" in requested and not constraints.allow_root_shell:
|
|
1181
|
+
return {
|
|
1182
|
+
"allowed": False,
|
|
1183
|
+
"reason": "charter_root_shell_not_allowed",
|
|
1184
|
+
"evidence_refs": combined,
|
|
1185
|
+
}
|
|
1186
|
+
if "messaging:send" in requested:
|
|
1187
|
+
if not constraints.allow_messaging:
|
|
1188
|
+
return {
|
|
1189
|
+
"allowed": False,
|
|
1190
|
+
"reason": "charter_messaging_not_allowed",
|
|
1191
|
+
"evidence_refs": combined,
|
|
1192
|
+
}
|
|
1193
|
+
# GoalProposalV1 deliberately has no recipient field. Until a
|
|
1194
|
+
# digest-bound recipient envelope exists, even an explicit
|
|
1195
|
+
# charter allowance cannot infer one from the concern/model.
|
|
1196
|
+
return {
|
|
1197
|
+
"allowed": False,
|
|
1198
|
+
"reason": "charter_recipient_envelope_required",
|
|
1199
|
+
"evidence_refs": combined,
|
|
1200
|
+
}
|
|
1201
|
+
if proposal.shareability not in set(constraints.allowed_shareability):
|
|
1202
|
+
return {
|
|
1203
|
+
"allowed": False,
|
|
1204
|
+
"reason": "charter_shareability_not_allowed",
|
|
1205
|
+
"evidence_refs": combined,
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
"allowed": True,
|
|
1209
|
+
"reason": "active_owner_ratified_charter",
|
|
1210
|
+
"evidence_refs": combined,
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return _validator
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def _attach_drive_governance(
|
|
1217
|
+
*,
|
|
1218
|
+
state_dir: Path,
|
|
1219
|
+
cognition_spine,
|
|
1220
|
+
workspace,
|
|
1221
|
+
project_store,
|
|
1222
|
+
directive_manager,
|
|
1223
|
+
approval_authority=None,
|
|
1224
|
+
):
|
|
1225
|
+
"""Attach P7 only to the complete, durable P3 dependency graph."""
|
|
1226
|
+
|
|
1227
|
+
from apsimo.cognition.drive_governance import (
|
|
1228
|
+
DriveGovernance,
|
|
1229
|
+
DriveGovernanceStore,
|
|
1230
|
+
DriveRanker,
|
|
1231
|
+
drive_governance_mode,
|
|
1232
|
+
)
|
|
1233
|
+
from apsimo.initiatives.approval_authority import (
|
|
1234
|
+
ApprovalAuthorityStore,
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
set_drive_governance(None, None, None)
|
|
1238
|
+
mode = drive_governance_mode()
|
|
1239
|
+
if mode == "off":
|
|
1240
|
+
return None
|
|
1241
|
+
|
|
1242
|
+
required = {
|
|
1243
|
+
"cognition_spine": cognition_spine,
|
|
1244
|
+
"workspace": workspace,
|
|
1245
|
+
"project_store": project_store,
|
|
1246
|
+
"directive_manager": directive_manager,
|
|
1247
|
+
}
|
|
1248
|
+
missing = sorted(name for name, value in required.items() if value is None)
|
|
1249
|
+
cognition_store = getattr(cognition_spine, "store", None)
|
|
1250
|
+
project_engine = getattr(cognition_spine, "project_engine", None)
|
|
1251
|
+
if cognition_store is None:
|
|
1252
|
+
missing.append("cognition_store")
|
|
1253
|
+
if project_engine is None:
|
|
1254
|
+
missing.append("project_engine")
|
|
1255
|
+
elif getattr(project_engine, "store", None) is not project_store:
|
|
1256
|
+
missing.append("shared_project_store")
|
|
1257
|
+
if missing:
|
|
1258
|
+
raise RuntimeError(
|
|
1259
|
+
"P7 drive governance missing durable dependencies: "
|
|
1260
|
+
+ ", ".join(sorted(set(missing)))
|
|
1261
|
+
)
|
|
1262
|
+
resolver = getattr(cognition_store, "get_policy_decision", None)
|
|
1263
|
+
if not callable(resolver):
|
|
1264
|
+
raise RuntimeError(
|
|
1265
|
+
"P7 drive governance requires the P3 policy decision resolver"
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
store = DriveGovernanceStore(state_dir / "cognition-drive-governance.db")
|
|
1269
|
+
try:
|
|
1270
|
+
shared_authority = approval_authority
|
|
1271
|
+
if mode in {"bootstrap", "live"} and shared_authority is None:
|
|
1272
|
+
shared_authority = ApprovalAuthorityStore(
|
|
1273
|
+
state_dir / "approval_authority.db"
|
|
1274
|
+
)
|
|
1275
|
+
governance = DriveGovernance(store, shared_authority, mode=mode)
|
|
1276
|
+
ranker = DriveRanker(
|
|
1277
|
+
store,
|
|
1278
|
+
policy_decision_resolver=resolver,
|
|
1279
|
+
directive_manager=directive_manager,
|
|
1280
|
+
)
|
|
1281
|
+
except Exception:
|
|
1282
|
+
store.close()
|
|
1283
|
+
raise
|
|
1284
|
+
original_charter_validator = getattr(cognition_spine, "_charter", None)
|
|
1285
|
+
if mode == "live":
|
|
1286
|
+
if not callable(original_charter_validator):
|
|
1287
|
+
store.close()
|
|
1288
|
+
raise RuntimeError(
|
|
1289
|
+
"P7 live governance requires P3's charter validator"
|
|
1290
|
+
)
|
|
1291
|
+
cognition_spine._charter = _compose_p7_charter_admission(
|
|
1292
|
+
original_charter_validator, store, directive_manager,
|
|
1293
|
+
)
|
|
1294
|
+
runtime_bindings = getattr(cognition_spine, "_runtime_bindings", None)
|
|
1295
|
+
if isinstance(runtime_bindings, dict):
|
|
1296
|
+
runtime_bindings["charter_store"] = store
|
|
1297
|
+
workspace.drive_governance = governance
|
|
1298
|
+
workspace.drive_ranker = ranker
|
|
1299
|
+
set_drive_governance(governance, ranker, project_store)
|
|
1300
|
+
return {
|
|
1301
|
+
"mode": mode,
|
|
1302
|
+
"store": store,
|
|
1303
|
+
"governance": governance,
|
|
1304
|
+
"ranker": ranker,
|
|
1305
|
+
"approval_authority": shared_authority,
|
|
1306
|
+
"original_charter_validator": original_charter_validator,
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _initialize_controlled_learning(
|
|
1311
|
+
*,
|
|
1312
|
+
state_dir: Path,
|
|
1313
|
+
adaptive_params,
|
|
1314
|
+
journal=None,
|
|
1315
|
+
) -> dict:
|
|
1316
|
+
"""Build the one P4 evidence/experiment graph for this process.
|
|
1317
|
+
|
|
1318
|
+
The migration flags retain their historical meanings: P4 mode defaults to
|
|
1319
|
+
``off`` (legacy weekly evaluation), while the independent benchmark and
|
|
1320
|
+
experiment feature flags control whether their databases are opened at
|
|
1321
|
+
all. Setters are cleared before construction so a second lifespan in the
|
|
1322
|
+
same interpreter cannot retain stale authority or a stale writer.
|
|
1323
|
+
"""
|
|
1324
|
+
|
|
1325
|
+
from apsimo.api.routers.host import (
|
|
1326
|
+
set_benchmark,
|
|
1327
|
+
set_experiments,
|
|
1328
|
+
set_learning_feedback_store,
|
|
1329
|
+
)
|
|
1330
|
+
from apsimo.initiatives.approval_authority import (
|
|
1331
|
+
ApprovalAuthorityStore,
|
|
1332
|
+
)
|
|
1333
|
+
from apsimo.intelligence.learning.feedback_store import (
|
|
1334
|
+
FeedbackStore,
|
|
1335
|
+
)
|
|
1336
|
+
from apsimo.self_model.benchmark import (
|
|
1337
|
+
BenchmarkStore,
|
|
1338
|
+
SelfhoodBenchmark,
|
|
1339
|
+
benchmark_enabled,
|
|
1340
|
+
)
|
|
1341
|
+
from apsimo.self_model.experiments import (
|
|
1342
|
+
ExperimentEngine,
|
|
1343
|
+
ExperimentStore,
|
|
1344
|
+
experiment_pregrants_from_env,
|
|
1345
|
+
experiments_enabled,
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
set_learning_feedback_store(None)
|
|
1349
|
+
set_benchmark(None)
|
|
1350
|
+
set_experiments(None)
|
|
1351
|
+
|
|
1352
|
+
correction_store = FeedbackStore(
|
|
1353
|
+
db_path=str(state_dir / "colony-learning-feedback.db"))
|
|
1354
|
+
|
|
1355
|
+
benchmark = None
|
|
1356
|
+
if benchmark_enabled():
|
|
1357
|
+
benchmark = SelfhoodBenchmark(
|
|
1358
|
+
BenchmarkStore(db_path=str(state_dir / "colony-benchmark.db")),
|
|
1359
|
+
corrections=correction_store,
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
experiments = None
|
|
1363
|
+
approval_authority = None
|
|
1364
|
+
if experiments_enabled():
|
|
1365
|
+
if adaptive_params is None:
|
|
1366
|
+
raise RuntimeError(
|
|
1367
|
+
"P4 experiments require the adaptive parameter store")
|
|
1368
|
+
if benchmark is None:
|
|
1369
|
+
raise RuntimeError(
|
|
1370
|
+
"P4 experiments require the canonical SelfhoodBenchmark")
|
|
1371
|
+
# This is the same approval_authority.db used by the queue approval
|
|
1372
|
+
# routes. Do not create an experiment-specific authority ledger.
|
|
1373
|
+
approval_authority = ApprovalAuthorityStore()
|
|
1374
|
+
experiments = ExperimentEngine(
|
|
1375
|
+
ExperimentStore(
|
|
1376
|
+
db_path=str(state_dir / "colony-experiments.db")),
|
|
1377
|
+
params=adaptive_params,
|
|
1378
|
+
benchmark=benchmark,
|
|
1379
|
+
journal=journal,
|
|
1380
|
+
approval_authority=approval_authority,
|
|
1381
|
+
pregranted_ranges=experiment_pregrants_from_env(),
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
# Publish only after the complete configured graph constructed. A bad
|
|
1385
|
+
# pregrant or missing dependency must not expose a partially wired P4.
|
|
1386
|
+
set_learning_feedback_store(correction_store)
|
|
1387
|
+
set_benchmark(benchmark)
|
|
1388
|
+
set_experiments(experiments)
|
|
1389
|
+
|
|
1390
|
+
return {
|
|
1391
|
+
"corrections": correction_store,
|
|
1392
|
+
"benchmark": benchmark,
|
|
1393
|
+
"experiments": experiments,
|
|
1394
|
+
"approval_authority": approval_authority,
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def _wire_controlled_learning_pipeline(
|
|
1399
|
+
cognition_pipeline,
|
|
1400
|
+
controlled_learning: dict,
|
|
1401
|
+
) -> None:
|
|
1402
|
+
"""Attach P4's detector adapter and correction reader to shared objects."""
|
|
1403
|
+
|
|
1404
|
+
if cognition_pipeline is None:
|
|
1405
|
+
return
|
|
1406
|
+
correction_store = controlled_learning.get("corrections")
|
|
1407
|
+
if correction_store is not None:
|
|
1408
|
+
cognition_pipeline.meta_learner.set_feedback_store(correction_store)
|
|
1409
|
+
experiments = controlled_learning.get("experiments")
|
|
1410
|
+
if experiments is not None:
|
|
1411
|
+
# StrategyAdjuster is proposal-only; ExperimentEngine remains the sole
|
|
1412
|
+
# adaptive-parameter writer and the only component that can start.
|
|
1413
|
+
cognition_pipeline.strategy_adjuster.set_experiment_proposer(
|
|
1414
|
+
experiments)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
def _scheduler_health_check(autonomy_loop) -> dict:
|
|
1418
|
+
"""Periodic health_check task body (module-level so it is testable).
|
|
1419
|
+
|
|
1420
|
+
Reports the actual wiring state instead of an unconditional
|
|
1421
|
+
``{"status": "ok"}``. No blanket try/except: an exception here must
|
|
1422
|
+
surface as a scheduler failure receipt, never be swallowed into green.
|
|
1423
|
+
"""
|
|
1424
|
+
import apsimo.api.routers.host as _h
|
|
1425
|
+
wired = 0
|
|
1426
|
+
for _n in ("_commitment_store", "_goals_store", "_affect_store",
|
|
1427
|
+
"_contacts_store", "_delivery_bridge", "_workspace",
|
|
1428
|
+
"_metalearner"):
|
|
1429
|
+
if getattr(_h, _n, None) is not None:
|
|
1430
|
+
wired += 1
|
|
1431
|
+
return {"status": "ok" if wired else "degraded",
|
|
1432
|
+
"subsystems_wired": wired,
|
|
1433
|
+
"autonomy_running": bool(getattr(autonomy_loop, "_running", False))}
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
@asynccontextmanager
|
|
1437
|
+
async def lifespan(app: FastAPI):
|
|
1438
|
+
"""Initialize subsystems on startup, tear down on shutdown."""
|
|
1439
|
+
from apsimo.initiatives.approval_authority import (
|
|
1440
|
+
GRANT_MAX_TTL_ENV,
|
|
1441
|
+
GRANT_MAX_USES_ENV,
|
|
1442
|
+
GRANT_UNLIMITED_SENTINEL,
|
|
1443
|
+
resolve_grant_envelope,
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
try:
|
|
1447
|
+
grant_envelope = resolve_grant_envelope()
|
|
1448
|
+
except RuntimeError as exc:
|
|
1449
|
+
logger.error(
|
|
1450
|
+
"INVALID APPROVAL GRANT ENVELOPE — refusing startup: %s", exc,
|
|
1451
|
+
)
|
|
1452
|
+
raise
|
|
1453
|
+
app.state.grant_envelope = grant_envelope
|
|
1454
|
+
if grant_envelope.standing_dimensions:
|
|
1455
|
+
descriptions = []
|
|
1456
|
+
if grant_envelope.max_ttl_seconds is None:
|
|
1457
|
+
descriptions.append(f"{GRANT_MAX_TTL_ENV} (no expiry)")
|
|
1458
|
+
if grant_envelope.max_uses is None:
|
|
1459
|
+
descriptions.append(f"{GRANT_MAX_USES_ENV} (no use cap)")
|
|
1460
|
+
logger.warning(
|
|
1461
|
+
"STANDING APPROVAL AUTHORITY ENABLED — %s configured as %r; "
|
|
1462
|
+
"exact-scope grants persist in those dimensions until revoked",
|
|
1463
|
+
"; ".join(descriptions),
|
|
1464
|
+
GRANT_UNLIMITED_SENTINEL,
|
|
1465
|
+
)
|
|
1466
|
+
else:
|
|
1467
|
+
logger.info(
|
|
1468
|
+
"Approval grant envelope bounded: %s=%s, %s=%s",
|
|
1469
|
+
GRANT_MAX_TTL_ENV,
|
|
1470
|
+
grant_envelope.max_ttl_seconds,
|
|
1471
|
+
GRANT_MAX_USES_ENV,
|
|
1472
|
+
grant_envelope.max_uses,
|
|
1473
|
+
)
|
|
1474
|
+
state_dir = _state_dir()
|
|
1475
|
+
_p8_wiring = None
|
|
1476
|
+
# Clear stale process authority before any queue/worker can be constructed.
|
|
1477
|
+
# A repeated lifespan that later fails remains fail-closed in live mode.
|
|
1478
|
+
set_worker_governor(None)
|
|
1479
|
+
|
|
1480
|
+
# --- Phase C external text/system evidence intake ---
|
|
1481
|
+
_external_event_intake = None
|
|
1482
|
+
set_external_event_intake(None)
|
|
1483
|
+
try:
|
|
1484
|
+
from apsimo.cognition.external_events import (
|
|
1485
|
+
ExternalEventInboxStore,
|
|
1486
|
+
ExternalEventIntake,
|
|
1487
|
+
)
|
|
1488
|
+
_external_event_intake = ExternalEventIntake(
|
|
1489
|
+
ExternalEventInboxStore(
|
|
1490
|
+
str(state_dir / "external-cognition-events.db")
|
|
1491
|
+
)
|
|
1492
|
+
)
|
|
1493
|
+
set_external_event_intake(_external_event_intake)
|
|
1494
|
+
logger.info(
|
|
1495
|
+
"External cognition event intake initialized (text/system only; db=%s)",
|
|
1496
|
+
state_dir / "external-cognition-events.db",
|
|
1497
|
+
)
|
|
1498
|
+
except Exception as exc:
|
|
1499
|
+
logger.error("External cognition event intake failed: %s", exc)
|
|
1500
|
+
|
|
1501
|
+
# --- 0. Adaptive parameters (meta-learning read-back path) ---
|
|
1502
|
+
# Created first so downstream consumers (consolidator, graph recall,
|
|
1503
|
+
# cognition pipeline) can take a handle; the ActionJournal is attached
|
|
1504
|
+
# in the self-model section once it exists.
|
|
1505
|
+
_adaptive_params = None
|
|
1506
|
+
try:
|
|
1507
|
+
from apsimo.self_model.params import (
|
|
1508
|
+
AdaptiveParamStore, register_core_params,
|
|
1509
|
+
)
|
|
1510
|
+
_adaptive_params = AdaptiveParamStore(
|
|
1511
|
+
db_path=str(state_dir / "colony-params.db"))
|
|
1512
|
+
register_core_params(_adaptive_params)
|
|
1513
|
+
try:
|
|
1514
|
+
from apsimo.api.routers.host import set_adaptive_params
|
|
1515
|
+
set_adaptive_params(_adaptive_params)
|
|
1516
|
+
except ImportError:
|
|
1517
|
+
pass
|
|
1518
|
+
logger.info("AdaptiveParamStore initialized (db=%s)",
|
|
1519
|
+
state_dir / "colony-params.db")
|
|
1520
|
+
except Exception as exc:
|
|
1521
|
+
logger.warning("AdaptiveParamStore init failed: %s", exc)
|
|
1522
|
+
|
|
1523
|
+
# --- 1. LLM Router ---
|
|
1524
|
+
llm_router = None
|
|
1525
|
+
try:
|
|
1526
|
+
from apsimo.router.router import LLMRouter
|
|
1527
|
+
from apsimo.router.tiers import build_tiers_from_host
|
|
1528
|
+
import json as _json
|
|
1529
|
+
|
|
1530
|
+
config_path = state_dir / ".colony-llm-config.json"
|
|
1531
|
+
if config_path.exists():
|
|
1532
|
+
try:
|
|
1533
|
+
host_llm_config = _json.loads(config_path.read_text())
|
|
1534
|
+
llm_router = LLMRouter(tiers={})
|
|
1535
|
+
llm_router.configure(host_llm_config, config_path=config_path)
|
|
1536
|
+
logger.info(
|
|
1537
|
+
"LLMRouter initialized from persisted host config (provider=%s)",
|
|
1538
|
+
host_llm_config.get("provider", "unknown"),
|
|
1539
|
+
)
|
|
1540
|
+
except Exception as cfg_exc:
|
|
1541
|
+
logger.warning("Persisted LLM config unavailable: %s", type(cfg_exc).__name__)
|
|
1542
|
+
llm_router = LLMRouter(tiers={})
|
|
1543
|
+
llm_router.watch_config(config_path)
|
|
1544
|
+
else:
|
|
1545
|
+
llm_router = LLMRouter(tiers={})
|
|
1546
|
+
llm_router.watch_config(config_path)
|
|
1547
|
+
logger.info("LLMRouter awaiting explicit host config")
|
|
1548
|
+
except Exception as exc:
|
|
1549
|
+
logger.warning("LLMRouter init failed — reasoning will not be available: %s", exc)
|
|
1550
|
+
|
|
1551
|
+
if llm_router is not None:
|
|
1552
|
+
set_llm_router(llm_router)
|
|
1553
|
+
|
|
1554
|
+
# --- 2. Reasoning loop ---
|
|
1555
|
+
if llm_router is not None:
|
|
1556
|
+
try:
|
|
1557
|
+
from apsimo.reasoning import ReasoningLoop, ToolExecutor
|
|
1558
|
+
tool_executor = ToolExecutor()
|
|
1559
|
+
reasoning_loop = ReasoningLoop(model=llm_router, tools=tool_executor)
|
|
1560
|
+
set_reasoning_loop(reasoning_loop)
|
|
1561
|
+
# Native tools will be registered after search orchestrator is wired
|
|
1562
|
+
set_tool_executor(tool_executor)
|
|
1563
|
+
logger.info("ReasoningLoop initialized")
|
|
1564
|
+
except Exception as exc:
|
|
1565
|
+
logger.warning("ReasoningLoop init failed: %s", exc)
|
|
1566
|
+
|
|
1567
|
+
# --- 3. Neo4j Graph memory ---
|
|
1568
|
+
graph = None
|
|
1569
|
+
if os.environ.get("COLONY_GRAPH_ENABLED", "true").lower() not in {"0", "false", "off"}:
|
|
1570
|
+
try:
|
|
1571
|
+
from apsimo.intelligence.graph.client import ColonyGraph, GraphConfig
|
|
1572
|
+
from pydantic import SecretStr
|
|
1573
|
+
neo4j_uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
|
1574
|
+
neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
|
|
1575
|
+
neo4j_pass = os.environ.get("NEO4J_PASSWORD", "")
|
|
1576
|
+
# Neo4j Community Edition only has the "neo4j" database.
|
|
1577
|
+
# Enterprise users can override via NEO4J_DATABASE.
|
|
1578
|
+
neo4j_db = os.environ.get("NEO4J_DATABASE", "neo4j")
|
|
1579
|
+
graph_config = GraphConfig(
|
|
1580
|
+
uri=neo4j_uri,
|
|
1581
|
+
auth=(neo4j_user, SecretStr(neo4j_pass)) if neo4j_pass else None,
|
|
1582
|
+
database=neo4j_db,
|
|
1583
|
+
)
|
|
1584
|
+
graph = ColonyGraph(graph_config)
|
|
1585
|
+
# Apply graph schema constraints/indexes before any queries run
|
|
1586
|
+
try:
|
|
1587
|
+
from apsimo.intelligence.graph.migrations import run_migrations
|
|
1588
|
+
await run_migrations(graph.driver, database=neo4j_db)
|
|
1589
|
+
except Exception as exc:
|
|
1590
|
+
logger.warning("Graph migrations failed (queries may be degraded): %s", exc)
|
|
1591
|
+
set_graph(graph)
|
|
1592
|
+
# Wire graph into ToolExecutor for capability-gap detection
|
|
1593
|
+
try:
|
|
1594
|
+
import apsimo.api.routers.host as _host_router
|
|
1595
|
+
te = _host_router._tool_executor
|
|
1596
|
+
if te is not None:
|
|
1597
|
+
te._graph = graph
|
|
1598
|
+
except Exception:
|
|
1599
|
+
logger.warning("ToolExecutor graph wiring failed (capability-gap detection degraded)")
|
|
1600
|
+
logger.info("ColonyGraph initialized (uri=%s db=%s)", neo4j_uri, neo4j_db)
|
|
1601
|
+
|
|
1602
|
+
# Ensure Colony self-representation in graph (v0.11.0)
|
|
1603
|
+
try:
|
|
1604
|
+
await graph.ensure_colony_self()
|
|
1605
|
+
except Exception as self_exc:
|
|
1606
|
+
logger.warning("Colony self-representation setup skipped: %s", self_exc)
|
|
1607
|
+
|
|
1608
|
+
# Wire consolidator (adaptive merge threshold when params wired)
|
|
1609
|
+
try:
|
|
1610
|
+
from apsimo.intelligence.graph.consolidator import MemoryConsolidator
|
|
1611
|
+
consolidator = MemoryConsolidator(graph, params=_adaptive_params)
|
|
1612
|
+
set_consolidator(consolidator)
|
|
1613
|
+
logger.info("MemoryConsolidator initialized")
|
|
1614
|
+
except Exception as cexc:
|
|
1615
|
+
logger.warning("MemoryConsolidator init skipped: %s", cexc)
|
|
1616
|
+
if _adaptive_params is not None:
|
|
1617
|
+
try:
|
|
1618
|
+
graph.set_adaptive_params(_adaptive_params)
|
|
1619
|
+
except Exception:
|
|
1620
|
+
logger.debug("graph adaptive-params wiring failed", exc_info=True)
|
|
1621
|
+
except Exception as exc:
|
|
1622
|
+
logger.warning("ColonyGraph init failed — memory endpoints will be degraded: %s", exc)
|
|
1623
|
+
|
|
1624
|
+
else:
|
|
1625
|
+
set_graph(None)
|
|
1626
|
+
logger.info("Graph disabled; canonical source memory and SQLite state remain available")
|
|
1627
|
+
|
|
1628
|
+
# --- 4. Response Gate (safety pipeline) ---
|
|
1629
|
+
_gate_ref = None
|
|
1630
|
+
_gate_config = None
|
|
1631
|
+
_gate_audit = None
|
|
1632
|
+
try:
|
|
1633
|
+
from apsimo.gate import ResponseGate, GateConfig
|
|
1634
|
+
from apsimo.gate.audit import InMemoryAuditLog
|
|
1635
|
+
# L7 send-delay (the cancel window) is env-tunable; default 0 = no
|
|
1636
|
+
# hold. Set COLONY_GATE_SEND_DELAY_SECS>0 to enable a real cancel
|
|
1637
|
+
# window on the request-path gate.
|
|
1638
|
+
try:
|
|
1639
|
+
_gate_delay = float(os.environ.get("COLONY_GATE_SEND_DELAY_SECS", "0"))
|
|
1640
|
+
except ValueError:
|
|
1641
|
+
_gate_delay = 0.0
|
|
1642
|
+
gate_config = GateConfig(send_delay_seconds=_gate_delay)
|
|
1643
|
+
gate_audit = InMemoryAuditLog()
|
|
1644
|
+
gate = ResponseGate(gate_config, session_store=None, audit_log=gate_audit)
|
|
1645
|
+
set_response_gate(gate)
|
|
1646
|
+
# Stash refs for re-wiring after session store is available
|
|
1647
|
+
_gate_ref = gate
|
|
1648
|
+
_gate_config = gate_config
|
|
1649
|
+
_gate_audit = gate_audit
|
|
1650
|
+
logger.info("ResponseGate initialized (send_delay=%.1fs, secondary_review=%s)",
|
|
1651
|
+
_gate_delay, getattr(gate_config, "enable_secondary_review", False))
|
|
1652
|
+
|
|
1653
|
+
# Re-wire ResponseGate with session store once available
|
|
1654
|
+
except Exception as exc:
|
|
1655
|
+
logger.warning("ResponseGate init failed — safety checks will pass-through: %s", exc)
|
|
1656
|
+
|
|
1657
|
+
# --- 5. Signal Collector ---
|
|
1658
|
+
signal_collector = None
|
|
1659
|
+
if graph is not None:
|
|
1660
|
+
try:
|
|
1661
|
+
from apsimo.intelligence.mind_model.graph_baseline import GraphBaselineStore
|
|
1662
|
+
from apsimo.intelligence.mind_model.signal_collector import SignalCollector
|
|
1663
|
+
baseline_store = GraphBaselineStore(graph)
|
|
1664
|
+
signal_collector = SignalCollector(baseline_store=baseline_store, graph=graph)
|
|
1665
|
+
set_signal_collector(signal_collector)
|
|
1666
|
+
logger.info("SignalCollector initialized (GraphBaselineStore backed by Neo4j)")
|
|
1667
|
+
except Exception as exc:
|
|
1668
|
+
logger.warning("SignalCollector init failed: %s", exc)
|
|
1669
|
+
else:
|
|
1670
|
+
logger.warning("SignalCollector skipped — ColonyGraph not available")
|
|
1671
|
+
|
|
1672
|
+
# --- 6. Embedding pipeline ---
|
|
1673
|
+
embed_provider = os.environ.get("COLONY_EMBED_PROVIDER", "")
|
|
1674
|
+
embed_model = os.environ.get("COLONY_EMBED_MODEL", "")
|
|
1675
|
+
embed_dims = os.environ.get("COLONY_EMBED_DIMS", "")
|
|
1676
|
+
reranker_model = os.environ.get("COLONY_RERANKER_MODEL", "")
|
|
1677
|
+
|
|
1678
|
+
# Auto-detect tier if not explicitly configured
|
|
1679
|
+
if embed_provider != "skip" and (not embed_provider or not embed_model):
|
|
1680
|
+
try:
|
|
1681
|
+
from apsimo.vector.scanner import scan
|
|
1682
|
+
from apsimo.vector.tiers import get_tier_by_memory
|
|
1683
|
+
hw = scan()
|
|
1684
|
+
tier = get_tier_by_memory(hw.vram_gb, hw.ram_gb)
|
|
1685
|
+
spec = tier.text_embedder
|
|
1686
|
+
if spec:
|
|
1687
|
+
if hw.gpu_type == "cuda":
|
|
1688
|
+
embed_provider = embed_provider or "cuda"
|
|
1689
|
+
elif hw.gpu_type == "mlx":
|
|
1690
|
+
# Prefer native MLX when the package is available
|
|
1691
|
+
try:
|
|
1692
|
+
import mlx_embeddings # noqa: F401
|
|
1693
|
+
embed_provider = embed_provider or "native_mlx"
|
|
1694
|
+
except ImportError:
|
|
1695
|
+
embed_provider = embed_provider or "mlx"
|
|
1696
|
+
else:
|
|
1697
|
+
embed_provider = embed_provider or "cpu"
|
|
1698
|
+
embed_model = embed_model or spec.model_id
|
|
1699
|
+
embed_dims = embed_dims or str(spec.dims)
|
|
1700
|
+
reranker_model = reranker_model or (tier.text_reranker.model_id if tier.text_reranker else "")
|
|
1701
|
+
logger.info(
|
|
1702
|
+
"Auto-detected embedding tier: %s (GPU=%s %dGB, RAM=%dGB) -> %s",
|
|
1703
|
+
tier.label, hw.gpu_name, hw.vram_gb, hw.ram_gb, spec.model_id,
|
|
1704
|
+
)
|
|
1705
|
+
except Exception as exc:
|
|
1706
|
+
logger.warning("Hardware scan failed, using defaults: %s", exc)
|
|
1707
|
+
embed_provider = embed_provider or "cpu"
|
|
1708
|
+
embed_model = embed_model or "sentence-transformers/all-MiniLM-L6-v2"
|
|
1709
|
+
embed_dims = embed_dims or "384"
|
|
1710
|
+
|
|
1711
|
+
try:
|
|
1712
|
+
from apsimo.vector.embedder import EmbeddingPipeline
|
|
1713
|
+
from apsimo.vector.config import EmbeddingConfig
|
|
1714
|
+
embed_config = EmbeddingConfig(
|
|
1715
|
+
provider=embed_provider,
|
|
1716
|
+
model_id=embed_model,
|
|
1717
|
+
dimensions=int(embed_dims) if embed_dims else 384,
|
|
1718
|
+
revision=os.environ.get("COLONY_EMBED_REVISION") or None,
|
|
1719
|
+
)
|
|
1720
|
+
from apsimo.vector.embedder import make_provider
|
|
1721
|
+
provider = make_provider(embed_config)
|
|
1722
|
+
if provider is not None and embed_provider == "openai_api" and hasattr(provider, "configure"):
|
|
1723
|
+
# openai_api needs explicit endpoint config (the text-only path
|
|
1724
|
+
# never called configure(); only the multimodal branch passed these).
|
|
1725
|
+
provider.configure(
|
|
1726
|
+
os.environ.get("COLONY_EMBED_BASE_URL", ""),
|
|
1727
|
+
os.environ.get("COLONY_EMBED_API_KEY", ""),
|
|
1728
|
+
)
|
|
1729
|
+
if provider is None:
|
|
1730
|
+
# 'skip' provider — embeddings disabled entirely
|
|
1731
|
+
logger.info("EmbeddingPipeline skipped (provider=skip) — embeddings disabled")
|
|
1732
|
+
else:
|
|
1733
|
+
pipeline = EmbeddingPipeline(provider)
|
|
1734
|
+
|
|
1735
|
+
# Wire up multimodal if enabled
|
|
1736
|
+
multimodal_enabled = os.environ.get("COLONY_MULTIMODAL", "false").lower() == "true"
|
|
1737
|
+
if multimodal_enabled:
|
|
1738
|
+
try:
|
|
1739
|
+
from apsimo.vector.multimodal_provider import make_multimodal_provider
|
|
1740
|
+
from apsimo.vector.image_store import make_image_store
|
|
1741
|
+
|
|
1742
|
+
mm_config = EmbeddingConfig(
|
|
1743
|
+
provider=embed_provider,
|
|
1744
|
+
model_id=embed_model,
|
|
1745
|
+
dimensions=int(embed_dims) if embed_dims else 1024,
|
|
1746
|
+
base_url=os.environ.get("COLONY_EMBED_BASE_URL"),
|
|
1747
|
+
api_key=os.environ.get("COLONY_EMBED_API_KEY"),
|
|
1748
|
+
)
|
|
1749
|
+
mm_provider = make_multimodal_provider(mm_config)
|
|
1750
|
+
img_store = make_image_store(
|
|
1751
|
+
mode=os.environ.get("COLONY_IMAGE_STORAGE", "local"),
|
|
1752
|
+
state_dir=os.environ.get("COLONY_STATE_DIR", "."),
|
|
1753
|
+
)
|
|
1754
|
+
pipeline = EmbeddingPipeline(
|
|
1755
|
+
provider=provider,
|
|
1756
|
+
multimodal_provider=mm_provider,
|
|
1757
|
+
image_store=img_store,
|
|
1758
|
+
)
|
|
1759
|
+
logger.info("Multimodal enabled (model=%s, storage=%s)", embed_model, os.environ.get("COLONY_IMAGE_STORAGE", "local"))
|
|
1760
|
+
except Exception as exc:
|
|
1761
|
+
logger.warning("Multimodal init failed, falling back to text-only: %s", exc)
|
|
1762
|
+
|
|
1763
|
+
await pipeline.warmup()
|
|
1764
|
+
set_embedder(pipeline)
|
|
1765
|
+
logger.info("EmbeddingPipeline initialized (provider=%s model=%s)", embed_provider, embed_model)
|
|
1766
|
+
|
|
1767
|
+
# Wire embedding pipeline into ColonyGraph for vector-backed recall
|
|
1768
|
+
try:
|
|
1769
|
+
from apsimo.vector.store import VectorStore
|
|
1770
|
+
from apsimo.vector.indexes import IndexCatalog
|
|
1771
|
+
from apsimo.turns import get_turn_idempotency_ledger
|
|
1772
|
+
from apsimo.vector import set_store, set_pipeline
|
|
1773
|
+
vector_db_path = os.path.join(state_dir, "lancedb")
|
|
1774
|
+
vs = VectorStore(data_dir=vector_db_path, identity=pipeline.index_identity,
|
|
1775
|
+
catalog=IndexCatalog(get_turn_idempotency_ledger(state_dir)))
|
|
1776
|
+
embed_dims = int(os.environ.get("COLONY_EMBED_DIMS", pipeline.dimensions or 384))
|
|
1777
|
+
await vs.connect(dimensions=embed_dims)
|
|
1778
|
+
await vs.ensure_collections(dimensions=embed_dims)
|
|
1779
|
+
set_store(vs)
|
|
1780
|
+
set_pipeline(pipeline)
|
|
1781
|
+
if graph is not None:
|
|
1782
|
+
graph.set_embed_fn(pipeline.embed)
|
|
1783
|
+
graph.set_vector_store(vs)
|
|
1784
|
+
logger.info("ColonyGraph wired to vector store (path=%s)", vector_db_path)
|
|
1785
|
+
|
|
1786
|
+
if graph is not None and graph._embed_fn and graph._vector_store:
|
|
1787
|
+
logger.info("ColonyGraph fully operational (Neo4j + embeddings + vector store)")
|
|
1788
|
+
else:
|
|
1789
|
+
logger.warning("ColonyGraph partially wired — memory may be degraded")
|
|
1790
|
+
except Exception as vexc:
|
|
1791
|
+
logger.warning("Vector store wiring failed (recall will use keyword fallback): %s", vexc)
|
|
1792
|
+
|
|
1793
|
+
# Pass LLM config to pipeline for auto-captioning
|
|
1794
|
+
llm_config_path = Path(os.environ.get("COLONY_STATE_DIR", ".")) / ".colony-llm-config.json"
|
|
1795
|
+
if llm_config_path.exists() and hasattr(pipeline, "set_llm_config"):
|
|
1796
|
+
try:
|
|
1797
|
+
llm_cfg = _json.loads(llm_config_path.read_text())
|
|
1798
|
+
pipeline.set_llm_config(llm_cfg)
|
|
1799
|
+
logger.info("LLM config passed to EmbeddingPipeline for auto-captioning")
|
|
1800
|
+
except Exception as exc:
|
|
1801
|
+
logger.debug("Could not pass LLM config to pipeline: %s", exc)
|
|
1802
|
+
|
|
1803
|
+
# Health check + model mismatch detection
|
|
1804
|
+
try:
|
|
1805
|
+
hc = await pipeline.health_check()
|
|
1806
|
+
if hc.get("status") != "ok":
|
|
1807
|
+
logger.warning("Embedder health check failed: %s", hc.get("error", "unknown"))
|
|
1808
|
+
else:
|
|
1809
|
+
logger.info("Embedder health check passed (latency=%.1fms)", hc.get("latency_ms", 0))
|
|
1810
|
+
except Exception as exc:
|
|
1811
|
+
logger.warning("Embedder health check exception: %s", exc)
|
|
1812
|
+
except Exception as exc:
|
|
1813
|
+
logger.warning("EmbeddingPipeline init failed: %s", exc)
|
|
1814
|
+
|
|
1815
|
+
# --- 6b. Reranker pipeline ---
|
|
1816
|
+
reranker_provider_name = os.environ.get("COLONY_RERANKER_PROVIDER", "")
|
|
1817
|
+
if reranker_model and reranker_model.lower() not in ("none", "", "null"):
|
|
1818
|
+
try:
|
|
1819
|
+
from apsimo.vector.reranker import (
|
|
1820
|
+
OpenAIAPIRerankerProvider,
|
|
1821
|
+
NativeMLXRerankerProvider,
|
|
1822
|
+
MLXRerankerProvider,
|
|
1823
|
+
CPURerankerProvider,
|
|
1824
|
+
CUDARerankerProvider,
|
|
1825
|
+
)
|
|
1826
|
+
reranker_base_url = os.environ.get("COLONY_RERANKER_BASE_URL", "")
|
|
1827
|
+
reranker_api_key = os.environ.get("COLONY_RERANKER_API_KEY", "")
|
|
1828
|
+
if reranker_provider_name == "openai_api" or reranker_base_url:
|
|
1829
|
+
# Remote reranker over an OpenAI/Jina-compatible /v1/rerank
|
|
1830
|
+
# endpoint, mirroring the embedder's openai_api path so the
|
|
1831
|
+
# model stays off-box instead of loading in-process.
|
|
1832
|
+
# COLONY_RERANKER_PROMPT_STYLE=qwen3 applies the Qwen3-Reranker
|
|
1833
|
+
# instruction template, without which its scores are noise.
|
|
1834
|
+
reranker_provider = OpenAIAPIRerankerProvider(reranker_model)
|
|
1835
|
+
reranker_provider.configure(
|
|
1836
|
+
reranker_base_url,
|
|
1837
|
+
reranker_api_key,
|
|
1838
|
+
os.environ.get("COLONY_RERANKER_PROMPT_STYLE", ""),
|
|
1839
|
+
)
|
|
1840
|
+
else:
|
|
1841
|
+
from apsimo.vector.scanner import scan
|
|
1842
|
+
hw = scan()
|
|
1843
|
+
if hw.gpu_type == "mlx":
|
|
1844
|
+
# Prefer native MLX when the package is available
|
|
1845
|
+
try:
|
|
1846
|
+
import mlx_lm # noqa: F401
|
|
1847
|
+
reranker_provider = NativeMLXRerankerProvider(reranker_model)
|
|
1848
|
+
except ImportError:
|
|
1849
|
+
reranker_provider = MLXRerankerProvider(reranker_model)
|
|
1850
|
+
elif hw.gpu_type == "cuda":
|
|
1851
|
+
reranker_provider = CUDARerankerProvider(reranker_model)
|
|
1852
|
+
else:
|
|
1853
|
+
reranker_provider = CPURerankerProvider(reranker_model)
|
|
1854
|
+
await reranker_provider.warmup()
|
|
1855
|
+
set_reranker(reranker_provider)
|
|
1856
|
+
logger.info(
|
|
1857
|
+
"Reranker initialized (provider=%s model=%s)",
|
|
1858
|
+
reranker_provider_name or "local", reranker_model,
|
|
1859
|
+
)
|
|
1860
|
+
# Wire the reranker into ColonyGraph recall (mirrors the
|
|
1861
|
+
# set_embed_fn wiring above). Registration alone changes
|
|
1862
|
+
# nothing: use is gated by COLONY_RECALL_RERANK (default off).
|
|
1863
|
+
if graph is not None and hasattr(graph, "set_rerank_fn"):
|
|
1864
|
+
from apsimo.intelligence.graph.recall import provider_calibration_metadata
|
|
1865
|
+
def recall_calibration_metadata():
|
|
1866
|
+
return provider_calibration_metadata(reranker_provider)
|
|
1867
|
+
graph.set_rerank_fn(
|
|
1868
|
+
reranker_provider.rerank,
|
|
1869
|
+
calibration_metadata=recall_calibration_metadata)
|
|
1870
|
+
logger.info(
|
|
1871
|
+
"ColonyGraph wired to reranker for recall "
|
|
1872
|
+
"(gated by COLONY_RECALL_RERANK)")
|
|
1873
|
+
except Exception as exc:
|
|
1874
|
+
logger.warning("Reranker init failed: %s", exc)
|
|
1875
|
+
else:
|
|
1876
|
+
logger.info("No reranker configured for this tier")
|
|
1877
|
+
|
|
1878
|
+
# --- 7. Goals engine ---
|
|
1879
|
+
goals_engine = None
|
|
1880
|
+
try:
|
|
1881
|
+
from apsimo.goals.engine import GoalEngine
|
|
1882
|
+
from apsimo.goals.store import GoalStore
|
|
1883
|
+
goals_db = os.path.join(state_dir, "colony-goals.db")
|
|
1884
|
+
goals_store = GoalStore(db_path=goals_db)
|
|
1885
|
+
goals_engine = GoalEngine(store=goals_store)
|
|
1886
|
+
set_goals_engine(goals_engine)
|
|
1887
|
+
logger.info("GoalEngine initialized (db=%s)", goals_db)
|
|
1888
|
+
except Exception as exc:
|
|
1889
|
+
logger.warning("GoalEngine init failed: %s", exc)
|
|
1890
|
+
|
|
1891
|
+
# --- 7b. Commitment Store ---
|
|
1892
|
+
try:
|
|
1893
|
+
from apsimo.commitments.store import CommitmentStore
|
|
1894
|
+
|
|
1895
|
+
commitments_db = state_dir / "colony-commitments.db"
|
|
1896
|
+
commitment_store = CommitmentStore(db_path=commitments_db)
|
|
1897
|
+
commitment_readiness = (
|
|
1898
|
+
commitment_store.resolution_recovery_readiness()
|
|
1899
|
+
)
|
|
1900
|
+
if commitment_readiness.get("ready") is not True:
|
|
1901
|
+
raise RuntimeError(
|
|
1902
|
+
"CommitmentStore resolution recovery is unavailable"
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
# Resolving a workspace concern raised from a commitment settles the
|
|
1906
|
+
# commitment itself — without this, the ingest loop re-raises the
|
|
1907
|
+
# concern from the still-open commitment and the resolve is cosmetic.
|
|
1908
|
+
from apsimo.self_model.settlement import register_settler
|
|
1909
|
+
|
|
1910
|
+
def _settle_commitment(source_id, *, outcome="done", note="",
|
|
1911
|
+
resolved_by="owner", operation_id=None,
|
|
1912
|
+
_cs=commitment_store):
|
|
1913
|
+
row = _cs.resolve(source_id, outcome=outcome, note=note,
|
|
1914
|
+
resolved_by=resolved_by,
|
|
1915
|
+
operation_id=operation_id)
|
|
1916
|
+
if not row:
|
|
1917
|
+
return None
|
|
1918
|
+
operation = (
|
|
1919
|
+
_cs.get_resolution_operation(source_id)
|
|
1920
|
+
if operation_id is not None else None
|
|
1921
|
+
)
|
|
1922
|
+
resolution = operation or (
|
|
1923
|
+
(row.get("metadata") or {}).get("resolution") or {}
|
|
1924
|
+
)
|
|
1925
|
+
return {
|
|
1926
|
+
"kind": "commitment",
|
|
1927
|
+
"status": row["status"],
|
|
1928
|
+
"operation_id": resolution.get("operation_id"),
|
|
1929
|
+
"outcome": resolution.get("outcome"),
|
|
1930
|
+
"note_digest": resolution.get("note_digest"),
|
|
1931
|
+
"resolved_by": (
|
|
1932
|
+
resolution.get("resolved_by")
|
|
1933
|
+
if operation is not None else resolution.get("by")
|
|
1934
|
+
),
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
register_settler("commitment", _settle_commitment, retry_safe=True)
|
|
1938
|
+
set_commitment_store(commitment_store)
|
|
1939
|
+
logger.info(
|
|
1940
|
+
"CommitmentStore initialized (db=%s, capability=%s)",
|
|
1941
|
+
commitments_db, commitment_readiness["capability"],
|
|
1942
|
+
)
|
|
1943
|
+
except Exception as exc:
|
|
1944
|
+
set_commitment_store(None)
|
|
1945
|
+
logger.error("CommitmentStore initialization failed")
|
|
1946
|
+
raise RuntimeError("CommitmentStore initialization failed") from exc
|
|
1947
|
+
|
|
1948
|
+
# --- 7c. Theory of Mind ---
|
|
1949
|
+
try:
|
|
1950
|
+
from apsimo.tom.affect import AffectStore
|
|
1951
|
+
from apsimo.tom.facts import SharedFactsStore
|
|
1952
|
+
|
|
1953
|
+
from apsimo.turns import get_turn_idempotency_ledger
|
|
1954
|
+
source_ledger = get_turn_idempotency_ledger(state_dir)
|
|
1955
|
+
affect_db = state_dir / "colony-affect.db"
|
|
1956
|
+
affect_store = AffectStore(db_path=affect_db, source_ledger=source_ledger)
|
|
1957
|
+
affect_store.purge_erased_sources()
|
|
1958
|
+
set_affect_store(affect_store)
|
|
1959
|
+
logger.info("AffectStore initialized (db=%s)", affect_db)
|
|
1960
|
+
|
|
1961
|
+
facts_db = state_dir / "colony-facts.db"
|
|
1962
|
+
facts_store = SharedFactsStore(db_path=facts_db, source_ledger=source_ledger)
|
|
1963
|
+
facts_store.purge_erased_sources()
|
|
1964
|
+
set_facts_store(facts_store)
|
|
1965
|
+
logger.info("SharedFactsStore initialized (db=%s)", facts_db)
|
|
1966
|
+
|
|
1967
|
+
try:
|
|
1968
|
+
_p8_wiring = _attach_p8_runtime(
|
|
1969
|
+
state_dir=state_dir, facts_store=facts_store, graph=graph)
|
|
1970
|
+
if _p8_wiring is not None:
|
|
1971
|
+
logger.info(
|
|
1972
|
+
"P8 visibility/arcs/recipient audit attached (shadow only)")
|
|
1973
|
+
except Exception:
|
|
1974
|
+
# P8 is an advisory shadow integration. Its persistence must not
|
|
1975
|
+
# take down the canonical SharedFactsStore or the rest of ToM.
|
|
1976
|
+
_p8_wiring = None
|
|
1977
|
+
logger.warning(
|
|
1978
|
+
"P8 shadow attachment failed; continuing with P8 off",
|
|
1979
|
+
exc_info=True,
|
|
1980
|
+
)
|
|
1981
|
+
|
|
1982
|
+
# Second-order theory of mind (tom2): refs-not-content inference
|
|
1983
|
+
# store + daily asymmetry engine. Inert unless COLONY_TOM2 is set
|
|
1984
|
+
# (default off; shadow = counts only).
|
|
1985
|
+
from apsimo.tom.tom2 import Tom2Store
|
|
1986
|
+
from apsimo.tom.asymmetry import AsymmetryEngine, tom2_mode
|
|
1987
|
+
from apsimo.api.routers.host import set_tom2_store, set_tom2_engine
|
|
1988
|
+
tom2_db = state_dir / "colony-tom2.db"
|
|
1989
|
+
tom2_store = Tom2Store(db_path=str(tom2_db))
|
|
1990
|
+
set_tom2_store(tom2_store)
|
|
1991
|
+
set_tom2_engine(AsymmetryEngine(facts_store, tom2_store))
|
|
1992
|
+
logger.info("Tom2Store + AsymmetryEngine initialized (db=%s, mode=%s)",
|
|
1993
|
+
tom2_db, tom2_mode())
|
|
1994
|
+
|
|
1995
|
+
# Level-2 exposure ledger (L2.3): refs-only record of any future
|
|
1996
|
+
# level-2 rendering + its budgets. Empty and inert until the leveled
|
|
1997
|
+
# rendering path is wired; the owner endpoint reads it regardless.
|
|
1998
|
+
from apsimo.tom.exposure import Tom2ExposureStore
|
|
1999
|
+
from apsimo.api.routers.host import set_tom2_exposure_store
|
|
2000
|
+
tom2_exposure_db = state_dir / "colony-tom2-exposure.db"
|
|
2001
|
+
set_tom2_exposure_store(Tom2ExposureStore(db_path=str(tom2_exposure_db)))
|
|
2002
|
+
logger.info("Tom2ExposureStore initialized (db=%s)", tom2_exposure_db)
|
|
2003
|
+
|
|
2004
|
+
# Conversation presence registry (L1.1): passive census of WHO was
|
|
2005
|
+
# seen in WHICH conversation, fed from the turns/sync attribution
|
|
2006
|
+
# chokepoint. Read by the environment-risk classifier; recording is
|
|
2007
|
+
# gated by COLONY_CONV_PRESENCE (default on).
|
|
2008
|
+
from apsimo.channels.presence import ConversationPresenceStore
|
|
2009
|
+
from apsimo.api.routers.host import set_presence_store
|
|
2010
|
+
presence_db = state_dir / "colony-presence.db"
|
|
2011
|
+
presence_store = ConversationPresenceStore(db_path=str(presence_db))
|
|
2012
|
+
set_presence_store(presence_store)
|
|
2013
|
+
logger.info("ConversationPresenceStore initialized (db=%s)", presence_db)
|
|
2014
|
+
|
|
2015
|
+
from apsimo.gate.context_provenance import (
|
|
2016
|
+
ContextProvenanceStore, ProvenanceCrossContextGuard)
|
|
2017
|
+
provenance_db = state_dir / "colony-context-provenance.db"
|
|
2018
|
+
provenance_store = ContextProvenanceStore(db_path=str(provenance_db))
|
|
2019
|
+
set_context_provenance_store(provenance_store)
|
|
2020
|
+
logger.info("ContextProvenanceStore initialized (db=%s)", provenance_db)
|
|
2021
|
+
|
|
2022
|
+
# Outbound response gate (opt-in; shadow by default). Applicability is
|
|
2023
|
+
# resolved by the static, deployment-neutral surface policy: guarded
|
|
2024
|
+
# text/artifacts and excluded real-time speech. Gateway labels grant
|
|
2025
|
+
# no bypass authority.
|
|
2026
|
+
from apsimo.gate.response_guard import ResponseGuard, GuardMode
|
|
2027
|
+
from apsimo.gate.surface_policy import (
|
|
2028
|
+
POLICY_DIGEST as _guard_policy_digest,
|
|
2029
|
+
POLICY_ID as _guard_policy_id,
|
|
2030
|
+
)
|
|
2031
|
+
from apsimo.world_model.extraction.conversation_extractor import (
|
|
2032
|
+
ConversationExtractor)
|
|
2033
|
+
_guard_mode = _resolve_guard_mode(os.environ.get("COLONY_GUARD_MODE"))
|
|
2034
|
+
from apsimo.gate.guard_audit import GuardAuditStore
|
|
2035
|
+
guard_audit_db = state_dir / "colony-guard-audit.db"
|
|
2036
|
+
guard_audit_store = GuardAuditStore(db_path=str(guard_audit_db))
|
|
2037
|
+
# Injection-taint registry (L3.1) + tom2 epistemic egress net (L3.2).
|
|
2038
|
+
# The check is INERT (zero findings) until the leveled tom2 wiring
|
|
2039
|
+
# registers a taint, which is why it may sit on the default enforce
|
|
2040
|
+
# allowlist from day one.
|
|
2041
|
+
from apsimo.gate.taint import TaintRegistry
|
|
2042
|
+
from apsimo.gate.layers.tom2_epistemic import Tom2EpistemicGuard
|
|
2043
|
+
from apsimo.api.routers.host import set_taint_registry
|
|
2044
|
+
taint_db = state_dir / "colony-tom2-taint.db"
|
|
2045
|
+
taint_registry = TaintRegistry(db_path=str(taint_db))
|
|
2046
|
+
set_taint_registry(taint_registry)
|
|
2047
|
+
_guard = ResponseGuard(
|
|
2048
|
+
cross_context=ProvenanceCrossContextGuard(provenance_store, extractor=ConversationExtractor()),
|
|
2049
|
+
default_mode=_guard_mode, audit_store=guard_audit_store,
|
|
2050
|
+
tom2_epistemic=Tom2EpistemicGuard(taint_registry, facts_store=facts_store))
|
|
2051
|
+
set_response_guard(_guard)
|
|
2052
|
+
logger.info(
|
|
2053
|
+
"ResponseGuard initialized (mode=%s, surface_policy=%s:%s, "
|
|
2054
|
+
"audit=%s, taints=%s)",
|
|
2055
|
+
_guard_mode.value, _guard_policy_id, _guard_policy_digest,
|
|
2056
|
+
guard_audit_db, taint_db,
|
|
2057
|
+
)
|
|
2058
|
+
# Verdict rows prove evaluation, not that an egress mediator actually
|
|
2059
|
+
# withheld or revised the exact bytes. Keep Tom2 capped at level 1
|
|
2060
|
+
# until a digest-bound applied-output receipt exists.
|
|
2061
|
+
from apsimo.tom.levels import set_evidence_probe
|
|
2062
|
+
set_evidence_probe(None)
|
|
2063
|
+
|
|
2064
|
+
from apsimo.tom.engagement import EngagementStore
|
|
2065
|
+
engagement_db = state_dir / "colony-engagement.db"
|
|
2066
|
+
engagement_store = EngagementStore(db_path=engagement_db, source_ledger=source_ledger)
|
|
2067
|
+
engagement_store.purge_erased_sources()
|
|
2068
|
+
set_engagement_store(engagement_store)
|
|
2069
|
+
logger.info("EngagementStore initialized (db=%s)", engagement_db)
|
|
2070
|
+
|
|
2071
|
+
from apsimo.contacts.comms import CommsLog
|
|
2072
|
+
comms_log = CommsLog(db_path=state_dir / "colony-comms.db")
|
|
2073
|
+
set_comms_log(comms_log)
|
|
2074
|
+
logger.info("CommsLog initialized")
|
|
2075
|
+
|
|
2076
|
+
# Owner preference learner — captures the owner's *explicit* directives
|
|
2077
|
+
# about how to communicate ("be concise", "use bullets", "no emoji") at
|
|
2078
|
+
# high confidence; complements the inferred per-contact EngagementStore.
|
|
2079
|
+
from apsimo.intelligence.components.preference_learner import PreferenceLearner
|
|
2080
|
+
from apsimo.self_model.perspective import SelfPerspective
|
|
2081
|
+
from apsimo.turns import get_turn_idempotency_ledger
|
|
2082
|
+
from apsimo.identity import get_owner_contact_id
|
|
2083
|
+
preference_learner = PreferenceLearner(db_path=str(state_dir / "colony-preferences.db"),
|
|
2084
|
+
perspective=SelfPerspective(get_turn_idempotency_ledger(state_dir), owner_id=get_owner_contact_id()))
|
|
2085
|
+
set_preference_learner(preference_learner)
|
|
2086
|
+
logger.info("PreferenceLearner initialized (db=%s)", state_dir / "colony-preferences.db")
|
|
2087
|
+
except Exception as exc:
|
|
2088
|
+
try:
|
|
2089
|
+
from apsimo.tom.levels import set_evidence_probe
|
|
2090
|
+
set_evidence_probe(None)
|
|
2091
|
+
except Exception:
|
|
2092
|
+
logger.debug("Tom2 evidence probe init cleanup failed", exc_info=True)
|
|
2093
|
+
logger.warning("Theory of Mind init failed: %s", exc)
|
|
2094
|
+
|
|
2095
|
+
# --- Directive / boundary memory (safety foundation) ---
|
|
2096
|
+
# Durable store of the owner's standing directives (MUST NOT / MUST) with an
|
|
2097
|
+
# enforcement guard consulted before autonomous actions. Boundaries must be
|
|
2098
|
+
# available before any action-taking, so this is wired unconditionally.
|
|
2099
|
+
try:
|
|
2100
|
+
from apsimo.directives import DirectiveManager, DirectiveStore
|
|
2101
|
+
from apsimo.api.routers.host import set_directive_manager
|
|
2102
|
+
from apsimo.turns import get_turn_idempotency_ledger
|
|
2103
|
+
_directive_store = DirectiveStore(db_path=str(state_dir / "colony-directives.db"),
|
|
2104
|
+
ledger=get_turn_idempotency_ledger(state_dir))
|
|
2105
|
+
_directive_manager = DirectiveManager(_directive_store)
|
|
2106
|
+
set_directive_manager(_directive_manager)
|
|
2107
|
+
if locals().get("tool_executor") is not None:
|
|
2108
|
+
tool_executor.configure_execution_policy(
|
|
2109
|
+
directive_manager=_directive_manager,
|
|
2110
|
+
boundary_required=True,
|
|
2111
|
+
)
|
|
2112
|
+
logger.info(
|
|
2113
|
+
"DirectiveManager initialized (db=%s, active=%d)",
|
|
2114
|
+
state_dir / "colony-directives.db", _directive_store.count_active(),
|
|
2115
|
+
)
|
|
2116
|
+
except Exception as exc:
|
|
2117
|
+
if locals().get("tool_executor") is not None:
|
|
2118
|
+
# A configured server may keep public information tools usable,
|
|
2119
|
+
# but private reads/mutations cannot silently lose their boundary.
|
|
2120
|
+
tool_executor.configure_execution_policy(
|
|
2121
|
+
directive_manager=None,
|
|
2122
|
+
boundary_required=True,
|
|
2123
|
+
)
|
|
2124
|
+
logger.warning("DirectiveManager init failed (boundaries disabled): %s", exc)
|
|
2125
|
+
|
|
2126
|
+
# --- Proposal store (self-directed thinking + research -> proposals) ---
|
|
2127
|
+
try:
|
|
2128
|
+
from apsimo.proposals import ProposalStore
|
|
2129
|
+
from apsimo.api.routers.host import set_proposal_store
|
|
2130
|
+
_proposal_store = ProposalStore(db_path=str(state_dir / "colony-proposals.db"))
|
|
2131
|
+
set_proposal_store(_proposal_store)
|
|
2132
|
+
logger.info("ProposalStore initialized (db=%s)", state_dir / "colony-proposals.db")
|
|
2133
|
+
except Exception as exc:
|
|
2134
|
+
logger.warning("ProposalStore init failed: %s", exc)
|
|
2135
|
+
|
|
2136
|
+
# --- Type feedback store (outcome-driven priority decay/boost) ---
|
|
2137
|
+
try:
|
|
2138
|
+
from apsimo.feedback import TypeFeedbackStore
|
|
2139
|
+
from apsimo.api.routers.host import set_feedback_store
|
|
2140
|
+
set_feedback_store(TypeFeedbackStore(db_path=str(state_dir / "colony-feedback.db")))
|
|
2141
|
+
logger.info("TypeFeedbackStore initialized (db=%s)", state_dir / "colony-feedback.db")
|
|
2142
|
+
except Exception as exc:
|
|
2143
|
+
logger.warning("TypeFeedbackStore init failed: %s", exc)
|
|
2144
|
+
|
|
2145
|
+
# --- Self-model / trust engine + action journal (item 4, Amendment 1) ---
|
|
2146
|
+
# Wired before directed action so approval tiering can consult trust.
|
|
2147
|
+
_sm_for_directed = None
|
|
2148
|
+
try:
|
|
2149
|
+
from apsimo.self_model import (
|
|
2150
|
+
ActionJournal, CompetenceStore, SelfModel, TrustEngine,
|
|
2151
|
+
self_model_enabled,
|
|
2152
|
+
)
|
|
2153
|
+
from apsimo.api.routers.host import (
|
|
2154
|
+
set_self_model, _feedback_store as _fb_for_trust,
|
|
2155
|
+
)
|
|
2156
|
+
if self_model_enabled():
|
|
2157
|
+
from apsimo.autonomy.registry import SubsystemRegistry as _Reg
|
|
2158
|
+
_competence = CompetenceStore(
|
|
2159
|
+
db_path=str(state_dir / "colony-self-model.db"))
|
|
2160
|
+
_journal = ActionJournal(
|
|
2161
|
+
db_path=str(state_dir / "colony-action-journal.db"))
|
|
2162
|
+
_trust = TrustEngine(
|
|
2163
|
+
_competence, db_path=str(state_dir / "colony-self-model.db"),
|
|
2164
|
+
feedback_store=_fb_for_trust, journal=_journal)
|
|
2165
|
+
_sm_for_directed = SelfModel(_competence, registry=_Reg(),
|
|
2166
|
+
trust=_trust, journal=_journal)
|
|
2167
|
+
_sm_for_directed.perspective = getattr(locals().get('preference_learner'), 'perspective', None)
|
|
2168
|
+
set_self_model(_sm_for_directed)
|
|
2169
|
+
if _adaptive_params is not None:
|
|
2170
|
+
_adaptive_params.set_journal(_journal)
|
|
2171
|
+
logger.info(
|
|
2172
|
+
"SelfModel/TrustEngine initialized (db=%s, journal=%s, "
|
|
2173
|
+
"autograduate=%s)",
|
|
2174
|
+
state_dir / "colony-self-model.db",
|
|
2175
|
+
state_dir / "colony-action-journal.db",
|
|
2176
|
+
os.environ.get("COLONY_TRUST_AUTOGRADUATE", "true"))
|
|
2177
|
+
else:
|
|
2178
|
+
logger.info("SelfModel disabled (COLONY_SELF_MODEL_ENABLED=false)")
|
|
2179
|
+
except Exception as exc:
|
|
2180
|
+
logger.warning("SelfModel init failed: %s", exc)
|
|
2181
|
+
|
|
2182
|
+
# --- P4 controlled learning: one evidence and authority graph ---
|
|
2183
|
+
_controlled_learning = {
|
|
2184
|
+
"corrections": None,
|
|
2185
|
+
"benchmark": None,
|
|
2186
|
+
"experiments": None,
|
|
2187
|
+
"approval_authority": None,
|
|
2188
|
+
}
|
|
2189
|
+
try:
|
|
2190
|
+
_controlled_learning = _initialize_controlled_learning(
|
|
2191
|
+
state_dir=state_dir,
|
|
2192
|
+
adaptive_params=_adaptive_params,
|
|
2193
|
+
journal=locals().get("_journal"),
|
|
2194
|
+
)
|
|
2195
|
+
if _controlled_learning["benchmark"] is not None:
|
|
2196
|
+
logger.info(
|
|
2197
|
+
"Selfhood benchmark ready (db=%s, corrections=%s)",
|
|
2198
|
+
state_dir / "colony-benchmark.db",
|
|
2199
|
+
state_dir / "colony-learning-feedback.db",
|
|
2200
|
+
)
|
|
2201
|
+
else:
|
|
2202
|
+
logger.info(
|
|
2203
|
+
"Selfhood benchmark disabled (COLONY_BENCHMARK_ENABLED=false)")
|
|
2204
|
+
if _controlled_learning["experiments"] is not None:
|
|
2205
|
+
logger.info(
|
|
2206
|
+
"Controlled experiment framework ready (db=%s, shared_authority=%s)",
|
|
2207
|
+
state_dir / "colony-experiments.db",
|
|
2208
|
+
state_dir / "approval_authority.db",
|
|
2209
|
+
)
|
|
2210
|
+
else:
|
|
2211
|
+
logger.info(
|
|
2212
|
+
"Experiment framework disabled "
|
|
2213
|
+
"(COLONY_EXPERIMENTS_ENABLED=false)")
|
|
2214
|
+
except Exception as exc:
|
|
2215
|
+
logger.error("Controlled learning init failed closed: %s", exc)
|
|
2216
|
+
|
|
2217
|
+
# --- Toolsmith (Mind M1): self-built, sandbox-verified tools ---
|
|
2218
|
+
try:
|
|
2219
|
+
from apsimo.toolsmith import (
|
|
2220
|
+
Toolsmith, ToolRegistry, toolsmith_enabled,
|
|
2221
|
+
)
|
|
2222
|
+
from apsimo.api.routers.host import set_toolsmith
|
|
2223
|
+
if toolsmith_enabled():
|
|
2224
|
+
_tool_registry = ToolRegistry(
|
|
2225
|
+
db_path=str(state_dir / "colony-toolsmith.db"),
|
|
2226
|
+
library_root=str(state_dir / "toolsmith_library"))
|
|
2227
|
+
_toolsmith = Toolsmith(_tool_registry)
|
|
2228
|
+
set_toolsmith(_toolsmith)
|
|
2229
|
+
# advertise graduated tools to the reasoning loop
|
|
2230
|
+
try:
|
|
2231
|
+
from apsimo.api.routers.host import _tool_executor
|
|
2232
|
+
if _tool_executor is not None:
|
|
2233
|
+
_tool_executor.set_dynamic_provider(
|
|
2234
|
+
_toolsmith.build_dynamic_provider())
|
|
2235
|
+
except Exception as texc:
|
|
2236
|
+
logger.warning("toolsmith dynamic provider wiring: %s", texc)
|
|
2237
|
+
logger.info("Toolsmith ready (mode=%s, db=%s)",
|
|
2238
|
+
os.environ.get("COLONY_TOOLSMITH", "off"),
|
|
2239
|
+
state_dir / "colony-toolsmith.db")
|
|
2240
|
+
else:
|
|
2241
|
+
logger.info("Toolsmith disabled (COLONY_TOOLSMITH=off)")
|
|
2242
|
+
except Exception as exc:
|
|
2243
|
+
logger.warning("Toolsmith init failed: %s", exc)
|
|
2244
|
+
|
|
2245
|
+
# --- Expectation engine (Mind M3a): predictions + surprise + calibration ---
|
|
2246
|
+
try:
|
|
2247
|
+
from apsimo.self_model.expectations import (
|
|
2248
|
+
ExpectationEngine, ExpectationStore, expectations_enabled,
|
|
2249
|
+
expectations_mode,
|
|
2250
|
+
)
|
|
2251
|
+
from apsimo.api.routers.host import set_expectations
|
|
2252
|
+
if expectations_enabled():
|
|
2253
|
+
_exp_store = ExpectationStore(
|
|
2254
|
+
db_path=str(state_dir / "colony-expectations.db"))
|
|
2255
|
+
_exp_journal = None
|
|
2256
|
+
try:
|
|
2257
|
+
from apsimo.api.routers.host import _self_model as _sm_e
|
|
2258
|
+
_exp_journal = getattr(_sm_e, "journal", None)
|
|
2259
|
+
except Exception:
|
|
2260
|
+
_exp_journal = None
|
|
2261
|
+
# workspace wired later; set_expectations stores the engine and the
|
|
2262
|
+
# autonomy phase links the workspace ref at runtime.
|
|
2263
|
+
_expectations = ExpectationEngine(_exp_store, journal=_exp_journal)
|
|
2264
|
+
set_expectations(_expectations)
|
|
2265
|
+
# World-model prediction classes (relationship-still-active,
|
|
2266
|
+
# property-unchanged) — registered here, guarded on the engine;
|
|
2267
|
+
# the resolvers fetch the world store lazily so boot order and
|
|
2268
|
+
# a missing world model are both safe (they resolve to None).
|
|
2269
|
+
try:
|
|
2270
|
+
from apsimo.world_model.expectation_resolvers import (
|
|
2271
|
+
register_world_resolvers,
|
|
2272
|
+
)
|
|
2273
|
+
register_world_resolvers(_expectations)
|
|
2274
|
+
except Exception as rexc:
|
|
2275
|
+
logger.warning("World expectation resolvers not registered: "
|
|
2276
|
+
"%s", rexc)
|
|
2277
|
+
logger.info("Expectation engine ready (mode=%s, db=%s)",
|
|
2278
|
+
expectations_mode(),
|
|
2279
|
+
state_dir / "colony-expectations.db")
|
|
2280
|
+
else:
|
|
2281
|
+
logger.info("Expectation engine disabled (COLONY_EXPECTATIONS=off)")
|
|
2282
|
+
except Exception as exc:
|
|
2283
|
+
logger.warning("Expectation engine init failed: %s", exc)
|
|
2284
|
+
|
|
2285
|
+
# --- Cognitive workspace (Mind M2): continuity of thought ---
|
|
2286
|
+
try:
|
|
2287
|
+
from apsimo.self_model.workspace import (
|
|
2288
|
+
ConcernStore, WorkspaceEngine, workspace_enabled, workspace_mode,
|
|
2289
|
+
)
|
|
2290
|
+
from apsimo.self_model.thinker import build_thinker
|
|
2291
|
+
from apsimo.self_model.event_concerns import (
|
|
2292
|
+
ConversationTurnConcernReducer,
|
|
2293
|
+
EventConcernReducer,
|
|
2294
|
+
ExternalEventConcernReducer,
|
|
2295
|
+
event_concerns_enabled,
|
|
2296
|
+
event_concern_mode,
|
|
2297
|
+
external_event_concerns_enabled,
|
|
2298
|
+
external_event_concern_mode,
|
|
2299
|
+
turn_concerns_enabled,
|
|
2300
|
+
turn_concern_channels,
|
|
2301
|
+
turn_concern_mode,
|
|
2302
|
+
)
|
|
2303
|
+
from apsimo.api.routers.host import set_workspace
|
|
2304
|
+
if workspace_enabled():
|
|
2305
|
+
_concern_store = ConcernStore(
|
|
2306
|
+
db_path=str(state_dir / "colony-workspace.db"))
|
|
2307
|
+
_thinker = (build_thinker(llm_router, graph=graph)
|
|
2308
|
+
if llm_router is not None else None)
|
|
2309
|
+
_ws_journal = None
|
|
2310
|
+
try:
|
|
2311
|
+
from apsimo.api.routers.host import _self_model as _sm_ws
|
|
2312
|
+
_ws_journal = getattr(_sm_ws, "journal", None)
|
|
2313
|
+
except Exception:
|
|
2314
|
+
_ws_journal = None
|
|
2315
|
+
_workspace = WorkspaceEngine(
|
|
2316
|
+
_concern_store, thinker=_thinker, journal=_ws_journal)
|
|
2317
|
+
if event_concerns_enabled():
|
|
2318
|
+
_workspace.event_reducer = EventConcernReducer(_concern_store)
|
|
2319
|
+
logger.info(
|
|
2320
|
+
"Durable event-to-concern reducer ready (mode=%s, bootstrap=%s)",
|
|
2321
|
+
event_concern_mode(),
|
|
2322
|
+
os.environ.get("COLONY_EVENT_CONCERNS_BOOTSTRAP", "tail"),
|
|
2323
|
+
)
|
|
2324
|
+
if external_event_concerns_enabled():
|
|
2325
|
+
_workspace.external_event_reducer = ExternalEventConcernReducer(
|
|
2326
|
+
_concern_store,
|
|
2327
|
+
)
|
|
2328
|
+
logger.info(
|
|
2329
|
+
"External event-to-concern reducer ready "
|
|
2330
|
+
"(mode=%s, bootstrap=replay)",
|
|
2331
|
+
external_event_concern_mode(),
|
|
2332
|
+
)
|
|
2333
|
+
if turn_concerns_enabled():
|
|
2334
|
+
_workspace.turn_event_reducer = ConversationTurnConcernReducer(
|
|
2335
|
+
_concern_store,
|
|
2336
|
+
)
|
|
2337
|
+
logger.info(
|
|
2338
|
+
"Conversation turn-to-concern reducer ready "
|
|
2339
|
+
"(mode=%s, bootstrap=%s, channels=%s)",
|
|
2340
|
+
turn_concern_mode(),
|
|
2341
|
+
os.environ.get("COLONY_TURN_CONCERNS_BOOTSTRAP", "tail"),
|
|
2342
|
+
",".join(turn_concern_channels()) or "<none>",
|
|
2343
|
+
)
|
|
2344
|
+
set_workspace(_workspace)
|
|
2345
|
+
logger.info("Cognitive workspace ready (mode=%s, db=%s)",
|
|
2346
|
+
workspace_mode(), state_dir / "colony-workspace.db")
|
|
2347
|
+
else:
|
|
2348
|
+
logger.info("Cognitive workspace disabled (COLONY_WORKSPACE=off)")
|
|
2349
|
+
if external_event_concerns_enabled():
|
|
2350
|
+
logger.warning(
|
|
2351
|
+
"External event concerns requested but workspace is disabled"
|
|
2352
|
+
)
|
|
2353
|
+
if turn_concerns_enabled():
|
|
2354
|
+
logger.warning(
|
|
2355
|
+
"Conversation turn concerns requested but workspace is disabled"
|
|
2356
|
+
)
|
|
2357
|
+
except Exception as exc:
|
|
2358
|
+
logger.warning("Workspace init failed: %s", exc)
|
|
2359
|
+
|
|
2360
|
+
# --- Skills memory (procedure memory, item 3) ---
|
|
2361
|
+
_skills_mem_store = None
|
|
2362
|
+
try:
|
|
2363
|
+
from apsimo.skills_memory import SkillStore, skills_distill_mode
|
|
2364
|
+
from apsimo.api.routers.host import set_skill_store
|
|
2365
|
+
_skills_mem_store = SkillStore(
|
|
2366
|
+
db_path=str(state_dir / "colony-skills.db"))
|
|
2367
|
+
set_skill_store(_skills_mem_store)
|
|
2368
|
+
logger.info("SkillStore initialized (db=%s, %d skill(s), distill=%s)",
|
|
2369
|
+
state_dir / "colony-skills.db",
|
|
2370
|
+
_skills_mem_store.count(), skills_distill_mode())
|
|
2371
|
+
except Exception as exc:
|
|
2372
|
+
logger.warning("SkillStore init failed: %s", exc)
|
|
2373
|
+
|
|
2374
|
+
# --- Mining: escalation miner + verbatim turn capture (corpus source) ---
|
|
2375
|
+
try:
|
|
2376
|
+
from apsimo.mining import EscalationMiner, MiningStore, mining_mode
|
|
2377
|
+
from apsimo.api.routers.mining import set_mining
|
|
2378
|
+
|
|
2379
|
+
if mining_mode() != "off":
|
|
2380
|
+
_mining_store_obj = MiningStore(
|
|
2381
|
+
db_path=str(state_dir / "colony-mining.db"))
|
|
2382
|
+
|
|
2383
|
+
def _mining_router_getter():
|
|
2384
|
+
try:
|
|
2385
|
+
from apsimo.api.routers.host import _reasoning_loop
|
|
2386
|
+
return getattr(_reasoning_loop, "_model", None)
|
|
2387
|
+
except Exception:
|
|
2388
|
+
return None
|
|
2389
|
+
|
|
2390
|
+
_mining_engine_obj = EscalationMiner(
|
|
2391
|
+
_mining_store_obj,
|
|
2392
|
+
skill_store=_skills_mem_store,
|
|
2393
|
+
router_getter=_mining_router_getter,
|
|
2394
|
+
)
|
|
2395
|
+
set_mining(_mining_store_obj, _mining_engine_obj, state_dir)
|
|
2396
|
+
logger.info("EscalationMiner initialized (db=%s, mode=%s)",
|
|
2397
|
+
state_dir / "colony-mining.db", mining_mode())
|
|
2398
|
+
else:
|
|
2399
|
+
logger.info("Mining disabled (COLONY_ESCALATION_MINING=off)")
|
|
2400
|
+
except Exception as exc:
|
|
2401
|
+
logger.warning("Mining init failed: %s", exc)
|
|
2402
|
+
|
|
2403
|
+
# --- Read-only repo mirrors + directed action (option A) ---
|
|
2404
|
+
try:
|
|
2405
|
+
from apsimo.repos import RepoMirrorManager
|
|
2406
|
+
from apsimo.directed import (
|
|
2407
|
+
DirectedActionService, ScopedTaskStore, directed_mode,
|
|
2408
|
+
)
|
|
2409
|
+
from apsimo.api.routers.host import (
|
|
2410
|
+
set_repo_mirrors, set_directed_service,
|
|
2411
|
+
get_directive_manager as _get_dm2,
|
|
2412
|
+
)
|
|
2413
|
+
_mirrors_mgr = RepoMirrorManager(
|
|
2414
|
+
mirror_dir=str(state_dir / "repo-mirrors"),
|
|
2415
|
+
directive_manager=_get_dm2(),
|
|
2416
|
+
)
|
|
2417
|
+
set_repo_mirrors(_mirrors_mgr)
|
|
2418
|
+
_n_repos = len(_mirrors_mgr.configured())
|
|
2419
|
+
if _n_repos:
|
|
2420
|
+
# Clone/pull in the background so boot is not blocked by network.
|
|
2421
|
+
async def _sync_mirrors():
|
|
2422
|
+
try:
|
|
2423
|
+
loop_ = asyncio.get_event_loop()
|
|
2424
|
+
results = await loop_.run_in_executor(None, _mirrors_mgr.refresh_all)
|
|
2425
|
+
logger.info(
|
|
2426
|
+
"Repo mirrors synced: %s",
|
|
2427
|
+
{k: v.get("action") or v.get("reason") for k, v in results.items()},
|
|
2428
|
+
)
|
|
2429
|
+
from apsimo.api.routers.host import _world_store as _ws
|
|
2430
|
+
n = await _mirrors_mgr.register_entities(_ws)
|
|
2431
|
+
if n:
|
|
2432
|
+
logger.info("Registered %d repo(s) as Project entities", n)
|
|
2433
|
+
except Exception:
|
|
2434
|
+
logger.debug("mirror sync failed", exc_info=True)
|
|
2435
|
+
asyncio.create_task(_sync_mirrors())
|
|
2436
|
+
logger.info("RepoMirrorManager initialized (%d repo(s) configured)", _n_repos)
|
|
2437
|
+
|
|
2438
|
+
async def _directed_deliver(payload: dict) -> bool:
|
|
2439
|
+
# Late-bound: route through the autonomy loop's guarded delivery
|
|
2440
|
+
# (boundary + sanitize + rate + shadow), same as every reach-out.
|
|
2441
|
+
try:
|
|
2442
|
+
from apsimo.api.routers.host import _autonomy_loop, _delivery_bridge
|
|
2443
|
+
if _autonomy_loop is not None and _delivery_bridge is not None:
|
|
2444
|
+
return await _autonomy_loop._route_reachout_delivery(
|
|
2445
|
+
payload, _delivery_bridge)
|
|
2446
|
+
except Exception:
|
|
2447
|
+
logger.debug("directed deliver failed", exc_info=True)
|
|
2448
|
+
return False
|
|
2449
|
+
|
|
2450
|
+
from apsimo.api.routers.host import _feedback_store as _fb_store
|
|
2451
|
+
_directed_svc = DirectedActionService(
|
|
2452
|
+
store=ScopedTaskStore(db_path=str(state_dir / "colony-directed.db")),
|
|
2453
|
+
directive_manager=_get_dm2(),
|
|
2454
|
+
mirrors=_mirrors_mgr,
|
|
2455
|
+
feedback_store=_fb_store,
|
|
2456
|
+
delivery_router=_directed_deliver,
|
|
2457
|
+
self_model=_sm_for_directed,
|
|
2458
|
+
)
|
|
2459
|
+
set_directed_service(_directed_svc)
|
|
2460
|
+
logger.info("DirectedActionService initialized (mode=%s)", directed_mode())
|
|
2461
|
+
|
|
2462
|
+
# The once-per-boundary critical flag rides the same guarded delivery.
|
|
2463
|
+
_dm_for_flags = _get_dm2()
|
|
2464
|
+
if _dm_for_flags is not None:
|
|
2465
|
+
_dm_for_flags.set_delivery_router(_directed_deliver)
|
|
2466
|
+
except Exception as exc:
|
|
2467
|
+
logger.warning("Directed-action init failed: %s", exc)
|
|
2468
|
+
|
|
2469
|
+
# --- Pattern Extraction + Surprise ---
|
|
2470
|
+
try:
|
|
2471
|
+
from apsimo.patterns.store import PatternStore
|
|
2472
|
+
from apsimo.surprise.store import SurpriseStore
|
|
2473
|
+
|
|
2474
|
+
patterns_db = state_dir / "colony-patterns.db"
|
|
2475
|
+
pattern_store = PatternStore(db_path=patterns_db)
|
|
2476
|
+
set_pattern_store(pattern_store)
|
|
2477
|
+
logger.info("PatternStore initialized (db=%s)", patterns_db)
|
|
2478
|
+
|
|
2479
|
+
surprise_db = state_dir / "colony-surprise.db"
|
|
2480
|
+
surprise_store = SurpriseStore(db_path=surprise_db)
|
|
2481
|
+
set_surprise_store(surprise_store)
|
|
2482
|
+
logger.info("SurpriseStore initialized (db=%s)", surprise_db)
|
|
2483
|
+
|
|
2484
|
+
# Close the surprise loop: the condition worker's
|
|
2485
|
+
# surprise.accumulation event now lands as a workspace concern
|
|
2486
|
+
# (the consumer checks workspace enablement at event time, so this
|
|
2487
|
+
# registration is unconditional and a clean no-op when it is off).
|
|
2488
|
+
from apsimo.surprise.accumulation import (
|
|
2489
|
+
register as register_surprise_consumer,
|
|
2490
|
+
)
|
|
2491
|
+
register_surprise_consumer()
|
|
2492
|
+
except Exception as exc:
|
|
2493
|
+
logger.warning("Pattern/Surprise init failed: %s", exc)
|
|
2494
|
+
|
|
2495
|
+
# --- ToM LLM Extractor ---
|
|
2496
|
+
try:
|
|
2497
|
+
if llm_router is not None:
|
|
2498
|
+
from apsimo.tom.extractor import TomExtractor
|
|
2499
|
+
tom_extractor = TomExtractor(llm_router)
|
|
2500
|
+
set_tom_extractor(tom_extractor)
|
|
2501
|
+
logger.info("ToM LLM Extractor initialized (router=%s)", type(llm_router).__name__)
|
|
2502
|
+
else:
|
|
2503
|
+
logger.info("ToM LLM Extractor skipped — no LLM router")
|
|
2504
|
+
except Exception as exc:
|
|
2505
|
+
logger.warning("ToM Extractor init failed: %s", exc)
|
|
2506
|
+
|
|
2507
|
+
# --- 7e. Channel Registration Store ---
|
|
2508
|
+
channel_store = None
|
|
2509
|
+
try:
|
|
2510
|
+
from apsimo.channels.store import ChannelStore
|
|
2511
|
+
from apsimo.channels.router import set_channel_store
|
|
2512
|
+
from apsimo.channels.phone_gateways import set_channel_store_ref
|
|
2513
|
+
|
|
2514
|
+
channels_db = os.path.join(state_dir, "colony-channels.db")
|
|
2515
|
+
channel_store = ChannelStore(db_path=channels_db)
|
|
2516
|
+
channel_store.connect()
|
|
2517
|
+
set_channel_store(channel_store)
|
|
2518
|
+
set_channel_store_ref(channel_store)
|
|
2519
|
+
from apsimo.api.routers.host import set_channel_store as _host_set_channel_store
|
|
2520
|
+
_host_set_channel_store(channel_store) # turn traffic auto-registers + touches channels
|
|
2521
|
+
logger.info("ChannelStore initialized (db=%s)", channels_db)
|
|
2522
|
+
except Exception as exc:
|
|
2523
|
+
logger.warning("ChannelStore init failed: %s", exc)
|
|
2524
|
+
|
|
2525
|
+
# --- 8. Contacts ---
|
|
2526
|
+
contacts_store = None
|
|
2527
|
+
try:
|
|
2528
|
+
from apsimo.contacts.config import ContactsConfig
|
|
2529
|
+
from apsimo.contacts.store import SQLiteContactStore
|
|
2530
|
+
contacts_config = ContactsConfig.from_env()
|
|
2531
|
+
contacts_store = SQLiteContactStore(config=contacts_config, graph=graph)
|
|
2532
|
+
await contacts_store.connect()
|
|
2533
|
+
set_contacts_store(contacts_store)
|
|
2534
|
+
logger.info("ContactsStore initialized (path=%s)", contacts_config.sqlite_path)
|
|
2535
|
+
except Exception as exc:
|
|
2536
|
+
logger.warning("ContactsStore init failed: %s", exc)
|
|
2537
|
+
|
|
2538
|
+
# --- 8b. Contact-World Model Bridge ---
|
|
2539
|
+
if contacts_store is not None and graph is not None:
|
|
2540
|
+
try:
|
|
2541
|
+
from apsimo.contacts.world_bridge import WorldModelContactBridge
|
|
2542
|
+
bridge = WorldModelContactBridge(graph=graph, store=contacts_store)
|
|
2543
|
+
# Backfill all substantive Person nodes on startup
|
|
2544
|
+
backfill_stats = await bridge.backfill_all_people()
|
|
2545
|
+
logger.info(
|
|
2546
|
+
"WorldModelContactBridge initialized — backfill created=%d linked=%d skipped=%d",
|
|
2547
|
+
backfill_stats["created"], backfill_stats["linked"], backfill_stats["skipped"],
|
|
2548
|
+
)
|
|
2549
|
+
# Prune shadow contacts whose Person node no longer exists
|
|
2550
|
+
pruned = await bridge.prune_orphaned_shadows()
|
|
2551
|
+
if pruned:
|
|
2552
|
+
logger.info("Pruned %d orphaned shadow contacts", pruned)
|
|
2553
|
+
except Exception as exc:
|
|
2554
|
+
logger.warning("WorldModelContactBridge init failed: %s", exc)
|
|
2555
|
+
else:
|
|
2556
|
+
logger.info("WorldModelContactBridge skipped — contacts_store or graph unavailable")
|
|
2557
|
+
|
|
2558
|
+
# --- 8d. Relationship profiler (standing + psyche + approach briefs) ---
|
|
2559
|
+
if contacts_store is not None:
|
|
2560
|
+
try:
|
|
2561
|
+
from apsimo.intelligence.relationships.profiler import (
|
|
2562
|
+
RelationshipProfiler,
|
|
2563
|
+
)
|
|
2564
|
+
import apsimo.api.routers.host as _host_mod
|
|
2565
|
+
_rel_profiler = RelationshipProfiler(
|
|
2566
|
+
contacts_store=contacts_store,
|
|
2567
|
+
comms_log=_host_mod._comms_log,
|
|
2568
|
+
affect_store=_host_mod._affect_store,
|
|
2569
|
+
facts_store=_host_mod._facts_store,
|
|
2570
|
+
engagement_store=_host_mod._engagement_store,
|
|
2571
|
+
p8_runtime=_p8_wiring,
|
|
2572
|
+
db_path=str(state_dir / "colony-relationships.db"),
|
|
2573
|
+
)
|
|
2574
|
+
_host_mod.set_relationship_profiler(_rel_profiler)
|
|
2575
|
+
logger.info("RelationshipProfiler initialized (db=%s)",
|
|
2576
|
+
state_dir / "colony-relationships.db")
|
|
2577
|
+
except Exception as exc:
|
|
2578
|
+
logger.warning("RelationshipProfiler init failed: %s", exc)
|
|
2579
|
+
|
|
2580
|
+
# --- 9. Briefings ---
|
|
2581
|
+
try:
|
|
2582
|
+
from apsimo.briefings.engine import BriefingEngine
|
|
2583
|
+
briefings = BriefingEngine()
|
|
2584
|
+
set_briefings_engine(briefings)
|
|
2585
|
+
logger.info("BriefingEngine initialized")
|
|
2586
|
+
except Exception as exc:
|
|
2587
|
+
logger.warning("BriefingEngine init failed: %s", exc)
|
|
2588
|
+
|
|
2589
|
+
# --- 10. World model ---
|
|
2590
|
+
world_store = None
|
|
2591
|
+
try:
|
|
2592
|
+
from apsimo.world_model.store import WorldModelStore
|
|
2593
|
+
from apsimo.world_model.config import WorldModelConfig
|
|
2594
|
+
_wm_backend = os.environ.get("WORLD_MODEL_BACKEND", "sqlite")
|
|
2595
|
+
world_store = WorldModelStore(WorldModelConfig(backend=_wm_backend))
|
|
2596
|
+
await world_store.connect()
|
|
2597
|
+
set_world_store(world_store)
|
|
2598
|
+
logger.info("WorldModelStore initialized and connected (backend=%s)", _wm_backend)
|
|
2599
|
+
|
|
2600
|
+
# World-model population from conversation (shadow-first). Boundary-checked
|
|
2601
|
+
# via the directive manager. Mode from COLONY_WORLD_POPULATE_MODE
|
|
2602
|
+
# (off|shadow|live, default shadow).
|
|
2603
|
+
try:
|
|
2604
|
+
from apsimo.world_model.populator import WorldModelPopulator, populate_mode
|
|
2605
|
+
from apsimo.api.routers.host import (
|
|
2606
|
+
get_directive_manager as _get_dm, set_world_populator,
|
|
2607
|
+
)
|
|
2608
|
+
_populator = WorldModelPopulator(world_store, directive_manager=_get_dm())
|
|
2609
|
+
set_world_populator(_populator)
|
|
2610
|
+
logger.info("WorldModelPopulator initialized (mode=%s)", populate_mode())
|
|
2611
|
+
except Exception as pexc:
|
|
2612
|
+
logger.warning("WorldModelPopulator init failed: %s", pexc)
|
|
2613
|
+
|
|
2614
|
+
# LLM-assisted world-model extraction (batch, journaled; daily phase).
|
|
2615
|
+
try:
|
|
2616
|
+
from apsimo.world_model.llm_extract import (
|
|
2617
|
+
WorldLLMExtractor, llm_extract_mode,
|
|
2618
|
+
)
|
|
2619
|
+
from apsimo.api.routers.host import (
|
|
2620
|
+
get_directive_manager as _get_dm3, set_world_llm_extractor, get_llm_router,
|
|
2621
|
+
)
|
|
2622
|
+
_wle = WorldLLMExtractor(
|
|
2623
|
+
world_store, graph=graph, directive_manager=_get_dm3(),
|
|
2624
|
+
journal=getattr(_sm_for_directed, "journal", None),
|
|
2625
|
+
self_model=_sm_for_directed,
|
|
2626
|
+
source_ledger=get_turn_idempotency_ledger(state_dir), router_provider=get_llm_router)
|
|
2627
|
+
set_world_llm_extractor(_wle)
|
|
2628
|
+
logger.info("WorldLLMExtractor initialized (mode=%s)",
|
|
2629
|
+
llm_extract_mode())
|
|
2630
|
+
except Exception as wexc:
|
|
2631
|
+
logger.warning("WorldLLMExtractor init failed: %s", wexc)
|
|
2632
|
+
|
|
2633
|
+
# Belief maintenance (item 7): contradiction detection, resolution,
|
|
2634
|
+
# stale decay + the inline property-supersession audit hook.
|
|
2635
|
+
try:
|
|
2636
|
+
from apsimo.beliefs import BeliefEngine, BeliefStore, beliefs_mode
|
|
2637
|
+
from apsimo.api.routers.host import set_belief_engine
|
|
2638
|
+
from apsimo.world_model.store import set_property_audit_hook
|
|
2639
|
+
_belief_store = BeliefStore(
|
|
2640
|
+
db_path=str(state_dir / "colony-beliefs.db"))
|
|
2641
|
+
_belief_eng = BeliefEngine(
|
|
2642
|
+
_belief_store, world_store=world_store, graph=graph,
|
|
2643
|
+
initiative_store=None, # attached below once wired
|
|
2644
|
+
journal=getattr(_sm_for_directed, "journal", None),
|
|
2645
|
+
self_model=_sm_for_directed)
|
|
2646
|
+
set_belief_engine(_belief_eng)
|
|
2647
|
+
set_property_audit_hook(_belief_eng.note_property_update)
|
|
2648
|
+
logger.info("BeliefEngine initialized (db=%s, mode=%s)",
|
|
2649
|
+
state_dir / "colony-beliefs.db", beliefs_mode())
|
|
2650
|
+
except Exception as bexc:
|
|
2651
|
+
logger.warning("BeliefEngine init failed: %s", bexc)
|
|
2652
|
+
|
|
2653
|
+
# Wire extraction pipeline
|
|
2654
|
+
try:
|
|
2655
|
+
from apsimo.world_model.extraction.pipeline import ExtractionPipeline
|
|
2656
|
+
from apsimo.world_model.extraction.formats import (
|
|
2657
|
+
TextExtractor, JSONExtractor, CSVExtractor,
|
|
2658
|
+
PDFExtractor, HTMLExtractor,
|
|
2659
|
+
)
|
|
2660
|
+
extractors = [TextExtractor(), JSONExtractor(), CSVExtractor()]
|
|
2661
|
+
if PDFExtractor:
|
|
2662
|
+
extractors.append(PDFExtractor())
|
|
2663
|
+
if HTMLExtractor:
|
|
2664
|
+
extractors.append(HTMLExtractor())
|
|
2665
|
+
llm_extract_fn = None
|
|
2666
|
+
if llm_router is not None:
|
|
2667
|
+
try:
|
|
2668
|
+
from apsimo.world_model.extraction.llm_extractor import (
|
|
2669
|
+
build_llm_extract_fn,
|
|
2670
|
+
)
|
|
2671
|
+
llm_extract_fn = build_llm_extract_fn(llm_router)
|
|
2672
|
+
except Exception as llm_exc:
|
|
2673
|
+
logger.warning("LLM extraction fallback disabled: %s", llm_exc)
|
|
2674
|
+
|
|
2675
|
+
pipeline = ExtractionPipeline(
|
|
2676
|
+
extractors=extractors,
|
|
2677
|
+
llm_extract_fn=llm_extract_fn,
|
|
2678
|
+
)
|
|
2679
|
+
set_extraction_pipeline(pipeline)
|
|
2680
|
+
logger.info(
|
|
2681
|
+
"Extraction pipeline initialized (%d format extractors, llm_fallback=%s)",
|
|
2682
|
+
len(extractors),
|
|
2683
|
+
"on" if llm_extract_fn is not None else "off",
|
|
2684
|
+
)
|
|
2685
|
+
except Exception as eexc:
|
|
2686
|
+
logger.warning("Extraction pipeline init skipped: %s", eexc)
|
|
2687
|
+
except Exception as exc:
|
|
2688
|
+
logger.warning("WorldModelStore init failed: %s", exc)
|
|
2689
|
+
# Try without connect() — some operations work without it
|
|
2690
|
+
try:
|
|
2691
|
+
world_store = WorldModelStore(WorldModelConfig(backend=_wm_backend))
|
|
2692
|
+
set_world_store(world_store)
|
|
2693
|
+
logger.info("WorldModelStore initialized (without connect)")
|
|
2694
|
+
except Exception as exc2:
|
|
2695
|
+
logger.error("WorldModelStore fallback init also failed: %s", exc2)
|
|
2696
|
+
|
|
2697
|
+
# --- 11. Cognition (CognitionPipeline) ---
|
|
2698
|
+
cognition_pipeline = None
|
|
2699
|
+
try:
|
|
2700
|
+
from apsimo.intelligence.cognition.registry import CognitionPipeline
|
|
2701
|
+
from apsimo.events.bus import EventBus
|
|
2702
|
+
|
|
2703
|
+
if graph is not None:
|
|
2704
|
+
# Create EventBus for real-time metrics
|
|
2705
|
+
event_bus = EventBus()
|
|
2706
|
+
|
|
2707
|
+
cognition_pipeline = CognitionPipeline(
|
|
2708
|
+
graph=graph,
|
|
2709
|
+
event_bus=event_bus,
|
|
2710
|
+
params=_adaptive_params,
|
|
2711
|
+
)
|
|
2712
|
+
_wire_controlled_learning_pipeline(
|
|
2713
|
+
cognition_pipeline, _controlled_learning)
|
|
2714
|
+
set_metalearner(cognition_pipeline.meta_learner)
|
|
2715
|
+
logger.info(
|
|
2716
|
+
"CognitionPipeline initialized (controlled proposals=%s, "
|
|
2717
|
+
"durable corrections=%s)",
|
|
2718
|
+
_controlled_learning.get("experiments") is not None,
|
|
2719
|
+
_controlled_learning.get("corrections") is not None,
|
|
2720
|
+
)
|
|
2721
|
+
else:
|
|
2722
|
+
logger.warning("CognitionPipeline skipped — ColonyGraph not available")
|
|
2723
|
+
except Exception as exc:
|
|
2724
|
+
logger.warning("CognitionPipeline init failed: %s", exc, exc_info=True)
|
|
2725
|
+
|
|
2726
|
+
# --- 12. Research pipeline ---
|
|
2727
|
+
try:
|
|
2728
|
+
from apsimo.research.search.orchestrator import SearchOrchestrator
|
|
2729
|
+
|
|
2730
|
+
# Wire search orchestrator
|
|
2731
|
+
search_orchestrator = SearchOrchestrator()
|
|
2732
|
+
search_provider = os.environ.get("COLONY_SEARCH_PROVIDER", "")
|
|
2733
|
+
if search_provider == "tavily" and os.environ.get("TAVILY_API_KEY"):
|
|
2734
|
+
from apsimo.research.search.tavily import TavilyProvider
|
|
2735
|
+
search_orchestrator.add_provider(TavilyProvider(os.environ["TAVILY_API_KEY"]))
|
|
2736
|
+
logger.info("Search provider: Tavily")
|
|
2737
|
+
elif search_provider == "serpapi" and os.environ.get("SERPAPI_KEY"):
|
|
2738
|
+
from apsimo.research.search.serpapi import SerpAPIProvider
|
|
2739
|
+
search_orchestrator.add_provider(SerpAPIProvider(os.environ["SERPAPI_KEY"]))
|
|
2740
|
+
logger.info("Search provider: SerpAPI")
|
|
2741
|
+
elif search_provider == "brave" and os.environ.get("BRAVE_API_KEY"):
|
|
2742
|
+
from apsimo.research.search.brave import BraveSearchProvider
|
|
2743
|
+
search_orchestrator.add_provider(BraveSearchProvider(os.environ["BRAVE_API_KEY"]))
|
|
2744
|
+
logger.info("Search provider: Brave")
|
|
2745
|
+
else:
|
|
2746
|
+
# Zero-config fallback so web_search works out of the box.
|
|
2747
|
+
from apsimo.research.search.duckduckgo import DuckDuckGoProvider
|
|
2748
|
+
search_orchestrator.add_provider(DuckDuckGoProvider())
|
|
2749
|
+
logger.info("Search provider: DuckDuckGo (default fallback)")
|
|
2750
|
+
|
|
2751
|
+
set_search_orchestrator(search_orchestrator)
|
|
2752
|
+
|
|
2753
|
+
# Register native tools with the ToolExecutor
|
|
2754
|
+
try:
|
|
2755
|
+
import apsimo.api.routers.host as _host_router
|
|
2756
|
+
te = _host_router._tool_executor
|
|
2757
|
+
except Exception:
|
|
2758
|
+
te = None
|
|
2759
|
+
if te is not None:
|
|
2760
|
+
sandbox_dir = os.environ.get("COLONY_SANDBOX_DIR", str(state_dir / "sandbox"))
|
|
2761
|
+
# Ensure the sandbox directory exists so file_ops don't fail on first call.
|
|
2762
|
+
Path(sandbox_dir).mkdir(parents=True, exist_ok=True)
|
|
2763
|
+
te.register_native_tools(
|
|
2764
|
+
search_orchestrator=search_orchestrator,
|
|
2765
|
+
sandbox_dir=sandbox_dir,
|
|
2766
|
+
)
|
|
2767
|
+
logger.info(
|
|
2768
|
+
"Native tools registered (calculate, web_search, file_ops; sandbox=%s)",
|
|
2769
|
+
sandbox_dir,
|
|
2770
|
+
)
|
|
2771
|
+
|
|
2772
|
+
research = _build_research_pipeline(
|
|
2773
|
+
graph=graph, p8_runtime=_p8_wiring)
|
|
2774
|
+
set_research_pipeline(research)
|
|
2775
|
+
logger.info("ResearchPipeline initialized")
|
|
2776
|
+
except Exception as exc:
|
|
2777
|
+
logger.warning("ResearchPipeline init failed: %s", exc, exc_info=True)
|
|
2778
|
+
|
|
2779
|
+
# --- 13. Delivery bridge ---
|
|
2780
|
+
try:
|
|
2781
|
+
from apsimo.delivery.bridge import ProactiveDeliveryBridge
|
|
2782
|
+
from apsimo.delivery.channels import ChannelRegistry
|
|
2783
|
+
channel_registry = ChannelRegistry.load(contacts_store=contacts_store, channel_store=channel_store)
|
|
2784
|
+
delivery = ProactiveDeliveryBridge(channel_registry=channel_registry)
|
|
2785
|
+
set_delivery_bridge(delivery)
|
|
2786
|
+
logger.info("Delivery bridge initialized")
|
|
2787
|
+
|
|
2788
|
+
# Adaptive daily cap (Amendment 1.6): the trust engine can EARN the
|
|
2789
|
+
# per-recipient cap upward with a proven delivery track record.
|
|
2790
|
+
try:
|
|
2791
|
+
_trust_for_cap = getattr(_sm_for_directed, "trust", None)
|
|
2792
|
+
if (_trust_for_cap is not None
|
|
2793
|
+
and getattr(delivery, "_rate_limiter", None) is not None):
|
|
2794
|
+
delivery._rate_limiter._cap_provider = _trust_for_cap.delivery_cap
|
|
2795
|
+
logger.info("Delivery rate cap wired to trust engine "
|
|
2796
|
+
"(adaptive, max=%s)",
|
|
2797
|
+
os.environ.get("COLONY_TRUST_DELIVERY_CAP_MAX", "6"))
|
|
2798
|
+
except Exception:
|
|
2799
|
+
logger.debug("adaptive cap wiring failed", exc_info=True)
|
|
2800
|
+
|
|
2801
|
+
# --- 13b. Briefings: full wiring (delivery + persistence + schedule) ---
|
|
2802
|
+
# The bare engine from section 9 can compose briefings but has no gateway, no
|
|
2803
|
+
# persistent store, and no scheduler -- proactive output never reaches anyone.
|
|
2804
|
+
# Rebuild it here, now that the delivery bridge exists: the bridge-backed
|
|
2805
|
+
# gateway auto-registers when a home channel is configured, the store persists
|
|
2806
|
+
# under COLONY_STATE_DIR, and the scheduler fires daily/weekly briefings and
|
|
2807
|
+
# drains pending deliveries. All deployment specifics come from env.
|
|
2808
|
+
try:
|
|
2809
|
+
from pathlib import Path as _P
|
|
2810
|
+
|
|
2811
|
+
from apsimo.briefings.config import BriefingConfig
|
|
2812
|
+
from apsimo.briefings.engine import BriefingEngine
|
|
2813
|
+
from apsimo.briefings.scheduler import BriefingScheduler
|
|
2814
|
+
from apsimo.briefings.store import BriefingStore
|
|
2815
|
+
|
|
2816
|
+
b_cfg = BriefingConfig()
|
|
2817
|
+
b_cfg.daily.time = os.environ.get("COLONY_BRIEFING_DAILY_TIME", b_cfg.daily.time)
|
|
2818
|
+
b_cfg.daily.timezone = os.environ.get("COLONY_BRIEFING_TZ", b_cfg.daily.timezone)
|
|
2819
|
+
b_cfg.weekly.timezone = b_cfg.daily.timezone
|
|
2820
|
+
b_cfg.delivery_gateway = os.environ.get("COLONY_BRIEFING_GATEWAY", "whatsapp")
|
|
2821
|
+
b_cfg.lm_enhancement_enabled = (
|
|
2822
|
+
os.environ.get("COLONY_BRIEFING_LM_ENHANCE", "0") not in ("0", "false", "no")
|
|
2823
|
+
)
|
|
2824
|
+
_b_state_dir = os.environ.get("COLONY_STATE_DIR", ".")
|
|
2825
|
+
b_store = BriefingStore(db_path=str(_P(_b_state_dir) / "briefings.db"))
|
|
2826
|
+
# Real aggregators where the backing subsystem exists — without
|
|
2827
|
+
# them the composer silently falls back to stubs and every data
|
|
2828
|
+
# section of every briefing is empty. Calendar/anomaly/mind/
|
|
2829
|
+
# synthesis still lack concrete aggregators (see docs/KNOWN-GAPS.md).
|
|
2830
|
+
_aggs = {}
|
|
2831
|
+
try:
|
|
2832
|
+
if graph is not None:
|
|
2833
|
+
from apsimo.briefings.aggregators import RelationshipAggregator
|
|
2834
|
+
_aggs["relationship_aggregator"] = RelationshipAggregator(
|
|
2835
|
+
scorer=None, graph=graph)
|
|
2836
|
+
except Exception:
|
|
2837
|
+
logger.debug("relationship aggregator wiring failed", exc_info=True)
|
|
2838
|
+
try:
|
|
2839
|
+
if goals_engine is not None:
|
|
2840
|
+
from apsimo.briefings.aggregators import GoalEngineAggregator
|
|
2841
|
+
_aggs["goal_aggregator"] = GoalEngineAggregator(goals_engine)
|
|
2842
|
+
except Exception:
|
|
2843
|
+
logger.debug("goal aggregator wiring failed", exc_info=True)
|
|
2844
|
+
try:
|
|
2845
|
+
# Both resolve their subsystems lazily off host globals at
|
|
2846
|
+
# call time (the anomaly detector doesn't even exist until
|
|
2847
|
+
# the autonomy registry builds it, well after this point).
|
|
2848
|
+
from apsimo.briefings.aggregators import (
|
|
2849
|
+
AnomalyDetectorAggregator, DiscovererSynthesisAggregator)
|
|
2850
|
+
_aggs["anomaly_aggregator"] = AnomalyDetectorAggregator()
|
|
2851
|
+
_aggs["synthesis_aggregator"] = DiscovererSynthesisAggregator()
|
|
2852
|
+
except Exception:
|
|
2853
|
+
logger.debug("anomaly/synthesis aggregator wiring failed", exc_info=True)
|
|
2854
|
+
try:
|
|
2855
|
+
# resolves the enabled calendar connector instance(s) — base
|
|
2856
|
+
# or per-account — at call time; harmless when none enabled
|
|
2857
|
+
from apsimo.briefings.aggregators import ConnectorCalendarAggregator
|
|
2858
|
+
_aggs["calendar_aggregator"] = ConnectorCalendarAggregator()
|
|
2859
|
+
except Exception:
|
|
2860
|
+
logger.debug("calendar aggregator wiring failed", exc_info=True)
|
|
2861
|
+
briefings = BriefingEngine(config=b_cfg, store=b_store,
|
|
2862
|
+
delivery_bridge=delivery, **_aggs)
|
|
2863
|
+
set_briefings_engine(briefings)
|
|
2864
|
+
if os.environ.get("COLONY_BRIEFINGS_SCHEDULE", "1") not in ("0", "false", "no"):
|
|
2865
|
+
b_sched = BriefingScheduler(config=b_cfg, engine=briefings, store=b_store)
|
|
2866
|
+
briefings.attach_scheduler(b_sched)
|
|
2867
|
+
b_sched.start()
|
|
2868
|
+
logger.info(
|
|
2869
|
+
"BriefingEngine rewired: gateway=%s daily=%s %s scheduler=on",
|
|
2870
|
+
b_cfg.delivery_gateway, b_cfg.daily.time, b_cfg.daily.timezone,
|
|
2871
|
+
)
|
|
2872
|
+
except Exception as exc:
|
|
2873
|
+
logger.warning("Briefing delivery wiring failed: %s", exc)
|
|
2874
|
+
except Exception as exc:
|
|
2875
|
+
logger.warning("ProactiveDeliveryBridge init failed: %s", exc)
|
|
2876
|
+
|
|
2877
|
+
# --- 14. Synthesis (ConnectionDiscoverer) ---
|
|
2878
|
+
try:
|
|
2879
|
+
from apsimo.intelligence.synthesis.connection_discoverer import ConnectionDiscoverer
|
|
2880
|
+
if graph is not None:
|
|
2881
|
+
discoverer = ConnectionDiscoverer(graph_client=graph)
|
|
2882
|
+
set_connection_discoverer(discoverer)
|
|
2883
|
+
logger.info("ConnectionDiscoverer initialized")
|
|
2884
|
+
else:
|
|
2885
|
+
logger.warning("ConnectionDiscoverer skipped — ColonyGraph not available")
|
|
2886
|
+
except Exception as exc:
|
|
2887
|
+
logger.warning("ConnectionDiscoverer init failed: %s", exc)
|
|
2888
|
+
|
|
2889
|
+
# Insight overlay store (tracks dismissed-insight IDs).
|
|
2890
|
+
try:
|
|
2891
|
+
from apsimo.intelligence.synthesis.insight_store import InsightStore
|
|
2892
|
+
insight_store = InsightStore(state_dir / "insights.db")
|
|
2893
|
+
set_insight_store(insight_store)
|
|
2894
|
+
logger.info("InsightStore initialized")
|
|
2895
|
+
except Exception as exc:
|
|
2896
|
+
logger.warning("InsightStore init failed: %s", exc)
|
|
2897
|
+
|
|
2898
|
+
# --- 15. Continuous learner ---
|
|
2899
|
+
try:
|
|
2900
|
+
from apsimo.intelligence.learning.continuous_learner import ContinuousLearner
|
|
2901
|
+
learner = ContinuousLearner()
|
|
2902
|
+
set_learner(learner)
|
|
2903
|
+
logger.info("ContinuousLearner initialized")
|
|
2904
|
+
except Exception as exc:
|
|
2905
|
+
logger.warning("ContinuousLearner init failed: %s", exc)
|
|
2906
|
+
|
|
2907
|
+
# --- 16. Skills registry + executor ---
|
|
2908
|
+
skills_registry = None
|
|
2909
|
+
try:
|
|
2910
|
+
from apsimo.skills.registry import SkillRegistry
|
|
2911
|
+
skills_registry = SkillRegistry()
|
|
2912
|
+
set_skills_registry(skills_registry)
|
|
2913
|
+
logger.info("SkillRegistry initialized (%d skills)", len(skills_registry.list_skills()))
|
|
2914
|
+
|
|
2915
|
+
try:
|
|
2916
|
+
from apsimo.skills.executor import SkillExecutor
|
|
2917
|
+
from apsimo.skills.security.guards import CapabilityGuard
|
|
2918
|
+
from apsimo.skills.security.scanner import ASTScanner
|
|
2919
|
+
skill_executor = SkillExecutor(
|
|
2920
|
+
registry=skills_registry,
|
|
2921
|
+
guard=CapabilityGuard(),
|
|
2922
|
+
scanner=ASTScanner(),
|
|
2923
|
+
)
|
|
2924
|
+
set_skill_executor(skill_executor)
|
|
2925
|
+
logger.info("SkillExecutor initialized")
|
|
2926
|
+
except Exception as sexc:
|
|
2927
|
+
logger.warning("SkillExecutor init failed: %s", sexc)
|
|
2928
|
+
except Exception as exc:
|
|
2929
|
+
logger.warning("SkillRegistry init failed: %s", exc)
|
|
2930
|
+
|
|
2931
|
+
# --- 17. Chain / Identity ---
|
|
2932
|
+
try:
|
|
2933
|
+
from apsimo.chain.identity import (
|
|
2934
|
+
get_or_create_colony_id,
|
|
2935
|
+
load_genesis_manifest,
|
|
2936
|
+
resolve_genesis_manifest_path,
|
|
2937
|
+
)
|
|
2938
|
+
colony_id = get_or_create_colony_id(state_dir)
|
|
2939
|
+
|
|
2940
|
+
# Load the Genesis manifest: this colony's own if it claimed Genesis,
|
|
2941
|
+
# otherwise the bundled network trust anchor.
|
|
2942
|
+
genesis_path = resolve_genesis_manifest_path(state_dir)
|
|
2943
|
+
if genesis_path is None:
|
|
2944
|
+
logger.warning("No Genesis manifest found; trust anchor stays unverified")
|
|
2945
|
+
else:
|
|
2946
|
+
load_genesis_manifest(genesis_path)
|
|
2947
|
+
|
|
2948
|
+
from apsimo.chain.manager import ChainManager
|
|
2949
|
+
chain = ChainManager(
|
|
2950
|
+
db_path=state_dir / "chain.db",
|
|
2951
|
+
colony_id=colony_id,
|
|
2952
|
+
)
|
|
2953
|
+
set_chain_manager(chain)
|
|
2954
|
+
logger.info("ChainManager initialized (colony_id=%s)", colony_id)
|
|
2955
|
+
|
|
2956
|
+
# Wire local key manager
|
|
2957
|
+
try:
|
|
2958
|
+
from apsimo.chain.local_keys import LocalKeyManager
|
|
2959
|
+
keys_dir = state_dir / "colony-keys"
|
|
2960
|
+
key_passphrase = os.environ.get("COLONY_KEY_PASSPHRASE", "")
|
|
2961
|
+
passphrase = key_passphrase.encode() if key_passphrase else None
|
|
2962
|
+
|
|
2963
|
+
if (keys_dir / "private.pem").exists():
|
|
2964
|
+
key_mgr = LocalKeyManager(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
|
|
2965
|
+
logger.info("LocalKeyManager loaded (public_key=%s...)", key_mgr.public_key_hex()[:16])
|
|
2966
|
+
else:
|
|
2967
|
+
key_mgr = LocalKeyManager.generate(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
|
|
2968
|
+
logger.info("LocalKeyManager generated new keypair for colony %s", colony_id)
|
|
2969
|
+
|
|
2970
|
+
chain._key_manager = key_mgr # Attach to chain for access
|
|
2971
|
+
except Exception as kexc:
|
|
2972
|
+
logger.warning("LocalKeyManager init skipped: %s", kexc)
|
|
2973
|
+
|
|
2974
|
+
# Initialize node identity
|
|
2975
|
+
try:
|
|
2976
|
+
from apsimo.chain.node import get_or_create_node_id, ensure_node_keypair, create_node_certificate
|
|
2977
|
+
node_id = get_or_create_node_id(state_dir)
|
|
2978
|
+
node_km = ensure_node_keypair(state_dir)
|
|
2979
|
+
logger.info("Node identity: %s (public_key=%s...)", node_id, node_km.public_key_hex()[:16])
|
|
2980
|
+
|
|
2981
|
+
# Create node certificate if missing
|
|
2982
|
+
cert_path = Path(state_dir) / "node-cert.json"
|
|
2983
|
+
if not cert_path.exists():
|
|
2984
|
+
create_node_certificate(state_dir, colony_key_manager=key_mgr)
|
|
2985
|
+
logger.info("Node certificate created and signed by Colony key")
|
|
2986
|
+
else:
|
|
2987
|
+
logger.info("Node certificate exists")
|
|
2988
|
+
except Exception as nexc:
|
|
2989
|
+
logger.warning("Node identity init skipped: %s", nexc)
|
|
2990
|
+
|
|
2991
|
+
# The LOCAL identity anchor (colony_id, node keypair, signed node
|
|
2992
|
+
# cert) is supported. The REMOTE multi-agent surface (agent connect,
|
|
2993
|
+
# cert-chain verification, block/consensus) is EXPERIMENTAL: no
|
|
2994
|
+
# consensus loop runs and the remote handshake is not production
|
|
2995
|
+
# verified. See docs/MULTI_AGENT.md.
|
|
2996
|
+
logger.info("Chain: local identity anchor ready; remote multi-agent "
|
|
2997
|
+
"+ consensus are EXPERIMENTAL (no consensus loop started)")
|
|
2998
|
+
except Exception as exc:
|
|
2999
|
+
logger.warning("ChainManager init failed: %s", exc)
|
|
3000
|
+
|
|
3001
|
+
# --- 18. Secrets ---
|
|
3002
|
+
try:
|
|
3003
|
+
from apsimo.secrets.manager import SecretsManager
|
|
3004
|
+
secrets = SecretsManager()
|
|
3005
|
+
set_secrets_manager(secrets)
|
|
3006
|
+
logger.info("SecretsManager initialized")
|
|
3007
|
+
except Exception as exc:
|
|
3008
|
+
logger.warning("SecretsManager init failed: %s", exc)
|
|
3009
|
+
|
|
3010
|
+
# --- 19. Session store ---
|
|
3011
|
+
try:
|
|
3012
|
+
from apsimo.sessions.store import InMemorySessionStore
|
|
3013
|
+
session_store = InMemorySessionStore()
|
|
3014
|
+
set_session_store(session_store)
|
|
3015
|
+
logger.info("InMemorySessionStore initialized")
|
|
3016
|
+
|
|
3017
|
+
# Re-wire ResponseGate now that session store is available
|
|
3018
|
+
if _gate_ref is not None:
|
|
3019
|
+
from apsimo.gate import ResponseGate
|
|
3020
|
+
new_gate = ResponseGate(_gate_config, session_store=session_store, audit_log=_gate_audit)
|
|
3021
|
+
set_response_gate(new_gate)
|
|
3022
|
+
logger.info("ResponseGate re-wired with SessionStore")
|
|
3023
|
+
except Exception as exc:
|
|
3024
|
+
logger.warning("SessionStore init failed: %s", exc)
|
|
3025
|
+
|
|
3026
|
+
# --- 20. Task queue ---
|
|
3027
|
+
task_queue = None
|
|
3028
|
+
try:
|
|
3029
|
+
from apsimo.task_queue.queue_manager import TaskQueueManager
|
|
3030
|
+
task_queue = await TaskQueueManager.initialize(
|
|
3031
|
+
db_path=state_dir / "task_queue.db",
|
|
3032
|
+
)
|
|
3033
|
+
# The queue facade becomes process-visible below, before the project
|
|
3034
|
+
# verifier and ProjectStore are constructed. Keep persisted
|
|
3035
|
+
# cognition WorkOrders fail-closed throughout that startup window;
|
|
3036
|
+
# successful project wiring replaces this with the digest-bound
|
|
3037
|
+
# callback later.
|
|
3038
|
+
_install_cognition_work_order_startup_fence(task_queue)
|
|
3039
|
+
task_queue.queue.set_execution_ready(False, "scheduler_starting")
|
|
3040
|
+
set_task_queue(task_queue)
|
|
3041
|
+
logger.info("TaskQueueManager initialized")
|
|
3042
|
+
except Exception as exc:
|
|
3043
|
+
logger.warning("TaskQueueManager init failed: %s", exc)
|
|
3044
|
+
|
|
3045
|
+
# The embedded worker starts only after the central governor is installed.
|
|
3046
|
+
worker_task = None
|
|
3047
|
+
queue_scheduler = None
|
|
3048
|
+
queue_scheduler_task = None
|
|
3049
|
+
queue_execution_ready = False
|
|
3050
|
+
agent_bridge_service = None
|
|
3051
|
+
agent_bridge_task = None
|
|
3052
|
+
|
|
3053
|
+
# --- 20c. Multi-Agent System (v0.7.0) ---
|
|
3054
|
+
try:
|
|
3055
|
+
from apsimo.agents.store import AgentStore, InviteStore
|
|
3056
|
+
from apsimo.initiatives.store import InitiativeStore
|
|
3057
|
+
from apsimo.initiatives.assignment import AssignmentEngine
|
|
3058
|
+
from apsimo.agents.websocket import WebSocketManager
|
|
3059
|
+
|
|
3060
|
+
agent_store = AgentStore(state_dir=state_dir)
|
|
3061
|
+
invite_store = InviteStore(state_dir=state_dir)
|
|
3062
|
+
set_agent_store(agent_store)
|
|
3063
|
+
set_invite_store(invite_store)
|
|
3064
|
+
logger.info("AgentStore initialized (state_dir=%s)", state_dir)
|
|
3065
|
+
|
|
3066
|
+
initiative_store = InitiativeStore(state_dir=state_dir)
|
|
3067
|
+
set_initiative_store(initiative_store)
|
|
3068
|
+
logger.info("InitiativeStore initialized (state_dir=%s)", state_dir)
|
|
3069
|
+
|
|
3070
|
+
# Observation store (v0.16.0) — agent-reported domain snapshots
|
|
3071
|
+
try:
|
|
3072
|
+
from apsimo.observations.store import ObservationStore
|
|
3073
|
+
from apsimo.api.routers.observations import set_observation_store
|
|
3074
|
+
observation_store = ObservationStore(state_dir=state_dir)
|
|
3075
|
+
set_observation_store(observation_store)
|
|
3076
|
+
logger.info("ObservationStore initialized (state_dir=%s)", state_dir)
|
|
3077
|
+
except Exception as exc:
|
|
3078
|
+
logger.warning("ObservationStore init failed (non-fatal): %s", exc)
|
|
3079
|
+
|
|
3080
|
+
assignment_engine = AssignmentEngine(
|
|
3081
|
+
agent_store=agent_store,
|
|
3082
|
+
initiative_store=initiative_store,
|
|
3083
|
+
)
|
|
3084
|
+
set_assignment_engine(assignment_engine)
|
|
3085
|
+
logger.info("AssignmentEngine initialized")
|
|
3086
|
+
|
|
3087
|
+
websocket_manager = WebSocketManager(
|
|
3088
|
+
agent_store=agent_store,
|
|
3089
|
+
initiative_store=initiative_store,
|
|
3090
|
+
)
|
|
3091
|
+
set_websocket_manager(websocket_manager)
|
|
3092
|
+
logger.info("WebSocketManager initialized")
|
|
3093
|
+
except Exception as exc:
|
|
3094
|
+
logger.warning("Multi-Agent System init failed: %s", exc)
|
|
3095
|
+
|
|
3096
|
+
# --- 21. Autonomy loop ---
|
|
3097
|
+
autonomy_config = None
|
|
3098
|
+
registry = None
|
|
3099
|
+
scheduler = None
|
|
3100
|
+
autonomy_loop = None
|
|
3101
|
+
try:
|
|
3102
|
+
from apsimo.autonomy.loop import AutonomyLoop
|
|
3103
|
+
from apsimo.autonomy.config import AutonomyConfig
|
|
3104
|
+
from apsimo.autonomy.registry import SubsystemRegistry
|
|
3105
|
+
from apsimo.autonomy.scheduler import AutonomyScheduler
|
|
3106
|
+
autonomy_config = AutonomyConfig.from_env()
|
|
3107
|
+
|
|
3108
|
+
# H4.2 annunciation: when the loop mode came from the preset (via
|
|
3109
|
+
# COLONY_PRESET_LOOP_COUPLING, default on) rather than an explicit
|
|
3110
|
+
# COLONY_AUTONOMY_MODE, say so loudly at startup and leave ONE
|
|
3111
|
+
# durable journal record the first time a deployment boots coupled —
|
|
3112
|
+
# a default flip an operator can always see and always roll back.
|
|
3113
|
+
if getattr(autonomy_config, "mode_source", "") == "preset":
|
|
3114
|
+
try:
|
|
3115
|
+
from apsimo.util.autonomy_preset import preset_name
|
|
3116
|
+
_preset = preset_name() or "(unknown)"
|
|
3117
|
+
except Exception:
|
|
3118
|
+
_preset = "(unknown)"
|
|
3119
|
+
logger.warning(
|
|
3120
|
+
"Autonomy loop mode %s inherited from COLONY_AUTONOMY_PRESET=%s "
|
|
3121
|
+
"via preset-loop coupling (COLONY_PRESET_LOOP_COUPLING=on by "
|
|
3122
|
+
"default). Set COLONY_AUTONOMY_MODE explicitly to override, or "
|
|
3123
|
+
"COLONY_PRESET_LOOP_COUPLING=off to restore env-only resolution.",
|
|
3124
|
+
autonomy_config.mode.value, _preset)
|
|
3125
|
+
try:
|
|
3126
|
+
from apsimo.api.routers.host import _self_model as _sm_j
|
|
3127
|
+
_cj = getattr(_sm_j, "journal", None)
|
|
3128
|
+
if _cj is not None and not _cj.recent(
|
|
3129
|
+
limit=1, domain="preset_coupling"):
|
|
3130
|
+
_cj.record(
|
|
3131
|
+
"preset_coupling",
|
|
3132
|
+
f"First coupled boot: loop mode {autonomy_config.mode.value} "
|
|
3133
|
+
f"inherited from preset '{_preset}'",
|
|
3134
|
+
reasoning="COLONY_AUTONOMY_MODE unset; "
|
|
3135
|
+
"COLONY_PRESET_LOOP_COUPLING on (default). "
|
|
3136
|
+
"Rollback: set COLONY_AUTONOMY_MODE=reactive "
|
|
3137
|
+
"or COLONY_PRESET_LOOP_COUPLING=off.",
|
|
3138
|
+
reversibility="reversible", decision="noted",
|
|
3139
|
+
ref="preset-loop-coupling")
|
|
3140
|
+
except Exception:
|
|
3141
|
+
pass # annunciation must never block startup
|
|
3142
|
+
|
|
3143
|
+
registry = SubsystemRegistry()
|
|
3144
|
+
|
|
3145
|
+
# Wire scheduler BEFORE the loop so the loop gets a direct reference.
|
|
3146
|
+
scheduler = AutonomyScheduler(db_path=str(state_dir / "schedules.db"))
|
|
3147
|
+
set_scheduler(scheduler)
|
|
3148
|
+
logger.info("AutonomyScheduler initialized")
|
|
3149
|
+
|
|
3150
|
+
autonomy_loop = AutonomyLoop(
|
|
3151
|
+
registry=registry,
|
|
3152
|
+
config=autonomy_config,
|
|
3153
|
+
scheduler=scheduler,
|
|
3154
|
+
)
|
|
3155
|
+
set_autonomy_loop(autonomy_loop)
|
|
3156
|
+
|
|
3157
|
+
# Register default periodic tasks. Every registered task does REAL
|
|
3158
|
+
# work or reports skipped — a no-op lambda returning {"status":"ok"}
|
|
3159
|
+
# makes the loop count a subsystem as running when it never does
|
|
3160
|
+
# (signal ingest happens inline at the API; briefings are fired by
|
|
3161
|
+
# the BriefingScheduler wired in section 13b — neither needs a task
|
|
3162
|
+
# here, so neither gets a fake one).
|
|
3163
|
+
def _run_health_check():
|
|
3164
|
+
return _scheduler_health_check(autonomy_loop)
|
|
3165
|
+
|
|
3166
|
+
scheduler.register("health_check", _run_health_check, interval_seconds=300, metadata={"description": "Subsystem health check (reports wired count)"})
|
|
3167
|
+
|
|
3168
|
+
async def _run_memory_consolidate():
|
|
3169
|
+
from apsimo.api.routers.host import _consolidator as c
|
|
3170
|
+
if c is None:
|
|
3171
|
+
return {"status": "skipped", "reason": "consolidator_not_wired"}
|
|
3172
|
+
result = await c.run()
|
|
3173
|
+
# ConsolidationResult exposes pairs_merged, not merged_count — the
|
|
3174
|
+
# old attr name silently reported merged:0 every run.
|
|
3175
|
+
return {"status": "ok", "merged": getattr(result, "pairs_merged", 0)}
|
|
3176
|
+
|
|
3177
|
+
scheduler.register("memory_consolidate", _run_memory_consolidate, interval_seconds=3600, metadata={"description": "Deduplicate and merge near-duplicate memories"})
|
|
3178
|
+
|
|
3179
|
+
async def _run_cpi_track():
|
|
3180
|
+
from apsimo.api.routers.host import _metalearner as ml
|
|
3181
|
+
if ml is None:
|
|
3182
|
+
return {"status": "skipped", "reason": "metalearner_not_wired"}
|
|
3183
|
+
cpi = await ml.evaluate()
|
|
3184
|
+
return {"status": "ok", "overall": round(float(getattr(cpi, "overall", 0.0)), 4)}
|
|
3185
|
+
|
|
3186
|
+
scheduler.register("cpi_track", _run_cpi_track, interval_seconds=86400, metadata={"description": "Calculate Cognitive Performance Index"})
|
|
3187
|
+
|
|
3188
|
+
async def _run_world_model_prune():
|
|
3189
|
+
from apsimo.api.routers.host import _world_store as ws
|
|
3190
|
+
if ws is None:
|
|
3191
|
+
return {"status": "skipped", "reason": "world_model_not_wired"}
|
|
3192
|
+
return await ws.prune()
|
|
3193
|
+
|
|
3194
|
+
scheduler.register("world_model_prune", _run_world_model_prune, interval_seconds=86400, metadata={"description": "Remove stale low-confidence world model entities (config TTL)"})
|
|
3195
|
+
|
|
3196
|
+
async def _run_mining_prune():
|
|
3197
|
+
from apsimo.api.routers.mining import get_mining_store
|
|
3198
|
+
store = get_mining_store()
|
|
3199
|
+
if store is None:
|
|
3200
|
+
return {"status": "skipped", "reason": "mining_not_wired"}
|
|
3201
|
+
try:
|
|
3202
|
+
retention_days = int(os.environ.get(
|
|
3203
|
+
"COLONY_MINING_RETENTION_DAYS", "0"))
|
|
3204
|
+
max_turns = int(os.environ.get(
|
|
3205
|
+
"COLONY_MINING_MAX_TURNS", "0"))
|
|
3206
|
+
except ValueError:
|
|
3207
|
+
retention_days = max_turns = 0
|
|
3208
|
+
if retention_days <= 0 and max_turns <= 0:
|
|
3209
|
+
# Default posture: the verbatim turn bank is unbounded
|
|
3210
|
+
# unless a deployment opts into retention.
|
|
3211
|
+
return {"status": "skipped", "reason": "retention_unbounded"}
|
|
3212
|
+
return {"status": "ok", **store.prune_turns(
|
|
3213
|
+
retention_days=retention_days, max_turns=max_turns)}
|
|
3214
|
+
|
|
3215
|
+
scheduler.register("mining_prune", _run_mining_prune, interval_seconds=86400, metadata={"description": "Prune banked mining turns per COLONY_MINING_RETENTION_DAYS / COLONY_MINING_MAX_TURNS (0 = keep everything)"})
|
|
3216
|
+
|
|
3217
|
+
# Daily pattern extraction (U25): the pattern store fed compute_surprise
|
|
3218
|
+
# but only ever filled via the manual /patterns/extract endpoint, so
|
|
3219
|
+
# surprise scoring ran against an empty store. Flag-gated off by
|
|
3220
|
+
# default; registered only when on so the loop never counts a
|
|
3221
|
+
# deliberately-disabled subsystem as running.
|
|
3222
|
+
if os.environ.get("COLONY_PATTERNS_SCHEDULE",
|
|
3223
|
+
"off").strip().lower() == "on":
|
|
3224
|
+
async def _run_pattern_extract():
|
|
3225
|
+
from apsimo.api.routers.host import (
|
|
3226
|
+
_pattern_store as ps, _world_store as ws,
|
|
3227
|
+
)
|
|
3228
|
+
if ws is None or ps is None:
|
|
3229
|
+
return {"status": "skipped",
|
|
3230
|
+
"reason": "stores_not_wired"}
|
|
3231
|
+
from apsimo.patterns.extract import extract_patterns
|
|
3232
|
+
return {"status": "ok",
|
|
3233
|
+
**extract_patterns(world_store=ws, pattern_store=ps)}
|
|
3234
|
+
|
|
3235
|
+
scheduler.register("pattern_extract", _run_pattern_extract, interval_seconds=86400, metadata={"description": "Extract world-model patterns into the pattern store (COLONY_PATTERNS_SCHEDULE=on)"})
|
|
3236
|
+
|
|
3237
|
+
async def _run_surprise_ttl():
|
|
3238
|
+
from apsimo.api.routers.host import _surprise_store as ss
|
|
3239
|
+
if ss is None:
|
|
3240
|
+
return {"status": "skipped",
|
|
3241
|
+
"reason": "surprise_store_not_wired"}
|
|
3242
|
+
try:
|
|
3243
|
+
ttl_days = float(os.environ.get(
|
|
3244
|
+
"COLONY_SURPRISE_TTL_DAYS", "14"))
|
|
3245
|
+
except ValueError:
|
|
3246
|
+
ttl_days = 14.0
|
|
3247
|
+
if ttl_days <= 0:
|
|
3248
|
+
return {"status": "skipped", "reason": "ttl_disabled"}
|
|
3249
|
+
return {"status": "ok", "auto_resolved": ss.resolve_stale(ttl_days)}
|
|
3250
|
+
|
|
3251
|
+
scheduler.register("surprise_ttl", _run_surprise_ttl, interval_seconds=86400, metadata={"description": "Auto-resolve surprises unaddressed past COLONY_SURPRISE_TTL_DAYS (default 14; 0 disables)"})
|
|
3252
|
+
|
|
3253
|
+
async def _run_digest_flush():
|
|
3254
|
+
from apsimo.api.routers.host import _delivery_bridge as bridge
|
|
3255
|
+
if bridge is None:
|
|
3256
|
+
return {"status": "skipped", "reason": "delivery_bridge_not_wired"}
|
|
3257
|
+
header = os.environ.get("COLONY_DIGEST_HEADER", "Daily digest")
|
|
3258
|
+
return await bridge.flush_digests_to_gateway(header=header)
|
|
3259
|
+
|
|
3260
|
+
digest_interval = int(os.environ.get("COLONY_DIGEST_INTERVAL_SECONDS", "86400"))
|
|
3261
|
+
scheduler.register(
|
|
3262
|
+
"digest_flush",
|
|
3263
|
+
_run_digest_flush,
|
|
3264
|
+
interval_seconds=digest_interval,
|
|
3265
|
+
metadata={"description": "Bundle and deliver accumulated DIGEST-channel items"},
|
|
3266
|
+
)
|
|
3267
|
+
|
|
3268
|
+
logger.info(
|
|
3269
|
+
"AutonomyLoop initialized (tick=%ds, scheduler=%d tasks)",
|
|
3270
|
+
autonomy_config.tick_interval_secs,
|
|
3271
|
+
len(scheduler.list_schedules()),
|
|
3272
|
+
)
|
|
3273
|
+
|
|
3274
|
+
# Auto-start the autonomy loop as a background task
|
|
3275
|
+
asyncio.create_task(autonomy_loop.start())
|
|
3276
|
+
logger.info("AutonomyLoop auto-start scheduled")
|
|
3277
|
+
except Exception as exc:
|
|
3278
|
+
logger.warning("AutonomyLoop init failed: %s", exc)
|
|
3279
|
+
|
|
3280
|
+
# Wire SubsystemRegistry into ToolExecutor so Colony-native tools
|
|
3281
|
+
# (memory_search, goals, relationships, etc.) are available to the
|
|
3282
|
+
# initiative executor's reasoning loop.
|
|
3283
|
+
if registry is not None and locals().get("tool_executor") is not None:
|
|
3284
|
+
te = locals()["tool_executor"]
|
|
3285
|
+
from apsimo.tools.handlers import TOOL_HANDLERS as _colony_handlers
|
|
3286
|
+
for _tname, _thandler in _colony_handlers.items():
|
|
3287
|
+
if _tname not in te._handlers:
|
|
3288
|
+
te._handlers[_tname] = lambda args, h=_thandler, r=registry: h(args, r)
|
|
3289
|
+
logger.info(
|
|
3290
|
+
"Colony tool handlers wired into ToolExecutor (%d colony tools, %d total)",
|
|
3291
|
+
len(_colony_handlers),
|
|
3292
|
+
len(te._handlers),
|
|
3293
|
+
)
|
|
3294
|
+
|
|
3295
|
+
# --- 22b. Project engine (goal persistence, cognition item 1) ---
|
|
3296
|
+
try:
|
|
3297
|
+
from apsimo.projects import ProjectEngine, ProjectStore, projects_mode
|
|
3298
|
+
from apsimo.work_orders import (
|
|
3299
|
+
QueueWorkOrderAdapter,
|
|
3300
|
+
load_receipt_verifier_from_env,
|
|
3301
|
+
)
|
|
3302
|
+
from apsimo.api.routers.host import (
|
|
3303
|
+
set_project_engine,
|
|
3304
|
+
get_directive_manager as _get_dm_p,
|
|
3305
|
+
_directed_service as _dsvc_for_projects,
|
|
3306
|
+
_proposal_store as _pstore_for_projects,
|
|
3307
|
+
_feedback_store as _fb_for_projects,
|
|
3308
|
+
)
|
|
3309
|
+
|
|
3310
|
+
async def _project_deliver(payload: dict) -> bool:
|
|
3311
|
+
try:
|
|
3312
|
+
from apsimo.api.routers.host import (
|
|
3313
|
+
_autonomy_loop, _delivery_bridge,
|
|
3314
|
+
)
|
|
3315
|
+
if _autonomy_loop is not None and _delivery_bridge is not None:
|
|
3316
|
+
return await _autonomy_loop._route_reachout_delivery(
|
|
3317
|
+
payload, _delivery_bridge)
|
|
3318
|
+
except Exception:
|
|
3319
|
+
logger.debug("project deliver failed", exc_info=True)
|
|
3320
|
+
return False
|
|
3321
|
+
|
|
3322
|
+
# A failed verifier import/configuration must not leave a project
|
|
3323
|
+
# engine from an earlier lifespan reachable in this process.
|
|
3324
|
+
set_project_engine(None)
|
|
3325
|
+
_receipt_verifier = load_receipt_verifier_from_env()
|
|
3326
|
+
_project_store = ProjectStore(db_path=str(state_dir / "colony-projects.db"))
|
|
3327
|
+
_project_concern_store = locals().get("_concern_store")
|
|
3328
|
+
|
|
3329
|
+
def _project_hold_reason(project) -> str:
|
|
3330
|
+
from apsimo.self_model.event_concerns import (
|
|
3331
|
+
project_turn_concern_hold_reason,
|
|
3332
|
+
)
|
|
3333
|
+
return project_turn_concern_hold_reason(
|
|
3334
|
+
project, _project_concern_store,
|
|
3335
|
+
)
|
|
3336
|
+
|
|
3337
|
+
_project_engine_obj = ProjectEngine(
|
|
3338
|
+
_project_store,
|
|
3339
|
+
directive_manager=_get_dm_p(),
|
|
3340
|
+
llm_router=llm_router,
|
|
3341
|
+
reasoning_loop=locals().get("reasoning_loop"),
|
|
3342
|
+
tool_executor=locals().get("tool_executor"),
|
|
3343
|
+
directed_service=_dsvc_for_projects,
|
|
3344
|
+
proposal_store=_pstore_for_projects,
|
|
3345
|
+
feedback_store=_fb_for_projects,
|
|
3346
|
+
self_model=_sm_for_directed,
|
|
3347
|
+
skill_store=_skills_mem_store,
|
|
3348
|
+
delivery_router=_project_deliver,
|
|
3349
|
+
initiative_store=locals().get("initiative_store"),
|
|
3350
|
+
work_order_adapter=(
|
|
3351
|
+
QueueWorkOrderAdapter(
|
|
3352
|
+
task_queue,
|
|
3353
|
+
project_store=_project_store,
|
|
3354
|
+
receipt_verifier=_receipt_verifier,
|
|
3355
|
+
)
|
|
3356
|
+
if task_queue is not None else None
|
|
3357
|
+
),
|
|
3358
|
+
project_hold_reason=_project_hold_reason,
|
|
3359
|
+
)
|
|
3360
|
+
if task_queue is not None:
|
|
3361
|
+
_install_cognition_work_order_runtime_fence(
|
|
3362
|
+
task_queue, _project_store, _project_concern_store,
|
|
3363
|
+
)
|
|
3364
|
+
await task_queue.queue.reconcile_runtime_claim_holds()
|
|
3365
|
+
set_project_engine(_project_engine_obj)
|
|
3366
|
+
if _receipt_verifier is not None:
|
|
3367
|
+
logger.info(
|
|
3368
|
+
"External WorkOrder receipt verifier initialized (%s)",
|
|
3369
|
+
getattr(_receipt_verifier, "identity", type(_receipt_verifier).__name__),
|
|
3370
|
+
)
|
|
3371
|
+
logger.info("ProjectEngine initialized (db=%s, mode=%s)",
|
|
3372
|
+
state_dir / "colony-projects.db", projects_mode())
|
|
3373
|
+
# Late-attach the initiative store to the belief engine (it is wired
|
|
3374
|
+
# after the world-model section where the engine was created).
|
|
3375
|
+
try:
|
|
3376
|
+
from apsimo.api.routers.host import _belief_engine as _be
|
|
3377
|
+
if _be is not None:
|
|
3378
|
+
_be._initiatives = locals().get("initiative_store")
|
|
3379
|
+
except Exception:
|
|
3380
|
+
pass
|
|
3381
|
+
except Exception as exc:
|
|
3382
|
+
logger.warning("ProjectEngine init failed: %s", exc)
|
|
3383
|
+
|
|
3384
|
+
# --- 22b.1 Receipt-derived cognition evidence (migration-gated) ---
|
|
3385
|
+
_evidence_wiring = None
|
|
3386
|
+
try:
|
|
3387
|
+
_evidence_wiring = _attach_cognition_evidence(
|
|
3388
|
+
state_dir=state_dir,
|
|
3389
|
+
project_store=locals().get("_project_store"),
|
|
3390
|
+
self_model=_sm_for_directed,
|
|
3391
|
+
expectations=locals().get("_expectations"),
|
|
3392
|
+
scheduler=locals().get("scheduler"),
|
|
3393
|
+
)
|
|
3394
|
+
if _evidence_wiring is not None:
|
|
3395
|
+
logger.info(
|
|
3396
|
+
"Cognition evidence attached (mode=%s, db=%s, initial=%s)",
|
|
3397
|
+
_evidence_wiring["mode"],
|
|
3398
|
+
(
|
|
3399
|
+
state_dir / "colony-cognition-evidence.db"
|
|
3400
|
+
if _evidence_wiring["store"] is not None else "disabled"
|
|
3401
|
+
),
|
|
3402
|
+
_evidence_wiring["initial_status"],
|
|
3403
|
+
)
|
|
3404
|
+
except Exception as exc:
|
|
3405
|
+
# A requested live pipeline suppresses the legacy direct competence
|
|
3406
|
+
# writer independently, so attachment failure loses no authority by
|
|
3407
|
+
# silently falling back to self-reported outcomes.
|
|
3408
|
+
logger.error("Cognition evidence attachment failed closed: %s", exc)
|
|
3409
|
+
|
|
3410
|
+
# --- 22b.2 Typed cognition/goal spine (P3, migration-gated) ---
|
|
3411
|
+
try:
|
|
3412
|
+
_cognition_spine = _attach_cognition_spine(
|
|
3413
|
+
state_dir=state_dir,
|
|
3414
|
+
task_queue=task_queue,
|
|
3415
|
+
workspace=locals().get("_workspace"),
|
|
3416
|
+
concern_store=locals().get("_concern_store"),
|
|
3417
|
+
project_store=locals().get("_project_store"),
|
|
3418
|
+
project_engine=locals().get("_project_engine_obj"),
|
|
3419
|
+
directive_manager=(
|
|
3420
|
+
_get_dm_p() if "_get_dm_p" in locals() else None
|
|
3421
|
+
),
|
|
3422
|
+
llm_router=llm_router,
|
|
3423
|
+
embedded_worker_enabled=_configured_embedded_worker_enabled(),
|
|
3424
|
+
proposal_store=locals().get("_proposal_store"),
|
|
3425
|
+
)
|
|
3426
|
+
if _cognition_spine is not None:
|
|
3427
|
+
from apsimo.cognition.goal_spine import cognition_spine_mode
|
|
3428
|
+
logger.info(
|
|
3429
|
+
"Typed cognition spine attached (db=%s, mode=%s)",
|
|
3430
|
+
state_dir / "colony-cognition.db",
|
|
3431
|
+
cognition_spine_mode(),
|
|
3432
|
+
)
|
|
3433
|
+
except Exception as exc:
|
|
3434
|
+
# In live mode, P3's legacy-writer guards remain active and the
|
|
3435
|
+
# workspace loop refuses a missing spine. The failure is therefore
|
|
3436
|
+
# fail-closed for autonomous goal creation without taking the whole
|
|
3437
|
+
# sidecar (and daily owner-directed work) offline.
|
|
3438
|
+
from apsimo.cognition.goal_spine import cognition_spine_mode
|
|
3439
|
+
failed_mode = cognition_spine_mode()
|
|
3440
|
+
set_cognition_attachment_status({
|
|
3441
|
+
"configured_mode": failed_mode,
|
|
3442
|
+
"state": "failed",
|
|
3443
|
+
"reason": str(exc)[:500],
|
|
3444
|
+
"configured_handler_catalog": (
|
|
3445
|
+
["thought"] if failed_mode in {"shadow", "live"} else []
|
|
3446
|
+
),
|
|
3447
|
+
"effective_handler_catalog": [],
|
|
3448
|
+
})
|
|
3449
|
+
logger.error("Typed cognition spine attachment failed: %s", exc)
|
|
3450
|
+
|
|
3451
|
+
# --- 22b.3 Evidence-derived situation spine (P6, migration-gated) ---
|
|
3452
|
+
_situation_wiring = None
|
|
3453
|
+
try:
|
|
3454
|
+
_situation_wiring = _attach_situation_spine(
|
|
3455
|
+
state_dir=state_dir,
|
|
3456
|
+
cognition_spine=locals().get("_cognition_spine"),
|
|
3457
|
+
scheduler=locals().get("scheduler"),
|
|
3458
|
+
task_queue=task_queue,
|
|
3459
|
+
)
|
|
3460
|
+
if _situation_wiring is not None:
|
|
3461
|
+
logger.info(
|
|
3462
|
+
"Situation spine attached (db=%s, mode=%s, initial=%s)",
|
|
3463
|
+
state_dir / "colony-situation.db",
|
|
3464
|
+
_situation_wiring["mode"],
|
|
3465
|
+
_situation_wiring["initial_status"],
|
|
3466
|
+
)
|
|
3467
|
+
except Exception as exc:
|
|
3468
|
+
# A configured live P6 that cannot attach must not silently leave P3's
|
|
3469
|
+
# capacity-only allow path active. Preserve capacity denials, then hold
|
|
3470
|
+
# everything else until a healthy pinned restart.
|
|
3471
|
+
try:
|
|
3472
|
+
from apsimo.self_model.situation import situation_spine_mode
|
|
3473
|
+
cognition = locals().get("_cognition_spine")
|
|
3474
|
+
existing = getattr(cognition, "_situation", None)
|
|
3475
|
+
if situation_spine_mode() == "live" and callable(existing):
|
|
3476
|
+
cognition._situation = _capacity_plus_attachment_failure(
|
|
3477
|
+
existing, "situation_attachment_failed_closed",
|
|
3478
|
+
)
|
|
3479
|
+
except Exception:
|
|
3480
|
+
logger.exception("Could not install P6 attachment failure gate")
|
|
3481
|
+
logger.error("Situation spine attachment failed closed: %s", exc)
|
|
3482
|
+
|
|
3483
|
+
# --- 22b.4 Owner-ratified drive governance (P7, migration-gated) ---
|
|
3484
|
+
_drive_wiring = None
|
|
3485
|
+
try:
|
|
3486
|
+
_drive_wiring = _attach_drive_governance(
|
|
3487
|
+
state_dir=state_dir,
|
|
3488
|
+
cognition_spine=locals().get("_cognition_spine"),
|
|
3489
|
+
workspace=locals().get("_workspace"),
|
|
3490
|
+
project_store=locals().get("_project_store"),
|
|
3491
|
+
directive_manager=(
|
|
3492
|
+
_get_dm_p() if "_get_dm_p" in locals() else None
|
|
3493
|
+
),
|
|
3494
|
+
approval_authority=_controlled_learning.get(
|
|
3495
|
+
"approval_authority"
|
|
3496
|
+
),
|
|
3497
|
+
)
|
|
3498
|
+
if _drive_wiring is not None:
|
|
3499
|
+
logger.info(
|
|
3500
|
+
"Drive governance attached (db=%s, mode=%s, shared_authority=%s)",
|
|
3501
|
+
state_dir / "cognition-drive-governance.db",
|
|
3502
|
+
_drive_wiring["mode"],
|
|
3503
|
+
getattr(
|
|
3504
|
+
_drive_wiring["approval_authority"], "path", None,
|
|
3505
|
+
),
|
|
3506
|
+
)
|
|
3507
|
+
except Exception as exc:
|
|
3508
|
+
logger.error("Drive governance attachment failed closed: %s", exc)
|
|
3509
|
+
|
|
3510
|
+
# --- 22c. Worker governor (server-side queue enforcement, item 5) ---
|
|
3511
|
+
try:
|
|
3512
|
+
from apsimo.api.routers.host import (
|
|
3513
|
+
get_directive_manager as _get_dm_w,
|
|
3514
|
+
_feedback_store as _fb_for_workers,
|
|
3515
|
+
)
|
|
3516
|
+
from apsimo.task_queue.governor import WorkerGovernor, workers_mode
|
|
3517
|
+
|
|
3518
|
+
async def _worker_deliver(payload: dict) -> bool:
|
|
3519
|
+
try:
|
|
3520
|
+
from apsimo.api.routers.host import (
|
|
3521
|
+
_autonomy_loop, _delivery_bridge,
|
|
3522
|
+
)
|
|
3523
|
+
if _autonomy_loop is not None and _delivery_bridge is not None:
|
|
3524
|
+
return await _autonomy_loop._route_reachout_delivery(
|
|
3525
|
+
payload, _delivery_bridge)
|
|
3526
|
+
except Exception:
|
|
3527
|
+
logger.debug("worker deliver failed", exc_info=True)
|
|
3528
|
+
return False
|
|
3529
|
+
|
|
3530
|
+
_worker_gov = WorkerGovernor(
|
|
3531
|
+
directive_manager=_get_dm_w(),
|
|
3532
|
+
feedback_store=_fb_for_workers,
|
|
3533
|
+
self_model=_sm_for_directed,
|
|
3534
|
+
delivery_router=_worker_deliver,
|
|
3535
|
+
proposal_store=locals().get("_proposal_store"),
|
|
3536
|
+
skill_store=_skills_mem_store,
|
|
3537
|
+
llm_router=llm_router,
|
|
3538
|
+
boundary_required=True,
|
|
3539
|
+
)
|
|
3540
|
+
set_worker_governor(_worker_gov)
|
|
3541
|
+
logger.info("WorkerGovernor initialized (mode=%s)", workers_mode())
|
|
3542
|
+
except Exception as exc:
|
|
3543
|
+
logger.warning("WorkerGovernor init failed: %s", exc)
|
|
3544
|
+
|
|
3545
|
+
# Queue maintenance is independent from execution-worker enablement. It
|
|
3546
|
+
# expires deadlines and leases, reconciles holds, and drains durable
|
|
3547
|
+
# outcome evidence even during a health-only deployment cutover.
|
|
3548
|
+
if task_queue is not None:
|
|
3549
|
+
try:
|
|
3550
|
+
from apsimo.task_queue.scheduler import Scheduler
|
|
3551
|
+
|
|
3552
|
+
queue_scheduler = Scheduler(
|
|
3553
|
+
queue=task_queue.queue,
|
|
3554
|
+
tick_interval_secs=_queue_seconds_env(
|
|
3555
|
+
"COLONY_QUEUE_SCHEDULER_TICK_SECS", 2.0,
|
|
3556
|
+
),
|
|
3557
|
+
claim_timeout_secs=_queue_seconds_env(
|
|
3558
|
+
"COLONY_QUEUE_CLAIM_TIMEOUT_SECS", 30.0,
|
|
3559
|
+
),
|
|
3560
|
+
readiness_callback=task_queue.queue.set_execution_ready,
|
|
3561
|
+
)
|
|
3562
|
+
# A successful synchronous first tick is the execution-readiness
|
|
3563
|
+
# gate. Configuration/import/database failures leave the API and
|
|
3564
|
+
# read surfaces online but no worker may claim work.
|
|
3565
|
+
await queue_scheduler.tick_once()
|
|
3566
|
+
queue_scheduler_task = asyncio.create_task(
|
|
3567
|
+
queue_scheduler.run()
|
|
3568
|
+
)
|
|
3569
|
+
app.state.queue_scheduler = queue_scheduler
|
|
3570
|
+
app.state.queue_scheduler_task = queue_scheduler_task
|
|
3571
|
+
queue_readiness = task_queue.queue.execution_readiness()
|
|
3572
|
+
queue_execution_ready = bool(queue_readiness.get("ready"))
|
|
3573
|
+
if queue_execution_ready:
|
|
3574
|
+
logger.info("Task queue scheduler started")
|
|
3575
|
+
else:
|
|
3576
|
+
logger.error(
|
|
3577
|
+
"Task queue maintenance started but execution remains "
|
|
3578
|
+
"held: %s",
|
|
3579
|
+
queue_readiness.get("reason"),
|
|
3580
|
+
)
|
|
3581
|
+
except Exception as exc:
|
|
3582
|
+
queue_execution_ready = False
|
|
3583
|
+
task_queue.queue.set_execution_ready(
|
|
3584
|
+
False, f"scheduler_unavailable:{exc}",
|
|
3585
|
+
)
|
|
3586
|
+
logger.error(
|
|
3587
|
+
"Task queue scheduler unavailable; execution workers disabled: %s",
|
|
3588
|
+
exc,
|
|
3589
|
+
exc_info=True,
|
|
3590
|
+
)
|
|
3591
|
+
|
|
3592
|
+
# --- 22c.0 Agent Bridge (after claim authority + scheduler readiness) ---
|
|
3593
|
+
# The bridge can still forward initiatives when queue execution is held,
|
|
3594
|
+
# but it receives no queue handle and therefore cannot claim jobs.
|
|
3595
|
+
try:
|
|
3596
|
+
from apsimo.services.agent_bridge import (
|
|
3597
|
+
create_from_env as _create_bridge,
|
|
3598
|
+
)
|
|
3599
|
+
agent_bridge_service = _create_bridge(
|
|
3600
|
+
initiative_store=locals().get("initiative_store"),
|
|
3601
|
+
autonomy_loop=autonomy_loop,
|
|
3602
|
+
task_queue=(task_queue if queue_execution_ready else None),
|
|
3603
|
+
observation_store=locals().get("observation_store"),
|
|
3604
|
+
)
|
|
3605
|
+
if agent_bridge_service is not None:
|
|
3606
|
+
set_agent_bridge(agent_bridge_service)
|
|
3607
|
+
agent_bridge_task = asyncio.create_task(
|
|
3608
|
+
agent_bridge_service.start()
|
|
3609
|
+
)
|
|
3610
|
+
logger.info("AgentBridgeService auto-start scheduled")
|
|
3611
|
+
except Exception as exc:
|
|
3612
|
+
logger.warning("AgentBridgeService init failed (non-fatal): %s", exc)
|
|
3613
|
+
|
|
3614
|
+
# --- 22c.1 Embedded worker (after mandatory central authority wiring) ---
|
|
3615
|
+
if task_queue is not None and _embedded_worker_enabled():
|
|
3616
|
+
try:
|
|
3617
|
+
if not queue_execution_ready:
|
|
3618
|
+
raise RuntimeError("queue scheduler is not execution-ready")
|
|
3619
|
+
import asyncio as _asyncio
|
|
3620
|
+
from apsimo.task_queue.worker import WorkerNode
|
|
3621
|
+
from apsimo.task_queue.handlers.registry import build_default_handlers
|
|
3622
|
+
from apsimo.chain.node import get_or_create_node_id
|
|
3623
|
+
import apsimo.api.routers.host as _host_mod
|
|
3624
|
+
|
|
3625
|
+
worker_node_id = get_or_create_node_id(state_dir)
|
|
3626
|
+
from apsimo.cognition.goal_spine import cognition_spine_mode
|
|
3627
|
+
worker_profile = _cognition_worker_profile(
|
|
3628
|
+
configured_mode=cognition_spine_mode(),
|
|
3629
|
+
attached=locals().get("_cognition_spine") is not None,
|
|
3630
|
+
)
|
|
3631
|
+
if worker_profile == "held":
|
|
3632
|
+
raise RuntimeError(
|
|
3633
|
+
"configured cognition owner is held because P3 attachment "
|
|
3634
|
+
"is unavailable"
|
|
3635
|
+
)
|
|
3636
|
+
if worker_profile == "thought_only":
|
|
3637
|
+
handlers, worker_capabilities = _cognition_owner_spec(
|
|
3638
|
+
router=_host_mod._llm_router,
|
|
3639
|
+
node_id=worker_node_id,
|
|
3640
|
+
)
|
|
3641
|
+
else:
|
|
3642
|
+
handlers = build_default_handlers(
|
|
3643
|
+
router=_host_mod._llm_router,
|
|
3644
|
+
world_model_store=_host_mod._world_store,
|
|
3645
|
+
contact_store=_host_mod._contacts_store,
|
|
3646
|
+
response_gate=_host_mod._response_gate,
|
|
3647
|
+
node_id=worker_node_id,
|
|
3648
|
+
)
|
|
3649
|
+
worker_capabilities = None
|
|
3650
|
+
worker = WorkerNode(
|
|
3651
|
+
node_id=worker_node_id,
|
|
3652
|
+
queue=task_queue.queue,
|
|
3653
|
+
handlers=handlers,
|
|
3654
|
+
capabilities=worker_capabilities,
|
|
3655
|
+
)
|
|
3656
|
+
worker_task = _asyncio.create_task(worker.start())
|
|
3657
|
+
app.state.worker = worker
|
|
3658
|
+
app.state.worker_task = worker_task
|
|
3659
|
+
if worker_profile == "thought_only":
|
|
3660
|
+
set_cognition_attachment_status({
|
|
3661
|
+
"configured_mode": cognition_spine_mode(),
|
|
3662
|
+
"state": "attached",
|
|
3663
|
+
"reason": "cognition_thought_worker_started",
|
|
3664
|
+
"configured_handler_catalog": ["thought"],
|
|
3665
|
+
"effective_handler_catalog": ["thought"],
|
|
3666
|
+
})
|
|
3667
|
+
logger.info(
|
|
3668
|
+
"WorkerNode started after central governor "
|
|
3669
|
+
"(node=%s, handlers=%s, governance=%s)",
|
|
3670
|
+
worker_node_id,
|
|
3671
|
+
[jt.value for jt in handlers.keys()],
|
|
3672
|
+
task_queue.queue.governance_configuration(),
|
|
3673
|
+
)
|
|
3674
|
+
except Exception as exc:
|
|
3675
|
+
logger.warning(
|
|
3676
|
+
"WorkerNode init failed — queued jobs will not execute: %s",
|
|
3677
|
+
exc,
|
|
3678
|
+
exc_info=True,
|
|
3679
|
+
)
|
|
3680
|
+
|
|
3681
|
+
# --- 22d. Exploration sandbox (gated isolated execution, item 6) ---
|
|
3682
|
+
try:
|
|
3683
|
+
from apsimo.sandbox import SandboxManager, sandbox_mode
|
|
3684
|
+
from apsimo.api.routers.host import (
|
|
3685
|
+
set_sandbox, get_directive_manager as _get_dm_sb,
|
|
3686
|
+
)
|
|
3687
|
+
_sandbox_mgr = SandboxManager(
|
|
3688
|
+
directive_manager=_get_dm_sb(),
|
|
3689
|
+
self_model=_sm_for_directed,
|
|
3690
|
+
)
|
|
3691
|
+
set_sandbox(_sandbox_mgr)
|
|
3692
|
+
logger.info("SandboxManager initialized (mode=%s, backend=%s)",
|
|
3693
|
+
sandbox_mode(), _sandbox_mgr.backend_name())
|
|
3694
|
+
except Exception as exc:
|
|
3695
|
+
logger.warning("SandboxManager init failed: %s", exc)
|
|
3696
|
+
|
|
3697
|
+
# --- 22e. Connector framework (read-only pull senses, item 2) ---
|
|
3698
|
+
try:
|
|
3699
|
+
from apsimo.connectors import (
|
|
3700
|
+
ConnectorManager, connectors_mode,
|
|
3701
|
+
)
|
|
3702
|
+
from apsimo.api.routers.host import (
|
|
3703
|
+
set_connector_manager, get_directive_manager as _get_dm_c,
|
|
3704
|
+
_world_populator as _pop_for_conn,
|
|
3705
|
+
)
|
|
3706
|
+
_conn_mgr = ConnectorManager(
|
|
3707
|
+
observation_store=locals().get("observation_store"),
|
|
3708
|
+
populator=_pop_for_conn,
|
|
3709
|
+
directive_manager=_get_dm_c(),
|
|
3710
|
+
self_model=_sm_for_directed,
|
|
3711
|
+
)
|
|
3712
|
+
n_conn = _conn_mgr.register_default_connectors()
|
|
3713
|
+
set_connector_manager(_conn_mgr)
|
|
3714
|
+
logger.info("ConnectorManager initialized (mode=%s, %d connector(s) enabled)",
|
|
3715
|
+
connectors_mode(), n_conn)
|
|
3716
|
+
except Exception as exc:
|
|
3717
|
+
logger.warning("ConnectorManager init failed: %s", exc)
|
|
3718
|
+
|
|
3719
|
+
# --- 23. Initiative Executor service (autonomous initiative processing) ---
|
|
3720
|
+
try:
|
|
3721
|
+
from apsimo.services.initiative_executor import (
|
|
3722
|
+
create_from_env as _create_executor,
|
|
3723
|
+
)
|
|
3724
|
+
from apsimo.api.routers.host import get_directive_manager as _get_dm
|
|
3725
|
+
_executor_svc = _create_executor(
|
|
3726
|
+
initiative_store=locals().get("initiative_store"),
|
|
3727
|
+
reasoning_loop=locals().get("reasoning_loop"),
|
|
3728
|
+
tool_executor=locals().get("tool_executor"),
|
|
3729
|
+
directive_manager=_get_dm(),
|
|
3730
|
+
skill_store=_skills_mem_store,
|
|
3731
|
+
self_model=_sm_for_directed,
|
|
3732
|
+
)
|
|
3733
|
+
if _executor_svc is not None:
|
|
3734
|
+
set_initiative_executor(_executor_svc)
|
|
3735
|
+
asyncio.create_task(_executor_svc.start())
|
|
3736
|
+
logger.info("InitiativeExecutorService auto-start scheduled")
|
|
3737
|
+
except Exception as exc:
|
|
3738
|
+
logger.warning("InitiativeExecutorService init failed (non-fatal): %s", exc)
|
|
3739
|
+
|
|
3740
|
+
from apsimo.telemetry import TelemetryStore
|
|
3741
|
+
telemetry = TelemetryStore()
|
|
3742
|
+
telemetry.load() # restore last_*_at across restart (v0.21.0)
|
|
3743
|
+
telemetry.started_at = datetime.now(timezone.utc)
|
|
3744
|
+
app.state.telemetry = telemetry
|
|
3745
|
+
set_telemetry(telemetry)
|
|
3746
|
+
logger.info("TelemetryStore initialized")
|
|
3747
|
+
|
|
3748
|
+
# Session report store (cross-session context bridge)
|
|
3749
|
+
from apsimo.sessions.reports import SessionReportStore
|
|
3750
|
+
session_report_store = SessionReportStore()
|
|
3751
|
+
set_session_report_store(session_report_store)
|
|
3752
|
+
logger.info("SessionReportStore initialized")
|
|
3753
|
+
|
|
3754
|
+
# Register conversation synthesis task (periodic memory scan for goals)
|
|
3755
|
+
try:
|
|
3756
|
+
if (
|
|
3757
|
+
autonomy_config is not None
|
|
3758
|
+
and getattr(autonomy_config, "conversation_synthesis_enabled", True)
|
|
3759
|
+
and registry is not None
|
|
3760
|
+
and scheduler is not None
|
|
3761
|
+
):
|
|
3762
|
+
from apsimo.autonomy.synthesis import ConversationSynthesisTask
|
|
3763
|
+
_synthesis_task = ConversationSynthesisTask(
|
|
3764
|
+
registry=registry,
|
|
3765
|
+
lookback_hours=getattr(autonomy_config, "conversation_synthesis_lookback_hours", 2.0),
|
|
3766
|
+
min_confidence=getattr(autonomy_config, "conversation_synthesis_min_confidence", 0.35),
|
|
3767
|
+
telemetry=telemetry,
|
|
3768
|
+
)
|
|
3769
|
+
synthesis_interval = int(getattr(autonomy_config, "conversation_synthesis_interval_secs", 1800.0))
|
|
3770
|
+
scheduler.register(
|
|
3771
|
+
"conversation_synthesis",
|
|
3772
|
+
_synthesis_task.run,
|
|
3773
|
+
interval_seconds=synthesis_interval,
|
|
3774
|
+
metadata={"description": "Scan conversation memories for implicit goals and commitments"},
|
|
3775
|
+
)
|
|
3776
|
+
logger.info(
|
|
3777
|
+
"Conversation synthesis registered (lookback=%.1fh, interval=%ds, min_conf=%.2f)",
|
|
3778
|
+
getattr(autonomy_config, "conversation_synthesis_lookback_hours", 2.0),
|
|
3779
|
+
synthesis_interval,
|
|
3780
|
+
getattr(autonomy_config, "conversation_synthesis_min_confidence", 0.35),
|
|
3781
|
+
)
|
|
3782
|
+
except Exception as exc:
|
|
3783
|
+
logger.warning("Conversation synthesis registration failed: %s", exc)
|
|
3784
|
+
|
|
3785
|
+
logger.info("Sidecar capabilities: %s", supported_capabilities())
|
|
3786
|
+
|
|
3787
|
+
# Dedicated, owner-bound governed action execution. This ledger is
|
|
3788
|
+
# separate from the general API and task queue so a crash after an effect
|
|
3789
|
+
# starts can be recovered honestly as ambiguous without ever retrying the
|
|
3790
|
+
# mutation. No live action is enabled merely by constructing the service;
|
|
3791
|
+
# the exact scoped keyring principal is still required at the HTTP edge.
|
|
3792
|
+
governed_action_service = None
|
|
3793
|
+
try:
|
|
3794
|
+
import apsimo.api.routers.host as _host_actions
|
|
3795
|
+
from apsimo.api.routers.governed_actions import (
|
|
3796
|
+
set_governed_action_service,
|
|
3797
|
+
)
|
|
3798
|
+
from apsimo.governed_actions import (
|
|
3799
|
+
ColonySubsystemActionExecutor,
|
|
3800
|
+
GovernedActionLedger,
|
|
3801
|
+
GovernedActionService,
|
|
3802
|
+
)
|
|
3803
|
+
|
|
3804
|
+
async def _autonomy_enable():
|
|
3805
|
+
await _host_actions.autonomy_start()
|
|
3806
|
+
|
|
3807
|
+
async def _autonomy_disable():
|
|
3808
|
+
await _governed_autonomy_stop_signal(_host_actions._autonomy_loop)
|
|
3809
|
+
|
|
3810
|
+
def _autonomy_running():
|
|
3811
|
+
loop = _host_actions._autonomy_loop
|
|
3812
|
+
return bool(loop is not None and loop.is_running)
|
|
3813
|
+
|
|
3814
|
+
governed_action_service = GovernedActionService(
|
|
3815
|
+
GovernedActionLedger(
|
|
3816
|
+
state_dir / "governed-actions" / "ledger.db"
|
|
3817
|
+
),
|
|
3818
|
+
ColonySubsystemActionExecutor(
|
|
3819
|
+
graph=_host_actions._graph,
|
|
3820
|
+
goals=_host_actions._goals_store,
|
|
3821
|
+
commitments=_host_actions._commitment_store,
|
|
3822
|
+
initiatives=_host_actions._initiative_store,
|
|
3823
|
+
projects=_host_actions._project_engine,
|
|
3824
|
+
feedback=_host_actions._feedback_store,
|
|
3825
|
+
autonomy_enable=_autonomy_enable,
|
|
3826
|
+
autonomy_disable=_autonomy_disable,
|
|
3827
|
+
autonomy_running=_autonomy_running,
|
|
3828
|
+
),
|
|
3829
|
+
)
|
|
3830
|
+
set_governed_action_service(governed_action_service)
|
|
3831
|
+
logger.info("Governed action ledger initialized")
|
|
3832
|
+
except Exception as exc:
|
|
3833
|
+
logger.warning("Governed action ledger init failed: %s", exc)
|
|
3834
|
+
source_claim_task = None
|
|
3835
|
+
claims_enabled = os.environ.get("COLONY_SOURCE_CLAIMS", "on").strip().lower() in {"on", "1", "true"}
|
|
3836
|
+
from apsimo.vector import get_store as source_vector_store
|
|
3837
|
+
if claims_enabled or source_vector_store() is not None:
|
|
3838
|
+
from apsimo.turns import get_turn_idempotency_ledger
|
|
3839
|
+
from apsimo.beliefs.source_projection import run_source_claim_worker
|
|
3840
|
+
from apsimo.api.routers import host as source_claim_host
|
|
3841
|
+
source_claim_task = asyncio.create_task(run_source_claim_worker(
|
|
3842
|
+
get_turn_idempotency_ledger(state_dir), lambda: source_claim_host._llm_router,
|
|
3843
|
+
claims_enabled=claims_enabled))
|
|
3844
|
+
yield
|
|
3845
|
+
|
|
3846
|
+
if source_claim_task is not None:
|
|
3847
|
+
source_claim_task.cancel()
|
|
3848
|
+
try:
|
|
3849
|
+
await source_claim_task
|
|
3850
|
+
except asyncio.CancelledError:
|
|
3851
|
+
pass
|
|
3852
|
+
|
|
3853
|
+
# Shutdown — close connections
|
|
3854
|
+
set_external_event_intake(None)
|
|
3855
|
+
if _external_event_intake is not None:
|
|
3856
|
+
try:
|
|
3857
|
+
_external_event_intake.close()
|
|
3858
|
+
except Exception:
|
|
3859
|
+
logger.debug(
|
|
3860
|
+
"external cognition event intake shutdown failed",
|
|
3861
|
+
exc_info=True,
|
|
3862
|
+
)
|
|
3863
|
+
# Stop the queue-consuming bridge before its stores, governor, or queue.
|
|
3864
|
+
try:
|
|
3865
|
+
if agent_bridge_service is not None:
|
|
3866
|
+
await agent_bridge_service.stop()
|
|
3867
|
+
if agent_bridge_task is not None:
|
|
3868
|
+
try:
|
|
3869
|
+
await asyncio.wait_for(agent_bridge_task, timeout=5.0)
|
|
3870
|
+
except asyncio.TimeoutError:
|
|
3871
|
+
agent_bridge_task.cancel()
|
|
3872
|
+
try:
|
|
3873
|
+
await agent_bridge_task
|
|
3874
|
+
except asyncio.CancelledError:
|
|
3875
|
+
pass
|
|
3876
|
+
except Exception:
|
|
3877
|
+
logger.debug("Agent bridge shutdown error", exc_info=True)
|
|
3878
|
+
if graph is not None:
|
|
3879
|
+
try:
|
|
3880
|
+
await graph.close()
|
|
3881
|
+
except Exception:
|
|
3882
|
+
logger.debug("Graph close failed", exc_info=True)
|
|
3883
|
+
if world_store is not None:
|
|
3884
|
+
try:
|
|
3885
|
+
await world_store.close()
|
|
3886
|
+
except Exception:
|
|
3887
|
+
logger.debug("WorldStore close failed", exc_info=True)
|
|
3888
|
+
if skills_registry is not None:
|
|
3889
|
+
try:
|
|
3890
|
+
skills_registry.close()
|
|
3891
|
+
except Exception:
|
|
3892
|
+
logger.debug("SkillRegistry close failed", exc_info=True)
|
|
3893
|
+
set_llm_router(None)
|
|
3894
|
+
set_reasoning_loop(None)
|
|
3895
|
+
set_graph(None)
|
|
3896
|
+
set_response_gate(None)
|
|
3897
|
+
set_signal_collector(None)
|
|
3898
|
+
set_embedder(None)
|
|
3899
|
+
set_goals_engine(None)
|
|
3900
|
+
set_contacts_store(None)
|
|
3901
|
+
set_briefings_engine(None)
|
|
3902
|
+
set_world_store(None)
|
|
3903
|
+
set_metalearner(None)
|
|
3904
|
+
try:
|
|
3905
|
+
from apsimo.api.routers.host import (
|
|
3906
|
+
set_adaptive_params as _set_adaptive_params,
|
|
3907
|
+
set_benchmark as _set_benchmark,
|
|
3908
|
+
set_experiments as _set_experiments,
|
|
3909
|
+
set_learning_feedback_store as _set_learning_feedback_store,
|
|
3910
|
+
)
|
|
3911
|
+
_set_experiments(None)
|
|
3912
|
+
_set_benchmark(None)
|
|
3913
|
+
_set_learning_feedback_store(None)
|
|
3914
|
+
_set_adaptive_params(None)
|
|
3915
|
+
if _adaptive_params is not None:
|
|
3916
|
+
_adaptive_params.close()
|
|
3917
|
+
except Exception:
|
|
3918
|
+
logger.debug("controlled learning shutdown failed", exc_info=True)
|
|
3919
|
+
set_research_pipeline(None)
|
|
3920
|
+
set_delivery_bridge(None)
|
|
3921
|
+
set_connection_discoverer(None)
|
|
3922
|
+
set_learner(None)
|
|
3923
|
+
set_skills_registry(None)
|
|
3924
|
+
set_commitment_store(None)
|
|
3925
|
+
set_affect_store(None)
|
|
3926
|
+
try:
|
|
3927
|
+
if _p8_wiring is not None:
|
|
3928
|
+
_p8_wiring.close()
|
|
3929
|
+
except Exception:
|
|
3930
|
+
logger.debug("P8 runtime shutdown failed", exc_info=True)
|
|
3931
|
+
set_p8_runtime(None)
|
|
3932
|
+
set_facts_store(None)
|
|
3933
|
+
try:
|
|
3934
|
+
from apsimo.api.routers.host import (
|
|
3935
|
+
set_presence_store as _set_presence_store,
|
|
3936
|
+
_presence_store as _presence_ref,
|
|
3937
|
+
)
|
|
3938
|
+
if _presence_ref is not None:
|
|
3939
|
+
_presence_ref.close()
|
|
3940
|
+
_set_presence_store(None)
|
|
3941
|
+
except Exception:
|
|
3942
|
+
logger.debug("presence store shutdown failed", exc_info=True)
|
|
3943
|
+
set_context_provenance_store(None)
|
|
3944
|
+
set_response_guard(None)
|
|
3945
|
+
try:
|
|
3946
|
+
from apsimo.tom.levels import set_evidence_probe
|
|
3947
|
+
set_evidence_probe(None)
|
|
3948
|
+
except Exception:
|
|
3949
|
+
logger.debug("Tom2 evidence probe shutdown failed", exc_info=True)
|
|
3950
|
+
set_pattern_store(None)
|
|
3951
|
+
set_surprise_store(None)
|
|
3952
|
+
set_tom_extractor(None)
|
|
3953
|
+
if channel_store is not None:
|
|
3954
|
+
try:
|
|
3955
|
+
channel_store.close()
|
|
3956
|
+
except Exception:
|
|
3957
|
+
logger.debug("ChannelStore close failed", exc_info=True)
|
|
3958
|
+
try:
|
|
3959
|
+
from apsimo.channels.router import set_channel_store as _set_ch_store
|
|
3960
|
+
_set_ch_store(None)
|
|
3961
|
+
except Exception:
|
|
3962
|
+
pass
|
|
3963
|
+
set_chain_manager(None)
|
|
3964
|
+
set_secrets_manager(None)
|
|
3965
|
+
set_session_store(None)
|
|
3966
|
+
set_session_report_store(None)
|
|
3967
|
+
set_agent_bridge(None)
|
|
3968
|
+
set_initiative_executor(None)
|
|
3969
|
+
# Stop worker node (before queue so in-flight jobs can drain).
|
|
3970
|
+
try:
|
|
3971
|
+
worker = getattr(app.state, "worker", None)
|
|
3972
|
+
if worker is not None:
|
|
3973
|
+
await worker.stop(drain_timeout=10.0)
|
|
3974
|
+
worker_task = getattr(app.state, "worker_task", None)
|
|
3975
|
+
if worker_task is not None:
|
|
3976
|
+
worker_task.cancel()
|
|
3977
|
+
except Exception:
|
|
3978
|
+
logger.debug("Worker shutdown error", exc_info=True)
|
|
3979
|
+
# Stop queue maintenance only after the execution worker has drained, and
|
|
3980
|
+
# before closing the shared queue connection.
|
|
3981
|
+
try:
|
|
3982
|
+
if queue_scheduler is not None:
|
|
3983
|
+
await queue_scheduler.stop()
|
|
3984
|
+
if queue_scheduler_task is not None:
|
|
3985
|
+
try:
|
|
3986
|
+
await asyncio.wait_for(queue_scheduler_task, timeout=5.0)
|
|
3987
|
+
except asyncio.TimeoutError:
|
|
3988
|
+
queue_scheduler_task.cancel()
|
|
3989
|
+
try:
|
|
3990
|
+
await queue_scheduler_task
|
|
3991
|
+
except asyncio.CancelledError:
|
|
3992
|
+
pass
|
|
3993
|
+
except Exception:
|
|
3994
|
+
logger.debug("Task queue scheduler shutdown error", exc_info=True)
|
|
3995
|
+
# No stopped/repeated lifespan may retain a usable authority handle.
|
|
3996
|
+
set_worker_governor(None)
|
|
3997
|
+
# Stop task queue
|
|
3998
|
+
try:
|
|
3999
|
+
from apsimo.api.routers.host import _task_queue
|
|
4000
|
+
if _task_queue is not None:
|
|
4001
|
+
await _task_queue.queue.stop()
|
|
4002
|
+
except Exception:
|
|
4003
|
+
logger.warning("Task queue shutdown failed")
|
|
4004
|
+
set_task_queue(None)
|
|
4005
|
+
# Stop autonomy loop if running
|
|
4006
|
+
try:
|
|
4007
|
+
from apsimo.api.routers.host import _autonomy_loop
|
|
4008
|
+
if _autonomy_loop is not None and _autonomy_loop.is_running:
|
|
4009
|
+
await _autonomy_loop.stop()
|
|
4010
|
+
except Exception:
|
|
4011
|
+
logger.warning("Autonomy loop shutdown failed")
|
|
4012
|
+
set_autonomy_loop(None)
|
|
4013
|
+
# Evidence/P6 periodic reducers are owned by the autonomy scheduler. Close
|
|
4014
|
+
# them only after that loop is stopped so an in-flight tick cannot race a
|
|
4015
|
+
# closed SQLite handle. Clear HTTP handles before releasing connections.
|
|
4016
|
+
set_cognition_evidence(None, None, None, {
|
|
4017
|
+
"configured_mode": "off",
|
|
4018
|
+
"state": "off",
|
|
4019
|
+
"reason": "sidecar_stopped",
|
|
4020
|
+
})
|
|
4021
|
+
try:
|
|
4022
|
+
evidence_wiring = locals().get("_evidence_wiring")
|
|
4023
|
+
if evidence_wiring is not None and evidence_wiring.get("store") is not None:
|
|
4024
|
+
evidence_wiring["store"].close()
|
|
4025
|
+
except Exception:
|
|
4026
|
+
logger.debug("cognition evidence shutdown failed", exc_info=True)
|
|
4027
|
+
set_situation_spine(None, None)
|
|
4028
|
+
set_drive_governance(None, None, None)
|
|
4029
|
+
try:
|
|
4030
|
+
situation_wiring = locals().get("_situation_wiring")
|
|
4031
|
+
if situation_wiring is not None:
|
|
4032
|
+
original = situation_wiring.get("original_validator")
|
|
4033
|
+
cognition = locals().get("_cognition_spine")
|
|
4034
|
+
if original is not None and cognition is not None:
|
|
4035
|
+
cognition._situation = original
|
|
4036
|
+
situation_wiring["store"].close()
|
|
4037
|
+
except Exception:
|
|
4038
|
+
logger.debug("situation spine shutdown failed", exc_info=True)
|
|
4039
|
+
try:
|
|
4040
|
+
drive_wiring = locals().get("_drive_wiring")
|
|
4041
|
+
if drive_wiring is not None:
|
|
4042
|
+
drive_wiring["store"].close()
|
|
4043
|
+
except Exception:
|
|
4044
|
+
logger.debug("drive governance shutdown failed", exc_info=True)
|
|
4045
|
+
try:
|
|
4046
|
+
from apsimo.api.routers.host import set_project_engine
|
|
4047
|
+
set_project_engine(None)
|
|
4048
|
+
project_store = locals().get("_project_store")
|
|
4049
|
+
if project_store is not None:
|
|
4050
|
+
project_store.close()
|
|
4051
|
+
except Exception:
|
|
4052
|
+
logger.debug("project store shutdown failed", exc_info=True)
|
|
4053
|
+
set_session_store(None)
|
|
4054
|
+
set_task_queue(None)
|
|
4055
|
+
# Multi-Agent cleanup
|
|
4056
|
+
set_agent_store(None)
|
|
4057
|
+
set_invite_store(None)
|
|
4058
|
+
set_initiative_store(None)
|
|
4059
|
+
try:
|
|
4060
|
+
from apsimo.api.routers.governed_actions import (
|
|
4061
|
+
set_governed_action_service,
|
|
4062
|
+
)
|
|
4063
|
+
set_governed_action_service(None)
|
|
4064
|
+
if governed_action_service is not None:
|
|
4065
|
+
governed_action_service.close()
|
|
4066
|
+
except Exception:
|
|
4067
|
+
logger.debug("Governed action ledger shutdown failed", exc_info=True)
|
|
4068
|
+
set_assignment_engine(None)
|
|
4069
|
+
set_websocket_manager(None)
|
|
4070
|
+
try:
|
|
4071
|
+
from apsimo.api.routers.observations import set_observation_store
|
|
4072
|
+
set_observation_store(None)
|
|
4073
|
+
except Exception:
|
|
4074
|
+
pass
|
|
4075
|
+
auth_telemetry = getattr(app.state, "auth_telemetry", None)
|
|
4076
|
+
if auth_telemetry is not None:
|
|
4077
|
+
try:
|
|
4078
|
+
auth_telemetry.close()
|
|
4079
|
+
except Exception:
|
|
4080
|
+
logger.debug("Auth telemetry shutdown failed", exc_info=True)
|
|
4081
|
+
logger.info("Sidecar shutdown complete")
|
|
4082
|
+
|
|
4083
|
+
|
|
4084
|
+
def create_app() -> FastAPI:
|
|
4085
|
+
"""Build and return the FastAPI application."""
|
|
4086
|
+
app = FastAPI(
|
|
4087
|
+
title="Apsimo",
|
|
4088
|
+
version="0.1.0",
|
|
4089
|
+
lifespan=lifespan,
|
|
4090
|
+
)
|
|
4091
|
+
|
|
4092
|
+
# API authentication (skips health/docs; loopback dev mode if no auth set).
|
|
4093
|
+
# The scoped keyring runs alongside COLONY_API_KEY during migration.
|
|
4094
|
+
from apsimo.api.auth_telemetry import AuthTelemetry
|
|
4095
|
+
from apsimo.api.contact_grants import ContactGrantRegistry
|
|
4096
|
+
from apsimo.api.middleware import ApiKeyMiddleware, BodySizeLimitMiddleware
|
|
4097
|
+
|
|
4098
|
+
# Body-size cap runs before auth so oversized payloads are rejected with
|
|
4099
|
+
# 413 regardless of the auth state.
|
|
4100
|
+
try:
|
|
4101
|
+
max_body = int(os.environ.get("COLONY_MAX_BODY_BYTES", "") or 10 * 1024 * 1024)
|
|
4102
|
+
except ValueError:
|
|
4103
|
+
max_body = 10 * 1024 * 1024
|
|
4104
|
+
app.add_middleware(BodySizeLimitMiddleware, max_bytes=max_body)
|
|
4105
|
+
|
|
4106
|
+
api_key = os.environ.get("COLONY_API_KEY")
|
|
4107
|
+
keyring_path = os.environ.get("COLONY_API_KEYRING_PATH")
|
|
4108
|
+
state_dir = _state_dir()
|
|
4109
|
+
telemetry_path = os.environ.get(
|
|
4110
|
+
"COLONY_AUTH_TELEMETRY_PATH",
|
|
4111
|
+
str(state_dir / "colony-auth-telemetry.db"),
|
|
4112
|
+
)
|
|
4113
|
+
contact_grants_path = os.environ.get(
|
|
4114
|
+
"COLONY_API_CONTACT_GRANTS_PATH",
|
|
4115
|
+
str(state_dir / "api-contact-grants.json"),
|
|
4116
|
+
)
|
|
4117
|
+
auth_telemetry = AuthTelemetry(telemetry_path)
|
|
4118
|
+
contact_grants = ContactGrantRegistry(contact_grants_path)
|
|
4119
|
+
# HTTP middleware uses request.state; WebSocket/admin status paths use
|
|
4120
|
+
# app.state because BaseHTTPMiddleware does not wrap WebSocket frames.
|
|
4121
|
+
app.state.auth_telemetry = auth_telemetry
|
|
4122
|
+
app.state.contact_grants = contact_grants
|
|
4123
|
+
app.add_middleware(
|
|
4124
|
+
ApiKeyMiddleware,
|
|
4125
|
+
api_key=api_key,
|
|
4126
|
+
keyring_path=keyring_path,
|
|
4127
|
+
auth_telemetry=auth_telemetry,
|
|
4128
|
+
contact_grants=contact_grants,
|
|
4129
|
+
)
|
|
4130
|
+
if api_key and keyring_path:
|
|
4131
|
+
logger.info("Legacy and scoped API authentication enabled (dual-accept)")
|
|
4132
|
+
elif keyring_path:
|
|
4133
|
+
logger.info("Scoped API authentication enabled")
|
|
4134
|
+
elif api_key:
|
|
4135
|
+
logger.info("Legacy API key authentication enabled")
|
|
4136
|
+
else:
|
|
4137
|
+
logger.warning("No API auth configured — API is open (loopback dev mode)")
|
|
4138
|
+
|
|
4139
|
+
app.include_router(host_router)
|
|
4140
|
+
app.include_router(host_v2_router)
|
|
4141
|
+
from apsimo.api.routers import executions as executions_router
|
|
4142
|
+
app.include_router(executions_router.router)
|
|
4143
|
+
from apsimo.api.routers import commitment_work as commitment_work_router
|
|
4144
|
+
app.include_router(commitment_work_router.router)
|
|
4145
|
+
from apsimo.api.routers import initiative_work as initiative_work_router
|
|
4146
|
+
app.include_router(initiative_work_router.router)
|
|
4147
|
+
from apsimo.api.routers import social_state as social_state_router
|
|
4148
|
+
app.include_router(social_state_router.router)
|
|
4149
|
+
from apsimo.api.routers import temporal_followups as temporal_followups_router
|
|
4150
|
+
app.include_router(temporal_followups_router.router)
|
|
4151
|
+
from apsimo.api.routers import transport as transport_router
|
|
4152
|
+
app.include_router(transport_router.router)
|
|
4153
|
+
from apsimo.api.routers import followup_plans
|
|
4154
|
+
app.include_router(followup_plans.router)
|
|
4155
|
+
|
|
4156
|
+
# Exact PUT/GET action endpoint. Middleware maps these methods to
|
|
4157
|
+
# actions:execute/actions:verify; the router independently rejects legacy,
|
|
4158
|
+
# unscoped, non-owner, and differently named principals.
|
|
4159
|
+
from apsimo.api.routers import governed_actions as governed_actions_router
|
|
4160
|
+
app.include_router(governed_actions_router.router)
|
|
4161
|
+
|
|
4162
|
+
# Channel registration router
|
|
4163
|
+
from apsimo.channels.router import router as channels_router
|
|
4164
|
+
app.include_router(channels_router)
|
|
4165
|
+
|
|
4166
|
+
# Task queue router (v0.13.0)
|
|
4167
|
+
from apsimo.api.routers import task_queue as task_queue_router
|
|
4168
|
+
app.include_router(task_queue_router.router)
|
|
4169
|
+
|
|
4170
|
+
# Observations router (v0.16.0) — agent-as-sensor ingestion
|
|
4171
|
+
from apsimo.api.routers import observations as observations_router
|
|
4172
|
+
app.include_router(observations_router.router)
|
|
4173
|
+
from apsimo.api.routers import mining as mining_router
|
|
4174
|
+
app.include_router(mining_router.router)
|
|
4175
|
+
|
|
4176
|
+
# Context gate (v0.32.0) — budget-aware context preparation
|
|
4177
|
+
from apsimo.api.routers import context_gate as context_gate_router
|
|
4178
|
+
app.include_router(context_gate_router.router)
|
|
4179
|
+
|
|
4180
|
+
# MCP streamable HTTP endpoint
|
|
4181
|
+
try:
|
|
4182
|
+
from apsimo.mcp.server import create_server
|
|
4183
|
+
mcp_server = create_server()
|
|
4184
|
+
# Mount MCP ASGI app at /mcp
|
|
4185
|
+
mcp_asgi = mcp_server.streamable_http_app()
|
|
4186
|
+
app.mount("/mcp", mcp_asgi)
|
|
4187
|
+
logger.info("MCP endpoint mounted at /mcp (streamable HTTP)")
|
|
4188
|
+
except ImportError:
|
|
4189
|
+
logger.debug("MCP SDK not installed — /mcp endpoint not available (install colonyai[mcp])")
|
|
4190
|
+
except Exception as exc:
|
|
4191
|
+
logger.warning("Could not mount MCP endpoint: %s", exc)
|
|
4192
|
+
|
|
4193
|
+
return app
|
|
4194
|
+
|
|
4195
|
+
|
|
4196
|
+
# Uvicorn entry point
|
|
4197
|
+
app = create_app()
|