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,914 @@
|
|
|
1
|
+
"""Built-in Initiative Executor service -- closes the autonomy circuit.
|
|
2
|
+
|
|
3
|
+
Processes pending initiatives using Colony's own reasoning pipeline
|
|
4
|
+
(ReasoningLoop + LLMRouter + tools). This is the generic execution
|
|
5
|
+
backend that replaces external webhook delivery for same-machine
|
|
6
|
+
deployments.
|
|
7
|
+
|
|
8
|
+
What it does every cycle (default 30s):
|
|
9
|
+
- Claims pending initiatives from the store
|
|
10
|
+
- Builds a reasoning prompt from the initiative context
|
|
11
|
+
- Runs the ReasoningLoop (LLM + Colony tools) to determine and
|
|
12
|
+
execute the appropriate action
|
|
13
|
+
- Reports results back (complete/fail with retry)
|
|
14
|
+
- Respects rate limits and concurrency caps
|
|
15
|
+
|
|
16
|
+
Auto-starts at sidecar boot when COLONY_EXECUTOR_ENABLED is "true"
|
|
17
|
+
and an LLM provider is configured. Stays dormant otherwise.
|
|
18
|
+
|
|
19
|
+
Environment / config:
|
|
20
|
+
COLONY_EXECUTOR_ENABLED "false" (default) / "true"
|
|
21
|
+
COLONY_EXECUTOR_CYCLE_SECS cycle interval (default 30)
|
|
22
|
+
COLONY_EXECUTOR_MAX_PER_CYCLE max initiatives per cycle (default 5)
|
|
23
|
+
COLONY_EXECUTOR_MODEL_TIER LLM tier: small/medium/large (default "small")
|
|
24
|
+
COLONY_EXECUTOR_TYPES comma-separated initiative types to handle
|
|
25
|
+
(default: all non-self types)
|
|
26
|
+
COLONY_EXECUTOR_AGENT_ID agent identity (default "colony-executor")
|
|
27
|
+
COLONY_EXECUTOR_MAX_TOOL_ITERS max tool-continuation rounds per initiative
|
|
28
|
+
when the reasoning loop returns needs_tool
|
|
29
|
+
(default 8)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
import json
|
|
36
|
+
import logging
|
|
37
|
+
import os
|
|
38
|
+
import time
|
|
39
|
+
from datetime import datetime, timedelta, timezone
|
|
40
|
+
from typing import Any, Optional
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# Every initiative type this internal-tool backend is capable of processing.
|
|
45
|
+
_EXECUTABLE_TYPES = {
|
|
46
|
+
"follow_up", "relationship", "introduction", "health", "scheduling",
|
|
47
|
+
"coding", "subsystem_health", "data_quality", "operational",
|
|
48
|
+
"capability_gap", "knowledge_acquisition", "behavioral_correction",
|
|
49
|
+
"agent_action", "commitment", "calendar", "research", "task",
|
|
50
|
+
"project", "system",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _default_allowed_types() -> set[str]:
|
|
55
|
+
"""Internal-execution types = everything except reach-out types.
|
|
56
|
+
|
|
57
|
+
Reach-out (person-facing) initiatives are delivered through the autonomy
|
|
58
|
+
loop's guarded delivery path (push_initiative -> Hermes), where the host
|
|
59
|
+
agent composes and sends them under the delivery rate limiter. Executing
|
|
60
|
+
them here with the internal-only toolset would just swallow them into
|
|
61
|
+
bookkeeping and starve delivery, so they are excluded by default. A
|
|
62
|
+
deployment can still opt them back in via COLONY_EXECUTOR_TYPES.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
from apsimo.delivery.classification import reachout_types
|
|
66
|
+
return set(_EXECUTABLE_TYPES) - set(reachout_types())
|
|
67
|
+
except Exception:
|
|
68
|
+
return set(_EXECUTABLE_TYPES)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_DEFAULT_TYPES = _default_allowed_types()
|
|
72
|
+
|
|
73
|
+
# System prompts compose through the shared cognition charter
|
|
74
|
+
# (cognition/charter.py, role "executor"); see _build_system_prompt below.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_initiative_prompt(initiative: Any) -> str:
|
|
78
|
+
itype = getattr(initiative, "type", "") or getattr(initiative, "initiative_type", "")
|
|
79
|
+
desc = getattr(initiative, "description", "")
|
|
80
|
+
rationale = getattr(initiative, "rationale", "")
|
|
81
|
+
action_hint = getattr(initiative, "action_hint", "")
|
|
82
|
+
entity_id = getattr(initiative, "entity_id", "")
|
|
83
|
+
priority = getattr(initiative, "priority", 0.5)
|
|
84
|
+
context = getattr(initiative, "context", None) or {}
|
|
85
|
+
|
|
86
|
+
parts = [f"## Initiative: {itype}", f"**Priority**: {priority:.2f}"]
|
|
87
|
+
if desc:
|
|
88
|
+
parts.append(f"**Description**: {desc}")
|
|
89
|
+
if rationale:
|
|
90
|
+
parts.append(f"**Rationale**: {rationale}")
|
|
91
|
+
if action_hint:
|
|
92
|
+
parts.append(f"**Suggested action**: {action_hint}")
|
|
93
|
+
if entity_id:
|
|
94
|
+
parts.append(f"**Related entity**: {entity_id}")
|
|
95
|
+
if context:
|
|
96
|
+
parts.append(f"**Context**: {json.dumps(context, default=str, indent=2)}")
|
|
97
|
+
|
|
98
|
+
parts.append(
|
|
99
|
+
"\nProcess this initiative using the available tools. "
|
|
100
|
+
"When done, summarize what you accomplished in 1-2 sentences."
|
|
101
|
+
)
|
|
102
|
+
return "\n".join(parts)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_TIMEOUT_MARKERS = (
|
|
106
|
+
"timeout", "timed out", "deadline", "read operation timed out",
|
|
107
|
+
"readtimeout", "timeouterror", "etimedout",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_TERM_STOP = frozenset({
|
|
111
|
+
"the", "a", "an", "to", "of", "on", "in", "for", "with", "and", "or",
|
|
112
|
+
"follow", "up", "check", "review", "investigate", "diagnose", "verify",
|
|
113
|
+
"initiative", "task", "this", "that", "about", "into", "your", "my",
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_timeout_error(err: Optional[str]) -> bool:
|
|
118
|
+
if not err:
|
|
119
|
+
return False
|
|
120
|
+
low = str(err).lower()
|
|
121
|
+
return any(m in low for m in _TIMEOUT_MARKERS)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _subject_terms(text: Optional[str]) -> set:
|
|
125
|
+
"""Significant tokens of an initiative subject, for repeat-work matching."""
|
|
126
|
+
import re as _re
|
|
127
|
+
toks = _re.findall(r"[a-z0-9][a-z0-9_\-]{2,}", (text or "").lower())
|
|
128
|
+
return {t for t in toks if t not in _TERM_STOP}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _assistant_tool_message(
|
|
132
|
+
content: str, tool_calls: list[dict[str, Any]],
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
"""Build an OpenAI-shaped assistant message carrying tool calls.
|
|
135
|
+
|
|
136
|
+
Mirrors ``ReasoningLoop._build_assistant_message`` so the continuation
|
|
137
|
+
messages we feed back into ``run_turn`` have the exact shape the model
|
|
138
|
+
provider expects (tool_call ids on the assistant turn must match the
|
|
139
|
+
following ``role: "tool"`` result turns).
|
|
140
|
+
"""
|
|
141
|
+
msg: dict[str, Any] = {"role": "assistant", "content": content or None}
|
|
142
|
+
if tool_calls:
|
|
143
|
+
msg["tool_calls"] = [
|
|
144
|
+
{
|
|
145
|
+
"id": tc.get("id", ""),
|
|
146
|
+
"type": "function",
|
|
147
|
+
"function": {
|
|
148
|
+
"name": tc.get("name", ""),
|
|
149
|
+
"arguments": (
|
|
150
|
+
tc["arguments"]
|
|
151
|
+
if isinstance(tc.get("arguments"), str)
|
|
152
|
+
else json.dumps(tc.get("arguments", {}))
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
for tc in tool_calls
|
|
157
|
+
]
|
|
158
|
+
return msg
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class InitiativeExecutorService:
|
|
162
|
+
"""Async service that executes pending initiatives via the reasoning loop."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
initiative_store: Any,
|
|
167
|
+
reasoning_loop: Any,
|
|
168
|
+
tool_executor: Any = None,
|
|
169
|
+
cycle_secs: float = 30.0,
|
|
170
|
+
max_per_cycle: int = 5,
|
|
171
|
+
model_tier: str = "small",
|
|
172
|
+
allowed_types: Optional[set[str]] = None,
|
|
173
|
+
agent_id: str = "colony-executor",
|
|
174
|
+
max_tool_iterations: int = 8,
|
|
175
|
+
directive_manager: Any = None,
|
|
176
|
+
skill_store: Any = None,
|
|
177
|
+
self_model: Any = None,
|
|
178
|
+
):
|
|
179
|
+
self._store = initiative_store
|
|
180
|
+
self._reasoning = reasoning_loop
|
|
181
|
+
self._tools = tool_executor
|
|
182
|
+
# Boundary enforcement: consulted before executing an initiative so the
|
|
183
|
+
# executor never acts on a subject the owner told it to leave alone.
|
|
184
|
+
self._directives = directive_manager
|
|
185
|
+
# Compounding learning (item 3) + self-model/trust (item 4).
|
|
186
|
+
self._skills = skill_store
|
|
187
|
+
self._self_model = self_model
|
|
188
|
+
self._cycle_secs = cycle_secs
|
|
189
|
+
self._max_per_cycle = max_per_cycle
|
|
190
|
+
self._model_tier = model_tier
|
|
191
|
+
self._allowed_types = allowed_types or _DEFAULT_TYPES
|
|
192
|
+
self._agent_id = agent_id
|
|
193
|
+
# Upper bound on continuation rounds when the reasoning loop returns
|
|
194
|
+
# ``needs_tool`` (its internal per-call budget was exhausted mid-action).
|
|
195
|
+
# Each round executes the pending tool calls and re-enters run_turn.
|
|
196
|
+
self._max_tool_iterations = max(1, int(max_tool_iterations))
|
|
197
|
+
|
|
198
|
+
self._running = False
|
|
199
|
+
self._stop_event = asyncio.Event()
|
|
200
|
+
# Skill attribution: which skills were retrieved into the prompt for
|
|
201
|
+
# each in-flight initiative, so their win/loss record reflects the
|
|
202
|
+
# outcome of the runs they informed (closes the item-3 feedback loop).
|
|
203
|
+
self._skills_used: dict[str, list[str]] = {}
|
|
204
|
+
self._stats = {
|
|
205
|
+
"cycles": 0,
|
|
206
|
+
"initiatives_processed": 0,
|
|
207
|
+
"initiatives_completed": 0,
|
|
208
|
+
"initiatives_failed": 0,
|
|
209
|
+
"total_tool_calls": 0,
|
|
210
|
+
"total_tokens": 0,
|
|
211
|
+
"boundary_check_errors": 0,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def is_running(self) -> bool:
|
|
216
|
+
return self._running
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def stats(self) -> dict:
|
|
220
|
+
return dict(self._stats)
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
# Lifecycle
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
async def start(self) -> None:
|
|
227
|
+
if self._reasoning is None:
|
|
228
|
+
logger.info("Initiative executor: no reasoning loop, staying dormant")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
self._running = True
|
|
232
|
+
self._stop_event.clear()
|
|
233
|
+
logger.info(
|
|
234
|
+
"Initiative executor starting (cycle=%ds, max=%d/cycle, tier=%s, types=%s)",
|
|
235
|
+
int(self._cycle_secs),
|
|
236
|
+
self._max_per_cycle,
|
|
237
|
+
self._model_tier,
|
|
238
|
+
",".join(sorted(self._allowed_types)),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
while not self._stop_event.is_set():
|
|
243
|
+
await self._cycle()
|
|
244
|
+
try:
|
|
245
|
+
await asyncio.wait_for(
|
|
246
|
+
self._stop_event.wait(), timeout=self._cycle_secs
|
|
247
|
+
)
|
|
248
|
+
except asyncio.TimeoutError:
|
|
249
|
+
pass
|
|
250
|
+
finally:
|
|
251
|
+
self._running = False
|
|
252
|
+
logger.info("Initiative executor stopped. Stats: %s", self._stats)
|
|
253
|
+
|
|
254
|
+
async def stop(self) -> None:
|
|
255
|
+
logger.info("Initiative executor stop requested")
|
|
256
|
+
self._stop_event.set()
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# Main cycle
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
async def _cycle(self) -> None:
|
|
263
|
+
self._stats["cycles"] += 1
|
|
264
|
+
|
|
265
|
+
pending = await self._claim_pending()
|
|
266
|
+
if not pending:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
for initiative in pending:
|
|
270
|
+
if self._stop_event.is_set():
|
|
271
|
+
break
|
|
272
|
+
await self._execute_one(initiative)
|
|
273
|
+
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
# Claim pending initiatives
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
async def _claim_pending(self) -> list:
|
|
279
|
+
if self._store is None:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
loop = asyncio.get_event_loop()
|
|
284
|
+
pending = await loop.run_in_executor(
|
|
285
|
+
None,
|
|
286
|
+
lambda: self._store.list(
|
|
287
|
+
status=["pending"],
|
|
288
|
+
limit=self._max_per_cycle,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
logger.warning("Failed to list pending initiatives: %s", exc)
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
claimed = []
|
|
296
|
+
for initiative in pending:
|
|
297
|
+
itype = getattr(initiative, "type", "") or getattr(initiative, "initiative_type", "")
|
|
298
|
+
if itype not in self._allowed_types:
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
iid = getattr(initiative, "id", "")
|
|
302
|
+
try:
|
|
303
|
+
loop = asyncio.get_event_loop()
|
|
304
|
+
result = await loop.run_in_executor(
|
|
305
|
+
None,
|
|
306
|
+
lambda id_=iid: self._store.assign(id_, self._agent_id),
|
|
307
|
+
)
|
|
308
|
+
if result is not None:
|
|
309
|
+
claimed.append(result)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
logger.debug("Failed to claim initiative %s: %s", iid, exc)
|
|
312
|
+
|
|
313
|
+
if claimed:
|
|
314
|
+
logger.info("Claimed %d initiatives for execution", len(claimed))
|
|
315
|
+
return claimed
|
|
316
|
+
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
# Execute a single initiative
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
async def _execute_one(self, initiative: Any) -> None:
|
|
322
|
+
iid = getattr(initiative, "id", "")
|
|
323
|
+
itype = getattr(initiative, "type", "") or getattr(initiative, "initiative_type", "")
|
|
324
|
+
start = time.monotonic()
|
|
325
|
+
model_provenance = {'requested_role': self._model_tier}
|
|
326
|
+
|
|
327
|
+
logger.info("Executing initiative %s (%s)", iid, itype)
|
|
328
|
+
|
|
329
|
+
# Boundary gate: refuse to act on anything the owner set as off-limits.
|
|
330
|
+
if self._directives is not None:
|
|
331
|
+
try:
|
|
332
|
+
from apsimo.directives import Action
|
|
333
|
+
desc = getattr(initiative, "description", "") or ""
|
|
334
|
+
rationale = getattr(initiative, "rationale", "") or ""
|
|
335
|
+
entity_id = getattr(initiative, "entity_id", "") or ""
|
|
336
|
+
verdict = self._directives.check(Action(
|
|
337
|
+
kind="execute",
|
|
338
|
+
text=f"{desc} {rationale}",
|
|
339
|
+
target=entity_id,
|
|
340
|
+
entity_id=entity_id,
|
|
341
|
+
))
|
|
342
|
+
if not verdict.allowed:
|
|
343
|
+
logger.warning(
|
|
344
|
+
"Initiative %s (%s) REFUSED by boundary: %s",
|
|
345
|
+
iid, itype, verdict.reason,
|
|
346
|
+
)
|
|
347
|
+
# Terminal: a boundary is not a transient failure, so do not
|
|
348
|
+
# retry (it would just be refused again until lifted).
|
|
349
|
+
await self._fail_initiative(
|
|
350
|
+
iid, f"refused: {verdict.reason}", retry=False,
|
|
351
|
+
)
|
|
352
|
+
self._stats["initiatives_failed"] += 1
|
|
353
|
+
self._stats["initiatives_processed"] += 1
|
|
354
|
+
return
|
|
355
|
+
except Exception:
|
|
356
|
+
# An owner boundary we cannot evaluate must not be assumed
|
|
357
|
+
# permissive: fail CLOSED by default (a delayed initiative is
|
|
358
|
+
# recoverable; acting on a forbidden subject is not). Retryable:
|
|
359
|
+
# a healthy store on a later cycle evaluates it properly.
|
|
360
|
+
self._stats["boundary_check_errors"] += 1
|
|
361
|
+
from apsimo.directives.guard import boundary_fail_closed
|
|
362
|
+
if boundary_fail_closed():
|
|
363
|
+
logger.warning(
|
|
364
|
+
"Initiative %s (%s) REFUSED: boundary_check_error "
|
|
365
|
+
"(boundary check raised; failing closed)",
|
|
366
|
+
iid, itype, exc_info=True,
|
|
367
|
+
)
|
|
368
|
+
await self._fail_initiative(
|
|
369
|
+
iid, "boundary_check_error: boundary check raised; "
|
|
370
|
+
"failing closed", retry=True,
|
|
371
|
+
)
|
|
372
|
+
self._stats["initiatives_failed"] += 1
|
|
373
|
+
self._stats["initiatives_processed"] += 1
|
|
374
|
+
return
|
|
375
|
+
logger.debug("boundary pre-check failed (allowing)", exc_info=True)
|
|
376
|
+
|
|
377
|
+
# Repeat-work suppression: if a recent completion already covers this
|
|
378
|
+
# subject, close it with a pointer instead of re-running the reasoning.
|
|
379
|
+
try:
|
|
380
|
+
dup = await self._find_recent_completion(initiative)
|
|
381
|
+
except Exception:
|
|
382
|
+
dup = None
|
|
383
|
+
if dup is not None:
|
|
384
|
+
dup_id = getattr(dup, "id", "?")
|
|
385
|
+
dup_res = (getattr(dup, "result", "") or "")[:200]
|
|
386
|
+
logger.info("Initiative %s (%s) already covered by %s — skipping re-run",
|
|
387
|
+
iid, itype, dup_id)
|
|
388
|
+
await self._complete_initiative(
|
|
389
|
+
iid, f"Already addressed by recent completion {dup_id}. {dup_res}")
|
|
390
|
+
self._stats["initiatives_completed"] += 1
|
|
391
|
+
self._stats["initiatives_processed"] += 1
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
prompt = _build_initiative_prompt(initiative)
|
|
396
|
+
session_id = f"executor-{iid}"
|
|
397
|
+
is_research = itype in ("research", "knowledge_acquisition")
|
|
398
|
+
|
|
399
|
+
# Charter-composed system prompt: shared doctrine + executor role
|
|
400
|
+
# + injected self-model brief, boundaries (soft layer atop the
|
|
401
|
+
# hard gate above and the per-tool gate below), retrieved
|
|
402
|
+
# procedure memory, and avoid-lines from past failures.
|
|
403
|
+
system_prompt = self._build_system_prompt(initiative, itype)
|
|
404
|
+
|
|
405
|
+
# Conversation accumulated across continuation rounds. The reasoning
|
|
406
|
+
# loop runs its own internal tool loop, but returns ``needs_tool``
|
|
407
|
+
# when its per-call iteration budget is exhausted while an action is
|
|
408
|
+
# still pending. We then execute the pending tool calls ourselves and
|
|
409
|
+
# re-enter run_turn with the results appended, extending the budget.
|
|
410
|
+
working: list[dict[str, Any]] = [{"role": "user", "content": prompt}]
|
|
411
|
+
total_tokens = 0
|
|
412
|
+
iterations = 0
|
|
413
|
+
done = False
|
|
414
|
+
|
|
415
|
+
while iterations < self._max_tool_iterations:
|
|
416
|
+
iterations += 1
|
|
417
|
+
result = await self._run_turn_resilient(
|
|
418
|
+
session_id=session_id,
|
|
419
|
+
messages=working,
|
|
420
|
+
system_prompt=system_prompt,
|
|
421
|
+
is_research=is_research,
|
|
422
|
+
)
|
|
423
|
+
model_provenance.update(getattr(result, 'model_provenance', {}) or {})
|
|
424
|
+
|
|
425
|
+
usage = result.usage or {}
|
|
426
|
+
total_tokens += usage.get("total_tokens", 0)
|
|
427
|
+
|
|
428
|
+
if result.status == "completed":
|
|
429
|
+
response_text = ""
|
|
430
|
+
if result.message:
|
|
431
|
+
response_text = result.message.get("content", "") or ""
|
|
432
|
+
await self._complete_initiative(iid, response_text)
|
|
433
|
+
self._stats["initiatives_completed"] += 1
|
|
434
|
+
self._stats["total_tokens"] += total_tokens
|
|
435
|
+
elapsed = time.monotonic() - start
|
|
436
|
+
logger.info(
|
|
437
|
+
"Initiative %s completed in %.1fs (%d tokens, %d round(s)): %s",
|
|
438
|
+
iid, elapsed, total_tokens, iterations,
|
|
439
|
+
response_text[:120],
|
|
440
|
+
)
|
|
441
|
+
self._record_outcome(itype, "success", elapsed, initiative=initiative, model_provenance=model_provenance)
|
|
442
|
+
await self._maybe_distill(initiative, itype, response_text)
|
|
443
|
+
done = True
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
if result.status == "error":
|
|
447
|
+
await self._fail_initiative(
|
|
448
|
+
iid, result.error or "reasoning error", retry=True
|
|
449
|
+
)
|
|
450
|
+
self._stats["initiatives_failed"] += 1
|
|
451
|
+
self._stats["total_tokens"] += total_tokens
|
|
452
|
+
logger.warning("Initiative %s failed: %s", iid, result.error)
|
|
453
|
+
self._record_outcome(
|
|
454
|
+
itype,
|
|
455
|
+
"timeout" if _is_timeout_error(result.error) else "failure",
|
|
456
|
+
time.monotonic() - start, initiative=initiative, model_provenance=model_provenance)
|
|
457
|
+
self._note_failure(initiative, itype, result.error or "")
|
|
458
|
+
done = True
|
|
459
|
+
break
|
|
460
|
+
|
|
461
|
+
if result.status == "needs_tool":
|
|
462
|
+
pending = list(result.tool_calls or [])
|
|
463
|
+
if not pending:
|
|
464
|
+
# needs_tool with nothing to run: no forward progress
|
|
465
|
+
# possible, so stop rather than spin.
|
|
466
|
+
await self._fail_initiative(
|
|
467
|
+
iid,
|
|
468
|
+
"needs_tool returned no tool calls",
|
|
469
|
+
retry=True,
|
|
470
|
+
)
|
|
471
|
+
self._stats["initiatives_failed"] += 1
|
|
472
|
+
self._stats["total_tokens"] += total_tokens
|
|
473
|
+
logger.warning(
|
|
474
|
+
"Initiative %s: needs_tool with empty tool_calls", iid
|
|
475
|
+
)
|
|
476
|
+
done = True
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
if self._tools is None:
|
|
480
|
+
await self._fail_initiative(
|
|
481
|
+
iid,
|
|
482
|
+
"needs_tool but no tool executor is wired",
|
|
483
|
+
retry=True,
|
|
484
|
+
)
|
|
485
|
+
self._stats["initiatives_failed"] += 1
|
|
486
|
+
self._stats["total_tokens"] += total_tokens
|
|
487
|
+
logger.warning(
|
|
488
|
+
"Initiative %s needs a tool but no ToolExecutor is "
|
|
489
|
+
"injected; cannot proceed", iid,
|
|
490
|
+
)
|
|
491
|
+
done = True
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
# Per-tool boundary gate (defense in depth): a boundary-
|
|
495
|
+
# violating tool call the model produced mid-reasoning is
|
|
496
|
+
# dropped and reported back so it adapts, rather than run.
|
|
497
|
+
if self._directives is not None:
|
|
498
|
+
pending = self._filter_tool_calls_by_boundary(
|
|
499
|
+
pending, working,
|
|
500
|
+
)
|
|
501
|
+
if not pending:
|
|
502
|
+
# everything was blocked; give the model the refusal
|
|
503
|
+
# and let it continue or wrap up.
|
|
504
|
+
working.append({
|
|
505
|
+
"role": "user",
|
|
506
|
+
"content": "Those actions violate a standing boundary "
|
|
507
|
+
"from the owner and were refused. Do not "
|
|
508
|
+
"attempt them; summarise and stop.",
|
|
509
|
+
})
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
# Execute the pending tool calls through the SAME executor the
|
|
513
|
+
# reasoning loop uses internally (identical gating/handlers),
|
|
514
|
+
# then feed the results back for another round.
|
|
515
|
+
tool_results = await self._tools.execute_batch(
|
|
516
|
+
pending, session_id=session_id,
|
|
517
|
+
)
|
|
518
|
+
self._stats["total_tool_calls"] += len(pending)
|
|
519
|
+
|
|
520
|
+
working.append(_assistant_tool_message(
|
|
521
|
+
result.message.get("content", "") if result.message else "",
|
|
522
|
+
pending,
|
|
523
|
+
))
|
|
524
|
+
for tr in tool_results:
|
|
525
|
+
working.append({
|
|
526
|
+
"role": "tool",
|
|
527
|
+
"tool_call_id": tr.get("tool_call_id", ""),
|
|
528
|
+
"content": tr.get("content", ""),
|
|
529
|
+
})
|
|
530
|
+
logger.info(
|
|
531
|
+
"Initiative %s round %d: executed %d tool call(s), continuing",
|
|
532
|
+
iid, iterations, len(pending),
|
|
533
|
+
)
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
# Any status outside the known domain must never be dropped
|
|
537
|
+
# silently again.
|
|
538
|
+
await self._fail_initiative(
|
|
539
|
+
iid, f"unexpected status: {result.status}", retry=True
|
|
540
|
+
)
|
|
541
|
+
self._stats["initiatives_failed"] += 1
|
|
542
|
+
self._stats["total_tokens"] += total_tokens
|
|
543
|
+
logger.warning(
|
|
544
|
+
"Initiative %s: unexpected reasoning status %r", iid, result.status
|
|
545
|
+
)
|
|
546
|
+
done = True
|
|
547
|
+
break
|
|
548
|
+
|
|
549
|
+
if not done:
|
|
550
|
+
# Hit the continuation cap without completing.
|
|
551
|
+
await self._fail_initiative(
|
|
552
|
+
iid,
|
|
553
|
+
f"tool-loop cap reached after {self._max_tool_iterations} rounds",
|
|
554
|
+
retry=True,
|
|
555
|
+
)
|
|
556
|
+
self._stats["initiatives_failed"] += 1
|
|
557
|
+
self._stats["total_tokens"] += total_tokens
|
|
558
|
+
self._record_outcome(itype, "failure",
|
|
559
|
+
time.monotonic() - start,
|
|
560
|
+
initiative=initiative, model_provenance=model_provenance)
|
|
561
|
+
logger.warning(
|
|
562
|
+
"Initiative %s hit tool-loop cap (%d rounds) without completing",
|
|
563
|
+
iid, self._max_tool_iterations,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
except Exception as exc:
|
|
567
|
+
elapsed = time.monotonic() - start
|
|
568
|
+
logger.error("Initiative %s execution error (%.1fs): %s", iid, elapsed, exc)
|
|
569
|
+
await self._fail_initiative(iid, str(exc), retry=True)
|
|
570
|
+
self._stats["initiatives_failed"] += 1
|
|
571
|
+
self._record_outcome(
|
|
572
|
+
itype, "timeout" if _is_timeout_error(str(exc)) else "failure",
|
|
573
|
+
elapsed, initiative=initiative, model_provenance=model_provenance)
|
|
574
|
+
|
|
575
|
+
self._stats["initiatives_processed"] += 1
|
|
576
|
+
|
|
577
|
+
# ------------------------------------------------------------------
|
|
578
|
+
# Self-model + skills-memory hooks (items 3 + 4)
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
def _build_system_prompt(self, initiative: Any, itype: str) -> str:
|
|
582
|
+
"""Compose the executor prompt through the shared charter."""
|
|
583
|
+
self_brief = boundaries = skills_block = None
|
|
584
|
+
corrections: list[str] = []
|
|
585
|
+
if self._directives is not None:
|
|
586
|
+
try:
|
|
587
|
+
boundaries = self._directives.context_brief() or None
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
if self._self_model is not None:
|
|
591
|
+
try:
|
|
592
|
+
self_brief = self._self_model.brief() or None
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
if self._skills is not None:
|
|
596
|
+
try:
|
|
597
|
+
from apsimo.skills_memory import (
|
|
598
|
+
format_block, relevant_skills, skills_enabled,
|
|
599
|
+
)
|
|
600
|
+
if skills_enabled():
|
|
601
|
+
retrieved = relevant_skills(
|
|
602
|
+
self._skills,
|
|
603
|
+
f"{itype} {getattr(initiative, 'description', '')}",
|
|
604
|
+
k=3, domain=itype)
|
|
605
|
+
skills_block = format_block(retrieved) or None
|
|
606
|
+
# Remember which skills informed this run so the outcome
|
|
607
|
+
# can be attributed back to them (wins/losses).
|
|
608
|
+
iid = getattr(initiative, "id", None)
|
|
609
|
+
if iid and retrieved:
|
|
610
|
+
self._skills_used[str(iid)] = [
|
|
611
|
+
s.id for s in retrieved if getattr(s, "id", None)]
|
|
612
|
+
# Failure post-mortems become avoid-lines.
|
|
613
|
+
note = self._skills.get_note(itype)
|
|
614
|
+
corrections = [ln.lstrip("- ").strip()
|
|
615
|
+
for ln in note.splitlines() if ln.strip()]
|
|
616
|
+
except Exception:
|
|
617
|
+
logger.debug("skills block failed", exc_info=True)
|
|
618
|
+
try:
|
|
619
|
+
from apsimo.cognition.charter import build_system_prompt
|
|
620
|
+
return build_system_prompt(
|
|
621
|
+
"executor", self_brief=self_brief, boundaries=boundaries,
|
|
622
|
+
skills=skills_block, corrections=corrections or None)
|
|
623
|
+
except Exception:
|
|
624
|
+
logger.debug("charter compose failed; minimal fallback",
|
|
625
|
+
exc_info=True)
|
|
626
|
+
return ("You execute one initiative for Colony using the "
|
|
627
|
+
"available tools; report a truthful result."
|
|
628
|
+
+ (f"\n\nStanding boundaries (obey):\n{boundaries}"
|
|
629
|
+
if boundaries else ""))
|
|
630
|
+
|
|
631
|
+
def _record_outcome(self, itype: str, outcome: str,
|
|
632
|
+
latency: Optional[float] = None,
|
|
633
|
+
initiative: Any = None, model_provenance: Optional[dict] = None) -> None:
|
|
634
|
+
# Attribute the outcome to the skills that informed this run
|
|
635
|
+
# (item 3 feedback edge): success bumps wins, anything else losses.
|
|
636
|
+
self._record_skill_outcomes(initiative, outcome == "success")
|
|
637
|
+
if self._self_model is None:
|
|
638
|
+
return
|
|
639
|
+
try:
|
|
640
|
+
# Stated-vs-realized calibration: a thinker/planner-stated
|
|
641
|
+
# confidence rides in the initiative context (charter contract).
|
|
642
|
+
stated = None
|
|
643
|
+
if initiative is not None:
|
|
644
|
+
ctx = getattr(initiative, "context", None) or {}
|
|
645
|
+
if isinstance(ctx, dict):
|
|
646
|
+
stated = ctx.get("stated_confidence")
|
|
647
|
+
try:
|
|
648
|
+
stated = float(stated) if stated is not None else None
|
|
649
|
+
except (TypeError, ValueError):
|
|
650
|
+
stated = None
|
|
651
|
+
iid = str(getattr(initiative, 'id', '') or '')
|
|
652
|
+
attempt = int(getattr(initiative, 'attempt_count', 0) or 0)
|
|
653
|
+
self._self_model.record(itype, outcome, latency_secs=latency,
|
|
654
|
+
stated_confidence=stated, source='initiative_executor',
|
|
655
|
+
source_ref=iid or None, event_key=f'{iid}:{attempt}:{outcome}' if iid else None,
|
|
656
|
+
evidence_status='observed', outcome_contract='colony.initiative-runtime-outcome/v1',
|
|
657
|
+
evidence={**(model_provenance or {}), 'attempt': attempt,
|
|
658
|
+
'meaning': 'runtime_completed_or_failed; semantic_success_unverified'})
|
|
659
|
+
except Exception:
|
|
660
|
+
pass
|
|
661
|
+
|
|
662
|
+
def _record_skill_outcomes(self, initiative: Any, win: bool) -> None:
|
|
663
|
+
"""Update wins/losses on every skill retrieved into this run's prompt."""
|
|
664
|
+
if self._skills is None or initiative is None:
|
|
665
|
+
return
|
|
666
|
+
iid = str(getattr(initiative, "id", "") or "")
|
|
667
|
+
skill_ids = self._skills_used.pop(iid, None)
|
|
668
|
+
if not skill_ids:
|
|
669
|
+
return
|
|
670
|
+
for sid in skill_ids:
|
|
671
|
+
try:
|
|
672
|
+
self._skills.record_outcome(sid, win)
|
|
673
|
+
except Exception:
|
|
674
|
+
logger.debug("skill outcome record failed for %s", sid,
|
|
675
|
+
exc_info=True)
|
|
676
|
+
|
|
677
|
+
def _note_failure(self, initiative: Any, itype: str, error: str) -> None:
|
|
678
|
+
"""Failure post-mortem: keep a short per-domain strategy note."""
|
|
679
|
+
if self._skills is None:
|
|
680
|
+
return
|
|
681
|
+
try:
|
|
682
|
+
desc = (getattr(initiative, "description", "") or "")[:80]
|
|
683
|
+
self._skills.record_failure_note(
|
|
684
|
+
itype, f"'{desc}' failed: {(error or 'unknown')[:120]}")
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
async def _maybe_distill(self, initiative: Any, itype: str,
|
|
689
|
+
response_text: str) -> None:
|
|
690
|
+
"""Distill a reusable procedure from a qualifying completion."""
|
|
691
|
+
if self._skills is None:
|
|
692
|
+
return
|
|
693
|
+
try:
|
|
694
|
+
from apsimo.skills_memory import (
|
|
695
|
+
distill_from_completion, should_distill, skills_distill_mode,
|
|
696
|
+
)
|
|
697
|
+
if skills_distill_mode() == "off":
|
|
698
|
+
return
|
|
699
|
+
attempts = int(getattr(initiative, "attempt_count", 0) or 0)
|
|
700
|
+
if not should_distill(max(0, attempts - 1), response_text,
|
|
701
|
+
self._skills):
|
|
702
|
+
return
|
|
703
|
+
router = getattr(self._reasoning, "_model", None)
|
|
704
|
+
await distill_from_completion(
|
|
705
|
+
router, self._skills, domain=itype,
|
|
706
|
+
task_text=getattr(initiative, "description", "") or "",
|
|
707
|
+
result_text=response_text,
|
|
708
|
+
source_ref=getattr(initiative, "id", "") or "")
|
|
709
|
+
except Exception:
|
|
710
|
+
logger.debug("skill distillation failed", exc_info=True)
|
|
711
|
+
|
|
712
|
+
def _filter_tool_calls_by_boundary(
|
|
713
|
+
self, pending: list, working: list,
|
|
714
|
+
) -> list:
|
|
715
|
+
"""Drop tool calls that violate a standing boundary. Returns survivors."""
|
|
716
|
+
if self._directives is None:
|
|
717
|
+
return pending
|
|
718
|
+
try:
|
|
719
|
+
from apsimo.directives import Action
|
|
720
|
+
from apsimo.directives.guard import boundary_fail_closed
|
|
721
|
+
except Exception:
|
|
722
|
+
# Cannot even construct a check: never assume permissive.
|
|
723
|
+
logger.warning(
|
|
724
|
+
"Tool boundary gate unavailable (directives import failed); "
|
|
725
|
+
"dropping %d pending tool call(s) (failing closed)",
|
|
726
|
+
len(pending), exc_info=True,
|
|
727
|
+
)
|
|
728
|
+
return []
|
|
729
|
+
survivors = []
|
|
730
|
+
for tc in pending:
|
|
731
|
+
name = tc.get("name", "")
|
|
732
|
+
args = tc.get("arguments", {}) if isinstance(tc.get("arguments"), dict) else {}
|
|
733
|
+
try:
|
|
734
|
+
verdict = self._directives.check(Action(
|
|
735
|
+
kind="execute_tool", tool_name=name, args=args,
|
|
736
|
+
text=name, high_risk=True,
|
|
737
|
+
))
|
|
738
|
+
except Exception:
|
|
739
|
+
# An owner boundary we cannot evaluate must not be assumed
|
|
740
|
+
# permissive: fail CLOSED by default and drop the call.
|
|
741
|
+
self._stats["boundary_check_errors"] += 1
|
|
742
|
+
if boundary_fail_closed():
|
|
743
|
+
logger.warning(
|
|
744
|
+
"Tool call %s REFUSED: boundary_check_error "
|
|
745
|
+
"(boundary check raised; failing closed)",
|
|
746
|
+
name, exc_info=True,
|
|
747
|
+
)
|
|
748
|
+
continue
|
|
749
|
+
logger.debug("tool boundary check failed (allowing)",
|
|
750
|
+
exc_info=True)
|
|
751
|
+
survivors.append(tc)
|
|
752
|
+
continue
|
|
753
|
+
if not verdict.allowed:
|
|
754
|
+
logger.warning(
|
|
755
|
+
"Tool call %s REFUSED by boundary: %s", name, verdict.reason,
|
|
756
|
+
)
|
|
757
|
+
continue
|
|
758
|
+
survivors.append(tc)
|
|
759
|
+
return survivors
|
|
760
|
+
|
|
761
|
+
# ------------------------------------------------------------------
|
|
762
|
+
# Store callbacks
|
|
763
|
+
# ------------------------------------------------------------------
|
|
764
|
+
|
|
765
|
+
async def _run_turn_resilient(self, *, session_id, messages, system_prompt, is_research):
|
|
766
|
+
"""run_turn with adaptive retry+backoff on M3 timeouts (resilience -- 5).
|
|
767
|
+
|
|
768
|
+
A transient timeout is absorbed by retrying in-call (no store attempt is
|
|
769
|
+
burned). Research-type work gets more retries. Only a persistent timeout
|
|
770
|
+
falls through to the normal error handling.
|
|
771
|
+
"""
|
|
772
|
+
max_retries = 3 if is_research else 2
|
|
773
|
+
backoff = 2.0
|
|
774
|
+
result = None
|
|
775
|
+
for attempt in range(max_retries + 1):
|
|
776
|
+
result = await self._reasoning.run_turn(
|
|
777
|
+
session_id=session_id, messages=messages,
|
|
778
|
+
system_prompt=system_prompt, model_override=self._model_tier,
|
|
779
|
+
)
|
|
780
|
+
if result.status != "error" or not _is_timeout_error(result.error):
|
|
781
|
+
return result
|
|
782
|
+
if attempt < max_retries:
|
|
783
|
+
logger.info("run_turn timeout (attempt %d/%d), backing off %.0fs",
|
|
784
|
+
attempt + 1, max_retries + 1, backoff)
|
|
785
|
+
try:
|
|
786
|
+
await asyncio.sleep(backoff)
|
|
787
|
+
except Exception:
|
|
788
|
+
pass
|
|
789
|
+
backoff *= 2
|
|
790
|
+
return result
|
|
791
|
+
|
|
792
|
+
async def _find_recent_completion(self, initiative: Any) -> Any:
|
|
793
|
+
"""A recently-completed initiative that already covers this subject."""
|
|
794
|
+
if self._store is None:
|
|
795
|
+
return None
|
|
796
|
+
itype = getattr(initiative, "type", "") or getattr(initiative, "initiative_type", "")
|
|
797
|
+
iid = getattr(initiative, "id", "")
|
|
798
|
+
entity_id = getattr(initiative, "entity_id", "") or ""
|
|
799
|
+
my_terms = _subject_terms(getattr(initiative, "description", ""))
|
|
800
|
+
if not itype:
|
|
801
|
+
return None
|
|
802
|
+
window_h = float(os.environ.get("COLONY_EXECUTOR_REPEAT_WINDOW_HOURS", "48"))
|
|
803
|
+
since = datetime.now(timezone.utc) - timedelta(hours=window_h)
|
|
804
|
+
try:
|
|
805
|
+
loop = asyncio.get_event_loop()
|
|
806
|
+
recent = await loop.run_in_executor(
|
|
807
|
+
None,
|
|
808
|
+
lambda: self._store.list(
|
|
809
|
+
status=["completed"], type=itype, created_after=since, limit=50),
|
|
810
|
+
)
|
|
811
|
+
except Exception:
|
|
812
|
+
return None
|
|
813
|
+
for r in recent or []:
|
|
814
|
+
if getattr(r, "id", "") == iid:
|
|
815
|
+
continue
|
|
816
|
+
if entity_id and getattr(r, "entity_id", "") == entity_id:
|
|
817
|
+
return r
|
|
818
|
+
rt = _subject_terms(getattr(r, "description", ""))
|
|
819
|
+
if my_terms and rt:
|
|
820
|
+
overlap = len(my_terms & rt) / max(1, min(len(my_terms), len(rt)))
|
|
821
|
+
if overlap >= 0.7:
|
|
822
|
+
return r
|
|
823
|
+
return None
|
|
824
|
+
|
|
825
|
+
async def _complete_initiative(self, initiative_id: str, result: str) -> None:
|
|
826
|
+
try:
|
|
827
|
+
loop = asyncio.get_event_loop()
|
|
828
|
+
await loop.run_in_executor(
|
|
829
|
+
None,
|
|
830
|
+
lambda: self._store.complete(
|
|
831
|
+
initiative_id,
|
|
832
|
+
agent_id=self._agent_id,
|
|
833
|
+
result=result,
|
|
834
|
+
),
|
|
835
|
+
)
|
|
836
|
+
except Exception as exc:
|
|
837
|
+
logger.warning("Failed to mark initiative %s complete: %s", initiative_id, exc)
|
|
838
|
+
|
|
839
|
+
async def _fail_initiative(
|
|
840
|
+
self, initiative_id: str, reason: str, retry: bool = False,
|
|
841
|
+
) -> None:
|
|
842
|
+
try:
|
|
843
|
+
loop = asyncio.get_event_loop()
|
|
844
|
+
await loop.run_in_executor(
|
|
845
|
+
None,
|
|
846
|
+
lambda: self._store.fail(
|
|
847
|
+
initiative_id,
|
|
848
|
+
agent_id=self._agent_id,
|
|
849
|
+
reason=reason,
|
|
850
|
+
retry=retry,
|
|
851
|
+
),
|
|
852
|
+
)
|
|
853
|
+
except Exception as exc:
|
|
854
|
+
logger.warning("Failed to mark initiative %s failed: %s", initiative_id, exc)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
# ---------------------------------------------------------------------------
|
|
858
|
+
# Factory + wiring
|
|
859
|
+
# ---------------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
def create_from_env(
|
|
862
|
+
initiative_store: Any = None,
|
|
863
|
+
reasoning_loop: Any = None,
|
|
864
|
+
tool_executor: Any = None,
|
|
865
|
+
directive_manager: Any = None,
|
|
866
|
+
skill_store: Any = None,
|
|
867
|
+
self_model: Any = None,
|
|
868
|
+
) -> Optional[InitiativeExecutorService]:
|
|
869
|
+
"""Create an InitiativeExecutorService from environment variables.
|
|
870
|
+
|
|
871
|
+
Returns None if COLONY_EXECUTOR_ENABLED is not "true" or required
|
|
872
|
+
dependencies are missing.
|
|
873
|
+
"""
|
|
874
|
+
from apsimo.util.autonomy_preset import resolve_bool
|
|
875
|
+
if not resolve_bool("COLONY_EXECUTOR_ENABLED", False):
|
|
876
|
+
logger.info(
|
|
877
|
+
"Initiative executor disabled (COLONY_EXECUTOR_ENABLED != true). "
|
|
878
|
+
"Set COLONY_EXECUTOR_ENABLED=true or COLONY_AUTONOMY_PRESET="
|
|
879
|
+
"calibration|autonomous to enable autonomous initiative processing."
|
|
880
|
+
)
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
if reasoning_loop is None:
|
|
884
|
+
logger.warning(
|
|
885
|
+
"Initiative executor: no ReasoningLoop available (LLM not configured). "
|
|
886
|
+
"Cannot execute initiatives without an LLM."
|
|
887
|
+
)
|
|
888
|
+
return None
|
|
889
|
+
|
|
890
|
+
if initiative_store is None:
|
|
891
|
+
logger.warning("Initiative executor: no InitiativeStore available.")
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
types_env = os.environ.get("COLONY_EXECUTOR_TYPES", "")
|
|
895
|
+
allowed_types = (
|
|
896
|
+
{t.strip() for t in types_env.split(",") if t.strip()}
|
|
897
|
+
if types_env
|
|
898
|
+
else None
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
return InitiativeExecutorService(
|
|
902
|
+
initiative_store=initiative_store,
|
|
903
|
+
reasoning_loop=reasoning_loop,
|
|
904
|
+
tool_executor=tool_executor,
|
|
905
|
+
directive_manager=directive_manager,
|
|
906
|
+
skill_store=skill_store,
|
|
907
|
+
self_model=self_model,
|
|
908
|
+
cycle_secs=float(os.environ.get("COLONY_EXECUTOR_CYCLE_SECS", "30")),
|
|
909
|
+
max_per_cycle=int(os.environ.get("COLONY_EXECUTOR_MAX_PER_CYCLE", "5")),
|
|
910
|
+
model_tier=os.environ.get("COLONY_EXECUTOR_MODEL_TIER", "small"),
|
|
911
|
+
allowed_types=allowed_types,
|
|
912
|
+
agent_id=os.environ.get("COLONY_EXECUTOR_AGENT_ID", "colony-executor"),
|
|
913
|
+
max_tool_iterations=int(os.environ.get("COLONY_EXECUTOR_MAX_TOOL_ITERS", "8")),
|
|
914
|
+
)
|