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,522 @@
|
|
|
1
|
+
"""WebSocket manager for remote agent connections.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Challenge-response authentication
|
|
5
|
+
- Connection management
|
|
6
|
+
- Initiative delivery
|
|
7
|
+
- Ping/pong timeout
|
|
8
|
+
- Circuit breaker integration
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
18
|
+
|
|
19
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WebSocketManager:
|
|
25
|
+
"""Manages WebSocket connections for remote agents."""
|
|
26
|
+
|
|
27
|
+
# Timeouts
|
|
28
|
+
AUTH_TIMEOUT = 30 # Seconds to complete auth
|
|
29
|
+
SESSION_TIMEOUT = timedelta(hours=24) # Re-auth after 24h
|
|
30
|
+
PING_INTERVAL = 30 # Seconds between pings
|
|
31
|
+
PONG_TIMEOUT = 10 # Seconds to wait for pong
|
|
32
|
+
ACK_TIMEOUT = 30 # Seconds to wait for initiative ACK
|
|
33
|
+
MAX_TIMESTAMP_SKEW = 300 # 5 minutes
|
|
34
|
+
|
|
35
|
+
# Limits
|
|
36
|
+
MAX_CONNECTIONS = 100
|
|
37
|
+
MAX_MESSAGE_SIZE = 1024 * 1024 # 1 MB
|
|
38
|
+
MAX_CONNECT_ATTEMPTS = 5
|
|
39
|
+
ATTEMPT_WINDOW = timedelta(minutes=1)
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
agent_store: Any,
|
|
44
|
+
initiative_store: Any,
|
|
45
|
+
circuit_breaker: Optional[Any] = None,
|
|
46
|
+
dead_letter_queue: Optional[Any] = None,
|
|
47
|
+
):
|
|
48
|
+
self._agent_store = agent_store
|
|
49
|
+
self._initiative_store = initiative_store
|
|
50
|
+
self._circuit_breaker = circuit_breaker
|
|
51
|
+
self._dlq = dead_letter_queue
|
|
52
|
+
|
|
53
|
+
# Active connections
|
|
54
|
+
self._active_connections: Dict[str, WebSocket] = {}
|
|
55
|
+
self._connection_info: Dict[str, Dict[str, Any]] = {}
|
|
56
|
+
|
|
57
|
+
# Rate limiting
|
|
58
|
+
self._connect_attempts: Dict[str, List[float]] = defaultdict(list)
|
|
59
|
+
|
|
60
|
+
# Pending auth challenges
|
|
61
|
+
self._pending_challenges: Dict[str, str] = {}
|
|
62
|
+
|
|
63
|
+
# Pending ACKs
|
|
64
|
+
self._pending_acks: Dict[str, asyncio.Future] = {}
|
|
65
|
+
|
|
66
|
+
# Message sequencing
|
|
67
|
+
self._seq: Dict[str, int] = defaultdict(int)
|
|
68
|
+
|
|
69
|
+
# Running tasks
|
|
70
|
+
self._tasks: Set[asyncio.Task] = set()
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Connection Handling
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
async def handle_connection(
|
|
77
|
+
self,
|
|
78
|
+
websocket: WebSocket,
|
|
79
|
+
agent_id: str,
|
|
80
|
+
client_ip: str,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Handle WebSocket connection from remote agent."""
|
|
83
|
+
# Check max connections
|
|
84
|
+
if len(self._active_connections) >= self.MAX_CONNECTIONS:
|
|
85
|
+
await websocket.close(code=1013, reason="Max connections reached")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Rate limit check
|
|
89
|
+
if not await self._check_rate_limit(client_ip):
|
|
90
|
+
await websocket.close(code=4003, reason="Rate limited")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Get agent
|
|
94
|
+
agent = await self._agent_store.get(agent_id)
|
|
95
|
+
if not agent:
|
|
96
|
+
await websocket.close(code=4004, reason="Agent not found")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Check if revoked
|
|
100
|
+
if self._agent_store.is_revoked(agent.node_id):
|
|
101
|
+
await websocket.close(code=4003, reason="Agent revoked")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Check if can reconnect
|
|
105
|
+
if not agent.status.can_reconnect():
|
|
106
|
+
await websocket.close(code=4003, reason=f"Agent status: {agent.status}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Accept connection
|
|
110
|
+
await websocket.accept()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Authenticate
|
|
114
|
+
if not await self._authenticate(websocket, agent, client_ip):
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Register connection
|
|
118
|
+
self._active_connections[agent_id] = websocket
|
|
119
|
+
self._connection_info[agent_id] = {
|
|
120
|
+
"connected_at": datetime.now(timezone.utc),
|
|
121
|
+
"client_ip": client_ip,
|
|
122
|
+
"last_pong": time.time(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Mark agent online
|
|
126
|
+
await self._agent_store.set_online(
|
|
127
|
+
agent_id,
|
|
128
|
+
websocket_connected=True,
|
|
129
|
+
metadata={"last_connection_ip": client_ip},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
logger.info("Agent %s connected from %s", agent_id, client_ip)
|
|
133
|
+
|
|
134
|
+
# Start ping task
|
|
135
|
+
ping_task = asyncio.create_task(
|
|
136
|
+
self._ping_task(agent_id, websocket)
|
|
137
|
+
)
|
|
138
|
+
self._tasks.add(ping_task)
|
|
139
|
+
|
|
140
|
+
# Message loop
|
|
141
|
+
await self._message_loop(agent_id, websocket)
|
|
142
|
+
|
|
143
|
+
except WebSocketDisconnect:
|
|
144
|
+
logger.info("Agent %s disconnected", agent_id)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error("Agent %s WebSocket error: %s", agent_id, e)
|
|
147
|
+
finally:
|
|
148
|
+
await self._cleanup_connection(agent_id)
|
|
149
|
+
|
|
150
|
+
async def _check_rate_limit(self, ip: str) -> bool:
|
|
151
|
+
"""Check if IP is rate limited."""
|
|
152
|
+
now = time.time()
|
|
153
|
+
window_start = now - self.ATTEMPT_WINDOW.total_seconds()
|
|
154
|
+
|
|
155
|
+
# Clean old attempts
|
|
156
|
+
self._connect_attempts[ip] = [
|
|
157
|
+
t for t in self._connect_attempts[ip] if t > window_start
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Check limit
|
|
161
|
+
if len(self._connect_attempts[ip]) >= self.MAX_CONNECT_ATTEMPTS:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# Record attempt
|
|
165
|
+
self._connect_attempts[ip].append(now)
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Authentication
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
async def _authenticate(
|
|
173
|
+
self,
|
|
174
|
+
websocket: WebSocket,
|
|
175
|
+
agent: Any,
|
|
176
|
+
client_ip: str,
|
|
177
|
+
) -> bool:
|
|
178
|
+
"""Challenge-response authentication."""
|
|
179
|
+
import secrets
|
|
180
|
+
|
|
181
|
+
# Generate challenge
|
|
182
|
+
nonce = secrets.token_hex(16)
|
|
183
|
+
timestamp = int(time.time())
|
|
184
|
+
|
|
185
|
+
self._pending_challenges[agent.agent_id] = nonce
|
|
186
|
+
|
|
187
|
+
# Send challenge
|
|
188
|
+
await websocket.send_json({
|
|
189
|
+
"type": "auth_challenge",
|
|
190
|
+
"nonce": nonce,
|
|
191
|
+
"timestamp": timestamp,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
# Wait for response
|
|
195
|
+
try:
|
|
196
|
+
data = await asyncio.wait_for(
|
|
197
|
+
websocket.receive_text(),
|
|
198
|
+
timeout=self.AUTH_TIMEOUT,
|
|
199
|
+
)
|
|
200
|
+
response = json.loads(data)
|
|
201
|
+
except asyncio.TimeoutError:
|
|
202
|
+
await websocket.close(code=4001, reason="Auth timeout")
|
|
203
|
+
return False
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
await websocket.close(code=4001, reason="Invalid JSON")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
# Verify response
|
|
209
|
+
if response.get("type") != "auth_response":
|
|
210
|
+
await websocket.close(code=4001, reason="Expected auth_response")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
if response.get("nonce") != nonce:
|
|
214
|
+
await websocket.close(code=4001, reason="Nonce mismatch")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Verify timestamp (replay prevention)
|
|
218
|
+
response_ts = response.get("timestamp", 0)
|
|
219
|
+
if abs(time.time() - response_ts) > self.MAX_TIMESTAMP_SKEW:
|
|
220
|
+
await websocket.close(code=4001, reason="Timestamp skew too large")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# Verify signature
|
|
224
|
+
signed_payload = f"{nonce}:{response_ts}:{agent.agent_id}".encode()
|
|
225
|
+
signature = response.get("signature", "")
|
|
226
|
+
|
|
227
|
+
if not self._verify_signature(agent.node_cert, signed_payload, signature):
|
|
228
|
+
await websocket.close(code=4003, reason="Invalid signature")
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
# Clean up challenge
|
|
232
|
+
self._pending_challenges.pop(agent.agent_id, None)
|
|
233
|
+
|
|
234
|
+
# Send success
|
|
235
|
+
await websocket.send_json({"type": "connected"})
|
|
236
|
+
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def _verify_signature(
|
|
240
|
+
self,
|
|
241
|
+
cert: Optional[Dict[str, Any]],
|
|
242
|
+
message: bytes,
|
|
243
|
+
signature_hex: str,
|
|
244
|
+
) -> bool:
|
|
245
|
+
"""Verify Ed25519 signature."""
|
|
246
|
+
if not cert:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
251
|
+
|
|
252
|
+
pubkey_hex = cert.get("node_public_key_ed25519")
|
|
253
|
+
if not pubkey_hex:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
pub_bytes = bytes.fromhex(pubkey_hex)
|
|
257
|
+
pub_key = Ed25519PublicKey.from_public_bytes(pub_bytes)
|
|
258
|
+
sig_bytes = bytes.fromhex(signature_hex)
|
|
259
|
+
pub_key.verify(sig_bytes, message)
|
|
260
|
+
return True
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.warning("Signature verification failed: %s", e)
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
# Message Handling
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
async def _message_loop(
|
|
270
|
+
self,
|
|
271
|
+
agent_id: str,
|
|
272
|
+
websocket: WebSocket,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Handle incoming WebSocket messages."""
|
|
275
|
+
while agent_id in self._active_connections:
|
|
276
|
+
try:
|
|
277
|
+
data = await websocket.receive_text()
|
|
278
|
+
|
|
279
|
+
# Check message size
|
|
280
|
+
if len(data) > self.MAX_MESSAGE_SIZE:
|
|
281
|
+
await websocket.send_json({
|
|
282
|
+
"type": "error",
|
|
283
|
+
"error": "message_too_large",
|
|
284
|
+
"message": f"Message exceeds {self.MAX_MESSAGE_SIZE} bytes",
|
|
285
|
+
})
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Parse message
|
|
289
|
+
try:
|
|
290
|
+
message = json.loads(data)
|
|
291
|
+
except json.JSONDecodeError as e:
|
|
292
|
+
await websocket.send_json({
|
|
293
|
+
"type": "error",
|
|
294
|
+
"error": "invalid_json",
|
|
295
|
+
"message": str(e),
|
|
296
|
+
})
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
# Validate structure
|
|
300
|
+
if not isinstance(message, dict):
|
|
301
|
+
await websocket.send_json({
|
|
302
|
+
"type": "error",
|
|
303
|
+
"error": "invalid_message",
|
|
304
|
+
"message": "Message must be a JSON object",
|
|
305
|
+
})
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
msg_type = message.get("type")
|
|
309
|
+
if not msg_type:
|
|
310
|
+
await websocket.send_json({
|
|
311
|
+
"type": "error",
|
|
312
|
+
"error": "missing_type",
|
|
313
|
+
"message": "Message must have 'type' field",
|
|
314
|
+
})
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Handle message
|
|
318
|
+
await self._handle_message(agent_id, websocket, message)
|
|
319
|
+
|
|
320
|
+
except WebSocketDisconnect:
|
|
321
|
+
break
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error("Message handling error for agent %s: %s", agent_id, e)
|
|
324
|
+
|
|
325
|
+
async def _handle_message(
|
|
326
|
+
self,
|
|
327
|
+
agent_id: str,
|
|
328
|
+
websocket: WebSocket,
|
|
329
|
+
message: Dict[str, Any],
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Handle specific message type."""
|
|
332
|
+
msg_type = message.get("type")
|
|
333
|
+
|
|
334
|
+
if msg_type == "pong":
|
|
335
|
+
# Update last pong time
|
|
336
|
+
if agent_id in self._connection_info:
|
|
337
|
+
self._connection_info[agent_id]["last_pong"] = time.time()
|
|
338
|
+
|
|
339
|
+
elif msg_type == "acknowledge":
|
|
340
|
+
# Initiative acknowledgment
|
|
341
|
+
initiative_id = message.get("initiative_id")
|
|
342
|
+
if initiative_id and initiative_id in self._pending_acks:
|
|
343
|
+
future = self._pending_acks.pop(initiative_id)
|
|
344
|
+
if not future.done():
|
|
345
|
+
future.set_result(True)
|
|
346
|
+
|
|
347
|
+
# Update store
|
|
348
|
+
await self._initiative_store.acknowledge(initiative_id, agent_id)
|
|
349
|
+
|
|
350
|
+
elif msg_type == "complete":
|
|
351
|
+
# Initiative completed
|
|
352
|
+
initiative_id = message.get("initiative_id")
|
|
353
|
+
result = message.get("result")
|
|
354
|
+
metadata = message.get("metadata", {})
|
|
355
|
+
|
|
356
|
+
await self._initiative_store.complete(
|
|
357
|
+
initiative_id,
|
|
358
|
+
agent_id,
|
|
359
|
+
result=result,
|
|
360
|
+
result_metadata=metadata,
|
|
361
|
+
)
|
|
362
|
+
await self._agent_store.decrement_assignments(agent_id)
|
|
363
|
+
|
|
364
|
+
elif msg_type == "fail":
|
|
365
|
+
# Initiative failed
|
|
366
|
+
initiative_id = message.get("initiative_id")
|
|
367
|
+
reason = message.get("reason", "Unknown")
|
|
368
|
+
retry = message.get("retry", False)
|
|
369
|
+
|
|
370
|
+
await self._initiative_store.fail(
|
|
371
|
+
initiative_id,
|
|
372
|
+
agent_id,
|
|
373
|
+
reason=reason,
|
|
374
|
+
retry=retry,
|
|
375
|
+
)
|
|
376
|
+
if not retry:
|
|
377
|
+
await self._agent_store.decrement_assignments(agent_id)
|
|
378
|
+
|
|
379
|
+
elif msg_type == "heartbeat":
|
|
380
|
+
# Heartbeat response
|
|
381
|
+
await self._agent_store.update(
|
|
382
|
+
agent_id,
|
|
383
|
+
last_seen_at=datetime.now(timezone.utc),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
else:
|
|
387
|
+
logger.warning("Unknown message type from agent %s: %s", agent_id, msg_type)
|
|
388
|
+
|
|
389
|
+
# ------------------------------------------------------------------
|
|
390
|
+
# Ping/Pong
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
async def _ping_task(self, agent_id: str, websocket: WebSocket) -> None:
|
|
394
|
+
"""Send periodic pings and check for timeout."""
|
|
395
|
+
while agent_id in self._active_connections:
|
|
396
|
+
await asyncio.sleep(self.PING_INTERVAL)
|
|
397
|
+
|
|
398
|
+
if agent_id not in self._connection_info:
|
|
399
|
+
break
|
|
400
|
+
|
|
401
|
+
info = self._connection_info[agent_id]
|
|
402
|
+
last_activity = info.get("last_pong", time.time())
|
|
403
|
+
|
|
404
|
+
# Check for timeout
|
|
405
|
+
if time.time() - last_activity > self.PING_INTERVAL + self.PONG_TIMEOUT:
|
|
406
|
+
logger.warning("Agent %s ping timeout", agent_id)
|
|
407
|
+
try:
|
|
408
|
+
await websocket.close(code=4001, reason="Ping timeout")
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
break
|
|
412
|
+
|
|
413
|
+
# Send ping
|
|
414
|
+
try:
|
|
415
|
+
seq = self._next_seq(agent_id)
|
|
416
|
+
await websocket.send_json({
|
|
417
|
+
"type": "ping",
|
|
418
|
+
"seq": seq,
|
|
419
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
420
|
+
})
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error("Failed to send ping to agent %s: %s", agent_id, e)
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
def _next_seq(self, agent_id: str) -> int:
|
|
426
|
+
"""Get next sequence number for agent."""
|
|
427
|
+
self._seq[agent_id] += 1
|
|
428
|
+
return self._seq[agent_id]
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
# Initiative Delivery
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
async def send_initiative(
|
|
435
|
+
self,
|
|
436
|
+
agent_id: str,
|
|
437
|
+
initiative: Dict[str, Any],
|
|
438
|
+
) -> bool:
|
|
439
|
+
"""Send initiative to agent and wait for ACK."""
|
|
440
|
+
# Check circuit breaker
|
|
441
|
+
if self._circuit_breaker and self._circuit_breaker.is_open(agent_id):
|
|
442
|
+
priority = initiative.get("priority", 1)
|
|
443
|
+
if priority < 2: # Not urgent
|
|
444
|
+
logger.warning("Circuit breaker open for agent %s", agent_id)
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
websocket = self._active_connections.get(agent_id)
|
|
448
|
+
if not websocket:
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
seq = self._next_seq(agent_id)
|
|
452
|
+
|
|
453
|
+
# Send initiative
|
|
454
|
+
try:
|
|
455
|
+
await websocket.send_json({
|
|
456
|
+
"type": "initiative",
|
|
457
|
+
"seq": seq,
|
|
458
|
+
"initiative": initiative,
|
|
459
|
+
})
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error("Failed to send initiative to agent %s: %s", agent_id, e)
|
|
462
|
+
if self._circuit_breaker:
|
|
463
|
+
self._circuit_breaker.record_failure(agent_id)
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
# Wait for ACK
|
|
467
|
+
ack_future = asyncio.Future()
|
|
468
|
+
self._pending_acks[initiative["id"]] = ack_future
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
await asyncio.wait_for(ack_future, timeout=self.ACK_TIMEOUT)
|
|
472
|
+
if self._circuit_breaker:
|
|
473
|
+
self._circuit_breaker.record_success(agent_id)
|
|
474
|
+
return True
|
|
475
|
+
except asyncio.TimeoutError:
|
|
476
|
+
logger.warning("No ACK from agent %s for initiative %s", agent_id, initiative["id"])
|
|
477
|
+
self._pending_acks.pop(initiative["id"], None)
|
|
478
|
+
|
|
479
|
+
if self._circuit_breaker:
|
|
480
|
+
self._circuit_breaker.record_failure(agent_id)
|
|
481
|
+
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
# ------------------------------------------------------------------
|
|
485
|
+
# Cleanup
|
|
486
|
+
# ------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
async def _cleanup_connection(self, agent_id: str) -> None:
|
|
489
|
+
"""Clean up after agent disconnects."""
|
|
490
|
+
# Remove from active
|
|
491
|
+
self._active_connections.pop(agent_id, None)
|
|
492
|
+
self._connection_info.pop(agent_id, None)
|
|
493
|
+
|
|
494
|
+
# Mark agent offline
|
|
495
|
+
await self._agent_store.set_offline(agent_id)
|
|
496
|
+
|
|
497
|
+
# Reassign pending initiatives
|
|
498
|
+
await self._initiative_store.reassign_from_agent(agent_id, only_pending=True)
|
|
499
|
+
|
|
500
|
+
# Cancel pending ACKs
|
|
501
|
+
for init_id, future in list(self._pending_acks.items()):
|
|
502
|
+
# Note: We'd need to track which agent each ACK belongs to
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
def get_connection_count(self) -> int:
|
|
506
|
+
"""Get number of active connections."""
|
|
507
|
+
return len(self._active_connections)
|
|
508
|
+
|
|
509
|
+
def get_connected_agents(self) -> List[str]:
|
|
510
|
+
"""Get list of connected agent IDs."""
|
|
511
|
+
return list(self._active_connections.keys())
|
|
512
|
+
|
|
513
|
+
async def close_all(self) -> None:
|
|
514
|
+
"""Close all connections."""
|
|
515
|
+
for agent_id, websocket in list(self._active_connections.items()):
|
|
516
|
+
try:
|
|
517
|
+
await websocket.close(code=1001, reason="Server shutdown")
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
self._active_connections.clear()
|
|
522
|
+
self._connection_info.clear()
|
apsimo/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# api
|