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,123 @@
|
|
|
1
|
+
"""Graduated approval policy helpers (v0.18.0) — outbound target checks.
|
|
2
|
+
|
|
3
|
+
The owner's policy: nothing waits on a manual approval unless it is
|
|
4
|
+
potentially destructive or an outreach to an individual the owner hasn't
|
|
5
|
+
authorized yet. The destructive half lives in the action registry's risk
|
|
6
|
+
tiers; this module answers the other half — *who* an OUTBOUND action
|
|
7
|
+
reaches, and whether that person is an authorized contact.
|
|
8
|
+
|
|
9
|
+
An OUTBOUND :class:`~apsimo.initiatives.action_registry.ActionSpec`
|
|
10
|
+
names the param holding its recipient via ``target_param``. The recipient
|
|
11
|
+
value is resolved against the contact store:
|
|
12
|
+
|
|
13
|
+
- ``gateway:address`` forms (``email:bob@x.com``, ``imessage:+1555...``)
|
|
14
|
+
resolve via ``resolve_handle``;
|
|
15
|
+
- ``cid-...`` resolves via ``get``;
|
|
16
|
+
- bare emails resolve via ``resolve_handle("email", ...)``;
|
|
17
|
+
- anything else falls back to ``find_by_name`` — and only an
|
|
18
|
+
unambiguous match counts (names are not unique; an ambiguous name must
|
|
19
|
+
not authorize the wrong person).
|
|
20
|
+
|
|
21
|
+
The check fails closed: missing store, missing/unresolvable target, or a
|
|
22
|
+
contact with ``interaction_allowed=False`` all deny auto-approval.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Dict, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from apsimo.contacts.models import GATEWAYS
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_target(params: Optional[Dict[str, Any]], spec: Any) -> Optional[str]:
|
|
36
|
+
"""Pull the recipient value named by ``spec.target_param``.
|
|
37
|
+
|
|
38
|
+
Looks in the job params themselves, then the nested ``context`` and
|
|
39
|
+
``params`` dicts (the autonomy loop carries initiative context under
|
|
40
|
+
``context``).
|
|
41
|
+
"""
|
|
42
|
+
key = getattr(spec, "target_param", None) if spec is not None else None
|
|
43
|
+
if not key or not isinstance(params, dict):
|
|
44
|
+
return None
|
|
45
|
+
containers = (params, params.get("context"), params.get("params"))
|
|
46
|
+
for container in containers:
|
|
47
|
+
if not isinstance(container, dict):
|
|
48
|
+
continue
|
|
49
|
+
value = container.get(key)
|
|
50
|
+
if value:
|
|
51
|
+
return str(value).strip()
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _resolve_contact(target: str, contacts_store: Any) -> Optional[Any]:
|
|
56
|
+
"""Resolve a recipient identifier to a Contact, or None."""
|
|
57
|
+
# gateway:address form (email:bob@x.com, imessage:+15551234567, ...)
|
|
58
|
+
if ":" in target:
|
|
59
|
+
gateway, _, address = target.partition(":")
|
|
60
|
+
gateway = gateway.strip().lower()
|
|
61
|
+
address = address.strip()
|
|
62
|
+
if gateway in GATEWAYS and address:
|
|
63
|
+
return await contacts_store.resolve_handle(gateway, address)
|
|
64
|
+
|
|
65
|
+
# Contact ID — primary key lookup.
|
|
66
|
+
if target.startswith("cid-"):
|
|
67
|
+
return await contacts_store.get(target)
|
|
68
|
+
|
|
69
|
+
# Bare email handle.
|
|
70
|
+
if "@" in target:
|
|
71
|
+
return await contacts_store.resolve_handle("email", target)
|
|
72
|
+
|
|
73
|
+
# Display name — only when unambiguous (mirrors IdentityResolver).
|
|
74
|
+
matches = await contacts_store.find_by_name(target, threshold=0.9)
|
|
75
|
+
wanted = target.lower()
|
|
76
|
+
exact = [
|
|
77
|
+
m for m in matches
|
|
78
|
+
if (getattr(m, "display_name", "") or "").strip().lower() == wanted
|
|
79
|
+
]
|
|
80
|
+
candidates = exact or matches
|
|
81
|
+
if len(candidates) == 1:
|
|
82
|
+
return candidates[0]
|
|
83
|
+
if len(candidates) > 1:
|
|
84
|
+
logger.warning(
|
|
85
|
+
"Ambiguous outbound target %r: %d contacts match — refusing "
|
|
86
|
+
"to authorize", target, len(candidates),
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def is_authorized_target(
|
|
92
|
+
params: Optional[Dict[str, Any]],
|
|
93
|
+
spec: Any,
|
|
94
|
+
contacts_store: Any,
|
|
95
|
+
) -> Tuple[bool, str]:
|
|
96
|
+
"""Is the recipient of an OUTBOUND action an authorized contact?
|
|
97
|
+
|
|
98
|
+
Returns ``(True, "contact:<cid>")`` only when the recipient named by
|
|
99
|
+
``spec.target_param`` resolves to a contact with
|
|
100
|
+
``interaction_allowed=True``. Everything else fails closed:
|
|
101
|
+
|
|
102
|
+
- no contact store wired → ``(False, "no_contact_store")``
|
|
103
|
+
- missing/unresolvable target → ``(False, "unknown_target")``
|
|
104
|
+
- contact found but not authorized → ``(False, "contact_not_authorized")``
|
|
105
|
+
"""
|
|
106
|
+
if contacts_store is None:
|
|
107
|
+
return False, "no_contact_store"
|
|
108
|
+
|
|
109
|
+
target = _extract_target(params, spec)
|
|
110
|
+
if not target:
|
|
111
|
+
return False, "unknown_target"
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
contact = await _resolve_contact(target, contacts_store)
|
|
115
|
+
except Exception as exc: # resolution must never break dispatch
|
|
116
|
+
logger.warning("Outbound target resolution failed for %r: %s", target, exc)
|
|
117
|
+
return False, "unknown_target"
|
|
118
|
+
|
|
119
|
+
if contact is None:
|
|
120
|
+
return False, "unknown_target"
|
|
121
|
+
if not getattr(contact, "interaction_allowed", False):
|
|
122
|
+
return False, "contact_not_authorized"
|
|
123
|
+
return True, f"contact:{getattr(contact, 'contact_id', 'unknown')}"
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Assignment Engine for multi-agent Colony.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Agent selection for initiatives
|
|
5
|
+
- Capability matching
|
|
6
|
+
- Load balancing
|
|
7
|
+
- Priority handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Initiative type → required capabilities
|
|
18
|
+
INITIATIVE_CAPABILITIES: Dict[str, List[str]] = {
|
|
19
|
+
"follow_up": [], # Any agent
|
|
20
|
+
"relationship": ["messaging"], # Needs messaging
|
|
21
|
+
"scheduling": ["calendar"], # Needs calendar
|
|
22
|
+
"coding": ["coding"], # Needs code execution
|
|
23
|
+
"health": [], # Any agent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Types that should prefer primary agent
|
|
27
|
+
USER_FACING_TYPES = ["follow_up", "relationship"]
|
|
28
|
+
|
|
29
|
+
# Default for unknown types
|
|
30
|
+
UNKNOWN_TYPE_POLICY = "allow_any" # "reject", "log_and_allow", "allow_any"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AssignmentEngine:
|
|
34
|
+
"""Selects the best agent for an initiative."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
agent_store: Any,
|
|
39
|
+
initiative_store: Any,
|
|
40
|
+
unknown_type_policy: str = UNKNOWN_TYPE_POLICY,
|
|
41
|
+
):
|
|
42
|
+
self._agent_store = agent_store
|
|
43
|
+
self._initiative_store = initiative_store
|
|
44
|
+
self._unknown_type_policy = unknown_type_policy
|
|
45
|
+
|
|
46
|
+
def select_agent(
|
|
47
|
+
self,
|
|
48
|
+
initiative: Any,
|
|
49
|
+
agents: Optional[List[Any]] = None,
|
|
50
|
+
) -> Optional[Any]:
|
|
51
|
+
"""Select the best agent for an initiative.
|
|
52
|
+
|
|
53
|
+
Priority:
|
|
54
|
+
1. Online status
|
|
55
|
+
2. Preferred agent (if specified)
|
|
56
|
+
3. Capability match
|
|
57
|
+
4. Type restrictions
|
|
58
|
+
5. Primary designation (for user-facing)
|
|
59
|
+
6. Load balancing
|
|
60
|
+
7. Capacity check
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
initiative: Initiative to assign (StoredInitiative or dict)
|
|
64
|
+
agents: List of agents to choose from (if None, loads from store)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Selected Agent or None if no suitable agent found
|
|
68
|
+
"""
|
|
69
|
+
# Get initiative type
|
|
70
|
+
init_type = self._get_initiative_type(initiative)
|
|
71
|
+
|
|
72
|
+
# Check if type is known
|
|
73
|
+
if init_type not in INITIATIVE_CAPABILITIES:
|
|
74
|
+
if self._unknown_type_policy == "reject":
|
|
75
|
+
logger.warning("Unknown initiative type: %s, rejecting", init_type)
|
|
76
|
+
return None
|
|
77
|
+
elif self._unknown_type_policy == "log_and_allow":
|
|
78
|
+
logger.warning("Unknown initiative type: %s, allowing any agent", init_type)
|
|
79
|
+
|
|
80
|
+
# Get agents if not provided
|
|
81
|
+
if agents is None:
|
|
82
|
+
agents = self._agent_store.list(status=["online"])
|
|
83
|
+
|
|
84
|
+
# Step 1: Only online agents
|
|
85
|
+
candidates = [a for a in agents if a.status == "online"]
|
|
86
|
+
if not candidates:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Step 2: Preferred agent (if specified)
|
|
90
|
+
preferred_id = self._get_preferred_agent_id(initiative)
|
|
91
|
+
if preferred_id:
|
|
92
|
+
preferred = next(
|
|
93
|
+
(a for a in candidates if a.agent_id == preferred_id),
|
|
94
|
+
None
|
|
95
|
+
)
|
|
96
|
+
if preferred and self._has_capacity(preferred):
|
|
97
|
+
return preferred
|
|
98
|
+
|
|
99
|
+
# Step 3: Capability filter
|
|
100
|
+
required_caps = INITIATIVE_CAPABILITIES.get(init_type, [])
|
|
101
|
+
if required_caps:
|
|
102
|
+
candidates = [
|
|
103
|
+
a for a in candidates
|
|
104
|
+
if a.has_capabilities(required_caps)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
if not candidates:
|
|
108
|
+
logger.debug(
|
|
109
|
+
"No agents with capabilities %s for initiative type %s",
|
|
110
|
+
required_caps,
|
|
111
|
+
init_type,
|
|
112
|
+
)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# Step 4: Type restrictions (excluded/included types)
|
|
116
|
+
filtered = []
|
|
117
|
+
for agent in candidates:
|
|
118
|
+
if not agent.can_handle_type(init_type):
|
|
119
|
+
continue
|
|
120
|
+
filtered.append(agent)
|
|
121
|
+
|
|
122
|
+
candidates = filtered if filtered else candidates
|
|
123
|
+
|
|
124
|
+
if not candidates:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Step 5: Primary preference for user-facing
|
|
128
|
+
if init_type in USER_FACING_TYPES:
|
|
129
|
+
primaries = [a for a in candidates if a.is_primary]
|
|
130
|
+
if primaries:
|
|
131
|
+
candidates = primaries
|
|
132
|
+
|
|
133
|
+
# Step 6: Sort by load (ascending), then priority (descending)
|
|
134
|
+
candidates.sort(key=lambda a: (
|
|
135
|
+
a.load,
|
|
136
|
+
-a.priority,
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
# Step 7: Capacity check
|
|
140
|
+
candidates = [a for a in candidates if a.has_capacity]
|
|
141
|
+
|
|
142
|
+
return candidates[0] if candidates else None
|
|
143
|
+
|
|
144
|
+
async def assign(
|
|
145
|
+
self,
|
|
146
|
+
initiative: Any,
|
|
147
|
+
agent: Optional[Any] = None,
|
|
148
|
+
) -> Optional[Any]:
|
|
149
|
+
"""Assign initiative to agent.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
initiative: Initiative to assign
|
|
153
|
+
agent: Specific agent to assign to (if None, auto-select)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Assigned agent or None if assignment failed
|
|
157
|
+
"""
|
|
158
|
+
# Get initiative ID and type
|
|
159
|
+
init_id = self._get_initiative_id(initiative)
|
|
160
|
+
if not init_id:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Select agent if not provided
|
|
164
|
+
if agent is None:
|
|
165
|
+
agent = self.select_agent(initiative)
|
|
166
|
+
|
|
167
|
+
if not agent:
|
|
168
|
+
logger.debug("No agent available for initiative %s", init_id)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Assign via store
|
|
172
|
+
assigned = self._initiative_store.assign(
|
|
173
|
+
init_id,
|
|
174
|
+
agent.agent_id,
|
|
175
|
+
agent_name=agent.name,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if assigned:
|
|
179
|
+
# Update agent assignment count
|
|
180
|
+
self._agent_store.increment_assignments(agent.agent_id)
|
|
181
|
+
logger.info(
|
|
182
|
+
"Assigned initiative %s to agent %s (%s)",
|
|
183
|
+
init_id,
|
|
184
|
+
agent.agent_id,
|
|
185
|
+
agent.name,
|
|
186
|
+
)
|
|
187
|
+
return agent
|
|
188
|
+
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
async def auto_assign_pending(self, limit: int = 50) -> int:
|
|
192
|
+
"""Auto-assign all pending initiatives.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
limit: Maximum initiatives to assign
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Number of initiatives assigned
|
|
199
|
+
"""
|
|
200
|
+
pending = self._initiative_store.list(
|
|
201
|
+
status=["pending"],
|
|
202
|
+
limit=limit,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assigned = 0
|
|
206
|
+
for initiative in pending:
|
|
207
|
+
agent = await self.assign(initiative)
|
|
208
|
+
if agent:
|
|
209
|
+
assigned += 1
|
|
210
|
+
|
|
211
|
+
return assigned
|
|
212
|
+
|
|
213
|
+
def _get_initiative_type(self, initiative: Any) -> str:
|
|
214
|
+
"""Get initiative type from object or dict."""
|
|
215
|
+
if hasattr(initiative, "type"):
|
|
216
|
+
return initiative.type.value if hasattr(initiative.type, "value") else str(initiative.type)
|
|
217
|
+
if isinstance(initiative, dict):
|
|
218
|
+
return initiative.get("type", "unknown")
|
|
219
|
+
return "unknown"
|
|
220
|
+
|
|
221
|
+
def _get_initiative_id(self, initiative: Any) -> Optional[str]:
|
|
222
|
+
"""Get initiative ID from object or dict."""
|
|
223
|
+
if hasattr(initiative, "id"):
|
|
224
|
+
return initiative.id
|
|
225
|
+
if isinstance(initiative, dict):
|
|
226
|
+
return initiative.get("id")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _get_preferred_agent_id(self, initiative: Any) -> Optional[str]:
|
|
230
|
+
"""Get preferred agent ID from initiative."""
|
|
231
|
+
if hasattr(initiative, "preferred_agent_id"):
|
|
232
|
+
return initiative.preferred_agent_id
|
|
233
|
+
if isinstance(initiative, dict):
|
|
234
|
+
return initiative.get("preferred_agent_id")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _has_capacity(self, agent: Any) -> bool:
|
|
238
|
+
"""Check if agent has capacity for more assignments."""
|
|
239
|
+
# Check max concurrent
|
|
240
|
+
if agent.current_assignments >= agent.max_concurrent:
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
# Check hourly rate limit. The agents table carries a
|
|
244
|
+
# max_initiatives_per_hour column (default 10) that previously had no
|
|
245
|
+
# enforcement — counting `assigned` rows from the assignment_history
|
|
246
|
+
# within the last hour closes that gap. A value of 0 disables the
|
|
247
|
+
# limit; <0 is treated as 0 (off).
|
|
248
|
+
max_per_hour = getattr(agent, "max_initiatives_per_hour", 0) or 0
|
|
249
|
+
if max_per_hour > 0:
|
|
250
|
+
try:
|
|
251
|
+
since = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
252
|
+
recent = self._initiative_store.count_agent_assignments_since(
|
|
253
|
+
agent.agent_id, since
|
|
254
|
+
)
|
|
255
|
+
if recent >= max_per_hour:
|
|
256
|
+
return False
|
|
257
|
+
except (AttributeError, TypeError):
|
|
258
|
+
# Store does not expose the counter (older deployments or
|
|
259
|
+
# unit-test doubles) — fall back to the max_concurrent gate
|
|
260
|
+
# rather than blocking all assignments.
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
return True
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Read a deployment-selected backup attempt receipt, without running a backup.
|
|
2
|
+
|
|
3
|
+
The producer owns capture and publication. A receipt reports capture completion,
|
|
4
|
+
not archive verification, restore readiness, or coverage of other destinations.
|
|
5
|
+
"""
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MAX_RECEIPT_BYTES = 65536
|
|
14
|
+
MAX_AGE_DAYS = 7 # Preserve the existing operational backup review interval.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read(path):
|
|
18
|
+
with path.open('rb') as stream:
|
|
19
|
+
raw = stream.read(MAX_RECEIPT_BYTES + 1)
|
|
20
|
+
if len(raw) > MAX_RECEIPT_BYTES:
|
|
21
|
+
raise ValueError('receipt exceeds bound')
|
|
22
|
+
return raw, json.loads(raw)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _time(value, now):
|
|
26
|
+
if not isinstance(value, str):
|
|
27
|
+
raise ValueError('missing timestamp')
|
|
28
|
+
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
29
|
+
if parsed.tzinfo is None or parsed > now:
|
|
30
|
+
raise ValueError('timestamp is naive or in the future')
|
|
31
|
+
return parsed.astimezone(timezone.utc)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def backup_review_task(configured_path: str, now: datetime):
|
|
35
|
+
"""Return a scoped review or None for a recent, bound successful receipt.
|
|
36
|
+
|
|
37
|
+
Contract v1: schema_version, status, completed_at, captured_at, receipt_path,
|
|
38
|
+
receipt_sha256. A captured attempt references an immutable local receipt by
|
|
39
|
+
relative path and SHA256; a failed attempt has null capture/reference fields.
|
|
40
|
+
Only the receipt bytes are verified here, never archive bytes or restoration.
|
|
41
|
+
"""
|
|
42
|
+
path = Path(configured_path).expanduser().absolute()
|
|
43
|
+
task = {
|
|
44
|
+
'entity_id': 'database_backup', 'entity_type': 'backup',
|
|
45
|
+
'evidence_scope': 'configured_backup_receipt', 'evidence_path': str(path),
|
|
46
|
+
'observed_at': now.isoformat(), 'receipt_status': 'unavailable',
|
|
47
|
+
'captured_at': None, 'completed_at': None,
|
|
48
|
+
'receipt_path': None, 'receipt_sha256': None,
|
|
49
|
+
'description': 'Review configured backup evidence: latest attempt receipt is unavailable; capture freshness and recovery coverage are unknown',
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
_, value = _read(path)
|
|
53
|
+
required = {'schema_version', 'status', 'completed_at', 'captured_at', 'receipt_path', 'receipt_sha256'}
|
|
54
|
+
if (not isinstance(value, dict) or not required.issubset(value)
|
|
55
|
+
or value.get('schema_version') != 1):
|
|
56
|
+
raise ValueError('unsupported receipt schema')
|
|
57
|
+
status = value.get('status')
|
|
58
|
+
completed = _time(value.get('completed_at'), now)
|
|
59
|
+
if status == 'captured':
|
|
60
|
+
captured = _time(value.get('captured_at'), now)
|
|
61
|
+
if captured > completed:
|
|
62
|
+
raise ValueError('capture follows completion')
|
|
63
|
+
relative = value.get('receipt_path')
|
|
64
|
+
digest = value.get('receipt_sha256')
|
|
65
|
+
if (not isinstance(relative, str) or not relative
|
|
66
|
+
or Path(relative).is_absolute()
|
|
67
|
+
or not isinstance(digest, str)
|
|
68
|
+
or not re.fullmatch('[0-9a-f]{64}', digest)):
|
|
69
|
+
raise ValueError('missing receipt reference')
|
|
70
|
+
receipt_path = (path.parent / relative).resolve()
|
|
71
|
+
if not receipt_path.is_relative_to(path.parent.resolve()):
|
|
72
|
+
raise ValueError('receipt is outside configured destination')
|
|
73
|
+
raw, receipt = _read(receipt_path)
|
|
74
|
+
if (hashlib.sha256(raw).hexdigest() != digest
|
|
75
|
+
or not isinstance(receipt, dict)
|
|
76
|
+
or receipt.get('status') != 'captured'
|
|
77
|
+
or _time(receipt.get('at'), now) != captured):
|
|
78
|
+
raise ValueError('capture receipt binding mismatch')
|
|
79
|
+
age = (now - captured).total_seconds() / 86400
|
|
80
|
+
if age <= MAX_AGE_DAYS:
|
|
81
|
+
return None
|
|
82
|
+
task.update(
|
|
83
|
+
captured_at=captured.isoformat(), receipt_path=str(receipt_path),
|
|
84
|
+
receipt_sha256=digest, age_days=age,
|
|
85
|
+
description=f'Review configured backup capture: latest completed capture is {age:.0f} days old; recovery readiness is unverified',
|
|
86
|
+
)
|
|
87
|
+
elif status == 'failed':
|
|
88
|
+
if any(value.get(key) is not None for key in ('captured_at', 'receipt_path', 'receipt_sha256')):
|
|
89
|
+
raise ValueError('failed attempt claims completed capture')
|
|
90
|
+
task['description'] = 'Review configured backup capture: latest attempt failed; earlier captures and recovery readiness require inspection'
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError('unsupported attempt status')
|
|
93
|
+
task.update(receipt_status=status, completed_at=completed.isoformat())
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
task['receipt_unavailable_reason'] = 'missing'
|
|
96
|
+
except OSError:
|
|
97
|
+
task['receipt_unavailable_reason'] = 'unreadable'
|
|
98
|
+
except (ValueError, TypeError, OverflowError):
|
|
99
|
+
task['receipt_unavailable_reason'] = 'invalid'
|
|
100
|
+
return task
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Context durability and freshness rules per initiative type (v0.16.0).
|
|
2
|
+
|
|
3
|
+
Context snapshots age differently per domain. ``days_since_contact`` is
|
|
4
|
+
still true hours later; ``"CI failing on main"`` or ``"meeting in 30
|
|
5
|
+
min"`` can become false while the initiative sits in the queue. Each
|
|
6
|
+
initiative type therefore declares its context durability:
|
|
7
|
+
|
|
8
|
+
- ``durable`` — snapshot at creation, safe to persist and read later.
|
|
9
|
+
- ``volatile`` — must be refreshed at read time, or checked against a
|
|
10
|
+
freshness TTL before the agent acts on it.
|
|
11
|
+
|
|
12
|
+
The agent-side contract: a volatile initiative's context carries
|
|
13
|
+
``context_captured_at``. Observation-backed proposals retain the actual source
|
|
14
|
+
observation time rather than renewing it when a stored row is read. Before acting,
|
|
15
|
+
the agent checks ``is_context_fresh()``; if stale, it calls
|
|
16
|
+
``POST /v1/host/initiatives/{id}/context/refresh`` which routes to the
|
|
17
|
+
engine's per-entity ``rebuild_context()``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Dict, Optional
|
|
24
|
+
|
|
25
|
+
DURABLE = "durable"
|
|
26
|
+
VOLATILE = "volatile"
|
|
27
|
+
|
|
28
|
+
# Durability by InitiativeType value. Unlisted types default to durable
|
|
29
|
+
# with a conservative short TTL applied by callers that care.
|
|
30
|
+
CONTEXT_DURABILITY: Dict[str, str] = {
|
|
31
|
+
# Existing types
|
|
32
|
+
"follow_up": DURABLE,
|
|
33
|
+
"relationship": DURABLE,
|
|
34
|
+
"introduction": DURABLE,
|
|
35
|
+
"health": VOLATILE,
|
|
36
|
+
"scheduling": DURABLE,
|
|
37
|
+
"coding": VOLATILE,
|
|
38
|
+
"subsystem_health": VOLATILE,
|
|
39
|
+
"data_quality": VOLATILE,
|
|
40
|
+
"operational": DURABLE,
|
|
41
|
+
"capability_gap": DURABLE,
|
|
42
|
+
"knowledge_acquisition": DURABLE,
|
|
43
|
+
"behavioral_correction": DURABLE,
|
|
44
|
+
"agent_action": VOLATILE,
|
|
45
|
+
# Autonomous work domains (v0.16.0)
|
|
46
|
+
"commitment": DURABLE,
|
|
47
|
+
"calendar": VOLATILE,
|
|
48
|
+
"research": DURABLE,
|
|
49
|
+
"task": DURABLE,
|
|
50
|
+
"project": DURABLE,
|
|
51
|
+
"system": VOLATILE,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Freshness TTL (seconds) for volatile types: how old a context snapshot
|
|
55
|
+
# may be before the agent must refresh it.
|
|
56
|
+
CONTEXT_FRESHNESS_TTL_SECONDS: Dict[str, int] = {
|
|
57
|
+
"calendar": 300,
|
|
58
|
+
"coding": 600,
|
|
59
|
+
"system": 300,
|
|
60
|
+
"health": 900,
|
|
61
|
+
"subsystem_health": 600,
|
|
62
|
+
"data_quality": 3600,
|
|
63
|
+
"agent_action": 600,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_DEFAULT_VOLATILE_TTL = 600
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def durability_for(type_value: str) -> str:
|
|
70
|
+
"""Declared durability for an initiative type ('durable'/'volatile')."""
|
|
71
|
+
return CONTEXT_DURABILITY.get(type_value, DURABLE)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def freshness_ttl_for(type_value: str) -> Optional[int]:
|
|
75
|
+
"""TTL in seconds for volatile types; None for durable types."""
|
|
76
|
+
if durability_for(type_value) == DURABLE:
|
|
77
|
+
return None
|
|
78
|
+
return CONTEXT_FRESHNESS_TTL_SECONDS.get(type_value, _DEFAULT_VOLATILE_TTL)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_context_fresh(
|
|
82
|
+
type_value: str,
|
|
83
|
+
captured_at: Optional[str],
|
|
84
|
+
now: Optional[datetime] = None,
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""Whether a context snapshot is still safe to act on.
|
|
87
|
+
|
|
88
|
+
Durable contexts are always fresh. Volatile contexts without a
|
|
89
|
+
capture timestamp, or with a future timestamp, are treated as stale.
|
|
90
|
+
"""
|
|
91
|
+
ttl = freshness_ttl_for(type_value)
|
|
92
|
+
if ttl is None:
|
|
93
|
+
return True
|
|
94
|
+
if not captured_at:
|
|
95
|
+
return False
|
|
96
|
+
try:
|
|
97
|
+
stamp = datetime.fromisoformat(captured_at.replace("Z", "+00:00"))
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
return False
|
|
100
|
+
if stamp.tzinfo is None:
|
|
101
|
+
stamp = stamp.replace(tzinfo=timezone.utc)
|
|
102
|
+
now = now or datetime.now(timezone.utc)
|
|
103
|
+
return 0 <= (now - stamp).total_seconds() <= ttl
|