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/goals/engine.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""GoalEngine — top-level coordinator for the Colony Goal Engine.
|
|
2
|
+
|
|
3
|
+
Wires together inference, decomposition, lifecycle management,
|
|
4
|
+
queue integration, and intelligence layer reporting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from apsimo.goals.config import GoalEngineConfig
|
|
14
|
+
from apsimo.goals.decomposer import GoalDecomposer
|
|
15
|
+
from apsimo.goals.inference import (
|
|
16
|
+
ConversationMessage,
|
|
17
|
+
GoalDeduplicator,
|
|
18
|
+
GoalInferencePipeline,
|
|
19
|
+
InferenceCandidate,
|
|
20
|
+
)
|
|
21
|
+
from apsimo.goals.models import (
|
|
22
|
+
Goal,
|
|
23
|
+
GoalDAG,
|
|
24
|
+
GoalPriority,
|
|
25
|
+
GoalSource,
|
|
26
|
+
GoalStatus,
|
|
27
|
+
GoalSummary,
|
|
28
|
+
GoalTransitionRecord,
|
|
29
|
+
)
|
|
30
|
+
from apsimo.goals.priority import GoalProgressTracker, GoalPriorityScorer, UserPreferenceProfile
|
|
31
|
+
from apsimo.goals.queue_bridge import GoalQueueBridge
|
|
32
|
+
from apsimo.goals.replan import (
|
|
33
|
+
FailureAnalysis,
|
|
34
|
+
ReplanEngine,
|
|
35
|
+
ReplanResult,
|
|
36
|
+
ReplanStrategy,
|
|
37
|
+
)
|
|
38
|
+
from apsimo.goals.store import GoalNotFoundError, GoalStore
|
|
39
|
+
from apsimo.task_queue.models import JobResult
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GoalEngine:
|
|
45
|
+
"""Top-level coordinator for the Colony Goal Engine.
|
|
46
|
+
|
|
47
|
+
Wires together inference, decomposition, lifecycle management,
|
|
48
|
+
queue integration, and intelligence layer reporting.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
store: Optional[GoalStore] = None,
|
|
54
|
+
decomposer: Optional[GoalDecomposer] = None,
|
|
55
|
+
queue_bridge: Optional[GoalQueueBridge] = None,
|
|
56
|
+
meta_learner: Optional[Any] = None,
|
|
57
|
+
config: Optional[GoalEngineConfig] = None,
|
|
58
|
+
quorum_manager: Optional[Any] = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.config = config or GoalEngineConfig()
|
|
61
|
+
self._store = store or GoalStore(db_path=self.config.db_path)
|
|
62
|
+
self._decomposer = decomposer or GoalDecomposer()
|
|
63
|
+
self._queue_bridge = queue_bridge or GoalQueueBridge()
|
|
64
|
+
self._meta_learner = meta_learner
|
|
65
|
+
self._quorum = quorum_manager # Optional[QuorumManager]
|
|
66
|
+
self._inference = GoalInferencePipeline()
|
|
67
|
+
self._deduplicator = GoalDeduplicator()
|
|
68
|
+
self._scorer = GoalPriorityScorer()
|
|
69
|
+
self._tracker = GoalProgressTracker()
|
|
70
|
+
self._replan_engine = ReplanEngine(max_replans=self.config.max_replans)
|
|
71
|
+
self._preferences = UserPreferenceProfile()
|
|
72
|
+
|
|
73
|
+
# ── Quorum guard ───────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _require_quorum(self, operation: str) -> None:
|
|
76
|
+
"""Raise QuorumError if the mesh quorum is unavailable.
|
|
77
|
+
|
|
78
|
+
No-ops when no quorum_manager was provided (e.g. single-node or tests).
|
|
79
|
+
"""
|
|
80
|
+
if self._quorum is not None:
|
|
81
|
+
self._quorum.require_quorum(operation)
|
|
82
|
+
|
|
83
|
+
# ── Goal Lifecycle ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def propose_goal(
|
|
86
|
+
self,
|
|
87
|
+
title: str,
|
|
88
|
+
description: str = "",
|
|
89
|
+
source: GoalSource = GoalSource.EXPLICIT,
|
|
90
|
+
priority: GoalPriority = GoalPriority.NORMAL,
|
|
91
|
+
deadline: Optional[datetime] = None,
|
|
92
|
+
context: Optional[Dict[str, Any]] = None,
|
|
93
|
+
tags: Optional[Dict[str, str]] = None,
|
|
94
|
+
parent_goal_id: Optional[str] = None,
|
|
95
|
+
) -> Goal:
|
|
96
|
+
"""Create a new goal in PROPOSED state."""
|
|
97
|
+
self._require_quorum("goal_propose")
|
|
98
|
+
goal = Goal(
|
|
99
|
+
title=title[:80],
|
|
100
|
+
description=description,
|
|
101
|
+
source=source,
|
|
102
|
+
status=GoalStatus.PROPOSED,
|
|
103
|
+
priority=priority,
|
|
104
|
+
deadline=deadline,
|
|
105
|
+
context=context or {},
|
|
106
|
+
tags=tags or {},
|
|
107
|
+
parent_goal_id=parent_goal_id,
|
|
108
|
+
)
|
|
109
|
+
self._store.save_goal(goal)
|
|
110
|
+
logger.info("Proposed goal %s: %r", goal.goal_id, goal.title)
|
|
111
|
+
return goal
|
|
112
|
+
|
|
113
|
+
def accept_goal(self, goal_id: str) -> Goal:
|
|
114
|
+
"""Transition a PROPOSED goal to ACCEPTED and queue decomposition."""
|
|
115
|
+
self._require_quorum("goal_accept")
|
|
116
|
+
goal = self._store.get_goal(goal_id)
|
|
117
|
+
if goal.status != GoalStatus.PROPOSED:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Cannot accept goal {goal_id} in state {goal.status.value}"
|
|
120
|
+
)
|
|
121
|
+
old_status = goal.status
|
|
122
|
+
goal.status = GoalStatus.ACCEPTED
|
|
123
|
+
goal.accepted_at = datetime.now(timezone.utc)
|
|
124
|
+
self._store.save_goal(goal)
|
|
125
|
+
self._store.log_transition(goal_id, old_status, GoalStatus.ACCEPTED, "user_accepted")
|
|
126
|
+
logger.info("Accepted goal %s", goal_id)
|
|
127
|
+
return goal
|
|
128
|
+
|
|
129
|
+
def activate_goal(self, goal_id: str) -> Goal:
|
|
130
|
+
"""Decompose an ACCEPTED goal and dispatch initial subtasks."""
|
|
131
|
+
self._require_quorum("goal_activate")
|
|
132
|
+
goal = self._store.get_goal(goal_id)
|
|
133
|
+
if goal.status != GoalStatus.ACCEPTED:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Cannot activate goal {goal_id} in state {goal.status.value}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if not self._queue_bridge.available:
|
|
139
|
+
# Acceptance is durable. Without an executor, neither a generated
|
|
140
|
+
# DAG nor a throwaway job ID establishes that work has started.
|
|
141
|
+
if goal.context.get("dispatch_unavailable") != "queue_backend_unconfigured":
|
|
142
|
+
goal.context["dispatch_unavailable"] = "queue_backend_unconfigured"
|
|
143
|
+
self._store.save_goal(goal)
|
|
144
|
+
logger.warning("Goal %s remains accepted: queue backend unconfigured", goal_id)
|
|
145
|
+
return goal
|
|
146
|
+
goal.context.pop("dispatch_unavailable", None)
|
|
147
|
+
|
|
148
|
+
# Decompose
|
|
149
|
+
dag = self._decomposer.decompose(goal)
|
|
150
|
+
self._store.save_dag(dag)
|
|
151
|
+
|
|
152
|
+
# Dispatch ready subtasks
|
|
153
|
+
dispatched = self._queue_bridge.dispatch_ready_subtasks(goal, dag)
|
|
154
|
+
self._store.save_dag(dag) # Updated subtask statuses
|
|
155
|
+
|
|
156
|
+
old_status = goal.status
|
|
157
|
+
goal.status = GoalStatus.ACTIVE
|
|
158
|
+
goal.estimated_hours = self._estimate_total_hours(dag)
|
|
159
|
+
self._store.save_goal(goal)
|
|
160
|
+
self._store.log_transition(goal_id, old_status, GoalStatus.ACTIVE, "decomposition_complete")
|
|
161
|
+
logger.info(
|
|
162
|
+
"Activated goal %s: %d subtasks, %d dispatched",
|
|
163
|
+
goal_id, len(dag.subtasks), dispatched,
|
|
164
|
+
)
|
|
165
|
+
return goal
|
|
166
|
+
|
|
167
|
+
def abandon_goal(self, goal_id: str, reason: str) -> Goal:
|
|
168
|
+
"""Transition any non-terminal goal to ABANDONED."""
|
|
169
|
+
self._require_quorum("goal_abandon")
|
|
170
|
+
goal = self._store.get_goal(goal_id)
|
|
171
|
+
if goal.is_terminal():
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Cannot abandon terminal goal {goal_id} (status={goal.status.value})"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Cancel running jobs
|
|
177
|
+
dag = self._store.get_dag(goal_id)
|
|
178
|
+
if dag:
|
|
179
|
+
self._queue_bridge.cancel_all_jobs(dag)
|
|
180
|
+
self._store.save_dag(dag)
|
|
181
|
+
|
|
182
|
+
old_status = goal.status
|
|
183
|
+
goal.status = GoalStatus.ABANDONED
|
|
184
|
+
goal.abandoned_at = datetime.now(timezone.utc)
|
|
185
|
+
goal.abandon_reason = reason
|
|
186
|
+
self._store.save_goal(goal)
|
|
187
|
+
self._store.log_transition(goal_id, old_status, GoalStatus.ABANDONED, "user_abandoned",
|
|
188
|
+
metadata={"reason": reason})
|
|
189
|
+
logger.info("Abandoned goal %s: %s", goal_id, reason)
|
|
190
|
+
|
|
191
|
+
# Emit telemetry
|
|
192
|
+
if self.config.telemetry_enabled:
|
|
193
|
+
self._emit_telemetry(goal, dag, success=False)
|
|
194
|
+
|
|
195
|
+
return goal
|
|
196
|
+
|
|
197
|
+
def block_goal(
|
|
198
|
+
self,
|
|
199
|
+
goal_id: str,
|
|
200
|
+
reason: str,
|
|
201
|
+
condition_type: Optional[str] = None,
|
|
202
|
+
condition_params: Optional[Dict[str, Any]] = None,
|
|
203
|
+
) -> Goal:
|
|
204
|
+
"""Transition an ACTIVE goal to BLOCKED.
|
|
205
|
+
|
|
206
|
+
With a ``condition_type`` (email_reply | deployment_health |
|
|
207
|
+
delivery_status | api_response | custom), the goal blocks on an
|
|
208
|
+
EXTERNAL condition: the autonomy loop's condition sweep polls it at
|
|
209
|
+
the type's cadence and unblocks the goal automatically when it's met.
|
|
210
|
+
Without one, the goal stays blocked until something explicitly
|
|
211
|
+
unblocks it."""
|
|
212
|
+
self._require_quorum("goal_block")
|
|
213
|
+
goal = self._store.get_goal(goal_id)
|
|
214
|
+
if goal.status != GoalStatus.ACTIVE:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"Cannot block goal {goal_id} in state {goal.status.value}"
|
|
217
|
+
)
|
|
218
|
+
old_status = goal.status
|
|
219
|
+
goal.status = GoalStatus.BLOCKED
|
|
220
|
+
goal.context["block_reason"] = reason
|
|
221
|
+
if condition_type:
|
|
222
|
+
goal.context["condition_type"] = condition_type
|
|
223
|
+
goal.context["condition_params"] = condition_params or {}
|
|
224
|
+
goal.context.pop("condition_last_check", None)
|
|
225
|
+
self._store.save_goal(goal)
|
|
226
|
+
self._store.log_transition(goal_id, old_status, GoalStatus.BLOCKED, "blocked",
|
|
227
|
+
metadata={"reason": reason,
|
|
228
|
+
**({"condition_type": condition_type}
|
|
229
|
+
if condition_type else {})})
|
|
230
|
+
logger.warning("Goal %s blocked: %s%s", goal_id, reason,
|
|
231
|
+
f" (awaiting {condition_type})" if condition_type else "")
|
|
232
|
+
return goal
|
|
233
|
+
|
|
234
|
+
def unblock_goal(self, goal_id: str) -> Goal:
|
|
235
|
+
"""Transition a BLOCKED goal back to ACTIVE."""
|
|
236
|
+
self._require_quorum("goal_unblock")
|
|
237
|
+
goal = self._store.get_goal(goal_id)
|
|
238
|
+
if goal.status != GoalStatus.BLOCKED:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Cannot unblock goal {goal_id} in state {goal.status.value}"
|
|
241
|
+
)
|
|
242
|
+
old_status = goal.status
|
|
243
|
+
goal.status = GoalStatus.ACTIVE
|
|
244
|
+
goal.context.pop("block_reason", None)
|
|
245
|
+
goal.context.pop("condition_type", None)
|
|
246
|
+
goal.context.pop("condition_params", None)
|
|
247
|
+
goal.context.pop("condition_last_check", None)
|
|
248
|
+
self._store.save_goal(goal)
|
|
249
|
+
self._store.log_transition(goal_id, old_status, GoalStatus.ACTIVE, "unblocked")
|
|
250
|
+
|
|
251
|
+
# Dispatch any newly ready subtasks
|
|
252
|
+
dag = self._store.get_dag(goal_id)
|
|
253
|
+
if dag:
|
|
254
|
+
self._queue_bridge.dispatch_ready_subtasks(goal, dag)
|
|
255
|
+
self._store.save_dag(dag)
|
|
256
|
+
|
|
257
|
+
logger.info("Unblocked goal %s", goal_id)
|
|
258
|
+
return goal
|
|
259
|
+
|
|
260
|
+
# ── Query ──────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def get_goal(self, goal_id: str) -> Goal:
|
|
263
|
+
return self._store.get_goal(goal_id)
|
|
264
|
+
|
|
265
|
+
def list_goals(
|
|
266
|
+
self,
|
|
267
|
+
status: Optional[GoalStatus] = None,
|
|
268
|
+
limit: int = 50,
|
|
269
|
+
offset: int = 0,
|
|
270
|
+
) -> List[Goal]:
|
|
271
|
+
return self._store.list_goals(status=status, limit=limit, offset=offset)
|
|
272
|
+
|
|
273
|
+
def get_dag(self, goal_id: str) -> Optional[GoalDAG]:
|
|
274
|
+
return self._store.get_dag(goal_id)
|
|
275
|
+
|
|
276
|
+
def get_audit_trail(self, goal_id: str) -> List[GoalTransitionRecord]:
|
|
277
|
+
return self._store.get_audit_trail(goal_id)
|
|
278
|
+
|
|
279
|
+
def get_active_summary(self) -> List[GoalSummary]:
|
|
280
|
+
"""Return human-readable summaries of all active goals."""
|
|
281
|
+
active_goals = self._store.list_goals(status=GoalStatus.ACTIVE)
|
|
282
|
+
summaries = []
|
|
283
|
+
for goal in active_goals:
|
|
284
|
+
dag = self._store.get_dag(goal.goal_id)
|
|
285
|
+
subtask_count = len(dag.subtasks) if dag else 0
|
|
286
|
+
from apsimo.goals.models import SubtaskStatus
|
|
287
|
+
completed = (
|
|
288
|
+
sum(1 for s in dag.subtasks.values()
|
|
289
|
+
if s.status in {SubtaskStatus.COMPLETED, SubtaskStatus.SKIPPED})
|
|
290
|
+
if dag else 0
|
|
291
|
+
)
|
|
292
|
+
summaries.append(GoalSummary(
|
|
293
|
+
goal_id=goal.goal_id,
|
|
294
|
+
title=goal.title,
|
|
295
|
+
status=goal.status.value,
|
|
296
|
+
priority=goal.priority.value,
|
|
297
|
+
progress_pct=goal.progress_pct,
|
|
298
|
+
estimated_hours=goal.estimated_hours,
|
|
299
|
+
deadline=goal.deadline,
|
|
300
|
+
subtask_count=subtask_count,
|
|
301
|
+
completed_subtasks=completed,
|
|
302
|
+
is_overdue=goal.is_overdue(),
|
|
303
|
+
replan_count=goal.replan_count,
|
|
304
|
+
))
|
|
305
|
+
return summaries
|
|
306
|
+
|
|
307
|
+
# ── Task Management (v0.7.10) ─────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
def complete_task(self, goal_id: str) -> bool:
|
|
310
|
+
"""Mark a goal/task as completed."""
|
|
311
|
+
return self._store.complete_task(goal_id)
|
|
312
|
+
|
|
313
|
+
def snooze_task(self, goal_id: str, hours: int, reason: str = "") -> bool:
|
|
314
|
+
"""Snooze a goal/task for N hours."""
|
|
315
|
+
return self._store.snooze_task(goal_id, hours, reason)
|
|
316
|
+
|
|
317
|
+
def dismiss_task(self, goal_id: str, reason: str = "stale") -> bool:
|
|
318
|
+
"""Dismiss a goal/task as no longer relevant."""
|
|
319
|
+
return self._store.dismiss_task(goal_id, reason)
|
|
320
|
+
|
|
321
|
+
def get_active_tasks(self, cooldown_hours: float = 12.0) -> List[Goal]:
|
|
322
|
+
"""Get goals that should generate initiatives."""
|
|
323
|
+
return self._store.get_active_tasks(cooldown_hours)
|
|
324
|
+
|
|
325
|
+
def mark_initiative_generated(self, goal_id: str) -> bool:
|
|
326
|
+
"""Mark that an initiative was just generated for this goal."""
|
|
327
|
+
return self._store.mark_initiative_generated(goal_id)
|
|
328
|
+
|
|
329
|
+
# ── Event Handlers ─────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
def on_job_completed(self, job_result: JobResult) -> None:
|
|
332
|
+
"""Handle a completed task queue job."""
|
|
333
|
+
goal_id = job_result.output.get("goal_id")
|
|
334
|
+
subtask_id = job_result.output.get("subtask_id")
|
|
335
|
+
|
|
336
|
+
if not goal_id or not subtask_id:
|
|
337
|
+
logger.warning("Received job result without goal_id/subtask_id: %s", job_result.job_id)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
goal = self._store.get_goal(goal_id)
|
|
342
|
+
except GoalNotFoundError:
|
|
343
|
+
logger.warning("on_job_completed: goal %s not found", goal_id)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
dag = self._store.get_dag(goal_id)
|
|
347
|
+
if dag is None:
|
|
348
|
+
logger.warning("on_job_completed: no DAG for goal %s", goal_id)
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
subtask = dag.subtasks.get(subtask_id)
|
|
352
|
+
if subtask is None:
|
|
353
|
+
logger.warning("on_job_completed: subtask %s not found in DAG", subtask_id)
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
if job_result.succeeded:
|
|
357
|
+
from apsimo.goals.models import SubtaskStatus
|
|
358
|
+
subtask.status = SubtaskStatus.COMPLETED
|
|
359
|
+
subtask.result = job_result.output
|
|
360
|
+
subtask.completed_at = job_result.completed_at or datetime.now(timezone.utc)
|
|
361
|
+
else:
|
|
362
|
+
from apsimo.goals.models import SubtaskStatus
|
|
363
|
+
subtask.status = SubtaskStatus.FAILED
|
|
364
|
+
subtask.error = job_result.error
|
|
365
|
+
self._trigger_replan(goal, dag, subtask)
|
|
366
|
+
self._store.save_dag(dag)
|
|
367
|
+
self._store.save_goal(goal)
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# Update goal progress
|
|
371
|
+
goal.progress_pct = self._tracker.update_progress(goal_id, dag)
|
|
372
|
+
|
|
373
|
+
# Check for goal completion
|
|
374
|
+
if dag.is_complete():
|
|
375
|
+
self._complete_goal(goal, dag)
|
|
376
|
+
else:
|
|
377
|
+
# Dispatch any newly ready subtasks
|
|
378
|
+
self._queue_bridge.dispatch_ready_subtasks(goal, dag)
|
|
379
|
+
|
|
380
|
+
# Check if goal is now blocked
|
|
381
|
+
block_reason = self._tracker.check_blocked(goal, dag)
|
|
382
|
+
if block_reason and goal.status == GoalStatus.ACTIVE:
|
|
383
|
+
self.block_goal(goal_id, block_reason)
|
|
384
|
+
|
|
385
|
+
self._store.save_goal(goal)
|
|
386
|
+
self._store.save_dag(dag)
|
|
387
|
+
|
|
388
|
+
def on_message(
|
|
389
|
+
self,
|
|
390
|
+
message: ConversationMessage,
|
|
391
|
+
history: Optional[List[ConversationMessage]] = None,
|
|
392
|
+
) -> Optional[InferenceCandidate]:
|
|
393
|
+
"""Process a conversation message for goal inference.
|
|
394
|
+
|
|
395
|
+
Returns a candidate if a new goal was inferred, else None.
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
from apsimo.cognition.goal_spine import cognition_spine_exclusive
|
|
399
|
+
if cognition_spine_exclusive():
|
|
400
|
+
return None
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
if not self.config.inference_enabled:
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
candidate = self._inference.process_message(message, history=history)
|
|
407
|
+
if candidate is None:
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
# Deduplication check
|
|
411
|
+
existing = self._store.list_goals(limit=100)
|
|
412
|
+
similarity = self._deduplicator.check(candidate, existing)
|
|
413
|
+
|
|
414
|
+
if similarity and similarity.recommendation == "duplicate":
|
|
415
|
+
logger.debug(
|
|
416
|
+
"Inference candidate %r is duplicate of goal %s (score=%.2f)",
|
|
417
|
+
candidate.title, similarity.goal_id_b, similarity.similarity_score,
|
|
418
|
+
)
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
if similarity and similarity.recommendation == "merge":
|
|
422
|
+
try:
|
|
423
|
+
base = self._store.get_goal(similarity.goal_id_b)
|
|
424
|
+
self._deduplicator.merge(base, candidate)
|
|
425
|
+
self._store.save_goal(base)
|
|
426
|
+
logger.info("Merged inference candidate into goal %s", base.goal_id)
|
|
427
|
+
except GoalNotFoundError:
|
|
428
|
+
pass
|
|
429
|
+
return candidate
|
|
430
|
+
|
|
431
|
+
# Preserve inference provenance and any detected deadline so the signal
|
|
432
|
+
# isn't silently lost. suggested_deadline is a matched phrase (e.g.
|
|
433
|
+
# "due by", "before the meeting"), not a parseable datetime, so it is
|
|
434
|
+
# carried as a context hint rather than the typed `deadline` field —
|
|
435
|
+
# downstream / the LLM can resolve it to a concrete date.
|
|
436
|
+
inferred_context: Dict[str, Any] = {
|
|
437
|
+
"inference_confidence": round(candidate.confidence, 3),
|
|
438
|
+
"inference_signals": [s.value for s in candidate.signals],
|
|
439
|
+
"source_messages": candidate.source_messages,
|
|
440
|
+
}
|
|
441
|
+
if candidate.suggested_deadline:
|
|
442
|
+
inferred_context["deadline_hint"] = candidate.suggested_deadline
|
|
443
|
+
|
|
444
|
+
goal = self.propose_goal(
|
|
445
|
+
title=candidate.title,
|
|
446
|
+
description=candidate.description,
|
|
447
|
+
source=GoalSource.INFERRED,
|
|
448
|
+
priority=candidate.priority,
|
|
449
|
+
context=inferred_context,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Auto-accept if confidence is high enough; otherwise leave PROPOSED for
|
|
453
|
+
# the user to confirm.
|
|
454
|
+
if candidate.should_auto_accept(self.config.auto_accept_threshold):
|
|
455
|
+
self.accept_goal(goal.goal_id)
|
|
456
|
+
logger.info("Auto-accepted inferred goal %s: %r", goal.goal_id, goal.title)
|
|
457
|
+
|
|
458
|
+
return candidate
|
|
459
|
+
|
|
460
|
+
# ── Replan ─────────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
def trigger_replan(
|
|
463
|
+
self,
|
|
464
|
+
goal_id: str,
|
|
465
|
+
reason: str,
|
|
466
|
+
strategy: Optional[ReplanStrategy] = None,
|
|
467
|
+
) -> ReplanResult:
|
|
468
|
+
"""Manually trigger a replan for a goal."""
|
|
469
|
+
goal = self._store.get_goal(goal_id)
|
|
470
|
+
dag = self._store.get_dag(goal_id)
|
|
471
|
+
if dag is None:
|
|
472
|
+
raise ValueError(f"No DAG found for goal {goal_id}")
|
|
473
|
+
|
|
474
|
+
# Find the first failed subtask to analyse
|
|
475
|
+
from apsimo.goals.models import SubtaskStatus
|
|
476
|
+
failed = next(
|
|
477
|
+
(s for s in dag.subtasks.values() if s.status == SubtaskStatus.FAILED),
|
|
478
|
+
None,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if failed is None:
|
|
482
|
+
# Create a synthetic analysis for scope change
|
|
483
|
+
from apsimo.goals.replan import FailureAnalysis, FailureClass
|
|
484
|
+
analysis = FailureAnalysis(
|
|
485
|
+
subtask_id=next(iter(dag.subtasks), ""),
|
|
486
|
+
failure_class=FailureClass.SCOPE_CHANGE,
|
|
487
|
+
error_message=reason,
|
|
488
|
+
retry_viable=False,
|
|
489
|
+
affected_subtasks=list(dag.subtasks.keys()),
|
|
490
|
+
suggested_strategy=(strategy or ReplanStrategy.FULL_REPLAN).value,
|
|
491
|
+
)
|
|
492
|
+
else:
|
|
493
|
+
analysis = self._replan_engine.analyze_failure(goal, dag, failed)
|
|
494
|
+
if strategy:
|
|
495
|
+
analysis.suggested_strategy = strategy.value
|
|
496
|
+
|
|
497
|
+
replan_result = self._replan_engine.generate_replan(goal, dag, analysis)
|
|
498
|
+
|
|
499
|
+
if not replan_result.requires_user_approval:
|
|
500
|
+
new_dag = self._replan_engine.apply_replan(goal, dag, replan_result)
|
|
501
|
+
self._store.save_goal(goal)
|
|
502
|
+
self._store.save_dag(new_dag)
|
|
503
|
+
self._queue_bridge.dispatch_ready_subtasks(goal, new_dag)
|
|
504
|
+
self._store.save_dag(new_dag)
|
|
505
|
+
|
|
506
|
+
return replan_result
|
|
507
|
+
|
|
508
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
def _trigger_replan(self, goal: Goal, dag: GoalDAG, failed_subtask) -> None:
|
|
511
|
+
"""Internal: automatically replan after subtask failure."""
|
|
512
|
+
analysis = self._replan_engine.analyze_failure(goal, dag, failed_subtask)
|
|
513
|
+
replan_result = self._replan_engine.generate_replan(goal, dag, analysis)
|
|
514
|
+
|
|
515
|
+
if replan_result.requires_user_approval:
|
|
516
|
+
logger.warning(
|
|
517
|
+
"Goal %s replan requires user approval: %s",
|
|
518
|
+
goal.goal_id, replan_result.escalation_message,
|
|
519
|
+
)
|
|
520
|
+
# Block the goal pending user action
|
|
521
|
+
if goal.status == GoalStatus.ACTIVE:
|
|
522
|
+
old_status = goal.status
|
|
523
|
+
goal.status = GoalStatus.BLOCKED
|
|
524
|
+
goal.context["escalation"] = replan_result.escalation_message
|
|
525
|
+
self._store.log_transition(
|
|
526
|
+
goal.goal_id, old_status, GoalStatus.BLOCKED,
|
|
527
|
+
"replan_requires_approval",
|
|
528
|
+
metadata={"strategy": replan_result.strategy.value},
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
new_dag = self._replan_engine.apply_replan(goal, dag, replan_result)
|
|
533
|
+
# Mutate the dag dict in-place so the caller's reference is updated
|
|
534
|
+
dag.subtasks = new_dag.subtasks
|
|
535
|
+
dag.version = new_dag.version
|
|
536
|
+
dag.root_ids = new_dag.root_ids
|
|
537
|
+
dag.leaf_ids = new_dag.leaf_ids
|
|
538
|
+
dag.critical_path = new_dag.critical_path
|
|
539
|
+
|
|
540
|
+
def _complete_goal(self, goal: Goal, dag: GoalDAG) -> None:
|
|
541
|
+
"""Finalise a goal after all subtasks complete."""
|
|
542
|
+
old_status = goal.status
|
|
543
|
+
goal.status = GoalStatus.COMPLETED
|
|
544
|
+
goal.completed_at = datetime.now(timezone.utc)
|
|
545
|
+
goal.progress_pct = 1.0
|
|
546
|
+
self._store.log_transition(
|
|
547
|
+
goal.goal_id, old_status, GoalStatus.COMPLETED, "all_subtasks_complete"
|
|
548
|
+
)
|
|
549
|
+
logger.info("Goal %s completed: %r", goal.goal_id, goal.title)
|
|
550
|
+
|
|
551
|
+
# Update preference profile
|
|
552
|
+
domain = goal.tags.get("domain", "")
|
|
553
|
+
if domain:
|
|
554
|
+
self._preferences.update_domain_completion(domain, success=True)
|
|
555
|
+
|
|
556
|
+
# Emit telemetry
|
|
557
|
+
if self.config.telemetry_enabled:
|
|
558
|
+
self._emit_telemetry(goal, dag, success=True)
|
|
559
|
+
|
|
560
|
+
def _emit_telemetry(self, goal: Goal, dag: Optional[GoalDAG], success: bool) -> None:
|
|
561
|
+
"""Emit goal completion telemetry to MetaLearner (best-effort)."""
|
|
562
|
+
if self._meta_learner is None:
|
|
563
|
+
return
|
|
564
|
+
try:
|
|
565
|
+
from apsimo.goals.models import SubtaskStatus
|
|
566
|
+
failed_count = 0
|
|
567
|
+
subtask_types: Dict[str, int] = {}
|
|
568
|
+
if dag:
|
|
569
|
+
failed_count = sum(
|
|
570
|
+
1 for s in dag.subtasks.values()
|
|
571
|
+
if s.status == SubtaskStatus.FAILED
|
|
572
|
+
)
|
|
573
|
+
for s in dag.subtasks.values():
|
|
574
|
+
subtask_types[s.job_type] = subtask_types.get(s.job_type, 0) + 1
|
|
575
|
+
|
|
576
|
+
actual_hours = 0.0
|
|
577
|
+
if goal.accepted_at and goal.completed_at:
|
|
578
|
+
actual_hours = (goal.completed_at - goal.accepted_at).total_seconds() / 3600.0
|
|
579
|
+
elif goal.accepted_at and goal.abandoned_at:
|
|
580
|
+
actual_hours = (goal.abandoned_at - goal.accepted_at).total_seconds() / 3600.0
|
|
581
|
+
|
|
582
|
+
estimation_error = None
|
|
583
|
+
if goal.estimated_hours and goal.estimated_hours > 0:
|
|
584
|
+
estimation_error = (actual_hours - goal.estimated_hours) / goal.estimated_hours
|
|
585
|
+
|
|
586
|
+
telemetry = {
|
|
587
|
+
"goal_id": goal.goal_id,
|
|
588
|
+
"goal_title": goal.title,
|
|
589
|
+
"goal_source": goal.source.value,
|
|
590
|
+
"priority": goal.priority.value,
|
|
591
|
+
"total_subtasks": len(dag.subtasks) if dag else 0,
|
|
592
|
+
"failed_subtasks": failed_count,
|
|
593
|
+
"replan_count": goal.replan_count,
|
|
594
|
+
"estimated_hours": goal.estimated_hours,
|
|
595
|
+
"actual_hours": actual_hours,
|
|
596
|
+
"estimation_error": estimation_error,
|
|
597
|
+
"subtask_type_breakdown": subtask_types,
|
|
598
|
+
"success": success,
|
|
599
|
+
"completed_at": (goal.completed_at or goal.abandoned_at or datetime.now(timezone.utc)).isoformat(),
|
|
600
|
+
}
|
|
601
|
+
# MetaLearner may expose record_metric or a custom hook
|
|
602
|
+
if hasattr(self._meta_learner, "_metric_hooks"):
|
|
603
|
+
for hook in self._meta_learner._metric_hooks:
|
|
604
|
+
hook(telemetry)
|
|
605
|
+
except Exception as exc:
|
|
606
|
+
logger.warning("Failed to emit goal telemetry: %s", exc)
|
|
607
|
+
|
|
608
|
+
def _estimate_total_hours(self, dag: GoalDAG) -> Optional[float]:
|
|
609
|
+
"""Estimate total goal hours from the critical path."""
|
|
610
|
+
if not dag.critical_path:
|
|
611
|
+
return None
|
|
612
|
+
total = sum(
|
|
613
|
+
(dag.subtasks[sid].estimated_hours or 1.0)
|
|
614
|
+
for sid in dag.critical_path
|
|
615
|
+
if sid in dag.subtasks
|
|
616
|
+
)
|
|
617
|
+
return round(total, 2) if total > 0 else None
|