apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Server-owned routing contracts for the shared ``agent_action`` job type."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
AGENT_SYNC_ROUTE = "agent_sync:v1"
|
|
10
|
+
ACTION_PLANE_ROUTE = "action_plane:v1"
|
|
11
|
+
WORK_ORDER_ROUTE = "work_order:v1"
|
|
12
|
+
HERMES_RUN_ROUTE = "hermes_run:v1"
|
|
13
|
+
THOUGHT_ROUTE = "thought_engine:v1"
|
|
14
|
+
|
|
15
|
+
AGENT_ACTION_ROUTE_CAPABILITIES = frozenset({
|
|
16
|
+
AGENT_SYNC_ROUTE,
|
|
17
|
+
ACTION_PLANE_ROUTE,
|
|
18
|
+
WORK_ORDER_ROUTE,
|
|
19
|
+
HERMES_RUN_ROUTE,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
_ROUTE_OWNER_ENV = {
|
|
23
|
+
AGENT_SYNC_ROUTE: "COLONY_AGENT_SYNC_WORKER_NODE_ID",
|
|
24
|
+
HERMES_RUN_ROUTE: "COLONY_HERMES_RUN_WORKER_NODE_ID",
|
|
25
|
+
ACTION_PLANE_ROUTE: "COLONY_ACTION_PLANE_WORKER_NODE_ID",
|
|
26
|
+
WORK_ORDER_ROUTE: "COLONY_ACTION_PLANE_WORKER_NODE_ID",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generic_agent_job_claims_enabled() -> bool:
|
|
31
|
+
"""Resolve the global generic agent-action containment switch."""
|
|
32
|
+
|
|
33
|
+
value = os.environ.get(
|
|
34
|
+
"COLONY_AGENT_JOB_CLAIMS_ENABLED", "true"
|
|
35
|
+
).strip().lower()
|
|
36
|
+
if value in {"1", "true", "yes", "on"}:
|
|
37
|
+
return True
|
|
38
|
+
if value in {"0", "false", "no", "off"}:
|
|
39
|
+
return False
|
|
40
|
+
# An invalid containment value never silently enables generic claims.
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def expected_agent_action_routes(job: Any) -> tuple[str, ...]:
|
|
45
|
+
"""Derive exact executor lanes from server-known schema/action policy."""
|
|
46
|
+
|
|
47
|
+
payload = getattr(job, "payload", {}) or {}
|
|
48
|
+
if payload.get("schema") == "WorkOrderV1":
|
|
49
|
+
from apsimo.work_orders import WorkOrderV1
|
|
50
|
+
|
|
51
|
+
order = WorkOrderV1.from_payload(payload)
|
|
52
|
+
if str(getattr(job, "job_id", "")) != order.work_order_id:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"WorkOrder queue job ID does not match authority digest"
|
|
55
|
+
)
|
|
56
|
+
# WorkOrders are receipt-aware Action Plane work. Requiring both
|
|
57
|
+
# capabilities prevents a generic protocol-aware worker from racing
|
|
58
|
+
# the deployment's authorized effect executor.
|
|
59
|
+
return (WORK_ORDER_ROUTE, ACTION_PLANE_ROUTE)
|
|
60
|
+
|
|
61
|
+
action_hint = str(payload.get("action_hint") or "").strip()
|
|
62
|
+
if action_hint:
|
|
63
|
+
from apsimo.initiatives.action_registry import (
|
|
64
|
+
RiskTier,
|
|
65
|
+
get_action,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
spec = get_action(action_hint)
|
|
69
|
+
if spec is not None:
|
|
70
|
+
canonical_risk = spec.risk.value
|
|
71
|
+
declared_risk = str(payload.get("risk") or "").strip().lower()
|
|
72
|
+
if declared_risk and declared_risk != canonical_risk:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"agent_action risk does not match server registry"
|
|
75
|
+
)
|
|
76
|
+
if spec.risk is RiskTier.READ_ONLY:
|
|
77
|
+
from apsimo.task_queue.governor import (
|
|
78
|
+
job_declares_effect,
|
|
79
|
+
)
|
|
80
|
+
if job_declares_effect(job):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"read-only agent action declares effect capabilities"
|
|
83
|
+
)
|
|
84
|
+
return (AGENT_SYNC_ROUTE,)
|
|
85
|
+
return (ACTION_PLANE_ROUTE,)
|
|
86
|
+
raise ValueError("agent_action action_hint is not server-registered")
|
|
87
|
+
|
|
88
|
+
from apsimo.task_queue.governor import job_declares_effect
|
|
89
|
+
|
|
90
|
+
if job_declares_effect(job):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"effectful agent_action has no server-registered executor route"
|
|
93
|
+
)
|
|
94
|
+
if payload.get("schema") == "HermesRunV1":
|
|
95
|
+
return (HERMES_RUN_ROUTE,)
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"agent_action requires WorkOrderV1, HermesRunV1, or a registered action"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def bind_agent_action_routes(
|
|
102
|
+
job: Any,
|
|
103
|
+
*,
|
|
104
|
+
allow_legacy_migration: bool = False,
|
|
105
|
+
) -> tuple[str, ...]:
|
|
106
|
+
"""Add derived route requirements and reject caller-selected mismatch."""
|
|
107
|
+
|
|
108
|
+
expected = expected_agent_action_routes(job)
|
|
109
|
+
payload = dict(getattr(job, "payload", None) or {})
|
|
110
|
+
action_hint = str(payload.get("action_hint") or "").strip()
|
|
111
|
+
if action_hint and payload.get("schema") != "WorkOrderV1":
|
|
112
|
+
from apsimo.initiatives.action_registry import get_action
|
|
113
|
+
|
|
114
|
+
spec = get_action(action_hint)
|
|
115
|
+
if spec is not None:
|
|
116
|
+
payload["risk"] = spec.risk.value
|
|
117
|
+
job.payload = payload
|
|
118
|
+
capabilities = list(getattr(job, "capabilities", None) or ())
|
|
119
|
+
names = {
|
|
120
|
+
str(getattr(capability, "name", capability))
|
|
121
|
+
for capability in capabilities
|
|
122
|
+
}
|
|
123
|
+
supplied_routes = names & AGENT_ACTION_ROUTE_CAPABILITIES
|
|
124
|
+
if (
|
|
125
|
+
supplied_routes
|
|
126
|
+
and supplied_routes != set(expected)
|
|
127
|
+
and not (
|
|
128
|
+
allow_legacy_migration
|
|
129
|
+
and payload.get("schema") == "WorkOrderV1"
|
|
130
|
+
)
|
|
131
|
+
):
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"agent_action route capabilities do not match server policy"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
from apsimo.task_queue.models import JobCapabilityRequirement
|
|
137
|
+
|
|
138
|
+
# Caller/legacy metadata can never weaken a route by marking it preferred,
|
|
139
|
+
# adding a nonsensical numeric minimum, or duplicating it. Replace every
|
|
140
|
+
# route entry with one canonical required requirement.
|
|
141
|
+
if payload.get("schema") == "WorkOrderV1":
|
|
142
|
+
from apsimo.work_orders import WorkOrderV1
|
|
143
|
+
|
|
144
|
+
order = WorkOrderV1.from_payload(payload)
|
|
145
|
+
# Compatibility fields consumed by the host are reconstructed from the
|
|
146
|
+
# verified authority object. Unknown executable-looking extras and
|
|
147
|
+
# caller-edited description/context never survive queue admission.
|
|
148
|
+
payload = order.payload()
|
|
149
|
+
job.payload = payload
|
|
150
|
+
capability_names = (
|
|
151
|
+
WORK_ORDER_ROUTE,
|
|
152
|
+
ACTION_PLANE_ROUTE,
|
|
153
|
+
*order.capability_allowlist,
|
|
154
|
+
)
|
|
155
|
+
# The embedded authority allowlist is the complete executor contract;
|
|
156
|
+
# arbitrary/missing queue metadata must not broaden or weaken it.
|
|
157
|
+
capabilities = []
|
|
158
|
+
else:
|
|
159
|
+
capability_names = expected
|
|
160
|
+
capabilities = [
|
|
161
|
+
capability for capability in capabilities
|
|
162
|
+
if str(getattr(capability, "name", capability))
|
|
163
|
+
not in AGENT_ACTION_ROUTE_CAPABILITIES
|
|
164
|
+
]
|
|
165
|
+
capabilities.extend(
|
|
166
|
+
JobCapabilityRequirement(
|
|
167
|
+
name=name,
|
|
168
|
+
minimum=None,
|
|
169
|
+
preferred=False,
|
|
170
|
+
)
|
|
171
|
+
for name in dict.fromkeys(capability_names)
|
|
172
|
+
)
|
|
173
|
+
job.capabilities = capabilities
|
|
174
|
+
tags = dict(getattr(job, "tags", None) or {})
|
|
175
|
+
tags["agent_action_route"] = "+".join(expected)
|
|
176
|
+
if payload.get("schema") == "WorkOrderV1":
|
|
177
|
+
tags.update({
|
|
178
|
+
"schema": order.schema,
|
|
179
|
+
"project_id": order.project_id,
|
|
180
|
+
"step_id": order.step_id,
|
|
181
|
+
"risk_class": order.risk_class,
|
|
182
|
+
"idempotency_key": order.idempotency_key,
|
|
183
|
+
"work_order_digest": order.work_order_digest,
|
|
184
|
+
"work_order_version": str(order.version),
|
|
185
|
+
"executor_protocol": WORK_ORDER_ROUTE,
|
|
186
|
+
"action_result_contract": "ExecutionResultV1",
|
|
187
|
+
})
|
|
188
|
+
elif expected == (ACTION_PLANE_ROUTE,):
|
|
189
|
+
from apsimo.initiatives.approval_authority import (
|
|
190
|
+
build_action_binding,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
binding = build_action_binding(
|
|
194
|
+
job_id=str(getattr(job, "job_id", "")),
|
|
195
|
+
job_type=str(
|
|
196
|
+
getattr(getattr(job, "job_type", None), "value", "")
|
|
197
|
+
or getattr(job, "job_type", "")
|
|
198
|
+
),
|
|
199
|
+
payload=payload,
|
|
200
|
+
)
|
|
201
|
+
tags.update({
|
|
202
|
+
"action_digest": binding.action_digest,
|
|
203
|
+
"action_result_contract": "ActionReceiptAttestationV1",
|
|
204
|
+
})
|
|
205
|
+
owners = {
|
|
206
|
+
os.environ.get(_ROUTE_OWNER_ENV[route], "").strip()
|
|
207
|
+
for route in expected
|
|
208
|
+
if os.environ.get(_ROUTE_OWNER_ENV[route], "").strip()
|
|
209
|
+
}
|
|
210
|
+
if len(owners) > 1:
|
|
211
|
+
raise ValueError("agent_action route owners conflict")
|
|
212
|
+
if owners:
|
|
213
|
+
tags["agent_action_route_node"] = next(iter(owners))
|
|
214
|
+
else:
|
|
215
|
+
tags.pop("agent_action_route_node", None)
|
|
216
|
+
job.tags = tags
|
|
217
|
+
return expected
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def worker_route_capabilities(*, sync: bool, text: bool) -> set[str]:
|
|
221
|
+
"""Build a generic non-effectful worker's explicit route set."""
|
|
222
|
+
|
|
223
|
+
routes: set[str] = set()
|
|
224
|
+
if sync:
|
|
225
|
+
routes.add(AGENT_SYNC_ROUTE)
|
|
226
|
+
if text:
|
|
227
|
+
routes.add(HERMES_RUN_ROUTE)
|
|
228
|
+
return routes
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def bind_thought_route(job: Any) -> str:
|
|
232
|
+
"""Canonicalize the private ThoughtJobV1 worker route."""
|
|
233
|
+
|
|
234
|
+
payload = getattr(job, "payload", {}) or {}
|
|
235
|
+
from apsimo.cognition.goal_spine import ThoughtJobV1
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
thought = ThoughtJobV1.from_payload(payload)
|
|
239
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
240
|
+
raise ValueError(f"invalid ThoughtJobV1 payload: {exc}") from exc
|
|
241
|
+
if str(getattr(job, "job_id", "")) != thought.thought_job_id:
|
|
242
|
+
raise ValueError("thought queue job ID does not match authority digest")
|
|
243
|
+
from apsimo.task_queue.models import JobCapabilityRequirement
|
|
244
|
+
|
|
245
|
+
job.capabilities = [
|
|
246
|
+
JobCapabilityRequirement(name="cognition_scoped"),
|
|
247
|
+
JobCapabilityRequirement(name=THOUGHT_ROUTE),
|
|
248
|
+
]
|
|
249
|
+
tags = dict(getattr(job, "tags", None) or {})
|
|
250
|
+
tags["thought_route"] = THOUGHT_ROUTE
|
|
251
|
+
tags.update({
|
|
252
|
+
"schema": thought.schema,
|
|
253
|
+
"concern_id": thought.concern_id,
|
|
254
|
+
"viewer_scope": thought.viewer_scope,
|
|
255
|
+
"shareability": thought.shareability,
|
|
256
|
+
"thought_job_digest": thought.thought_job_digest,
|
|
257
|
+
"risk_class": "read_only",
|
|
258
|
+
})
|
|
259
|
+
owner = os.environ.get("COLONY_THOUGHT_WORKER_NODE_ID", "").strip()
|
|
260
|
+
if owner:
|
|
261
|
+
tags["thought_route_node"] = owner
|
|
262
|
+
else:
|
|
263
|
+
tags.pop("thought_route_node", None)
|
|
264
|
+
job.tags = tags
|
|
265
|
+
return THOUGHT_ROUTE
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def configured_route_owners() -> dict[str, str | None]:
|
|
269
|
+
"""Return deployment-pinned owners by logical executor lane."""
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"agent_sync": os.environ.get(
|
|
273
|
+
"COLONY_AGENT_SYNC_WORKER_NODE_ID", ""
|
|
274
|
+
).strip() or None,
|
|
275
|
+
"action_plane": os.environ.get(
|
|
276
|
+
"COLONY_ACTION_PLANE_WORKER_NODE_ID", ""
|
|
277
|
+
).strip() or None,
|
|
278
|
+
"work_order": os.environ.get(
|
|
279
|
+
"COLONY_ACTION_PLANE_WORKER_NODE_ID", ""
|
|
280
|
+
).strip() or None,
|
|
281
|
+
"hermes_run": os.environ.get(
|
|
282
|
+
"COLONY_HERMES_RUN_WORKER_NODE_ID", ""
|
|
283
|
+
).strip() or None,
|
|
284
|
+
"thought": os.environ.get(
|
|
285
|
+
"COLONY_THOUGHT_WORKER_NODE_ID", ""
|
|
286
|
+
).strip() or None,
|
|
287
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Distributed Task Queue — Queen Scheduler.
|
|
2
|
+
|
|
3
|
+
Runs on the Sovereign node. Drives five scheduling phases per tick:
|
|
4
|
+
1. Expire past deadlines
|
|
5
|
+
2. Abandon silent jobs (heartbeat timeout)
|
|
6
|
+
3. Requeue retryable jobs
|
|
7
|
+
4. Reconcile explicit governance holds
|
|
8
|
+
5. Unblock ready jobs (deps met)
|
|
9
|
+
6. Assign queued jobs to available workers
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, Callable, Optional
|
|
18
|
+
|
|
19
|
+
from apsimo.task_queue.models import WorkerCapabilities
|
|
20
|
+
from apsimo.task_queue.queue_manager import QueueManager
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Scheduler:
|
|
26
|
+
"""Priority-aware, capability-matching job scheduler.
|
|
27
|
+
|
|
28
|
+
Runs on the Sovereign node. The Regent runs a passive replica
|
|
29
|
+
and takes over if the Sovereign is unavailable.
|
|
30
|
+
|
|
31
|
+
Usage::
|
|
32
|
+
|
|
33
|
+
scheduler = Scheduler(queue=queue_manager)
|
|
34
|
+
task = asyncio.create_task(scheduler.run())
|
|
35
|
+
...
|
|
36
|
+
await scheduler.stop()
|
|
37
|
+
await task
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
queue: QueueManager,
|
|
43
|
+
tick_interval_secs: float = 2.0,
|
|
44
|
+
heartbeat_timeout_secs: Optional[float] = None,
|
|
45
|
+
claim_timeout_secs: float = 30.0,
|
|
46
|
+
no_worker_warning_secs: float = 300.0,
|
|
47
|
+
event_bus: Optional[Any] = None,
|
|
48
|
+
readiness_callback: Optional[Callable[[bool, str], None]] = None,
|
|
49
|
+
approval_timeout_hours: Optional[float] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._queue = queue
|
|
52
|
+
self._tick = tick_interval_secs
|
|
53
|
+
if heartbeat_timeout_secs is not None:
|
|
54
|
+
# Backward-compatible constructor overrides update the queue-owned
|
|
55
|
+
# value rather than creating a second liveness interpretation.
|
|
56
|
+
self._queue.configure_worker_heartbeat_ttl(
|
|
57
|
+
heartbeat_timeout_secs
|
|
58
|
+
)
|
|
59
|
+
self._claim_timeout = claim_timeout_secs
|
|
60
|
+
self._no_worker_warning = no_worker_warning_secs
|
|
61
|
+
self._event_bus = event_bus
|
|
62
|
+
self._readiness_callback = readiness_callback
|
|
63
|
+
if approval_timeout_hours is None:
|
|
64
|
+
try:
|
|
65
|
+
import os
|
|
66
|
+
approval_timeout_hours = float(os.environ.get(
|
|
67
|
+
"COLONY_APPROVAL_TIMEOUT_HOURS", "72"
|
|
68
|
+
))
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
approval_timeout_hours = 72.0
|
|
71
|
+
if approval_timeout_hours <= 0:
|
|
72
|
+
raise ValueError("approval_timeout_hours must be positive")
|
|
73
|
+
self._approval_timeout_hours = approval_timeout_hours
|
|
74
|
+
self._running = False
|
|
75
|
+
self._last_tick_at: Optional[str] = None
|
|
76
|
+
self._last_error: Optional[str] = None
|
|
77
|
+
self._tick_count = 0
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def worker_heartbeat_ttl_secs(self) -> float:
|
|
81
|
+
return self._queue.worker_heartbeat_ttl_secs
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def health(self) -> dict:
|
|
85
|
+
return {
|
|
86
|
+
"running": self._running,
|
|
87
|
+
"healthy": self._last_error is None,
|
|
88
|
+
"last_tick_at": self._last_tick_at,
|
|
89
|
+
"last_error": self._last_error,
|
|
90
|
+
"tick_count": self._tick_count,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async def run(self) -> None:
|
|
94
|
+
"""Main scheduling loop. Blocks until stop() is called."""
|
|
95
|
+
self._running = True
|
|
96
|
+
logger.info("Scheduler started (tick=%.1fs)", self._tick)
|
|
97
|
+
while self._running:
|
|
98
|
+
try:
|
|
99
|
+
await self.tick_once()
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
self._last_error = str(exc)[:500]
|
|
102
|
+
if self._readiness_callback is not None:
|
|
103
|
+
self._readiness_callback(False, self._last_error)
|
|
104
|
+
logger.exception("Scheduler tick failed")
|
|
105
|
+
await asyncio.sleep(self._tick)
|
|
106
|
+
logger.info("Scheduler stopped")
|
|
107
|
+
|
|
108
|
+
async def stop(self) -> None:
|
|
109
|
+
"""Signal the scheduling loop to exit after the current tick."""
|
|
110
|
+
self._running = False
|
|
111
|
+
if self._readiness_callback is not None:
|
|
112
|
+
self._readiness_callback(False, "scheduler_stopped")
|
|
113
|
+
|
|
114
|
+
async def tick_once(self) -> None:
|
|
115
|
+
"""Public single-tick for testing or manual scheduling."""
|
|
116
|
+
await self._tick_once()
|
|
117
|
+
self._last_tick_at = datetime.now(timezone.utc).isoformat()
|
|
118
|
+
self._last_error = None
|
|
119
|
+
self._tick_count += 1
|
|
120
|
+
if self._readiness_callback is not None:
|
|
121
|
+
self._readiness_callback(True, "scheduler_tick_ok")
|
|
122
|
+
|
|
123
|
+
async def _tick_once(self) -> None:
|
|
124
|
+
now = datetime.now(timezone.utc)
|
|
125
|
+
|
|
126
|
+
# Expire bounded WorkControl acknowledgement leases first. Otherwise
|
|
127
|
+
# a dead/uncooperative worker's pending stop could suppress the very
|
|
128
|
+
# timeout/failure transition needed to close its attempt.
|
|
129
|
+
expired_controls = await self._queue.reconcile_expired_work_controls(
|
|
130
|
+
now,
|
|
131
|
+
)
|
|
132
|
+
if expired_controls:
|
|
133
|
+
logger.info(
|
|
134
|
+
"Scheduler: reconciled %d expired WorkControl commands",
|
|
135
|
+
expired_controls,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Phase 1: Expire deadlines
|
|
139
|
+
expired = await self._queue.expire_past_deadlines(now)
|
|
140
|
+
if expired:
|
|
141
|
+
logger.info("Scheduler: expired %d deadline-past jobs", expired)
|
|
142
|
+
|
|
143
|
+
execution_timeouts = await self._queue.expire_execution_timeouts(now)
|
|
144
|
+
if execution_timeouts:
|
|
145
|
+
logger.info(
|
|
146
|
+
"Scheduler: expired %d execution timeouts",
|
|
147
|
+
execution_timeouts,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
approval_repairs = await self._queue.reconcile_blocked_approval_authority()
|
|
151
|
+
if approval_repairs:
|
|
152
|
+
logger.info(
|
|
153
|
+
"Scheduler: reconciled %d owner-approval holds",
|
|
154
|
+
approval_repairs,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
approval_timeouts = await self._queue.expire_blocked_approvals(
|
|
158
|
+
now, timeout_hours=self._approval_timeout_hours,
|
|
159
|
+
)
|
|
160
|
+
if approval_timeouts:
|
|
161
|
+
logger.info(
|
|
162
|
+
"Scheduler: expired %d owner-approval holds",
|
|
163
|
+
approval_timeouts,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Phase 2: Detect abandoned jobs (heartbeat timeout)
|
|
167
|
+
abandoned = await self._queue.abandon_silent_jobs(
|
|
168
|
+
now,
|
|
169
|
+
timeout_secs=self.worker_heartbeat_ttl_secs,
|
|
170
|
+
claim_timeout_secs=self._claim_timeout,
|
|
171
|
+
)
|
|
172
|
+
if abandoned:
|
|
173
|
+
logger.info("Scheduler: abandoned %d silent jobs", abandoned)
|
|
174
|
+
|
|
175
|
+
# Phase 3: Redistribute abandoned/retryable jobs
|
|
176
|
+
requeued = await self._queue.requeue_retryable_jobs(now)
|
|
177
|
+
if requeued:
|
|
178
|
+
logger.info("Scheduler: requeued %d retryable jobs", requeued)
|
|
179
|
+
|
|
180
|
+
# Phase 4: Re-evaluate only explicit boundary/governor holds. This
|
|
181
|
+
# safely handles directive lifts and mode rollback without conflating
|
|
182
|
+
# them with dependency or approval state.
|
|
183
|
+
governance_released = await self._queue.reconcile_governance_holds()
|
|
184
|
+
if governance_released:
|
|
185
|
+
logger.info(
|
|
186
|
+
"Scheduler: released %d governance-held jobs",
|
|
187
|
+
governance_released,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Durable competence/governor events survive cancellation and process
|
|
191
|
+
# restart. Drain them independently from worker execution enablement.
|
|
192
|
+
# Governor delivery may involve an LLM or outbound notice. Schedule
|
|
193
|
+
# its bounded reconciler without ever stalling lease/deadline phases.
|
|
194
|
+
self._queue._schedule_worker_outcome_drain()
|
|
195
|
+
|
|
196
|
+
# Phase 5: Unblock jobs whose dependencies are now met
|
|
197
|
+
unblocked = await self._queue.unblock_ready_jobs()
|
|
198
|
+
if unblocked:
|
|
199
|
+
logger.info("Scheduler: unblocked %d dependent jobs", unblocked)
|
|
200
|
+
|
|
201
|
+
# Reconcile source-owned runtime holds after dependencies so a
|
|
202
|
+
# rollback/re-enable cycle converges before worker notification. The
|
|
203
|
+
# actual claim/start boundaries independently re-evaluate the fence.
|
|
204
|
+
source_reconciler = getattr(
|
|
205
|
+
self._queue, "reconcile_runtime_claim_holds", None,
|
|
206
|
+
)
|
|
207
|
+
source_released = (
|
|
208
|
+
await source_reconciler() if callable(source_reconciler) else 0
|
|
209
|
+
)
|
|
210
|
+
if source_released:
|
|
211
|
+
logger.info(
|
|
212
|
+
"Scheduler: released %d source-runtime-held jobs",
|
|
213
|
+
source_released,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Phase 6: Assign QUEUED jobs to available workers
|
|
217
|
+
await self._assign_queued_jobs(now)
|
|
218
|
+
|
|
219
|
+
async def _assign_queued_jobs(self, now: datetime) -> None:
|
|
220
|
+
queued = await self._queue.get_queued_jobs_sorted(now)
|
|
221
|
+
if not queued:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
workers = await self._queue.get_available_workers()
|
|
225
|
+
|
|
226
|
+
for job in queued:
|
|
227
|
+
best_worker: Optional[WorkerCapabilities] = None
|
|
228
|
+
best_score = 0.0
|
|
229
|
+
|
|
230
|
+
for worker in workers:
|
|
231
|
+
# Headroom bonus: prefer workers with more open slots
|
|
232
|
+
running_count = round(worker.load * worker.max_concurrent)
|
|
233
|
+
headroom = worker.max_concurrent - running_count
|
|
234
|
+
score = worker.affinity_score(job)
|
|
235
|
+
if headroom >= 2:
|
|
236
|
+
score += 0.05 * (headroom - 1)
|
|
237
|
+
if score > best_score:
|
|
238
|
+
best_score = score
|
|
239
|
+
best_worker = worker
|
|
240
|
+
|
|
241
|
+
if best_worker is None:
|
|
242
|
+
if job.deadline is not None:
|
|
243
|
+
remaining = (job.deadline - now).total_seconds()
|
|
244
|
+
if 0 < remaining < self._no_worker_warning:
|
|
245
|
+
logger.warning(
|
|
246
|
+
"No capable worker for job %s (type=%s, required=%s, "
|
|
247
|
+
"deadline in %.0fs)",
|
|
248
|
+
job.job_id, job.job_type,
|
|
249
|
+
job.required_capabilities(), remaining,
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
await self._queue.notify_worker(best_worker.node_id, job.job_id)
|