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,1681 @@
|
|
|
1
|
+
"""ProjectEngine -- sustained multi-tick pursuit of durable objectives.
|
|
2
|
+
|
|
3
|
+
Pursued from the autonomy loop's ``_phase_projects`` (after
|
|
4
|
+
``_phase_execute``). Each tick: adopt qualifying project-type initiatives,
|
|
5
|
+
plan any project still in ``planning`` (one LLM pass, deterministically
|
|
6
|
+
validated), then advance due active projects by one ready step each.
|
|
7
|
+
|
|
8
|
+
Safety posture:
|
|
9
|
+
- Every step is boundary-checked (DirectiveGuard) before dispatch; a blocked
|
|
10
|
+
step blocks the whole project (visible, never silent).
|
|
11
|
+
- Step dispatch routes through the sub-path that already gates that action
|
|
12
|
+
kind: reasoning turn (internal tools) for analyze/research/internal,
|
|
13
|
+
DirectedActionService (approval tiering + dry_run) for directed, the
|
|
14
|
+
guarded proposal path for deliver. This engine adds NO new outbound or
|
|
15
|
+
mutating primitive of its own; it is orchestration over existing gates.
|
|
16
|
+
- COLONY_PROJECTS_MODE=shadow (default): plans for real, logs the exact
|
|
17
|
+
intended step action with its boundary verdict, simulates advancement, and
|
|
18
|
+
stores milestone proposals with status "shadow" WITHOUT routing them to
|
|
19
|
+
delivery. Nothing leaves the machine.
|
|
20
|
+
- Uses the self-model (item 4) to defer pursuit under load, and skills memory
|
|
21
|
+
(item 3) to inform planning and distill procedures from completions.
|
|
22
|
+
|
|
23
|
+
For step EXECUTION beyond the sidecar, deployments point the directed
|
|
24
|
+
pipeline's env-configured delegate endpoint at their host framework's job
|
|
25
|
+
surface (e.g. a kanban/runs API); this engine deliberately reuses that seam
|
|
26
|
+
instead of growing a private execution runner.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
import time
|
|
39
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Tuple
|
|
40
|
+
import uuid
|
|
41
|
+
|
|
42
|
+
from apsimo.projects.models import (
|
|
43
|
+
Project, Step, projects_max_replans, projects_mode, projects_review_secs,
|
|
44
|
+
)
|
|
45
|
+
from apsimo.projects.store import ProjectStore
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# Canonical ``internal`` WorkOrders may quote a standalone owner safety clause,
|
|
50
|
+
# e.g. ``; do not contact anyone``. Rechecking that quote as affirmative
|
|
51
|
+
# action intent self-blocks the safe WorkOrder. Split only at hard clause
|
|
52
|
+
# boundaries and remove only a plainly negated whole clause. Reversal and
|
|
53
|
+
# double-negation language remains fail-closed.
|
|
54
|
+
_BOUNDARY_CLAUSE_SPLIT = re.compile(r"(?:[;\n]+|(?<=[.!?])\s+)")
|
|
55
|
+
_NEGATED_CONSTRAINT = re.compile(
|
|
56
|
+
r"^\s*(?:and\s+)?"
|
|
57
|
+
r"(?:do\s+not|don['’]t|never|must\s+not|shall\s+not|no)\b",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
_NEGATION_REVERSAL = re.compile(
|
|
61
|
+
r"\b(?:not|avoid\w*|refrain\w*|fail\w*|refus\w*|declin\w*|stop\w*)\b",
|
|
62
|
+
re.IGNORECASE,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _boundary_intent_text(*parts: object) -> str:
|
|
67
|
+
"""Return executable clauses from an internal action description."""
|
|
68
|
+
|
|
69
|
+
kept: List[str] = []
|
|
70
|
+
for part in parts:
|
|
71
|
+
for clause in _BOUNDARY_CLAUSE_SPLIT.split(str(part or "")):
|
|
72
|
+
clause = clause.strip()
|
|
73
|
+
if not clause:
|
|
74
|
+
continue
|
|
75
|
+
match = _NEGATED_CONSTRAINT.match(clause)
|
|
76
|
+
if match and not _NEGATION_REVERSAL.search(clause[match.end():]):
|
|
77
|
+
continue
|
|
78
|
+
kept.append(clause)
|
|
79
|
+
return " ".join(kept)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _step_boundary_action(project: Project, step: Step):
|
|
83
|
+
"""Build the boundary action without weakening effectful step checks."""
|
|
84
|
+
|
|
85
|
+
from apsimo.directives import Action
|
|
86
|
+
from apsimo.work_orders import action_authority
|
|
87
|
+
|
|
88
|
+
risk = action_authority(step.action_kind)[2]
|
|
89
|
+
if risk == "internal":
|
|
90
|
+
return Action(
|
|
91
|
+
kind=step.action_kind,
|
|
92
|
+
text=_boundary_intent_text(
|
|
93
|
+
step.description, project.title, project.objective,
|
|
94
|
+
),
|
|
95
|
+
target=step.boundary_subject,
|
|
96
|
+
high_risk=True,
|
|
97
|
+
)
|
|
98
|
+
return Action(
|
|
99
|
+
kind=step.action_kind,
|
|
100
|
+
text=f"{step.description} {step.boundary_subject}",
|
|
101
|
+
target=project.subject_text(),
|
|
102
|
+
high_risk=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Step execution composes through the shared cognition charter (role
|
|
106
|
+
# "executor"); this block scopes the turn to ONE project step.
|
|
107
|
+
_STEP_SCOPE_CONTEXT = """\
|
|
108
|
+
You are executing ONE STEP of a long-running project. Complete THIS STEP
|
|
109
|
+
ONLY, then summarize the outcome in 2-4 sentences; later steps are separate
|
|
110
|
+
work sessions. If the step cannot be completed, say precisely what is
|
|
111
|
+
missing."""
|
|
112
|
+
|
|
113
|
+
_MAX_TOOL_ROUNDS = 4
|
|
114
|
+
_AUTHORITY_BOUND_PROJECT_SOURCES = frozenset({
|
|
115
|
+
"cognition_spine", "governed_action",
|
|
116
|
+
})
|
|
117
|
+
_GOVERNED_HOLD_REASONS = frozenset({
|
|
118
|
+
"governed_action_requires_live_projects_mode",
|
|
119
|
+
"governed_action_requires_canonical_work_order_adapter",
|
|
120
|
+
})
|
|
121
|
+
_TURN_CONCERN_HOLD_REASON = "turn_concerns_current_mode_not_live"
|
|
122
|
+
_PROJECT_HOLD_REASON_UNAVAILABLE = "project_hold_reason_unavailable"
|
|
123
|
+
_TRANSIENT_PROJECT_HOLD_REASONS = frozenset({
|
|
124
|
+
_TURN_CONCERN_HOLD_REASON,
|
|
125
|
+
_PROJECT_HOLD_REASON_UNAVAILABLE,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _int_env(name: str, default: int) -> int:
|
|
130
|
+
try:
|
|
131
|
+
return int(os.environ.get(name, str(default)))
|
|
132
|
+
except (TypeError, ValueError):
|
|
133
|
+
return default
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ProjectEngine:
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
store: ProjectStore,
|
|
140
|
+
*,
|
|
141
|
+
directive_manager: Any = None,
|
|
142
|
+
llm_router: Any = None, # planning + distillation passes
|
|
143
|
+
reasoning_loop: Any = None, # analyze/research/internal steps
|
|
144
|
+
tool_executor: Any = None,
|
|
145
|
+
directed_service: Any = None, # directed steps
|
|
146
|
+
proposal_store: Any = None,
|
|
147
|
+
feedback_store: Any = None,
|
|
148
|
+
self_model: Any = None,
|
|
149
|
+
skill_store: Any = None,
|
|
150
|
+
delivery_router: Any = None, # async callable(payload) -> bool
|
|
151
|
+
initiative_store: Any = None, # adoption of project-type initiatives
|
|
152
|
+
work_order_adapter: Any = None, # canonical external execution bridge
|
|
153
|
+
project_hold_reason: Optional[Callable[[Project], str]] = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
self.store = store
|
|
156
|
+
self._directives = directive_manager
|
|
157
|
+
self._router = llm_router
|
|
158
|
+
self._reasoning = reasoning_loop
|
|
159
|
+
self._tools = tool_executor
|
|
160
|
+
self._directed = directed_service
|
|
161
|
+
self._proposals = proposal_store
|
|
162
|
+
self._feedback = feedback_store
|
|
163
|
+
self._self_model = self_model
|
|
164
|
+
self._skills = skill_store
|
|
165
|
+
self._deliver = delivery_router
|
|
166
|
+
self._initiatives = initiative_store
|
|
167
|
+
self._work_orders = work_order_adapter
|
|
168
|
+
self._project_hold_reason = project_hold_reason
|
|
169
|
+
|
|
170
|
+
def _apply_project_hold(self, project: Project) -> str:
|
|
171
|
+
reason = ""
|
|
172
|
+
if self._project_hold_reason is not None:
|
|
173
|
+
try:
|
|
174
|
+
reason = str(self._project_hold_reason(project) or "")[:200]
|
|
175
|
+
except Exception:
|
|
176
|
+
logger.exception("project hold callback failed closed")
|
|
177
|
+
reason = _PROJECT_HOLD_REASON_UNAVAILABLE
|
|
178
|
+
if reason in _TRANSIENT_PROJECT_HOLD_REASONS:
|
|
179
|
+
# Transient concern-mode holds are visible when they do not mask a
|
|
180
|
+
# pre-existing, unrelated project reason. The returned reason is
|
|
181
|
+
# still authoritative for eligibility either way.
|
|
182
|
+
if (
|
|
183
|
+
project.reason in _TRANSIENT_PROJECT_HOLD_REASONS
|
|
184
|
+
or not project.reason
|
|
185
|
+
) and project.reason != reason:
|
|
186
|
+
project.reason = reason
|
|
187
|
+
self.store.save_project(project)
|
|
188
|
+
return reason
|
|
189
|
+
if project.reason in _TRANSIENT_PROJECT_HOLD_REASONS:
|
|
190
|
+
project.reason = ""
|
|
191
|
+
self.store.save_project(project)
|
|
192
|
+
return reason
|
|
193
|
+
|
|
194
|
+
def open_capacity_used(self) -> int:
|
|
195
|
+
"""Count open work after reconciling exact turn-mode holds.
|
|
196
|
+
|
|
197
|
+
Only the exact, known turn-mode hold releases a capacity slot. A
|
|
198
|
+
callback outage and every ordinary/external/governed project remain
|
|
199
|
+
capacity-bearing and therefore fail conservatively.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
used = 0
|
|
203
|
+
for status in ("planning", "active"):
|
|
204
|
+
offset = 0
|
|
205
|
+
while True:
|
|
206
|
+
page = self.store.list_projects(
|
|
207
|
+
status=status, limit=100, offset=offset,
|
|
208
|
+
)
|
|
209
|
+
if not page:
|
|
210
|
+
break
|
|
211
|
+
offset += len(page)
|
|
212
|
+
for project in page:
|
|
213
|
+
hold = self._apply_project_hold(project)
|
|
214
|
+
if hold != _TURN_CONCERN_HOLD_REASON:
|
|
215
|
+
used += 1
|
|
216
|
+
if len(page) < 100:
|
|
217
|
+
break
|
|
218
|
+
return used
|
|
219
|
+
|
|
220
|
+
def _eligible_projects(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
status: str,
|
|
224
|
+
limit: int,
|
|
225
|
+
held_ids: Optional[Set[str]] = None,
|
|
226
|
+
due: bool = False,
|
|
227
|
+
) -> List[Project]:
|
|
228
|
+
"""Scan past held rows so they cannot starve ordinary work."""
|
|
229
|
+
|
|
230
|
+
selected: List[Project] = []
|
|
231
|
+
offset = 0
|
|
232
|
+
page_size = max(25, limit)
|
|
233
|
+
while len(selected) < limit:
|
|
234
|
+
page = (
|
|
235
|
+
self.store.due_for_review(limit=page_size, offset=offset)
|
|
236
|
+
if due else
|
|
237
|
+
self.store.list_projects(
|
|
238
|
+
status=status, limit=page_size, offset=offset,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
if not page:
|
|
242
|
+
break
|
|
243
|
+
offset += len(page)
|
|
244
|
+
for project in page:
|
|
245
|
+
if self._apply_project_hold(project):
|
|
246
|
+
if held_ids is not None:
|
|
247
|
+
held_ids.add(project.id)
|
|
248
|
+
continue
|
|
249
|
+
selected.append(project)
|
|
250
|
+
if len(selected) >= limit:
|
|
251
|
+
break
|
|
252
|
+
if len(page) < page_size:
|
|
253
|
+
break
|
|
254
|
+
return selected
|
|
255
|
+
|
|
256
|
+
def _has_canonical_work_order_adapter(self) -> bool:
|
|
257
|
+
try:
|
|
258
|
+
from apsimo.work_orders import QueueWorkOrderAdapter
|
|
259
|
+
except Exception:
|
|
260
|
+
return False
|
|
261
|
+
return (
|
|
262
|
+
isinstance(self._work_orders, QueueWorkOrderAdapter)
|
|
263
|
+
and self._work_orders.project_store is self.store
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _governed_hold_reason(self, mode: str) -> str:
|
|
267
|
+
if mode != "live" or projects_mode() != "live":
|
|
268
|
+
return "governed_action_requires_live_projects_mode"
|
|
269
|
+
if not self._has_canonical_work_order_adapter():
|
|
270
|
+
return "governed_action_requires_canonical_work_order_adapter"
|
|
271
|
+
return ""
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _governed_identity(project: Project) -> str:
|
|
275
|
+
source_refs = list(project.source_event_refs)
|
|
276
|
+
evidence_refs = list(project.evidence_refs)
|
|
277
|
+
if (
|
|
278
|
+
len(source_refs) != 2
|
|
279
|
+
or len(evidence_refs) != 4
|
|
280
|
+
or not source_refs[0].startswith("governed-action:")
|
|
281
|
+
or not source_refs[1].startswith("governed-intent:")
|
|
282
|
+
or not evidence_refs[0].startswith("action-digest:")
|
|
283
|
+
or not evidence_refs[1].startswith("intent-digest:")
|
|
284
|
+
or not evidence_refs[2].startswith("args-digest:")
|
|
285
|
+
or not evidence_refs[3].startswith("execution-digest:")
|
|
286
|
+
):
|
|
287
|
+
raise ValueError("governed research provenance is incomplete")
|
|
288
|
+
action_id = source_refs[0].split(":", 1)[1]
|
|
289
|
+
intent_id = source_refs[1].split(":", 1)[1]
|
|
290
|
+
try:
|
|
291
|
+
parsed_action_id = uuid.UUID(action_id)
|
|
292
|
+
except (AttributeError, ValueError) as exc:
|
|
293
|
+
raise ValueError("governed research action ID is invalid") from exc
|
|
294
|
+
if (
|
|
295
|
+
parsed_action_id.version != 4
|
|
296
|
+
or str(parsed_action_id) != action_id
|
|
297
|
+
or not re.fullmatch(r"hti_[0-9a-f]{32}", intent_id)
|
|
298
|
+
):
|
|
299
|
+
raise ValueError("governed research identity is invalid")
|
|
300
|
+
digests = []
|
|
301
|
+
for index, prefix in enumerate((
|
|
302
|
+
"action-digest:", "intent-digest:", "args-digest:",
|
|
303
|
+
"execution-digest:",
|
|
304
|
+
)):
|
|
305
|
+
if not evidence_refs[index].startswith(prefix):
|
|
306
|
+
raise ValueError("governed research digest provenance is invalid")
|
|
307
|
+
digests.append(evidence_refs[index].split(":", 1)[1])
|
|
308
|
+
if any(
|
|
309
|
+
len(value) != 64
|
|
310
|
+
or any(character not in "0123456789abcdef" for character in value)
|
|
311
|
+
for value in digests
|
|
312
|
+
):
|
|
313
|
+
raise ValueError("governed research identity is invalid")
|
|
314
|
+
action_digest, _intent_digest, _args_digest, execution_digest = digests
|
|
315
|
+
material = {
|
|
316
|
+
"schema": "ColonyGovernedResearchProjectIdentityV1",
|
|
317
|
+
"version": 1,
|
|
318
|
+
"owner_person_id": project.subject_person_id,
|
|
319
|
+
"action_id": action_id,
|
|
320
|
+
"action_digest": action_digest,
|
|
321
|
+
"execution_digest": execution_digest,
|
|
322
|
+
}
|
|
323
|
+
encoded = json.dumps(
|
|
324
|
+
material, sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
325
|
+
).encode("utf-8")
|
|
326
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
327
|
+
|
|
328
|
+
def prepare_governed_research(self, project: Project) -> None:
|
|
329
|
+
"""Validate one owner-approved, read-only durable project handoff."""
|
|
330
|
+
|
|
331
|
+
if not isinstance(project, Project) or project.source != "governed_action":
|
|
332
|
+
raise RuntimeError("governed research requires a governed Project")
|
|
333
|
+
if projects_mode() != "live":
|
|
334
|
+
raise RuntimeError("governed research requires ProjectEngine live mode")
|
|
335
|
+
if not self._has_canonical_work_order_adapter():
|
|
336
|
+
raise RuntimeError(
|
|
337
|
+
"governed research requires the canonical WorkOrder adapter"
|
|
338
|
+
)
|
|
339
|
+
try:
|
|
340
|
+
from apsimo.work_orders import action_authority
|
|
341
|
+
capabilities = list(action_authority("research")[1])
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
raise RuntimeError("governed research WorkOrder authority is unavailable") from exc
|
|
344
|
+
identity = self._governed_identity(project)
|
|
345
|
+
expected_id = "proj-governed-" + identity[:20]
|
|
346
|
+
expected_title = "Governed research " + identity[:8]
|
|
347
|
+
if (
|
|
348
|
+
project.id != expected_id
|
|
349
|
+
or project.goal_fingerprint != identity
|
|
350
|
+
or project.title != expected_title
|
|
351
|
+
or project.status != "planning"
|
|
352
|
+
or project.outcome != "pending"
|
|
353
|
+
or project.reason
|
|
354
|
+
or project.replans != 0
|
|
355
|
+
or project.next_review_at != 0.0
|
|
356
|
+
or project.entity_ids
|
|
357
|
+
or not project.subject_person_id
|
|
358
|
+
or project.viewer_scope != "owner"
|
|
359
|
+
or project.shareability != "owner_private"
|
|
360
|
+
or project.capability_allowlist != capabilities
|
|
361
|
+
):
|
|
362
|
+
raise RuntimeError("governed research Project authority is invalid")
|
|
363
|
+
prefix = "Research topic:\n"
|
|
364
|
+
if not project.objective.startswith(prefix) or "\nDepth: " not in project.objective:
|
|
365
|
+
raise RuntimeError("governed research objective is invalid")
|
|
366
|
+
topic, depth = project.objective[len(prefix):].rsplit("\nDepth: ", 1)
|
|
367
|
+
if not topic or len(topic) > 1400 or depth not in {"quick", "standard", "deep"}:
|
|
368
|
+
raise RuntimeError("governed research objective is outside its bound")
|
|
369
|
+
policy_refs = list(project.policy_decision_refs)
|
|
370
|
+
if (
|
|
371
|
+
len(policy_refs) != 4
|
|
372
|
+
or not policy_refs[0].startswith("approval:")
|
|
373
|
+
or not policy_refs[1].startswith("decision:")
|
|
374
|
+
or not policy_refs[2].startswith("approval-revision:")
|
|
375
|
+
or not policy_refs[3].startswith("authorization-receipt-digest:")
|
|
376
|
+
):
|
|
377
|
+
raise RuntimeError("governed research decision provenance is incomplete")
|
|
378
|
+
approval_id = policy_refs[0].split(":", 1)[1]
|
|
379
|
+
decision_id = policy_refs[1].split(":", 1)[1]
|
|
380
|
+
revision = policy_refs[2].split(":", 1)[1]
|
|
381
|
+
receipt_digest = policy_refs[3].split(":", 1)[1]
|
|
382
|
+
if (
|
|
383
|
+
not re.fullmatch(r"APR-[A-Z0-9]{12}", approval_id)
|
|
384
|
+
or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}", decision_id)
|
|
385
|
+
or revision != "1"
|
|
386
|
+
or not re.fullmatch(r"[0-9a-f]{64}", receipt_digest)
|
|
387
|
+
):
|
|
388
|
+
raise RuntimeError("governed research decision provenance is invalid")
|
|
389
|
+
refs = project.provenance_refs()
|
|
390
|
+
if (
|
|
391
|
+
len(refs) != 10
|
|
392
|
+
or len(set(refs)) != 10
|
|
393
|
+
or any(
|
|
394
|
+
not isinstance(ref, str)
|
|
395
|
+
or not ref
|
|
396
|
+
or len(ref) > 256
|
|
397
|
+
or any(ord(character) < 0x20 for character in ref)
|
|
398
|
+
for ref in refs
|
|
399
|
+
)
|
|
400
|
+
):
|
|
401
|
+
raise RuntimeError("governed research provenance is invalid")
|
|
402
|
+
|
|
403
|
+
# Existing exact replays already passed their boundary before insert.
|
|
404
|
+
# A new row must still pass the current standing project boundary.
|
|
405
|
+
if self.store.get_project(project.id) is None and self._directives is not None:
|
|
406
|
+
try:
|
|
407
|
+
from apsimo.directives import Action
|
|
408
|
+
verdict = self._directives.check(Action(
|
|
409
|
+
kind="project",
|
|
410
|
+
text=project.objective,
|
|
411
|
+
target=project.subject_person_id,
|
|
412
|
+
high_risk=True,
|
|
413
|
+
))
|
|
414
|
+
except Exception as exc:
|
|
415
|
+
raise RuntimeError(
|
|
416
|
+
"governed research boundary check failed closed"
|
|
417
|
+
) from exc
|
|
418
|
+
if getattr(verdict, "allowed", None) is not True:
|
|
419
|
+
raise RuntimeError("governed research boundary refused the project")
|
|
420
|
+
|
|
421
|
+
def enqueue_governed_research(self, project: Project) -> Project:
|
|
422
|
+
"""Durably enqueue or exactly replay one governed research Project."""
|
|
423
|
+
|
|
424
|
+
self.prepare_governed_research(project)
|
|
425
|
+
stored, _created = self.store.insert_authority_bound_project(project)
|
|
426
|
+
return stored
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _authority_gaps(
|
|
430
|
+
project: Project, steps: List[Step],
|
|
431
|
+
) -> Dict[int, List[str]]:
|
|
432
|
+
if project.source not in _AUTHORITY_BOUND_PROJECT_SOURCES:
|
|
433
|
+
return {}
|
|
434
|
+
from apsimo.work_orders import action_authority
|
|
435
|
+
allowed = set(project.capability_allowlist)
|
|
436
|
+
missing: Dict[int, List[str]] = {}
|
|
437
|
+
for step in steps:
|
|
438
|
+
if project.source == "cognition_spine" and step.action_kind == "deliver":
|
|
439
|
+
missing[step.ordinal] = [
|
|
440
|
+
"p3_deliver_held_missing_attested_recipient_artifact_envelope"
|
|
441
|
+
]
|
|
442
|
+
continue
|
|
443
|
+
required = set(action_authority(step.action_kind)[1])
|
|
444
|
+
excess = sorted(required - allowed)
|
|
445
|
+
if excess:
|
|
446
|
+
missing[step.ordinal] = excess
|
|
447
|
+
return missing
|
|
448
|
+
|
|
449
|
+
# ------------------------------------------------------------------
|
|
450
|
+
# Creation / adoption / abandonment
|
|
451
|
+
# ------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
def create_project(self, objective: str, *, title: str = "",
|
|
454
|
+
source: str = "owner",
|
|
455
|
+
entity_ids: Optional[List[str]] = None,
|
|
456
|
+
) -> Tuple[Optional[Project], str]:
|
|
457
|
+
"""Boundary-gated project creation (planning only; steps gate at
|
|
458
|
+
dispatch). Returns (project, reason)."""
|
|
459
|
+
objective = (objective or "").strip()
|
|
460
|
+
if not objective:
|
|
461
|
+
return None, "objective_required"
|
|
462
|
+
try:
|
|
463
|
+
from apsimo.cognition.goal_spine import cognition_spine_exclusive
|
|
464
|
+
if cognition_spine_exclusive():
|
|
465
|
+
if source == "cognition_spine":
|
|
466
|
+
return None, "typed_goal_proposal_required"
|
|
467
|
+
if source not in {"owner", "directive"}:
|
|
468
|
+
return None, "legacy_autonomous_project_writer_read_only"
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
if self._directives is not None:
|
|
472
|
+
try:
|
|
473
|
+
from apsimo.directives import Action
|
|
474
|
+
verdict = self._directives.check(Action(
|
|
475
|
+
kind="project", text=objective, target=title or objective,
|
|
476
|
+
high_risk=True))
|
|
477
|
+
if not verdict.allowed:
|
|
478
|
+
logger.warning("Project creation REFUSED by boundary: %s",
|
|
479
|
+
verdict.reason)
|
|
480
|
+
return None, verdict.reason
|
|
481
|
+
except Exception:
|
|
482
|
+
logger.warning(
|
|
483
|
+
"Project creation blocked: boundary check failed closed",
|
|
484
|
+
exc_info=True,
|
|
485
|
+
)
|
|
486
|
+
return None, "boundary_check_failed_closed"
|
|
487
|
+
title = (title or objective.split(".")[0]).strip()[:120]
|
|
488
|
+
project = Project(title=title, objective=objective, source=source,
|
|
489
|
+
entity_ids=list(entity_ids or []))
|
|
490
|
+
self.store.save_project(project)
|
|
491
|
+
logger.info("Project created: %s %r (source=%s, mode=%s)",
|
|
492
|
+
project.id, title, source, projects_mode())
|
|
493
|
+
return project, "ok"
|
|
494
|
+
|
|
495
|
+
async def create_owner_goal_work_order(
|
|
496
|
+
self,
|
|
497
|
+
objective: str,
|
|
498
|
+
*,
|
|
499
|
+
external_event_id: str,
|
|
500
|
+
external_event_digest: str,
|
|
501
|
+
intake_receipt: Mapping[str, Any],
|
|
502
|
+
subject_person_id: str,
|
|
503
|
+
viewer_scope: str,
|
|
504
|
+
shareability: str,
|
|
505
|
+
occurred_at: str,
|
|
506
|
+
) -> Dict[str, Any]:
|
|
507
|
+
"""Create one deterministic owner Project and initial WorkOrder.
|
|
508
|
+
|
|
509
|
+
This is the narrow durable target for a transport adapter that has
|
|
510
|
+
already authenticated an owner ``Goal:`` request. Intent recognition
|
|
511
|
+
happens before this method; no model decides whether the Project
|
|
512
|
+
exists. The first bounded ``analyze`` step lets the normal canonical
|
|
513
|
+
WorkOrder/Action Plane continue the goal without granting an external
|
|
514
|
+
effect at ingestion time.
|
|
515
|
+
|
|
516
|
+
Every identity and authority-bearing timestamp is derived from the
|
|
517
|
+
immutable external event. Replaying the event therefore validates and
|
|
518
|
+
returns the same Project, Step, and WorkOrder instead of creating work
|
|
519
|
+
twice. The external intake receipt and journal identity are reference
|
|
520
|
+
linked into the WorkOrder; external producer text never becomes an
|
|
521
|
+
authority or an execution receipt by itself.
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
from apsimo.work_orders import WorkOrderV1
|
|
525
|
+
|
|
526
|
+
normalized = " ".join(str(objective or "").split()).strip()
|
|
527
|
+
event_id = str(external_event_id or "").strip()
|
|
528
|
+
event_digest = str(external_event_digest or "").strip()
|
|
529
|
+
subject = str(subject_person_id or "").strip()
|
|
530
|
+
scope = str(viewer_scope or "").strip()
|
|
531
|
+
sharing = str(shareability or "").strip()
|
|
532
|
+
if not normalized or len(normalized) > 500:
|
|
533
|
+
raise ValueError("owner goal objective is outside bounds")
|
|
534
|
+
if not event_id or len(event_id) > 192:
|
|
535
|
+
raise ValueError("owner goal external event ID is outside bounds")
|
|
536
|
+
if not re.fullmatch(r"[0-9a-f]{64}", event_digest):
|
|
537
|
+
raise ValueError("owner goal external event digest is invalid")
|
|
538
|
+
if not subject or not scope or sharing != "owner_private":
|
|
539
|
+
raise ValueError("owner goal scope is invalid")
|
|
540
|
+
try:
|
|
541
|
+
issued = datetime.fromisoformat(
|
|
542
|
+
str(occurred_at or "").replace("Z", "+00:00")
|
|
543
|
+
)
|
|
544
|
+
except ValueError as exc:
|
|
545
|
+
raise ValueError("owner goal occurrence time is invalid") from exc
|
|
546
|
+
if issued.tzinfo is None:
|
|
547
|
+
raise ValueError("owner goal occurrence time requires a timezone")
|
|
548
|
+
issued = issued.astimezone(timezone.utc)
|
|
549
|
+
issued_at = issued.timestamp()
|
|
550
|
+
|
|
551
|
+
receipt = dict(intake_receipt or {})
|
|
552
|
+
receipt_ref = str(receipt.get("receipt_ref") or "").strip()
|
|
553
|
+
journal_event_id = str(receipt.get("journal_event_id") or "").strip()
|
|
554
|
+
journal_seq = receipt.get("journal_seq")
|
|
555
|
+
if (
|
|
556
|
+
receipt.get("status") != "projected"
|
|
557
|
+
or receipt.get("event_id") != event_id
|
|
558
|
+
or not receipt_ref.startswith("external-event-receipt:")
|
|
559
|
+
or not journal_event_id
|
|
560
|
+
or isinstance(journal_seq, bool)
|
|
561
|
+
or not isinstance(journal_seq, int)
|
|
562
|
+
or journal_seq < 1
|
|
563
|
+
):
|
|
564
|
+
raise ValueError("owner goal intake receipt is incomplete")
|
|
565
|
+
|
|
566
|
+
identity = hashlib.sha256(
|
|
567
|
+
("owner-goal-v1\0" + event_id).encode("utf-8")
|
|
568
|
+
).hexdigest()
|
|
569
|
+
project_id = f"proj-owner-goal-{identity[:20]}"
|
|
570
|
+
step_id = f"step-owner-goal-{identity[:20]}"
|
|
571
|
+
title = normalized.split(".", 1)[0].strip()[:120] or "Owner goal"
|
|
572
|
+
goal_fingerprint = hashlib.sha256(json.dumps(
|
|
573
|
+
{
|
|
574
|
+
"title": title.casefold(),
|
|
575
|
+
"objective": normalized.casefold(),
|
|
576
|
+
"subject_person_id": subject,
|
|
577
|
+
"viewer_scope": scope,
|
|
578
|
+
},
|
|
579
|
+
ensure_ascii=False,
|
|
580
|
+
sort_keys=True,
|
|
581
|
+
separators=(",", ":"),
|
|
582
|
+
).encode("utf-8")).hexdigest()
|
|
583
|
+
event_ref = f"xevent:{event_id}"
|
|
584
|
+
host_event_ref = f"event:{journal_event_id}"
|
|
585
|
+
evidence_refs = [
|
|
586
|
+
receipt_ref,
|
|
587
|
+
f"xdigest:{event_digest}",
|
|
588
|
+
f"journal:{journal_seq}:{journal_event_id}",
|
|
589
|
+
]
|
|
590
|
+
expected_project = Project(
|
|
591
|
+
id=project_id,
|
|
592
|
+
title=title,
|
|
593
|
+
objective=normalized,
|
|
594
|
+
source="owner",
|
|
595
|
+
status="active",
|
|
596
|
+
outcome="pending",
|
|
597
|
+
# ``xevent`` is the immutable producer identity. The cognition
|
|
598
|
+
# ThoughtJob intentionally carries only the server journal event
|
|
599
|
+
# identity, so retaining both lets its duplicate gate meet this
|
|
600
|
+
# Project without copying producer text into either authority.
|
|
601
|
+
source_event_refs=[event_ref, host_event_ref],
|
|
602
|
+
evidence_refs=evidence_refs,
|
|
603
|
+
subject_person_id=subject,
|
|
604
|
+
viewer_scope=scope,
|
|
605
|
+
shareability=sharing,
|
|
606
|
+
goal_fingerprint=goal_fingerprint,
|
|
607
|
+
created_at=issued_at,
|
|
608
|
+
updated_at=issued_at,
|
|
609
|
+
)
|
|
610
|
+
expected_step = Step(
|
|
611
|
+
id=step_id,
|
|
612
|
+
project_id=project_id,
|
|
613
|
+
ordinal=1,
|
|
614
|
+
description=(
|
|
615
|
+
"Analyze and advance the owner-authored goal, returning a "
|
|
616
|
+
f"receipt-backed result: {normalized}"
|
|
617
|
+
),
|
|
618
|
+
action_kind="analyze",
|
|
619
|
+
status="pending",
|
|
620
|
+
boundary_subject=subject,
|
|
621
|
+
confidence=1.0,
|
|
622
|
+
work_order_issued_at=issued_at,
|
|
623
|
+
created_at=issued_at,
|
|
624
|
+
updated_at=issued_at,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
project = self.store.get_project(project_id)
|
|
628
|
+
if project is None:
|
|
629
|
+
project = expected_project
|
|
630
|
+
else:
|
|
631
|
+
immutable = (
|
|
632
|
+
"title", "objective", "source", "source_event_refs",
|
|
633
|
+
"evidence_refs", "subject_person_id", "viewer_scope",
|
|
634
|
+
"shareability", "goal_fingerprint",
|
|
635
|
+
)
|
|
636
|
+
if any(
|
|
637
|
+
getattr(project, field) != getattr(expected_project, field)
|
|
638
|
+
for field in immutable
|
|
639
|
+
):
|
|
640
|
+
raise ValueError("deterministic owner goal Project collision")
|
|
641
|
+
|
|
642
|
+
steps = self.store.steps_for(project_id)
|
|
643
|
+
step = next((item for item in steps if item.id == step_id), None)
|
|
644
|
+
if step is None:
|
|
645
|
+
if steps:
|
|
646
|
+
raise ValueError("deterministic owner goal Step collision")
|
|
647
|
+
step = expected_step
|
|
648
|
+
else:
|
|
649
|
+
authority_fields = (
|
|
650
|
+
"project_id", "ordinal", "description", "action_kind",
|
|
651
|
+
"depends_on", "boundary_subject", "confidence",
|
|
652
|
+
)
|
|
653
|
+
if any(
|
|
654
|
+
getattr(step, field) != getattr(expected_step, field)
|
|
655
|
+
for field in authority_fields
|
|
656
|
+
):
|
|
657
|
+
raise ValueError("deterministic owner goal Step collision")
|
|
658
|
+
if (
|
|
659
|
+
step.work_order_issued_at
|
|
660
|
+
and abs(step.work_order_issued_at - issued_at) > 0.001
|
|
661
|
+
):
|
|
662
|
+
raise ValueError("owner goal WorkOrder issue time drifted")
|
|
663
|
+
if not step.work_order_issued_at:
|
|
664
|
+
step.work_order_issued_at = issued_at
|
|
665
|
+
|
|
666
|
+
refs = tuple(project.provenance_refs())
|
|
667
|
+
order = WorkOrderV1.for_project_step(
|
|
668
|
+
project, step, context_refs=refs, now=issued,
|
|
669
|
+
)
|
|
670
|
+
existing_order = self.store.get_work_order(order.work_order_id)
|
|
671
|
+
if existing_order is not None and (
|
|
672
|
+
existing_order["work_order_digest"] != order.work_order_digest
|
|
673
|
+
or existing_order["payload"] != order.payload()
|
|
674
|
+
):
|
|
675
|
+
raise ValueError("deterministic owner goal WorkOrder collision")
|
|
676
|
+
|
|
677
|
+
terminal = project.status in {"completed", "abandoned"} or (
|
|
678
|
+
step.status in {"done", "failed", "skipped"}
|
|
679
|
+
)
|
|
680
|
+
if terminal:
|
|
681
|
+
if existing_order is None:
|
|
682
|
+
raise ValueError("terminal owner goal is missing its WorkOrder")
|
|
683
|
+
return {
|
|
684
|
+
"schema": "OwnerGoalPromotionReceiptV1",
|
|
685
|
+
"status": "existing_terminal",
|
|
686
|
+
"external_event_id": event_id,
|
|
687
|
+
"external_receipt_ref": receipt_ref,
|
|
688
|
+
"project_id": project_id,
|
|
689
|
+
"step_id": step_id,
|
|
690
|
+
"work_order_ref": f"work-order:{order.work_order_id}",
|
|
691
|
+
"work_order_id": order.work_order_id,
|
|
692
|
+
"work_order_digest": order.work_order_digest,
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
blocked_reason = ""
|
|
696
|
+
if project.status == "blocked":
|
|
697
|
+
blocked_reason = project.reason or "owner_goal_blocked"
|
|
698
|
+
elif self._directives is not None:
|
|
699
|
+
try:
|
|
700
|
+
from apsimo.directives import Action
|
|
701
|
+
for action in (
|
|
702
|
+
Action(
|
|
703
|
+
kind="project",
|
|
704
|
+
text=_boundary_intent_text(normalized, title),
|
|
705
|
+
target="",
|
|
706
|
+
high_risk=True,
|
|
707
|
+
),
|
|
708
|
+
_step_boundary_action(project, step),
|
|
709
|
+
):
|
|
710
|
+
verdict = self._directives.check(action)
|
|
711
|
+
if not verdict.allowed:
|
|
712
|
+
blocked_reason = str(
|
|
713
|
+
verdict.reason or "owner_goal_boundary_refused"
|
|
714
|
+
)[:500]
|
|
715
|
+
break
|
|
716
|
+
except Exception:
|
|
717
|
+
blocked_reason = "boundary_check_failed_closed"
|
|
718
|
+
|
|
719
|
+
if blocked_reason:
|
|
720
|
+
if self.store.get_project(project_id) is None:
|
|
721
|
+
project.status = "blocked"
|
|
722
|
+
project.reason = blocked_reason
|
|
723
|
+
self.store.prepare_work_order(project, step, order)
|
|
724
|
+
return {
|
|
725
|
+
"schema": "OwnerGoalPromotionReceiptV1",
|
|
726
|
+
"status": "blocked",
|
|
727
|
+
"reason": blocked_reason,
|
|
728
|
+
"external_event_id": event_id,
|
|
729
|
+
"external_receipt_ref": receipt_ref,
|
|
730
|
+
"project_id": project_id,
|
|
731
|
+
"step_id": step_id,
|
|
732
|
+
"work_order_ref": f"work-order:{order.work_order_id}",
|
|
733
|
+
"work_order_id": order.work_order_id,
|
|
734
|
+
"work_order_digest": order.work_order_digest,
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if not self._has_canonical_work_order_adapter():
|
|
738
|
+
raise RuntimeError("canonical WorkOrder adapter is unavailable")
|
|
739
|
+
ok, result = await self._work_orders.execute(
|
|
740
|
+
project, step, context_refs=refs,
|
|
741
|
+
)
|
|
742
|
+
if ok is False:
|
|
743
|
+
raise RuntimeError(result or "owner goal WorkOrder issue failed")
|
|
744
|
+
persisted = self.store.get_work_order(order.work_order_id)
|
|
745
|
+
if persisted is None:
|
|
746
|
+
raise RuntimeError("owner goal WorkOrder did not persist")
|
|
747
|
+
return {
|
|
748
|
+
"schema": "OwnerGoalPromotionReceiptV1",
|
|
749
|
+
"status": "issued",
|
|
750
|
+
"queue_state": str(result or "").rsplit(":", 1)[-1],
|
|
751
|
+
"external_event_id": event_id,
|
|
752
|
+
"external_receipt_ref": receipt_ref,
|
|
753
|
+
"project_id": project_id,
|
|
754
|
+
"step_id": step_id,
|
|
755
|
+
"work_order_ref": persisted["work_order_ref"],
|
|
756
|
+
"work_order_id": order.work_order_id,
|
|
757
|
+
"work_order_digest": order.work_order_digest,
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
def abandon(self, project_id: str, reason: str = "owner_request",
|
|
761
|
+
) -> Optional[Project]:
|
|
762
|
+
project = self.store.get_project(project_id)
|
|
763
|
+
if project is None or project.status in ("completed", "abandoned"):
|
|
764
|
+
return project
|
|
765
|
+
project.status = "abandoned"
|
|
766
|
+
project.outcome = "failed"
|
|
767
|
+
project.reason = reason
|
|
768
|
+
self.store.save_project(project)
|
|
769
|
+
self._record_outcome("failure")
|
|
770
|
+
if self._feedback is not None:
|
|
771
|
+
try:
|
|
772
|
+
self._feedback.record("project", "dismissed")
|
|
773
|
+
except Exception:
|
|
774
|
+
pass
|
|
775
|
+
logger.info("Project %s abandoned: %s", project_id, reason)
|
|
776
|
+
return project
|
|
777
|
+
|
|
778
|
+
def project_status(self, project_id: str) -> Optional[Dict[str, Any]]:
|
|
779
|
+
project = self.store.get_project(project_id)
|
|
780
|
+
if project is None:
|
|
781
|
+
return None
|
|
782
|
+
steps = self.store.steps_for(project_id)
|
|
783
|
+
return {
|
|
784
|
+
"project": project.to_row(),
|
|
785
|
+
"steps": [s.to_row() for s in steps],
|
|
786
|
+
"done": sum(1 for s in steps if s.status == "done"),
|
|
787
|
+
"total": len(steps),
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
# ------------------------------------------------------------------
|
|
791
|
+
# Tick
|
|
792
|
+
# ------------------------------------------------------------------
|
|
793
|
+
|
|
794
|
+
def _effective_mode(self) -> str:
|
|
795
|
+
"""Env mode, graduated by the trust engine (Amendment 1.2).
|
|
796
|
+
|
|
797
|
+
"off" and "live" are owner overrides. "shadow" is the calibration
|
|
798
|
+
stage: once the "project" trust domain graduates (clean calibration
|
|
799
|
+
runs), pursuit goes live THROUGH the sub-gates, which carry their own
|
|
800
|
+
ask/approval semantics for anything outbound or mutating.
|
|
801
|
+
"""
|
|
802
|
+
mode = projects_mode()
|
|
803
|
+
if mode in ("off", "live"):
|
|
804
|
+
return mode
|
|
805
|
+
trust = getattr(self._self_model, "trust", None)
|
|
806
|
+
if trust is None:
|
|
807
|
+
return mode
|
|
808
|
+
try:
|
|
809
|
+
stage = trust.stage("project", default="shadow")
|
|
810
|
+
except Exception:
|
|
811
|
+
return mode
|
|
812
|
+
return "shadow" if stage == "shadow" else "live"
|
|
813
|
+
|
|
814
|
+
async def tick(self) -> Dict[str, Any]:
|
|
815
|
+
mode = self._effective_mode()
|
|
816
|
+
report: Dict[str, Any] = {"mode": mode, "adopted": 0, "planned": 0,
|
|
817
|
+
"steps_dispatched": 0, "deferred": False}
|
|
818
|
+
if mode == "off":
|
|
819
|
+
return report
|
|
820
|
+
report["held"] = 0
|
|
821
|
+
|
|
822
|
+
# Pursue-vs-defer via the self-model: under heavy load, hold off.
|
|
823
|
+
if self._self_model is not None:
|
|
824
|
+
try:
|
|
825
|
+
load = self._self_model.load()
|
|
826
|
+
if int(load.get("total") or 0) >= _int_env(
|
|
827
|
+
"COLONY_PROJECTS_DEFER_LOAD", 10):
|
|
828
|
+
logger.info("Project pursuit deferred (load=%s)", load)
|
|
829
|
+
report["deferred"] = True
|
|
830
|
+
return report
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
report["adopted"] = await self._adopt_initiatives()
|
|
836
|
+
except Exception:
|
|
837
|
+
logger.debug("project adoption failed", exc_info=True)
|
|
838
|
+
try:
|
|
839
|
+
held_ids: Set[str] = set()
|
|
840
|
+
report["planned"] = await self._plan_pending(mode, held_ids)
|
|
841
|
+
except Exception:
|
|
842
|
+
logger.debug("project planning failed", exc_info=True)
|
|
843
|
+
try:
|
|
844
|
+
report["steps_dispatched"] = await self._pursue_active(
|
|
845
|
+
mode, held_ids,
|
|
846
|
+
)
|
|
847
|
+
except Exception:
|
|
848
|
+
logger.debug("project pursuit failed", exc_info=True)
|
|
849
|
+
report["held"] = len(held_ids)
|
|
850
|
+
return report
|
|
851
|
+
|
|
852
|
+
async def reconcile_terminal_results(
|
|
853
|
+
self, *, limit: int = 25,
|
|
854
|
+
) -> Dict[str, Any]:
|
|
855
|
+
"""Reconcile durable queue outcomes without planning or pursuit.
|
|
856
|
+
|
|
857
|
+
This is intentionally separate from ``tick``: result projection is
|
|
858
|
+
bookkeeping for work that already ran, so it must not wait behind an
|
|
859
|
+
LLM planning pass, self-model deferral, review cadence, or a Project
|
|
860
|
+
boundary that changed after dispatch.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
reconcile = getattr(
|
|
864
|
+
self._work_orders, "reconcile_terminal_results", None,
|
|
865
|
+
)
|
|
866
|
+
if not callable(reconcile):
|
|
867
|
+
return {
|
|
868
|
+
"checked": 0,
|
|
869
|
+
"terminal": 0,
|
|
870
|
+
"projected": 0,
|
|
871
|
+
"errors": 0,
|
|
872
|
+
}
|
|
873
|
+
return await reconcile(limit=limit)
|
|
874
|
+
|
|
875
|
+
# ------------------------------------------------------------------
|
|
876
|
+
# Adoption: project-type initiatives become durable projects
|
|
877
|
+
# ------------------------------------------------------------------
|
|
878
|
+
|
|
879
|
+
async def _adopt_initiatives(self) -> int:
|
|
880
|
+
try:
|
|
881
|
+
from apsimo.cognition.goal_spine import cognition_spine_exclusive
|
|
882
|
+
if cognition_spine_exclusive():
|
|
883
|
+
return 0
|
|
884
|
+
except Exception:
|
|
885
|
+
pass
|
|
886
|
+
if self._initiatives is None:
|
|
887
|
+
return 0
|
|
888
|
+
open_projects = self.open_capacity_used()
|
|
889
|
+
if open_projects >= _int_env("COLONY_PROJECTS_MAX_CONCURRENT", 3):
|
|
890
|
+
return 0
|
|
891
|
+
try:
|
|
892
|
+
loop = asyncio.get_event_loop()
|
|
893
|
+
pending = await loop.run_in_executor(
|
|
894
|
+
None, lambda: self._initiatives.list(
|
|
895
|
+
status=["pending"], type="project", limit=5))
|
|
896
|
+
except Exception:
|
|
897
|
+
return 0
|
|
898
|
+
existing_titles = {p.title.strip().lower()
|
|
899
|
+
for p in self.store.list_projects(limit=200)}
|
|
900
|
+
for init in pending or []:
|
|
901
|
+
desc = (getattr(init, "description", "") or "").strip()
|
|
902
|
+
if not desc or desc.split(".")[0].strip()[:120].lower() in existing_titles:
|
|
903
|
+
continue
|
|
904
|
+
rationale = (getattr(init, "rationale", "") or "").strip()
|
|
905
|
+
objective = desc if not rationale else f"{desc}\n\nWhy: {rationale}"
|
|
906
|
+
project, reason = self.create_project(
|
|
907
|
+
objective, title=desc.split(".")[0][:120], source="thinker")
|
|
908
|
+
if project is None:
|
|
909
|
+
continue
|
|
910
|
+
try:
|
|
911
|
+
iid = getattr(init, "id", "")
|
|
912
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
913
|
+
None, lambda: self._initiatives.complete(
|
|
914
|
+
iid, agent_id="project-engine",
|
|
915
|
+
result=f"adopted as project {project.id}"))
|
|
916
|
+
except Exception:
|
|
917
|
+
logger.debug("initiative adoption closure failed", exc_info=True)
|
|
918
|
+
logger.info("Adopted initiative as project %s: %r",
|
|
919
|
+
project.id, project.title)
|
|
920
|
+
return 1
|
|
921
|
+
return 0
|
|
922
|
+
|
|
923
|
+
# ------------------------------------------------------------------
|
|
924
|
+
# Planning
|
|
925
|
+
# ------------------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
async def _plan_pending(
|
|
928
|
+
self, mode: str, held_ids: Optional[Set[str]] = None,
|
|
929
|
+
) -> int:
|
|
930
|
+
from apsimo.projects.planner import plan_project
|
|
931
|
+
planned = 0
|
|
932
|
+
now = time.time()
|
|
933
|
+
for project in self._eligible_projects(
|
|
934
|
+
status="planning", limit=10, held_ids=held_ids,
|
|
935
|
+
):
|
|
936
|
+
if project.next_review_at and project.next_review_at > now:
|
|
937
|
+
continue
|
|
938
|
+
if project.source == "governed_action":
|
|
939
|
+
hold_reason = self._governed_hold_reason(mode)
|
|
940
|
+
if hold_reason:
|
|
941
|
+
project.reason = hold_reason
|
|
942
|
+
project.next_review_at = now + projects_review_secs()
|
|
943
|
+
self.store.save_project(project)
|
|
944
|
+
continue
|
|
945
|
+
if project.reason in _GOVERNED_HOLD_REASONS:
|
|
946
|
+
project.reason = ""
|
|
947
|
+
skills_block = self._skills_block(project.objective)
|
|
948
|
+
brief = self._self_brief()
|
|
949
|
+
planning_context = ""
|
|
950
|
+
if project.source == "cognition_spine":
|
|
951
|
+
planning_context = (
|
|
952
|
+
"This autonomous goal is bound to these exact capabilities: "
|
|
953
|
+
+ ", ".join(project.capability_allowlist)
|
|
954
|
+
+ ". Do not create a deliver step: P3 delivery is held until "
|
|
955
|
+
"WorkOrder carries a transport-attested recipient and a "
|
|
956
|
+
"bounded message/artifact reference in its authority digest. "
|
|
957
|
+
"End with an internal receipt-backed artifact instead."
|
|
958
|
+
)
|
|
959
|
+
elif project.source == "governed_action":
|
|
960
|
+
planning_context = (
|
|
961
|
+
"This owner-governed research goal is bound to these exact "
|
|
962
|
+
"read-only capabilities: "
|
|
963
|
+
+ ", ".join(project.capability_allowlist)
|
|
964
|
+
+ ". Do not create directed or deliver steps, or any step "
|
|
965
|
+
"requiring capabilities outside this list. End with a "
|
|
966
|
+
"receipt-backed internal artifact."
|
|
967
|
+
)
|
|
968
|
+
steps = await plan_project(
|
|
969
|
+
self._router, project.objective, project_id=project.id,
|
|
970
|
+
context=planning_context,
|
|
971
|
+
skills_block=skills_block, self_brief=brief,
|
|
972
|
+
boundaries=self._boundaries_brief())
|
|
973
|
+
if self._apply_project_hold(project):
|
|
974
|
+
if held_ids is not None:
|
|
975
|
+
held_ids.add(project.id)
|
|
976
|
+
continue
|
|
977
|
+
if not steps:
|
|
978
|
+
project.replans += 1
|
|
979
|
+
if project.replans > projects_max_replans():
|
|
980
|
+
project.status = "abandoned"
|
|
981
|
+
project.outcome = "failed"
|
|
982
|
+
project.reason = "planning_failed"
|
|
983
|
+
self.store.save_project(project)
|
|
984
|
+
self._record_outcome("failure")
|
|
985
|
+
logger.warning("Project %s abandoned: planning failed %d times",
|
|
986
|
+
project.id, project.replans)
|
|
987
|
+
else:
|
|
988
|
+
project.next_review_at = now + projects_review_secs()
|
|
989
|
+
self.store.save_project(project)
|
|
990
|
+
continue
|
|
991
|
+
if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
|
|
992
|
+
try:
|
|
993
|
+
missing = self._authority_gaps(project, steps)
|
|
994
|
+
except Exception as exc:
|
|
995
|
+
project.status = "blocked"
|
|
996
|
+
project.reason = f"capability_validation_failed:{exc}"
|
|
997
|
+
self.store.save_project(project)
|
|
998
|
+
continue
|
|
999
|
+
if missing:
|
|
1000
|
+
project.status = "blocked"
|
|
1001
|
+
project.reason = "goal_authority_missing:" + json.dumps(
|
|
1002
|
+
missing, sort_keys=True, separators=(",", ":"),
|
|
1003
|
+
)
|
|
1004
|
+
self.store.save_project(project)
|
|
1005
|
+
continue
|
|
1006
|
+
for s in steps:
|
|
1007
|
+
self.store.save_step(s)
|
|
1008
|
+
project.status = "active"
|
|
1009
|
+
project.next_review_at = 0.0
|
|
1010
|
+
self.store.save_project(project)
|
|
1011
|
+
planned += 1
|
|
1012
|
+
logger.info(
|
|
1013
|
+
"Project %s planned[%s]: %d step(s): %s",
|
|
1014
|
+
project.id, mode, len(steps),
|
|
1015
|
+
" | ".join(f"{s.ordinal}.{s.action_kind}:{s.description[:60]}"
|
|
1016
|
+
for s in steps))
|
|
1017
|
+
return planned
|
|
1018
|
+
|
|
1019
|
+
# ------------------------------------------------------------------
|
|
1020
|
+
# Pursuit
|
|
1021
|
+
# ------------------------------------------------------------------
|
|
1022
|
+
|
|
1023
|
+
async def _pursue_active(
|
|
1024
|
+
self, mode: str, held_ids: Optional[Set[str]] = None,
|
|
1025
|
+
) -> int:
|
|
1026
|
+
dispatched = 0
|
|
1027
|
+
for project in self._eligible_projects(
|
|
1028
|
+
status="active", limit=3, held_ids=held_ids, due=True,
|
|
1029
|
+
):
|
|
1030
|
+
try:
|
|
1031
|
+
advanced = await self._advance_project(
|
|
1032
|
+
project, mode, held_ids=held_ids,
|
|
1033
|
+
)
|
|
1034
|
+
dispatched += 1 if advanced else 0
|
|
1035
|
+
except Exception:
|
|
1036
|
+
logger.error("project %s advance failed", project.id,
|
|
1037
|
+
exc_info=True)
|
|
1038
|
+
return dispatched
|
|
1039
|
+
|
|
1040
|
+
async def _advance_project(
|
|
1041
|
+
self, project: Project, mode: str,
|
|
1042
|
+
held_ids: Optional[Set[str]] = None,
|
|
1043
|
+
) -> bool:
|
|
1044
|
+
if self._apply_project_hold(project):
|
|
1045
|
+
if held_ids is not None:
|
|
1046
|
+
held_ids.add(project.id)
|
|
1047
|
+
return False
|
|
1048
|
+
if project.source == "governed_action":
|
|
1049
|
+
hold_reason = self._governed_hold_reason(mode)
|
|
1050
|
+
if hold_reason:
|
|
1051
|
+
project.reason = hold_reason
|
|
1052
|
+
project.next_review_at = time.time() + projects_review_secs()
|
|
1053
|
+
self.store.save_project(project)
|
|
1054
|
+
return False
|
|
1055
|
+
if project.reason in _GOVERNED_HOLD_REASONS:
|
|
1056
|
+
project.reason = ""
|
|
1057
|
+
steps = self.store.steps_for(project.id)
|
|
1058
|
+
if not steps:
|
|
1059
|
+
# active project with no steps: send back to planning
|
|
1060
|
+
project.status = "planning"
|
|
1061
|
+
self.store.save_project(project)
|
|
1062
|
+
return False
|
|
1063
|
+
|
|
1064
|
+
# Terminal check: everything done/skipped -> complete.
|
|
1065
|
+
if all(s.status in ("done", "skipped") for s in steps):
|
|
1066
|
+
await self._complete_project(project, steps, mode)
|
|
1067
|
+
return False
|
|
1068
|
+
|
|
1069
|
+
# A failed step triggers a bounded replan of the remaining work.
|
|
1070
|
+
if any(s.status == "failed" for s in steps):
|
|
1071
|
+
await self._replan_remaining(project, steps, mode)
|
|
1072
|
+
return False
|
|
1073
|
+
|
|
1074
|
+
done_ordinals = {s.ordinal for s in steps if s.status in ("done", "skipped")}
|
|
1075
|
+
ready = [s for s in steps if s.status == "pending"
|
|
1076
|
+
and all(d in done_ordinals for d in s.depends_on)]
|
|
1077
|
+
if not ready:
|
|
1078
|
+
# nothing ready (steps active or deps unmet) -- check again later
|
|
1079
|
+
project.next_review_at = time.time() + projects_review_secs()
|
|
1080
|
+
self.store.save_project(project)
|
|
1081
|
+
return False
|
|
1082
|
+
step = ready[0]
|
|
1083
|
+
|
|
1084
|
+
# A cognition-spine project is bound to the GoalProposal authority.
|
|
1085
|
+
# Recheck at dispatch so a hand-edited or stale plan cannot widen it.
|
|
1086
|
+
if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
|
|
1087
|
+
try:
|
|
1088
|
+
gaps = self._authority_gaps(project, [step])
|
|
1089
|
+
missing = gaps.get(step.ordinal, [])
|
|
1090
|
+
except Exception as exc:
|
|
1091
|
+
missing = [f"validation_error:{exc}"]
|
|
1092
|
+
if missing:
|
|
1093
|
+
project.status = "blocked"
|
|
1094
|
+
project.reason = "goal_authority_missing:" + ",".join(missing)
|
|
1095
|
+
self.store.save_project(project)
|
|
1096
|
+
return False
|
|
1097
|
+
|
|
1098
|
+
# Boundary gate on the concrete step.
|
|
1099
|
+
if self._directives is not None:
|
|
1100
|
+
try:
|
|
1101
|
+
verdict = self._directives.check(
|
|
1102
|
+
_step_boundary_action(project, step)
|
|
1103
|
+
)
|
|
1104
|
+
if not verdict.allowed:
|
|
1105
|
+
project.status = "blocked"
|
|
1106
|
+
project.reason = verdict.reason
|
|
1107
|
+
self.store.save_project(project)
|
|
1108
|
+
logger.warning(
|
|
1109
|
+
"Project %s BLOCKED at step %d by boundary: %s",
|
|
1110
|
+
project.id, step.ordinal, verdict.reason)
|
|
1111
|
+
await self._milestone(
|
|
1112
|
+
project, "blocked",
|
|
1113
|
+
f"Step {step.ordinal} ({step.description[:120]}) hit a "
|
|
1114
|
+
f"standing boundary: {verdict.reason}", mode)
|
|
1115
|
+
return False
|
|
1116
|
+
except Exception:
|
|
1117
|
+
project.status = "blocked"
|
|
1118
|
+
project.reason = "boundary_check_failed_closed"
|
|
1119
|
+
self.store.save_project(project)
|
|
1120
|
+
logger.warning(
|
|
1121
|
+
"Project %s BLOCKED at step %d: boundary check failed closed",
|
|
1122
|
+
project.id,
|
|
1123
|
+
step.ordinal,
|
|
1124
|
+
exc_info=True,
|
|
1125
|
+
)
|
|
1126
|
+
return False
|
|
1127
|
+
|
|
1128
|
+
# A boundary check may itself observe or trigger a concurrent mode
|
|
1129
|
+
# transition. Re-evaluate before either shadow bookkeeping or a live
|
|
1130
|
+
# dispatch so the ready step remains pending and exactly resumable.
|
|
1131
|
+
if self._apply_project_hold(project):
|
|
1132
|
+
if held_ids is not None:
|
|
1133
|
+
held_ids.add(project.id)
|
|
1134
|
+
return False
|
|
1135
|
+
|
|
1136
|
+
if mode == "shadow":
|
|
1137
|
+
logger.info(
|
|
1138
|
+
"SHADOW-PROJECT %s step %d/%d [%s]: would dispatch %r "
|
|
1139
|
+
"(boundary=allowed)",
|
|
1140
|
+
project.id, step.ordinal, len(steps), step.action_kind,
|
|
1141
|
+
step.description[:160])
|
|
1142
|
+
step.status = "skipped"
|
|
1143
|
+
step.result = "SKIPPED: SHADOW simulation (no action taken)"
|
|
1144
|
+
self.store.save_step(step)
|
|
1145
|
+
project.next_review_at = time.time() + projects_review_secs()
|
|
1146
|
+
self.store.save_project(project)
|
|
1147
|
+
# re-check terminal state so a finished shadow run completes
|
|
1148
|
+
steps = self.store.steps_for(project.id)
|
|
1149
|
+
if all(s.status in ("done", "skipped") for s in steps):
|
|
1150
|
+
await self._complete_project(project, steps, mode)
|
|
1151
|
+
return True
|
|
1152
|
+
|
|
1153
|
+
# LIVE dispatch through the kind's own gated sub-path.
|
|
1154
|
+
if self._apply_project_hold(project):
|
|
1155
|
+
if held_ids is not None:
|
|
1156
|
+
held_ids.add(project.id)
|
|
1157
|
+
return False
|
|
1158
|
+
step.status = "active"
|
|
1159
|
+
step.attempts += 1
|
|
1160
|
+
self.store.save_step(step)
|
|
1161
|
+
started = time.monotonic()
|
|
1162
|
+
ok, result = await self._dispatch_step(project, step)
|
|
1163
|
+
latency = time.monotonic() - started
|
|
1164
|
+
|
|
1165
|
+
if ok is None:
|
|
1166
|
+
# waiting on an external gate (e.g. directed approval): not an
|
|
1167
|
+
# attempt, re-check at next review.
|
|
1168
|
+
step.status = "pending"
|
|
1169
|
+
step.attempts = max(0, step.attempts - 1)
|
|
1170
|
+
step.result = result[:1000]
|
|
1171
|
+
self.store.save_step(step)
|
|
1172
|
+
elif ok:
|
|
1173
|
+
skipped = result.startswith("SKIPPED:")
|
|
1174
|
+
step.status = "skipped" if skipped else "done"
|
|
1175
|
+
step.result = result[:2000]
|
|
1176
|
+
self.store.save_step(step)
|
|
1177
|
+
if not skipped:
|
|
1178
|
+
receipt = (
|
|
1179
|
+
self.store.get_execution_result(step.result_ref)
|
|
1180
|
+
if step.result_ref else None
|
|
1181
|
+
)
|
|
1182
|
+
if (
|
|
1183
|
+
receipt
|
|
1184
|
+
and receipt.get("terminal_outcome") == "succeeded"
|
|
1185
|
+
and receipt.get("verification_result") == "verified"
|
|
1186
|
+
):
|
|
1187
|
+
self._record_outcome(
|
|
1188
|
+
"success", latency,
|
|
1189
|
+
stated_confidence=step.confidence,
|
|
1190
|
+
)
|
|
1191
|
+
else:
|
|
1192
|
+
self._record_outcome(
|
|
1193
|
+
"timeout" if "timeout" in (result or "").lower() else "failure",
|
|
1194
|
+
latency, stated_confidence=step.confidence)
|
|
1195
|
+
if step.attempts >= 2:
|
|
1196
|
+
step.status = "failed"
|
|
1197
|
+
step.result = result[:1000]
|
|
1198
|
+
self.store.save_step(step)
|
|
1199
|
+
if self._skills is not None:
|
|
1200
|
+
try:
|
|
1201
|
+
self._skills.record_failure_note(
|
|
1202
|
+
"project",
|
|
1203
|
+
f"step '{step.description[:80]}' failed: {result[:120]}")
|
|
1204
|
+
except Exception:
|
|
1205
|
+
pass
|
|
1206
|
+
else:
|
|
1207
|
+
step.status = "pending"
|
|
1208
|
+
step.result = f"attempt {step.attempts} failed: {result[:500]}"
|
|
1209
|
+
self.store.save_step(step)
|
|
1210
|
+
|
|
1211
|
+
project.next_review_at = time.time() + projects_review_secs()
|
|
1212
|
+
self.store.save_project(project)
|
|
1213
|
+
|
|
1214
|
+
steps = self.store.steps_for(project.id)
|
|
1215
|
+
if all(s.status in ("done", "skipped") for s in steps):
|
|
1216
|
+
await self._complete_project(project, steps, mode)
|
|
1217
|
+
return True
|
|
1218
|
+
|
|
1219
|
+
# ------------------------------------------------------------------
|
|
1220
|
+
# Step dispatch by kind (LIVE mode only)
|
|
1221
|
+
# ------------------------------------------------------------------
|
|
1222
|
+
|
|
1223
|
+
async def _dispatch_step(self, project: Project, step: Step,
|
|
1224
|
+
) -> Tuple[Optional[bool], str]:
|
|
1225
|
+
if (
|
|
1226
|
+
project.source == "governed_action"
|
|
1227
|
+
and not self._has_canonical_work_order_adapter()
|
|
1228
|
+
):
|
|
1229
|
+
return None, (
|
|
1230
|
+
"governed_action_requires_canonical_work_order_adapter"
|
|
1231
|
+
)
|
|
1232
|
+
if self._work_orders is not None:
|
|
1233
|
+
completed_refs = tuple(project.provenance_refs()) + tuple(
|
|
1234
|
+
"project-step:%s:%s" % (project.id, item.id)
|
|
1235
|
+
for item in self.store.steps_for(project.id)
|
|
1236
|
+
if item.status == "done"
|
|
1237
|
+
)
|
|
1238
|
+
return await self._work_orders.execute(
|
|
1239
|
+
project, step, context_refs=completed_refs
|
|
1240
|
+
)
|
|
1241
|
+
kind = step.action_kind
|
|
1242
|
+
try:
|
|
1243
|
+
if kind in ("analyze", "research", "internal"):
|
|
1244
|
+
return await self._run_reasoning_step(project, step)
|
|
1245
|
+
if kind == "directed":
|
|
1246
|
+
return await self._run_directed_step(project, step)
|
|
1247
|
+
if kind == "deliver":
|
|
1248
|
+
return await self._run_deliver_step(project, step)
|
|
1249
|
+
except Exception as exc:
|
|
1250
|
+
return False, f"dispatch error: {exc}"
|
|
1251
|
+
return False, f"unknown action_kind {kind}"
|
|
1252
|
+
|
|
1253
|
+
async def _run_reasoning_step(self, project: Project, step: Step,
|
|
1254
|
+
) -> Tuple[Optional[bool], str]:
|
|
1255
|
+
if self._reasoning is None:
|
|
1256
|
+
return False, "no reasoning loop wired"
|
|
1257
|
+
done = [s for s in self.store.steps_for(project.id)
|
|
1258
|
+
if s.status == "done" and s.result]
|
|
1259
|
+
context = "\n".join(
|
|
1260
|
+
f"- step {s.ordinal}: {s.result[:200]}" for s in done[-5:])
|
|
1261
|
+
prompt = (f"## Project: {project.title}\n"
|
|
1262
|
+
f"Objective: {project.objective[:800]}\n\n"
|
|
1263
|
+
+ (f"## Completed so far\n{context}\n\n" if context else "")
|
|
1264
|
+
+ f"## Current step ({step.action_kind})\n{step.description}")
|
|
1265
|
+
try:
|
|
1266
|
+
from apsimo.cognition.charter import build_system_prompt
|
|
1267
|
+
system = build_system_prompt(
|
|
1268
|
+
"executor",
|
|
1269
|
+
self_brief=self._self_brief() or None,
|
|
1270
|
+
boundaries=self._boundaries_brief() or None,
|
|
1271
|
+
skills=self._skills_block(step.description) or None,
|
|
1272
|
+
extra=_STEP_SCOPE_CONTEXT)
|
|
1273
|
+
except Exception:
|
|
1274
|
+
logger.debug("charter compose failed; minimal fallback",
|
|
1275
|
+
exc_info=True)
|
|
1276
|
+
system = _STEP_SCOPE_CONTEXT
|
|
1277
|
+
db = self._boundaries_brief()
|
|
1278
|
+
if db:
|
|
1279
|
+
system += ("\n## Standing boundaries from the owner "
|
|
1280
|
+
"(obey without exception)\n" + db)
|
|
1281
|
+
|
|
1282
|
+
working: List[Dict[str, Any]] = [{"role": "user", "content": prompt}]
|
|
1283
|
+
tier = os.environ.get("COLONY_PROJECTS_MODEL_TIER", "small")
|
|
1284
|
+
for _round in range(_MAX_TOOL_ROUNDS):
|
|
1285
|
+
result = await self._reasoning.run_turn(
|
|
1286
|
+
session_id=f"project-{project.id}-s{step.ordinal}",
|
|
1287
|
+
messages=working, system_prompt=system, model_override=tier)
|
|
1288
|
+
if result.status == "completed":
|
|
1289
|
+
text = (result.message or {}).get("content", "") or ""
|
|
1290
|
+
return True, text
|
|
1291
|
+
if result.status == "error":
|
|
1292
|
+
return False, result.error or "reasoning error"
|
|
1293
|
+
if result.status == "needs_tool":
|
|
1294
|
+
pending = list(result.tool_calls or [])
|
|
1295
|
+
if not pending or self._tools is None:
|
|
1296
|
+
return False, "needs_tool with no executable tool path"
|
|
1297
|
+
pending = self._boundary_filter_tools(pending)
|
|
1298
|
+
if not pending:
|
|
1299
|
+
working.append({
|
|
1300
|
+
"role": "user",
|
|
1301
|
+
"content": "Those actions violate a standing boundary "
|
|
1302
|
+
"and were refused. Summarise what you can "
|
|
1303
|
+
"and stop."})
|
|
1304
|
+
continue
|
|
1305
|
+
results = await self._tools.execute_batch(
|
|
1306
|
+
pending, session_id=f"project-{project.id}-s{step.ordinal}")
|
|
1307
|
+
working.append(_assistant_msg(
|
|
1308
|
+
(result.message or {}).get("content", ""), pending))
|
|
1309
|
+
for tr in results:
|
|
1310
|
+
working.append({"role": "tool",
|
|
1311
|
+
"tool_call_id": tr.get("tool_call_id", ""),
|
|
1312
|
+
"content": tr.get("content", "")})
|
|
1313
|
+
continue
|
|
1314
|
+
return False, f"unexpected reasoning status {result.status}"
|
|
1315
|
+
return False, f"tool-loop cap reached ({_MAX_TOOL_ROUNDS} rounds)"
|
|
1316
|
+
|
|
1317
|
+
def _boundary_filter_tools(self, pending: List[dict]) -> List[dict]:
|
|
1318
|
+
if self._directives is None:
|
|
1319
|
+
return pending
|
|
1320
|
+
try:
|
|
1321
|
+
from apsimo.directives import Action
|
|
1322
|
+
except Exception:
|
|
1323
|
+
logger.warning(
|
|
1324
|
+
"Project tool batch blocked: boundary action contract unavailable",
|
|
1325
|
+
exc_info=True,
|
|
1326
|
+
)
|
|
1327
|
+
return []
|
|
1328
|
+
survivors = []
|
|
1329
|
+
for tc in pending:
|
|
1330
|
+
name = tc.get("name", "")
|
|
1331
|
+
args = tc.get("arguments", {}) if isinstance(
|
|
1332
|
+
tc.get("arguments"), dict) else {}
|
|
1333
|
+
try:
|
|
1334
|
+
verdict = self._directives.check(Action(
|
|
1335
|
+
kind="execute_tool", tool_name=name, args=args, text=name,
|
|
1336
|
+
high_risk=True))
|
|
1337
|
+
except Exception:
|
|
1338
|
+
logger.warning(
|
|
1339
|
+
"Project tool call %s REFUSED: boundary check failed closed",
|
|
1340
|
+
name,
|
|
1341
|
+
exc_info=True,
|
|
1342
|
+
)
|
|
1343
|
+
continue
|
|
1344
|
+
if getattr(verdict, "allowed", None) is not True:
|
|
1345
|
+
logger.warning("Project tool call %s REFUSED by boundary: %s",
|
|
1346
|
+
name, getattr(verdict, "reason", "invalid_verdict"))
|
|
1347
|
+
continue
|
|
1348
|
+
survivors.append(tc)
|
|
1349
|
+
return survivors
|
|
1350
|
+
|
|
1351
|
+
async def _run_directed_step(self, project: Project, step: Step,
|
|
1352
|
+
) -> Tuple[Optional[bool], str]:
|
|
1353
|
+
if self._directed is None:
|
|
1354
|
+
return False, "no directed service wired"
|
|
1355
|
+
# An existing awaiting-approval task for this step: keep waiting.
|
|
1356
|
+
prior = (step.result or "")
|
|
1357
|
+
if prior.startswith("awaiting_approval:"):
|
|
1358
|
+
task_id = prior.split(":", 1)[1].strip()
|
|
1359
|
+
task = self._directed.store.get(task_id)
|
|
1360
|
+
if task is not None:
|
|
1361
|
+
if task.status == "awaiting_approval":
|
|
1362
|
+
return None, prior
|
|
1363
|
+
if task.status in ("approved",):
|
|
1364
|
+
out = await self._directed.dispatch(task.id)
|
|
1365
|
+
return True, f"directed task {task.id} dispatched: {json.dumps(out, default=str)[:300]}"
|
|
1366
|
+
if task.status in ("dispatched", "dispatched_dry", "completed"):
|
|
1367
|
+
return True, f"directed task {task.id} {task.status}"
|
|
1368
|
+
if task.status in ("refused", "violated", "failed", "expired"):
|
|
1369
|
+
return False, f"directed task {task.id} {task.status}: {task.refusal_reason}"
|
|
1370
|
+
task = await self._directed.intake(step.description)
|
|
1371
|
+
if task.status == "refused":
|
|
1372
|
+
return False, f"directed intake refused: {task.refusal_reason}"
|
|
1373
|
+
if task.status == "awaiting_approval":
|
|
1374
|
+
return None, f"awaiting_approval:{task.id}"
|
|
1375
|
+
out = await self._directed.dispatch(task.id)
|
|
1376
|
+
status = "dry_run" if out.get("dry_run") else (
|
|
1377
|
+
"dispatched" if out.get("dispatched") else
|
|
1378
|
+
f"not dispatched ({out.get('reason', '?')})")
|
|
1379
|
+
ok = bool(out.get("dispatched") or out.get("dry_run"))
|
|
1380
|
+
return (True, f"directed task {task.id}: {status}") if ok else (
|
|
1381
|
+
False, f"directed task {task.id}: {status}")
|
|
1382
|
+
|
|
1383
|
+
async def _run_deliver_step(self, project: Project, step: Step,
|
|
1384
|
+
) -> Tuple[Optional[bool], str]:
|
|
1385
|
+
done = [s for s in self.store.steps_for(project.id)
|
|
1386
|
+
if s.status == "done" and s.result
|
|
1387
|
+
and not s.result.startswith("SHADOW")]
|
|
1388
|
+
finding = "\n".join(
|
|
1389
|
+
f"- {s.result[:300]}" for s in done[-4:]) or project.objective[:300]
|
|
1390
|
+
try:
|
|
1391
|
+
from apsimo.proposals import Proposal, proposal_to_payload
|
|
1392
|
+
prop = Proposal(
|
|
1393
|
+
title=f"Project update: {project.title[:70]}",
|
|
1394
|
+
finding=f"{step.description[:200]}\n{finding}"[:1200],
|
|
1395
|
+
why_it_helps=f"progress on your project: {project.title[:100]}",
|
|
1396
|
+
suggested_action="Tell me to continue, adjust, or stop this project.",
|
|
1397
|
+
source=project.id, initiative_type="proposal", confidence=0.7)
|
|
1398
|
+
if self._proposals is not None:
|
|
1399
|
+
self._proposals.add(prop)
|
|
1400
|
+
if self._deliver is not None:
|
|
1401
|
+
ok = bool(await self._deliver(proposal_to_payload(prop)))
|
|
1402
|
+
return True, ("delivered" if ok else
|
|
1403
|
+
"routed to guarded delivery (held/gated)")
|
|
1404
|
+
return True, "proposal stored (no delivery router)"
|
|
1405
|
+
except Exception as exc:
|
|
1406
|
+
return False, f"deliver failed: {exc}"
|
|
1407
|
+
|
|
1408
|
+
# ------------------------------------------------------------------
|
|
1409
|
+
# Replan / complete / milestones
|
|
1410
|
+
# ------------------------------------------------------------------
|
|
1411
|
+
|
|
1412
|
+
async def _replan_remaining(self, project: Project, steps: List[Step],
|
|
1413
|
+
mode: str) -> None:
|
|
1414
|
+
from apsimo.projects.planner import plan_project
|
|
1415
|
+
next_replans = project.replans + 1
|
|
1416
|
+
if next_replans > projects_max_replans():
|
|
1417
|
+
project.replans = next_replans
|
|
1418
|
+
project.status = "abandoned"
|
|
1419
|
+
project.reason = "replan_limit"
|
|
1420
|
+
self.store.save_project(project)
|
|
1421
|
+
self._record_outcome("failure")
|
|
1422
|
+
if self._feedback is not None:
|
|
1423
|
+
try:
|
|
1424
|
+
self._feedback.record("project", "dismissed")
|
|
1425
|
+
except Exception:
|
|
1426
|
+
pass
|
|
1427
|
+
await self._milestone(
|
|
1428
|
+
project, "abandoned",
|
|
1429
|
+
f"replan limit reached after {project.replans - 1} replans", mode)
|
|
1430
|
+
return
|
|
1431
|
+
|
|
1432
|
+
done = [s for s in steps if s.status == "done"]
|
|
1433
|
+
failed = [s for s in steps if s.status == "failed"]
|
|
1434
|
+
context = ""
|
|
1435
|
+
if done:
|
|
1436
|
+
context += "Completed steps:\n" + "\n".join(
|
|
1437
|
+
f"- {s.description[:120]}: {s.result[:150]}" for s in done[-5:])
|
|
1438
|
+
if failed:
|
|
1439
|
+
context += "\nFailed steps (plan around these failures):\n" + "\n".join(
|
|
1440
|
+
f"- {s.description[:120]}: {s.result[:150]}" for s in failed[-3:])
|
|
1441
|
+
|
|
1442
|
+
new_steps = await plan_project(
|
|
1443
|
+
self._router,
|
|
1444
|
+
f"{project.objective}\n\nRe-plan ONLY the remaining work.",
|
|
1445
|
+
project_id=project.id, context=context,
|
|
1446
|
+
skills_block=self._skills_block(project.objective),
|
|
1447
|
+
self_brief=self._self_brief(),
|
|
1448
|
+
boundaries=self._boundaries_brief())
|
|
1449
|
+
if self._apply_project_hold(project):
|
|
1450
|
+
return
|
|
1451
|
+
project.replans = next_replans
|
|
1452
|
+
if project.source in _AUTHORITY_BOUND_PROJECT_SOURCES:
|
|
1453
|
+
try:
|
|
1454
|
+
missing = self._authority_gaps(project, new_steps)
|
|
1455
|
+
except Exception as exc:
|
|
1456
|
+
project.status = "blocked"
|
|
1457
|
+
project.reason = f"capability_validation_failed:{exc}"
|
|
1458
|
+
self.store.save_project(project)
|
|
1459
|
+
return
|
|
1460
|
+
if missing:
|
|
1461
|
+
project.status = "blocked"
|
|
1462
|
+
project.reason = "goal_authority_missing:" + json.dumps(
|
|
1463
|
+
missing, sort_keys=True, separators=(",", ":"),
|
|
1464
|
+
)
|
|
1465
|
+
self.store.save_project(project)
|
|
1466
|
+
return
|
|
1467
|
+
self.store.delete_steps(project.id, statuses=["pending", "failed", "active"])
|
|
1468
|
+
base = max((s.ordinal for s in done), default=0)
|
|
1469
|
+
for s in new_steps:
|
|
1470
|
+
s.ordinal += base
|
|
1471
|
+
s.depends_on = [d + base for d in s.depends_on]
|
|
1472
|
+
self.store.save_step(s)
|
|
1473
|
+
project.next_review_at = time.time() + projects_review_secs()
|
|
1474
|
+
if not new_steps and not done:
|
|
1475
|
+
project.status = "abandoned"
|
|
1476
|
+
project.reason = "replan_produced_no_steps"
|
|
1477
|
+
self._record_outcome("failure")
|
|
1478
|
+
elif not new_steps:
|
|
1479
|
+
# nothing left to do beyond what completed
|
|
1480
|
+
self.store.save_project(project)
|
|
1481
|
+
await self._complete_project(
|
|
1482
|
+
project, self.store.steps_for(project.id), mode)
|
|
1483
|
+
return
|
|
1484
|
+
self.store.save_project(project)
|
|
1485
|
+
logger.info("Project %s replanned (replan %d/%d): %d new step(s)",
|
|
1486
|
+
project.id, project.replans, projects_max_replans(),
|
|
1487
|
+
len(new_steps))
|
|
1488
|
+
|
|
1489
|
+
async def _complete_project(self, project: Project, steps: List[Step],
|
|
1490
|
+
mode: str) -> None:
|
|
1491
|
+
if project.status == "completed":
|
|
1492
|
+
return
|
|
1493
|
+
project.status = "completed"
|
|
1494
|
+
done = [step for step in steps if step.status == "done"]
|
|
1495
|
+
skipped = [step for step in steps if step.status == "skipped"]
|
|
1496
|
+
all_steps_done = bool(steps) and len(done) == len(steps)
|
|
1497
|
+
receipts_verified = all_steps_done
|
|
1498
|
+
if receipts_verified:
|
|
1499
|
+
for step in done:
|
|
1500
|
+
result = (
|
|
1501
|
+
self.store.get_execution_result(step.result_ref)
|
|
1502
|
+
if step.result_ref else None
|
|
1503
|
+
)
|
|
1504
|
+
payload = result.get("payload", {}) if result else {}
|
|
1505
|
+
if (
|
|
1506
|
+
not result
|
|
1507
|
+
or result.get("terminal_outcome") != "succeeded"
|
|
1508
|
+
or result.get("verification_result") != "verified"
|
|
1509
|
+
or not payload.get("receipt_refs")
|
|
1510
|
+
):
|
|
1511
|
+
receipts_verified = False
|
|
1512
|
+
break
|
|
1513
|
+
if all_steps_done:
|
|
1514
|
+
project.reason = "all_steps_done"
|
|
1515
|
+
elif done:
|
|
1516
|
+
project.reason = "completed_with_skips"
|
|
1517
|
+
else:
|
|
1518
|
+
project.reason = "all_steps_skipped"
|
|
1519
|
+
if mode == "shadow" or not all_steps_done:
|
|
1520
|
+
project.outcome = "neutral"
|
|
1521
|
+
elif receipts_verified:
|
|
1522
|
+
project.outcome = "succeeded"
|
|
1523
|
+
else:
|
|
1524
|
+
project.outcome = "unverified"
|
|
1525
|
+
self.store.save_project(project)
|
|
1526
|
+
# A skip means no outcome was observed. It is neither success nor
|
|
1527
|
+
# failure and must never graduate competence/trust. In particular,
|
|
1528
|
+
# shadow simulation is audit evidence, not a successful action.
|
|
1529
|
+
if project.outcome == "succeeded":
|
|
1530
|
+
self._record_outcome("success", shadow=(mode == "shadow"))
|
|
1531
|
+
if project.outcome == "succeeded" and self._feedback is not None:
|
|
1532
|
+
try:
|
|
1533
|
+
self._feedback.record("project", "actioned")
|
|
1534
|
+
except Exception:
|
|
1535
|
+
pass
|
|
1536
|
+
summary = "; ".join(
|
|
1537
|
+
s.result[:100] for s in steps if s.status == "done" and s.result)[:800]
|
|
1538
|
+
event = (
|
|
1539
|
+
"completed" if project.outcome == "succeeded"
|
|
1540
|
+
else "unverified" if all_steps_done and mode != "shadow"
|
|
1541
|
+
else "skipped"
|
|
1542
|
+
)
|
|
1543
|
+
neutral_summary = (
|
|
1544
|
+
f"{len(done)} completed step(s); {len(skipped)} skipped; "
|
|
1545
|
+
"no receipt-backed overall success recorded"
|
|
1546
|
+
)
|
|
1547
|
+
await self._milestone(
|
|
1548
|
+
project, event,
|
|
1549
|
+
summary if project.outcome == "succeeded" and summary else neutral_summary,
|
|
1550
|
+
mode,
|
|
1551
|
+
)
|
|
1552
|
+
# Skill hook (item 3): non-trivial completion -> distill a procedure.
|
|
1553
|
+
if project.outcome == "succeeded" and len(steps) >= 3 and mode == "live":
|
|
1554
|
+
try:
|
|
1555
|
+
from apsimo.skills_memory import distill_from_completion
|
|
1556
|
+
transcript = "\n".join(
|
|
1557
|
+
f"step {s.ordinal} ({s.action_kind}): {s.description[:150]} "
|
|
1558
|
+
f"-> {s.result[:200]}" for s in steps)
|
|
1559
|
+
await distill_from_completion(
|
|
1560
|
+
self._router, self._skills, domain="project",
|
|
1561
|
+
task_text=project.objective, result_text=transcript,
|
|
1562
|
+
source_ref=project.id)
|
|
1563
|
+
except Exception:
|
|
1564
|
+
logger.debug("project skill distillation failed", exc_info=True)
|
|
1565
|
+
logger.info(
|
|
1566
|
+
"Project %s %s[%s]: %s (done=%d skipped=%d)",
|
|
1567
|
+
project.id,
|
|
1568
|
+
"COMPLETED" if project.outcome == "succeeded" else "NEUTRAL",
|
|
1569
|
+
mode,
|
|
1570
|
+
project.title,
|
|
1571
|
+
len(done),
|
|
1572
|
+
len(skipped),
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
async def _milestone(self, project: Project, event: str, detail: str,
|
|
1576
|
+
mode: str) -> None:
|
|
1577
|
+
"""Status-change report as a Proposal. Shadow: stored + logged only."""
|
|
1578
|
+
try:
|
|
1579
|
+
from apsimo.proposals import Proposal, proposal_to_payload
|
|
1580
|
+
prop = Proposal(
|
|
1581
|
+
title=f"Project {event}: {project.title[:60]}",
|
|
1582
|
+
finding=detail[:1000],
|
|
1583
|
+
why_it_helps=("keeps you in control of work I am pursuing "
|
|
1584
|
+
"on your behalf"),
|
|
1585
|
+
suggested_action=(
|
|
1586
|
+
"Review and tell me whether to proceed differently"
|
|
1587
|
+
if event in ("blocked", "abandoned")
|
|
1588
|
+
else "Review the outcome; tell me any follow-ups"),
|
|
1589
|
+
source=project.id, initiative_type="proposal",
|
|
1590
|
+
confidence=0.85 if event == "blocked" else 0.7)
|
|
1591
|
+
if self._proposals is not None:
|
|
1592
|
+
self._proposals.add(prop)
|
|
1593
|
+
if mode == "shadow" or self._deliver is None:
|
|
1594
|
+
logger.info(
|
|
1595
|
+
"SHADOW-PROJECT-MILESTONE %s [%s]: %s -- %s",
|
|
1596
|
+
project.id, event, prop.title, detail[:200])
|
|
1597
|
+
return
|
|
1598
|
+
await self._deliver(proposal_to_payload(prop))
|
|
1599
|
+
except Exception:
|
|
1600
|
+
logger.debug("project milestone failed", exc_info=True)
|
|
1601
|
+
|
|
1602
|
+
# ------------------------------------------------------------------
|
|
1603
|
+
# Helpers
|
|
1604
|
+
# ------------------------------------------------------------------
|
|
1605
|
+
|
|
1606
|
+
def _skills_block(self, situation: str) -> str:
|
|
1607
|
+
if self._skills is None:
|
|
1608
|
+
return ""
|
|
1609
|
+
try:
|
|
1610
|
+
from apsimo.skills_memory import (
|
|
1611
|
+
format_block, relevant_skills, skills_enabled,
|
|
1612
|
+
)
|
|
1613
|
+
if not skills_enabled():
|
|
1614
|
+
return ""
|
|
1615
|
+
return format_block(
|
|
1616
|
+
relevant_skills(self._skills, situation, k=3, domain="project"),
|
|
1617
|
+
strategy_note=self._skills.get_note("project"))
|
|
1618
|
+
except Exception:
|
|
1619
|
+
return ""
|
|
1620
|
+
|
|
1621
|
+
def _boundaries_brief(self) -> str:
|
|
1622
|
+
if self._directives is None:
|
|
1623
|
+
return ""
|
|
1624
|
+
try:
|
|
1625
|
+
return self._directives.context_brief() or ""
|
|
1626
|
+
except Exception:
|
|
1627
|
+
return ""
|
|
1628
|
+
|
|
1629
|
+
def _self_brief(self) -> str:
|
|
1630
|
+
if self._self_model is None:
|
|
1631
|
+
return ""
|
|
1632
|
+
try:
|
|
1633
|
+
return self._self_model.brief()
|
|
1634
|
+
except Exception:
|
|
1635
|
+
return ""
|
|
1636
|
+
|
|
1637
|
+
def _record_outcome(self, outcome: str,
|
|
1638
|
+
latency: Optional[float] = None,
|
|
1639
|
+
shadow: bool = False,
|
|
1640
|
+
stated_confidence: Optional[float] = None) -> None:
|
|
1641
|
+
if self._self_model is None:
|
|
1642
|
+
return
|
|
1643
|
+
try:
|
|
1644
|
+
from apsimo.cognition.evidence_pipeline import (
|
|
1645
|
+
cognition_evidence_mode,
|
|
1646
|
+
)
|
|
1647
|
+
if cognition_evidence_mode() in {"shadow", "live"}:
|
|
1648
|
+
# The receipt-bound reducer is the sole project-competence
|
|
1649
|
+
# writer in live mode, while shadow must remain observation-
|
|
1650
|
+
# only. Direct in-process status is not durable evidence.
|
|
1651
|
+
return
|
|
1652
|
+
except Exception:
|
|
1653
|
+
# Import/configuration failure must not manufacture a new direct
|
|
1654
|
+
# success while the evidence path is requested live.
|
|
1655
|
+
if os.environ.get(
|
|
1656
|
+
"COLONY_COGNITION_EVIDENCE", "off"
|
|
1657
|
+
).strip().lower() in {"shadow", "live"}:
|
|
1658
|
+
return
|
|
1659
|
+
try:
|
|
1660
|
+
self._self_model.record("project", outcome, latency_secs=latency,
|
|
1661
|
+
shadow=shadow,
|
|
1662
|
+
stated_confidence=stated_confidence)
|
|
1663
|
+
except Exception:
|
|
1664
|
+
pass
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def _assistant_msg(content: str, tool_calls: List[dict]) -> Dict[str, Any]:
|
|
1668
|
+
"""OpenAI-shaped assistant message carrying tool calls (mirrors the
|
|
1669
|
+
initiative executor's continuation shape)."""
|
|
1670
|
+
return {
|
|
1671
|
+
"role": "assistant",
|
|
1672
|
+
"content": content or None,
|
|
1673
|
+
"tool_calls": [
|
|
1674
|
+
{"id": tc.get("id", ""), "type": "function",
|
|
1675
|
+
"function": {"name": tc.get("name", ""),
|
|
1676
|
+
"arguments": (tc["arguments"]
|
|
1677
|
+
if isinstance(tc.get("arguments"), str)
|
|
1678
|
+
else json.dumps(tc.get("arguments", {})))}}
|
|
1679
|
+
for tc in tool_calls
|
|
1680
|
+
],
|
|
1681
|
+
}
|