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,21 @@
|
|
|
1
|
+
"""Schedulable agent-side workers (v0.20.0+).
|
|
2
|
+
|
|
3
|
+
These are the agent-side halves of the Colony autonomy circuit, packaged
|
|
4
|
+
so pip installs ship them as console scripts:
|
|
5
|
+
|
|
6
|
+
- ``colony-agent-bridge`` (:mod:`apsimo.workers.agent_bridge`)
|
|
7
|
+
unified daemon that replaces the three cron scripts below with one
|
|
8
|
+
long-running process: initiative polling + job dispatch + skills sync
|
|
9
|
+
+ circuit health monitoring. Preferred for new installs.
|
|
10
|
+
- ``colony-queue-worker`` (:mod:`apsimo.workers.queue_worker`)
|
|
11
|
+
claims approved ``agent_action`` jobs and hands them to the agent.
|
|
12
|
+
- ``colony-skills-sync`` (:mod:`apsimo.workers.skills_sync`)
|
|
13
|
+
reports the agent's installed skill index to Colony.
|
|
14
|
+
|
|
15
|
+
All modules are deliberately stdlib-only: they must run from cron on
|
|
16
|
+
machines where only the agent (not the full sidecar dependency stack)
|
|
17
|
+
is present. Do not import heavy dependencies here.
|
|
18
|
+
|
|
19
|
+
The historical loose scripts under ``plugins/hermes-plugin/poller/``
|
|
20
|
+
remain as thin back-compat wrappers around these modules.
|
|
21
|
+
"""
|
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"""Colony Agent Bridge -- unified daemon for the agent-side autonomy circuit.
|
|
2
|
+
|
|
3
|
+
Replaces three separate cron scripts (initiative-poller, queue-worker,
|
|
4
|
+
skills-sync) with one long-running process that:
|
|
5
|
+
|
|
6
|
+
1. Polls for pending initiatives and forwards them to the agent webhook
|
|
7
|
+
2. Claims agent_action jobs from the task queue and dispatches them
|
|
8
|
+
3. Periodically syncs the agent's skill index to Colony
|
|
9
|
+
4. Monitors the full circuit health and surfaces silent failures
|
|
10
|
+
|
|
11
|
+
Silent failures this catches:
|
|
12
|
+
- Sidecar unreachable (connection refused / timeout)
|
|
13
|
+
- Autonomy loop not running or stuck (no ticks advancing)
|
|
14
|
+
- Initiatives generated but never executed (the exact bug that prompted this)
|
|
15
|
+
- Queue jobs stuck in QUEUED state with no worker claiming them
|
|
16
|
+
- Agent webhook unreachable (initiatives polled but can't be delivered)
|
|
17
|
+
|
|
18
|
+
Run as a daemon (``colony-agent-bridge``) or one-shot (``--once``).
|
|
19
|
+
All configuration via environment variables; no deployment-specific defaults.
|
|
20
|
+
|
|
21
|
+
Stdlib-only -- must run on machines where only the agent is installed.
|
|
22
|
+
|
|
23
|
+
Environment:
|
|
24
|
+
COLONY_URL sidecar URL (default http://127.0.0.1:7777)
|
|
25
|
+
COLONY_API_KEY API key (default dev-mode-no-key)
|
|
26
|
+
COLONY_INITIATIVE_WEBHOOK initiative webhook (default http://127.0.0.1:8644/webhooks/colony-initiatives)
|
|
27
|
+
COLONY_JOBS_WEBHOOK_URL jobs webhook (default http://127.0.0.1:8644/webhooks/colony-jobs)
|
|
28
|
+
COLONY_AGENT_NAME agent identity (default hermes)
|
|
29
|
+
COLONY_WORKER_MAX_JOBS jobs per cycle (default 1)
|
|
30
|
+
COLONY_BRIDGE_POLL_SECS main loop interval (default 60)
|
|
31
|
+
COLONY_BRIDGE_SKILLS_HOURS skills sync interval (default 24)
|
|
32
|
+
COLONY_BRIDGE_LOG_CHANNEL alert routing channel (optional)
|
|
33
|
+
COLONY_BRIDGE_PLATFORM platform for alerts (default unset; set to your messaging platform)
|
|
34
|
+
HERMES_SKILLS_DIR skills directory (default ~/.hermes/skills)
|
|
35
|
+
COLONY_BRIDGE_STATE_DIR state file directory (default ~/.colony/bridge)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import json
|
|
42
|
+
import logging
|
|
43
|
+
import os
|
|
44
|
+
import signal
|
|
45
|
+
import sys
|
|
46
|
+
import time
|
|
47
|
+
import urllib.error
|
|
48
|
+
import urllib.request
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any, Dict, Optional
|
|
52
|
+
|
|
53
|
+
from apsimo.workers.queue_worker import (
|
|
54
|
+
agent_action_capabilities,
|
|
55
|
+
encode_hermes_webhook,
|
|
56
|
+
work_order_webhook_fields,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("colony.bridge")
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Configuration
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _cfg() -> Dict[str, Any]:
|
|
66
|
+
state_dir = Path(
|
|
67
|
+
os.environ.get("COLONY_BRIDGE_STATE_DIR", "~/.colony/bridge")
|
|
68
|
+
).expanduser()
|
|
69
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
node_id = os.environ.get("COLONY_WORKER_NODE_ID") or (
|
|
72
|
+
os.environ.get("COLONY_AGENT_NAME", "hermes").strip().lower().replace(" ", "-")
|
|
73
|
+
+ "-agent"
|
|
74
|
+
)
|
|
75
|
+
return {
|
|
76
|
+
"colony_url": os.environ.get("COLONY_URL", "http://127.0.0.1:7777"),
|
|
77
|
+
"api_key": os.environ.get("COLONY_API_KEY", "dev-mode-no-key"),
|
|
78
|
+
"initiative_webhook": os.environ.get(
|
|
79
|
+
"COLONY_INITIATIVE_WEBHOOK",
|
|
80
|
+
"http://127.0.0.1:8644/webhooks/colony-initiatives",
|
|
81
|
+
),
|
|
82
|
+
"jobs_webhook": os.environ.get(
|
|
83
|
+
"COLONY_JOBS_WEBHOOK_URL",
|
|
84
|
+
"http://127.0.0.1:8644/webhooks/colony-jobs",
|
|
85
|
+
),
|
|
86
|
+
"node_id": node_id,
|
|
87
|
+
"max_jobs": int(os.environ.get("COLONY_WORKER_MAX_JOBS", "1")),
|
|
88
|
+
"poll_secs": int(os.environ.get("COLONY_BRIDGE_POLL_SECS", "60")),
|
|
89
|
+
"skills_hours": float(os.environ.get("COLONY_BRIDGE_SKILLS_HOURS", "24")),
|
|
90
|
+
"log_channel": os.environ.get("COLONY_BRIDGE_LOG_CHANNEL", ""),
|
|
91
|
+
"platform": os.environ.get("COLONY_BRIDGE_PLATFORM", ""),
|
|
92
|
+
"state_dir": state_dir,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _headers(cfg: Dict[str, Any]) -> Dict[str, str]:
|
|
97
|
+
return {"X-API-Key": cfg["api_key"], "Content-Type": "application/json"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get(cfg: Dict[str, Any], path: str, timeout: int = 10) -> Optional[dict]:
|
|
101
|
+
req = urllib.request.Request(
|
|
102
|
+
f"{cfg['colony_url']}{path}",
|
|
103
|
+
headers=_headers(cfg),
|
|
104
|
+
method="GET",
|
|
105
|
+
)
|
|
106
|
+
try:
|
|
107
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
108
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
109
|
+
except Exception:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _post(cfg: Dict[str, Any], url: str, body: dict, timeout: int = 15) -> Optional[dict]:
|
|
114
|
+
req = urllib.request.Request(
|
|
115
|
+
url,
|
|
116
|
+
data=json.dumps(body).encode("utf-8"),
|
|
117
|
+
headers=_headers(cfg),
|
|
118
|
+
method="POST",
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
122
|
+
raw = resp.read().decode("utf-8")
|
|
123
|
+
return json.loads(raw) if raw else {}
|
|
124
|
+
except Exception:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# State persistence (survives restarts)
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def _load_set(path: Path) -> set:
|
|
133
|
+
try:
|
|
134
|
+
return set(line.strip() for line in path.read_text().splitlines() if line.strip())
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
return set()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _save_set(path: Path, items: set) -> None:
|
|
140
|
+
path.write_text("\n".join(sorted(items)) + "\n" if items else "")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _load_json(path: Path) -> dict:
|
|
144
|
+
try:
|
|
145
|
+
return json.loads(path.read_text())
|
|
146
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _save_json(path: Path, data: dict) -> None:
|
|
151
|
+
path.write_text(json.dumps(data, indent=2, default=str) + "\n")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Health monitor
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
class HealthMonitor:
|
|
159
|
+
"""Tracks circuit health across cycles and detects silent failures."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, state_dir: Path):
|
|
162
|
+
self._path = state_dir / "health_state.json"
|
|
163
|
+
self._state = _load_json(self._path)
|
|
164
|
+
self._alerts: list = []
|
|
165
|
+
|
|
166
|
+
def check(self, cfg: Dict[str, Any]) -> dict:
|
|
167
|
+
self._alerts = []
|
|
168
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
169
|
+
|
|
170
|
+
health = _get(cfg, "/v1/host/health")
|
|
171
|
+
if not health: # unreachable OR an empty 200 body — both are failures
|
|
172
|
+
consecutive = self._state.get("consecutive_failures", 0) + 1
|
|
173
|
+
self._state["consecutive_failures"] = consecutive
|
|
174
|
+
self._state["last_failure"] = now
|
|
175
|
+
self._alert("sidecar_unreachable",
|
|
176
|
+
f"Colony sidecar at {cfg['colony_url']} is unreachable "
|
|
177
|
+
"or returned an empty health body "
|
|
178
|
+
f"({consecutive} consecutive failure(s))",
|
|
179
|
+
severity="critical" if consecutive >= 3 else "warning")
|
|
180
|
+
self._save()
|
|
181
|
+
return {"ok": False, "alerts": self._alerts}
|
|
182
|
+
|
|
183
|
+
self._state["consecutive_failures"] = 0
|
|
184
|
+
self._state["last_success"] = now
|
|
185
|
+
|
|
186
|
+
status = health.get("status")
|
|
187
|
+
ok = status == "ok"
|
|
188
|
+
if not ok:
|
|
189
|
+
self._alert("sidecar_degraded",
|
|
190
|
+
f"Sidecar reports health status {status!r} (not 'ok')",
|
|
191
|
+
severity="warning")
|
|
192
|
+
|
|
193
|
+
autonomy = _get(cfg, "/v1/host/autonomy/status")
|
|
194
|
+
if autonomy is None:
|
|
195
|
+
# The probe did not run — a skipped check is not a passed check.
|
|
196
|
+
self._alert("autonomy_status_unavailable",
|
|
197
|
+
"Autonomy status endpoint is unreachable — the "
|
|
198
|
+
"autonomy probe was skipped, not passed",
|
|
199
|
+
severity="warning")
|
|
200
|
+
else:
|
|
201
|
+
self._check_autonomy(autonomy)
|
|
202
|
+
self._state["last_autonomy"] = autonomy
|
|
203
|
+
|
|
204
|
+
self._save()
|
|
205
|
+
return {"ok": ok, "health": health, "autonomy": autonomy,
|
|
206
|
+
"autonomy_probe": "skipped" if autonomy is None else "checked",
|
|
207
|
+
"alerts": self._alerts}
|
|
208
|
+
|
|
209
|
+
def _check_autonomy(self, status: dict) -> None:
|
|
210
|
+
if not status.get("running"):
|
|
211
|
+
self._alert("autonomy_stopped",
|
|
212
|
+
"Autonomy loop is not running",
|
|
213
|
+
severity="warning")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
prev = self._state.get("last_autonomy", {})
|
|
217
|
+
prev_ticks = prev.get("ticks", 0)
|
|
218
|
+
curr_ticks = status.get("ticks", 0)
|
|
219
|
+
|
|
220
|
+
if prev_ticks > 0 and curr_ticks == prev_ticks:
|
|
221
|
+
self._alert("autonomy_stuck",
|
|
222
|
+
f"Autonomy loop stuck at tick {curr_ticks} (not advancing)",
|
|
223
|
+
severity="warning")
|
|
224
|
+
|
|
225
|
+
generated = status.get("initiatives_generated", 0)
|
|
226
|
+
executed = status.get("actions_executed", 0)
|
|
227
|
+
if generated > 100 and executed == 0:
|
|
228
|
+
self._alert("initiatives_never_executed",
|
|
229
|
+
f"Autonomy has generated {generated} initiatives but "
|
|
230
|
+
f"executed 0 actions. The delivery pipeline may be broken.",
|
|
231
|
+
severity="critical")
|
|
232
|
+
|
|
233
|
+
def _alert(self, alert_type: str, message: str, severity: str = "warning") -> None:
|
|
234
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
235
|
+
last_key = f"last_alert_{alert_type}"
|
|
236
|
+
last = self._state.get(last_key, "")
|
|
237
|
+
if last:
|
|
238
|
+
try:
|
|
239
|
+
elapsed = (
|
|
240
|
+
datetime.fromisoformat(now_iso)
|
|
241
|
+
- datetime.fromisoformat(last)
|
|
242
|
+
).total_seconds()
|
|
243
|
+
if elapsed < 3600:
|
|
244
|
+
return
|
|
245
|
+
except (ValueError, TypeError):
|
|
246
|
+
pass
|
|
247
|
+
self._state[last_key] = now_iso
|
|
248
|
+
entry = {"type": alert_type, "message": message, "severity": severity, "at": now_iso}
|
|
249
|
+
self._alerts.append(entry)
|
|
250
|
+
level = logging.CRITICAL if severity == "critical" else logging.WARNING
|
|
251
|
+
logger.log(level, "[HEALTH] %s: %s", alert_type, message)
|
|
252
|
+
|
|
253
|
+
def _save(self) -> None:
|
|
254
|
+
_save_json(self._path, self._state)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Initiative poller
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
class InitiativePoller:
|
|
262
|
+
"""Polls pending initiatives and forwards them to the agent webhook."""
|
|
263
|
+
|
|
264
|
+
def __init__(self, state_dir: Path):
|
|
265
|
+
self._seen_path = state_dir / "seen_initiatives.txt"
|
|
266
|
+
self._dedup_path = state_dir / "seen_dedup_keys.txt"
|
|
267
|
+
self._seen_ids = _load_set(self._seen_path)
|
|
268
|
+
self._seen_dedup = _load_set(self._dedup_path)
|
|
269
|
+
|
|
270
|
+
def poll(self, cfg: Dict[str, Any]) -> int:
|
|
271
|
+
data = _get(cfg, "/v1/host/initiatives")
|
|
272
|
+
if data is None:
|
|
273
|
+
return 0
|
|
274
|
+
|
|
275
|
+
items = data if isinstance(data, list) else data.get("initiatives", [])
|
|
276
|
+
fired = 0
|
|
277
|
+
|
|
278
|
+
for initiative in items:
|
|
279
|
+
iid = initiative.get("id", "")
|
|
280
|
+
status = initiative.get("status", "")
|
|
281
|
+
dedup_key = initiative.get("dedup_key", "")
|
|
282
|
+
|
|
283
|
+
if status != "pending":
|
|
284
|
+
continue
|
|
285
|
+
if iid in self._seen_ids:
|
|
286
|
+
continue
|
|
287
|
+
if dedup_key and dedup_key in self._seen_dedup:
|
|
288
|
+
self._seen_ids.add(iid)
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
payload = {
|
|
292
|
+
"type": "initiative",
|
|
293
|
+
"payload": initiative,
|
|
294
|
+
"occurred_at": initiative.get("created_at", ""),
|
|
295
|
+
"seq": 0,
|
|
296
|
+
"delivery_context": {
|
|
297
|
+
"log_channel": cfg.get("log_channel", ""),
|
|
298
|
+
"platform": cfg.get("platform", ""),
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
try:
|
|
302
|
+
body, headers = encode_hermes_webhook(payload)
|
|
303
|
+
req = urllib.request.Request(
|
|
304
|
+
cfg["initiative_webhook"],
|
|
305
|
+
data=body,
|
|
306
|
+
headers=headers,
|
|
307
|
+
method="POST",
|
|
308
|
+
)
|
|
309
|
+
urllib.request.urlopen(req, timeout=10)
|
|
310
|
+
self._seen_ids.add(iid)
|
|
311
|
+
if dedup_key:
|
|
312
|
+
self._seen_dedup.add(dedup_key)
|
|
313
|
+
fired += 1
|
|
314
|
+
logger.info("Fired initiative %s (%s)", iid,
|
|
315
|
+
initiative.get("initiative_type", "?"))
|
|
316
|
+
except Exception as exc:
|
|
317
|
+
logger.warning("Initiative webhook failed for %s: %s", iid, exc)
|
|
318
|
+
|
|
319
|
+
self._save()
|
|
320
|
+
if fired:
|
|
321
|
+
logger.info("Initiatives fired: %d", fired)
|
|
322
|
+
return fired
|
|
323
|
+
|
|
324
|
+
def _save(self) -> None:
|
|
325
|
+
if len(self._seen_ids) > 5000:
|
|
326
|
+
self._seen_ids = set(sorted(self._seen_ids)[-2000:])
|
|
327
|
+
if len(self._seen_dedup) > 5000:
|
|
328
|
+
self._seen_dedup = set(sorted(self._seen_dedup)[-2000:])
|
|
329
|
+
_save_set(self._seen_path, self._seen_ids)
|
|
330
|
+
_save_set(self._dedup_path, self._seen_dedup)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Queue worker
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
class QueueWorker:
|
|
338
|
+
"""Claims agent_action jobs and dispatches them to the agent webhook."""
|
|
339
|
+
|
|
340
|
+
def claim_and_dispatch(self, cfg: Dict[str, Any]) -> int:
|
|
341
|
+
if os.environ.get(
|
|
342
|
+
"COLONY_AGENT_JOB_CLAIMS_ENABLED", "true"
|
|
343
|
+
).strip().lower() not in {"1", "true", "yes", "on"}:
|
|
344
|
+
return 0
|
|
345
|
+
try:
|
|
346
|
+
capabilities = agent_action_capabilities()
|
|
347
|
+
except ValueError as exc:
|
|
348
|
+
logger.error("Worker routes are invalid: %s", exc)
|
|
349
|
+
return 0
|
|
350
|
+
try:
|
|
351
|
+
_post(cfg, f"{cfg['colony_url']}/v1/host/queue/workers/register", {
|
|
352
|
+
"node_id": cfg["node_id"],
|
|
353
|
+
"capabilities": capabilities,
|
|
354
|
+
"job_types": ["agent_action"],
|
|
355
|
+
"max_concurrent": 1,
|
|
356
|
+
"available": True,
|
|
357
|
+
"load": 0.0,
|
|
358
|
+
})
|
|
359
|
+
except Exception as exc:
|
|
360
|
+
logger.warning("Worker registration failed: %s", exc)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
dispatched = 0
|
|
364
|
+
for _ in range(cfg["max_jobs"]):
|
|
365
|
+
job = _post(cfg, f"{cfg['colony_url']}/v1/host/queue/jobs/claim", {
|
|
366
|
+
"node_id": cfg["node_id"],
|
|
367
|
+
"capabilities": capabilities,
|
|
368
|
+
"job_types": ["agent_action"],
|
|
369
|
+
"max_concurrent": 1,
|
|
370
|
+
})
|
|
371
|
+
if not job:
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
job_id = job.get("job_id") or job.get("id")
|
|
375
|
+
claim_attempt_id = job.get("claim_attempt_id")
|
|
376
|
+
params = job.get("payload") or job.get("params") or {}
|
|
377
|
+
colony_url = cfg["colony_url"]
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
started = _post(
|
|
381
|
+
cfg,
|
|
382
|
+
f"{colony_url}/v1/host/queue/jobs/{job_id}/start",
|
|
383
|
+
{"claim_attempt_id": claim_attempt_id},
|
|
384
|
+
)
|
|
385
|
+
if not isinstance(started, dict) or started.get("success") is not True:
|
|
386
|
+
raise RuntimeError("start transition was not accepted")
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
logger.warning("Job start failed for %s: %s", job_id, exc)
|
|
389
|
+
try:
|
|
390
|
+
_post(
|
|
391
|
+
cfg,
|
|
392
|
+
f"{colony_url}/v1/host/queue/jobs/{job_id}/release",
|
|
393
|
+
{"claim_attempt_id": claim_attempt_id},
|
|
394
|
+
)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
payload = {
|
|
400
|
+
"type": "agent_job",
|
|
401
|
+
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
|
402
|
+
"payload": {
|
|
403
|
+
"job_id": job_id,
|
|
404
|
+
"claim_attempt_id": claim_attempt_id,
|
|
405
|
+
"action_hint": params.get("action_hint", ""),
|
|
406
|
+
"domain": params.get("domain", ""),
|
|
407
|
+
"risk": params.get("risk", ""),
|
|
408
|
+
"description": params.get("description", ""),
|
|
409
|
+
"context": params.get("context", {}),
|
|
410
|
+
"report_example": params.get("report_example", {}),
|
|
411
|
+
**work_order_webhook_fields(params),
|
|
412
|
+
"colony_url": colony_url,
|
|
413
|
+
"observations_url": f"{colony_url}/v1/host/observations",
|
|
414
|
+
"heartbeat_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/heartbeat",
|
|
415
|
+
"complete_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/complete",
|
|
416
|
+
"fail_url": f"{colony_url}/v1/host/queue/jobs/{job_id}/fail",
|
|
417
|
+
"api_key_header": "X-API-Key",
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
try:
|
|
421
|
+
body, headers = encode_hermes_webhook(payload)
|
|
422
|
+
req = urllib.request.Request(
|
|
423
|
+
cfg["jobs_webhook"],
|
|
424
|
+
data=body,
|
|
425
|
+
headers=headers,
|
|
426
|
+
method="POST",
|
|
427
|
+
)
|
|
428
|
+
urllib.request.urlopen(req, timeout=15)
|
|
429
|
+
dispatched += 1
|
|
430
|
+
hint = params.get("action_hint", "?")
|
|
431
|
+
logger.info("Dispatched job %s (%s)", job_id, hint)
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
logger.warning("Job webhook failed for %s: %s", job_id, exc)
|
|
434
|
+
try:
|
|
435
|
+
_post(
|
|
436
|
+
cfg,
|
|
437
|
+
f"{colony_url}/v1/host/queue/jobs/{job_id}/release",
|
|
438
|
+
{"claim_attempt_id": claim_attempt_id},
|
|
439
|
+
)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
if dispatched:
|
|
444
|
+
logger.info("Jobs dispatched: %d", dispatched)
|
|
445
|
+
return dispatched
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Skills sync (periodic)
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
class SkillsSyncer:
|
|
453
|
+
"""Periodically reports the agent's skill index to Colony."""
|
|
454
|
+
|
|
455
|
+
def __init__(self, interval_hours: float):
|
|
456
|
+
self._interval = interval_hours * 3600
|
|
457
|
+
self._last_sync = 0.0
|
|
458
|
+
|
|
459
|
+
def sync_if_due(self, cfg: Dict[str, Any]) -> bool:
|
|
460
|
+
now = time.monotonic()
|
|
461
|
+
if self._last_sync > 0 and (now - self._last_sync) < self._interval:
|
|
462
|
+
return False
|
|
463
|
+
self._last_sync = now
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
from apsimo.workers.skills_sync import scan, report
|
|
467
|
+
observations = scan()
|
|
468
|
+
if observations:
|
|
469
|
+
status = report(observations)
|
|
470
|
+
logger.info("Skills sync: %d skills reported (HTTP %s)", len(observations), status)
|
|
471
|
+
return True
|
|
472
|
+
else:
|
|
473
|
+
logger.debug("Skills sync: no skills found")
|
|
474
|
+
return False
|
|
475
|
+
except ImportError:
|
|
476
|
+
logger.debug("Skills sync: apsimo.workers.skills_sync not importable, skipping")
|
|
477
|
+
return False
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
logger.warning("Skills sync failed: %s", exc)
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
# Alert delivery
|
|
485
|
+
# ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
def deliver_alerts(cfg: Dict[str, Any], alerts: list) -> None:
|
|
488
|
+
"""Forward health alerts to the agent webhook for surfacing."""
|
|
489
|
+
if not alerts:
|
|
490
|
+
return
|
|
491
|
+
for alert in alerts:
|
|
492
|
+
payload = {
|
|
493
|
+
"type": "alert",
|
|
494
|
+
"payload": {
|
|
495
|
+
"alert_type": alert["type"],
|
|
496
|
+
"severity": alert["severity"],
|
|
497
|
+
"message": alert["message"],
|
|
498
|
+
},
|
|
499
|
+
"occurred_at": alert.get("at", datetime.now(timezone.utc).isoformat()),
|
|
500
|
+
"delivery_context": {
|
|
501
|
+
"log_channel": cfg.get("log_channel", ""),
|
|
502
|
+
"platform": cfg.get("platform", ""),
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
try:
|
|
506
|
+
body, headers = encode_hermes_webhook(payload)
|
|
507
|
+
req = urllib.request.Request(
|
|
508
|
+
cfg["initiative_webhook"],
|
|
509
|
+
data=body,
|
|
510
|
+
headers=headers,
|
|
511
|
+
method="POST",
|
|
512
|
+
)
|
|
513
|
+
urllib.request.urlopen(req, timeout=10)
|
|
514
|
+
logger.info("Alert delivered: %s", alert["type"])
|
|
515
|
+
except Exception as exc:
|
|
516
|
+
logger.warning("Alert delivery failed (%s): %s", alert["type"], exc)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
# Main loop
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
class AgentBridge:
|
|
524
|
+
"""Unified daemon that keeps the Colony-to-agent circuit alive."""
|
|
525
|
+
|
|
526
|
+
def __init__(self, cfg: Dict[str, Any]):
|
|
527
|
+
self.cfg = cfg
|
|
528
|
+
self._health = HealthMonitor(cfg["state_dir"])
|
|
529
|
+
self._poller = InitiativePoller(cfg["state_dir"])
|
|
530
|
+
self._worker = QueueWorker()
|
|
531
|
+
self._skills = SkillsSyncer(cfg["skills_hours"])
|
|
532
|
+
self._running = True
|
|
533
|
+
|
|
534
|
+
def stop(self, *_args: Any) -> None:
|
|
535
|
+
logger.info("Shutdown signal received")
|
|
536
|
+
self._running = False
|
|
537
|
+
|
|
538
|
+
def cycle(self) -> dict:
|
|
539
|
+
"""Run one full cycle: health check, poll, dispatch, sync."""
|
|
540
|
+
result = {"ts": datetime.now(timezone.utc).isoformat()}
|
|
541
|
+
|
|
542
|
+
check = self._health.check(self.cfg)
|
|
543
|
+
result["health_ok"] = check.get("ok", False)
|
|
544
|
+
|
|
545
|
+
if check.get("alerts"):
|
|
546
|
+
deliver_alerts(self.cfg, check["alerts"])
|
|
547
|
+
result["alerts"] = len(check["alerts"])
|
|
548
|
+
|
|
549
|
+
if not check.get("ok"):
|
|
550
|
+
return result
|
|
551
|
+
|
|
552
|
+
result["initiatives_fired"] = self._poller.poll(self.cfg)
|
|
553
|
+
result["jobs_dispatched"] = self._worker.claim_and_dispatch(self.cfg)
|
|
554
|
+
result["skills_synced"] = self._skills.sync_if_due(self.cfg)
|
|
555
|
+
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
def run(self, once: bool = False) -> None:
|
|
559
|
+
logger.info(
|
|
560
|
+
"Colony Agent Bridge starting (poll=%ds, skills_sync=%dh, url=%s)",
|
|
561
|
+
self.cfg["poll_secs"],
|
|
562
|
+
int(self.cfg["skills_hours"]),
|
|
563
|
+
self.cfg["colony_url"],
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
signal.signal(signal.SIGTERM, self.stop)
|
|
567
|
+
signal.signal(signal.SIGINT, self.stop)
|
|
568
|
+
|
|
569
|
+
while self._running:
|
|
570
|
+
try:
|
|
571
|
+
result = self.cycle()
|
|
572
|
+
level = logging.DEBUG
|
|
573
|
+
if result.get("initiatives_fired") or result.get("jobs_dispatched"):
|
|
574
|
+
level = logging.INFO
|
|
575
|
+
if result.get("alerts"):
|
|
576
|
+
level = logging.WARNING
|
|
577
|
+
logger.log(level, "Cycle: %s", json.dumps(result, default=str))
|
|
578
|
+
except Exception as exc:
|
|
579
|
+
logger.error("Cycle error: %s", exc, exc_info=True)
|
|
580
|
+
|
|
581
|
+
if once:
|
|
582
|
+
break
|
|
583
|
+
|
|
584
|
+
try:
|
|
585
|
+
time.sleep(self.cfg["poll_secs"])
|
|
586
|
+
except (KeyboardInterrupt, SystemExit):
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
logger.info("Colony Agent Bridge stopped")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# ---------------------------------------------------------------------------
|
|
593
|
+
# CLI
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
def main(argv: Optional[list] = None) -> int:
|
|
597
|
+
parser = argparse.ArgumentParser(
|
|
598
|
+
prog="colony-agent-bridge",
|
|
599
|
+
description=(
|
|
600
|
+
"Unified daemon that bridges Colony's autonomy loop to the agent. "
|
|
601
|
+
"Replaces the separate initiative-poller, queue-worker, and "
|
|
602
|
+
"skills-sync cron jobs with one process that also monitors "
|
|
603
|
+
"circuit health."
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
parser.add_argument(
|
|
607
|
+
"--once", action="store_true",
|
|
608
|
+
help="run one cycle and exit (for cron or testing)",
|
|
609
|
+
)
|
|
610
|
+
parser.add_argument(
|
|
611
|
+
"--dry-run", action="store_true",
|
|
612
|
+
help="print configuration and exit without any network calls",
|
|
613
|
+
)
|
|
614
|
+
parser.add_argument(
|
|
615
|
+
"--verbose", "-v", action="store_true",
|
|
616
|
+
help="enable debug logging",
|
|
617
|
+
)
|
|
618
|
+
args = parser.parse_args(argv)
|
|
619
|
+
|
|
620
|
+
logging.basicConfig(
|
|
621
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
622
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
623
|
+
datefmt="%H:%M:%S",
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
cfg = _cfg()
|
|
627
|
+
|
|
628
|
+
if args.dry_run:
|
|
629
|
+
print("colony-agent-bridge (dry run):")
|
|
630
|
+
for k, v in sorted(cfg.items()):
|
|
631
|
+
print(f" {k}: {v}")
|
|
632
|
+
return 0
|
|
633
|
+
|
|
634
|
+
bridge = AgentBridge(cfg)
|
|
635
|
+
bridge.run(once=args.once)
|
|
636
|
+
return 0
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
if __name__ == "__main__":
|
|
640
|
+
raise SystemExit(main())
|