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,265 @@
|
|
|
1
|
+
"""Explicit work coordination, with existing participant/writer authority."""
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from contextlib import closing
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool
|
|
11
|
+
from pydantic import field_validator
|
|
12
|
+
|
|
13
|
+
from apsimo.api.authority import request_authority
|
|
14
|
+
from apsimo.api.routers.executions import authorized_viewer
|
|
15
|
+
from apsimo.commitments.work import CommitmentWork
|
|
16
|
+
from apsimo.commitments.local_work import LocalWork, LocalWorkConflict
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix='/v1/host/commitments', tags=['commitments'])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorkOperation(BaseModel):
|
|
22
|
+
model_config = ConfigDict(extra='forbid')
|
|
23
|
+
operation: Literal['claim', 'status', 'renew', 'release']
|
|
24
|
+
contact_id: str = Field(min_length=1, max_length=256)
|
|
25
|
+
session_id: str = Field(min_length=1, max_length=256)
|
|
26
|
+
task_id: str = Field(min_length=1, max_length=256)
|
|
27
|
+
turn_id: str = Field(min_length=1, max_length=256)
|
|
28
|
+
claim_id: str = Field(default='', pattern=r'^(?:[a-f0-9]{32})?$')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post('/{commitment_id}/work')
|
|
32
|
+
def operate(commitment_id: str, body: WorkOperation, request: Request):
|
|
33
|
+
person, _ = authorized_viewer(request, body.contact_id, scope='turns:write')
|
|
34
|
+
from apsimo.api.routers import host
|
|
35
|
+
if host._commitment_store is None:
|
|
36
|
+
raise HTTPException(503, detail='commitment store unavailable')
|
|
37
|
+
try:
|
|
38
|
+
return CommitmentWork(host._commitment_store).operate(commitment_id,
|
|
39
|
+
**body.model_dump(exclude={'contact_id'}), contact_id=person,
|
|
40
|
+
principal_id=request_authority(request).principal_id)
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise HTTPException(404, detail='unknown commitment') from None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LocalDraftHandoff(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra='forbid')
|
|
47
|
+
session_id: str = Field(min_length=1, max_length=256)
|
|
48
|
+
task_id: str = Field(min_length=1, max_length=256)
|
|
49
|
+
turn_id: str = Field(min_length=1, max_length=256)
|
|
50
|
+
claim_id: str = Field(pattern=r'^[a-f0-9]{32}$')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LocalDraftAcceptance(BaseModel):
|
|
54
|
+
model_config = ConfigDict(extra='forbid')
|
|
55
|
+
contact_id: str = Field(min_length=1, max_length=256)
|
|
56
|
+
session_id: str = Field(min_length=1, max_length=256)
|
|
57
|
+
turn_id: str = Field(min_length=1, max_length=256)
|
|
58
|
+
question: str = Field(min_length=1, max_length=2000)
|
|
59
|
+
sources: list[str] = Field(min_length=1, max_length=8)
|
|
60
|
+
origin: 'NativeDraftOrigin | None' = None
|
|
61
|
+
new_draft: StrictBool = False
|
|
62
|
+
handoff: LocalDraftHandoff | None = None
|
|
63
|
+
|
|
64
|
+
@field_validator('sources')
|
|
65
|
+
@classmethod
|
|
66
|
+
def explicit_paths(cls, value):
|
|
67
|
+
if (len(set(value)) != len(value)
|
|
68
|
+
or any(not Path(path).is_absolute() or len(path) > 4096 or '\x00' in path for path in value)):
|
|
69
|
+
raise ValueError('Distinct absolute local source paths are required')
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NativeDraftOrigin(BaseModel):
|
|
74
|
+
model_config = ConfigDict(extra='forbid')
|
|
75
|
+
platform: str = Field(min_length=1, max_length=64, pattern=r'^[a-z0-9_.-]+$')
|
|
76
|
+
chat_id: str = Field(min_length=1, max_length=512)
|
|
77
|
+
thread_id: str | None = Field(default=None, max_length=512)
|
|
78
|
+
user_id: str | None = Field(default=None, max_length=512)
|
|
79
|
+
user_id_alt: str | None = Field(default=None, max_length=512)
|
|
80
|
+
chat_type: str | None = Field(default=None, max_length=64)
|
|
81
|
+
notifier_profile: str | None = Field(default=None, max_length=256)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
LocalDraftAcceptance.model_rebuild()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NativeWorkRun(BaseModel):
|
|
88
|
+
model_config = ConfigDict(extra='forbid')
|
|
89
|
+
contact_id: str = Field(min_length=1, max_length=256)
|
|
90
|
+
native_job_id: str = Field(min_length=1, max_length=128)
|
|
91
|
+
native_execution_id: str = Field(pattern=r'^[a-f0-9]{32}$')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class LocalDraftResult(BaseModel):
|
|
95
|
+
model_config = ConfigDict(extra='forbid')
|
|
96
|
+
status: Literal['draft_created', 'unavailable']
|
|
97
|
+
summary: str = Field(default='', max_length=1600)
|
|
98
|
+
report_path: str = Field(default='', max_length=4096)
|
|
99
|
+
report_sha256: str = Field(default='', pattern=r'^(?:[a-f0-9]{64})?$')
|
|
100
|
+
sources: dict[str, str] = Field(default_factory=dict, max_length=8)
|
|
101
|
+
model: str = Field(default='', max_length=256)
|
|
102
|
+
error_type: str = Field(default='', max_length=128, pattern=r'^[A-Za-z0-9_]*$')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class NativeWorkFinish(NativeWorkRun):
|
|
106
|
+
result: LocalDraftResult
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class NativeTaskBinding(BaseModel):
|
|
110
|
+
model_config = ConfigDict(extra='forbid')
|
|
111
|
+
contact_id: str = Field(min_length=1, max_length=256)
|
|
112
|
+
native_board: str = Field(pattern=r'^[a-z0-9][a-z0-9_-]{0,63}$')
|
|
113
|
+
native_task_id: str = Field(min_length=1, max_length=128)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class NativeKanbanRun(NativeTaskBinding):
|
|
117
|
+
native_run_id: int = Field(ge=1)
|
|
118
|
+
native_claim_lock: str = Field(min_length=1, max_length=256)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class NativeKanbanFinish(NativeKanbanRun):
|
|
122
|
+
result: LocalDraftResult
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def accepted_native(body, initiative_id, person):
|
|
126
|
+
from apsimo.turns.hermes_kanban import task_snapshot
|
|
127
|
+
try:
|
|
128
|
+
# An accepted transition can wait briefly for a native writer, including
|
|
129
|
+
# Hermes' DELETE-journal fallback. Ordinary work projections stay fast.
|
|
130
|
+
return task_snapshot(initiative_id, person,
|
|
131
|
+
body.model_dump(exclude={'contact_id', 'result'}), read_timeout=2.)
|
|
132
|
+
except (OSError, sqlite3.Error):
|
|
133
|
+
raise HTTPException(503, detail='native_board_unavailable') from None
|
|
134
|
+
except (ValueError, KeyError, TypeError) as error:
|
|
135
|
+
raise HTTPException(409, detail=str(error)) from None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def local_store(request, contact_id, *, enabled=False):
|
|
139
|
+
person, owner = authorized_viewer(request, contact_id, scope='turns:write')
|
|
140
|
+
if not owner:
|
|
141
|
+
raise HTTPException(403, detail='owner_local_work_required')
|
|
142
|
+
if enabled and os.environ.get('COLONY_LOCAL_WORK_ENABLED', '').lower() != 'true':
|
|
143
|
+
raise HTTPException(503, detail='local_work_not_enabled')
|
|
144
|
+
from apsimo.api.routers import host
|
|
145
|
+
if host._initiative_store is None or host._commitment_store is None:
|
|
146
|
+
raise HTTPException(503, detail='local_work_store_unavailable')
|
|
147
|
+
return LocalWork(host._initiative_store, host._commitment_store), person
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def native_run(body, *, finishing=False):
|
|
151
|
+
from apsimo.turns.hermes_work import selected_home
|
|
152
|
+
home = selected_home()
|
|
153
|
+
if not home or body.native_job_id != os.environ.get('COLONY_LOCAL_WORK_JOB_ID'):
|
|
154
|
+
raise HTTPException(409, detail='selected_local_work_job_required')
|
|
155
|
+
home_id = hashlib.sha256(str(home).encode()).hexdigest()
|
|
156
|
+
def terminal(context):
|
|
157
|
+
if context.get('source_home_id') != home_id or context.get('native_job_id') != body.native_job_id:
|
|
158
|
+
return None
|
|
159
|
+
with closing(sqlite3.connect((home/'cron/executions.db').as_uri()+'?mode=ro', uri=True, timeout=.2)) as db:
|
|
160
|
+
row = db.execute('SELECT status FROM executions WHERE id=? AND job_id=?',
|
|
161
|
+
(context.get('native_execution_id'), body.native_job_id)).fetchone()
|
|
162
|
+
return row[0] if row else None
|
|
163
|
+
native = {'source_home_id': home_id, 'native_job_id': body.native_job_id,
|
|
164
|
+
'native_execution_id': body.native_execution_id}
|
|
165
|
+
try:
|
|
166
|
+
status = terminal(native)
|
|
167
|
+
except (OSError, sqlite3.Error):
|
|
168
|
+
raise HTTPException(503, detail='native_execution_ledger_unavailable') from None
|
|
169
|
+
if status not in ({'running', 'completed', 'failed'} if finishing else {'running'}):
|
|
170
|
+
raise HTTPException(409, detail='running_native_execution_required')
|
|
171
|
+
return native, terminal
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.post('/{commitment_id}/local-draft')
|
|
175
|
+
def accept_local_draft(commitment_id: str, body: LocalDraftAcceptance, request: Request):
|
|
176
|
+
return _accept_local_draft(commitment_id, body, request)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@router.post('/local-draft')
|
|
180
|
+
def accept_standalone_local_draft(body: LocalDraftAcceptance, request: Request):
|
|
181
|
+
return _accept_local_draft(None, body, request)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _accept_local_draft(commitment_id, body, request):
|
|
185
|
+
store, person = local_store(request, body.contact_id, enabled=True)
|
|
186
|
+
backend = os.environ.get('COLONY_LOCAL_WORK_EXECUTOR', 'cron')
|
|
187
|
+
if backend not in {'cron', 'kanban'}:
|
|
188
|
+
raise HTTPException(503, detail='local_work_executor_unavailable')
|
|
189
|
+
try:
|
|
190
|
+
return store.accept(commitment_id, **body.model_dump(exclude={'contact_id'}),
|
|
191
|
+
contact_id=person, principal_id=request_authority(request).principal_id,
|
|
192
|
+
execution_backend=backend)
|
|
193
|
+
except KeyError:
|
|
194
|
+
raise HTTPException(404, detail='unknown_commitment') from None
|
|
195
|
+
except LocalWorkConflict as error:
|
|
196
|
+
raise HTTPException(409, detail={'reason': str(error), 'initiative_id': error.initiative_id}) from None
|
|
197
|
+
except ValueError as error:
|
|
198
|
+
raise HTTPException(409, detail=str(error)) from None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.post('/local-work/next')
|
|
202
|
+
def next_local_draft(body: NativeWorkRun, request: Request):
|
|
203
|
+
store, person = local_store(request, body.contact_id, enabled=True)
|
|
204
|
+
native, terminal = native_run(body)
|
|
205
|
+
return {'assignment': store.select(person, native, terminal)}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.get('/local-work/pending')
|
|
209
|
+
def pending_native_drafts(contact_id: str, request: Request):
|
|
210
|
+
store, person = local_store(request, contact_id, enabled=True)
|
|
211
|
+
if os.environ.get('COLONY_LOCAL_WORK_EXECUTOR', 'cron') != 'kanban':
|
|
212
|
+
raise HTTPException(409, detail='native_execution_backend_required')
|
|
213
|
+
return store.native_pending(person)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@router.post('/local-work/{initiative_id}/native-task')
|
|
217
|
+
def attach_native_draft(initiative_id: str, body: NativeTaskBinding, request: Request):
|
|
218
|
+
store, person = local_store(request, body.contact_id, enabled=True)
|
|
219
|
+
native, _ = accepted_native(body, initiative_id, person)
|
|
220
|
+
try:
|
|
221
|
+
return store.attach_native_task(initiative_id, person, native)
|
|
222
|
+
except KeyError:
|
|
223
|
+
raise HTTPException(404, detail='unknown_local_work') from None
|
|
224
|
+
except ValueError as error:
|
|
225
|
+
raise HTTPException(409, detail=str(error)) from None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@router.post('/local-work/{initiative_id}/native-run')
|
|
229
|
+
def bind_native_draft(initiative_id: str, body: NativeKanbanRun, request: Request):
|
|
230
|
+
store, person = local_store(request, body.contact_id, enabled=True)
|
|
231
|
+
native, state = accepted_native(body, initiative_id, person)
|
|
232
|
+
try:
|
|
233
|
+
return store.bind_native_run(initiative_id, person, native, attempt_count=state['attempt_count'])
|
|
234
|
+
except KeyError:
|
|
235
|
+
raise HTTPException(404, detail='unknown_local_work') from None
|
|
236
|
+
except ValueError as error:
|
|
237
|
+
raise HTTPException(409, detail=str(error)) from None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@router.get('/local-work/{initiative_id}')
|
|
241
|
+
def local_draft_status(initiative_id: str, contact_id: str, request: Request):
|
|
242
|
+
store, person = local_store(request, contact_id)
|
|
243
|
+
try:
|
|
244
|
+
return store.status(initiative_id, person)
|
|
245
|
+
except KeyError:
|
|
246
|
+
raise HTTPException(404, detail='unknown_local_work') from None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@router.post('/local-work/{initiative_id}/finish')
|
|
250
|
+
def finish_local_draft(initiative_id: str, body: NativeWorkFinish | NativeKanbanFinish, request: Request):
|
|
251
|
+
store, person = local_store(request, body.contact_id)
|
|
252
|
+
native, _ = (accepted_native(body, initiative_id, person) if isinstance(body, NativeKanbanFinish)
|
|
253
|
+
else native_run(body, finishing=True))
|
|
254
|
+
result = body.result.model_dump()
|
|
255
|
+
if result['status'] == 'draft_created':
|
|
256
|
+
if not result['report_sha256'] or not Path(result['report_path']).is_absolute() or not result['sources']:
|
|
257
|
+
raise HTTPException(422, detail='local_report_receipt_required')
|
|
258
|
+
elif not result['error_type']:
|
|
259
|
+
raise HTTPException(422, detail='failure_reason_required')
|
|
260
|
+
try:
|
|
261
|
+
return store.finish(initiative_id, person, native, result)
|
|
262
|
+
except KeyError:
|
|
263
|
+
raise HTTPException(404, detail='unknown_local_work') from None
|
|
264
|
+
except ValueError as error:
|
|
265
|
+
raise HTTPException(409, detail=str(error)) from None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Context-gate API: budget-aware context preparation for any agent.
|
|
2
|
+
|
|
3
|
+
POST /v1/context/prepare — decide whether content fits a model's useful
|
|
4
|
+
context window whole, and if not, chunk + retrieve (query-focused) or
|
|
5
|
+
coverage-sample (holistic) down to budget. See
|
|
6
|
+
:mod:`apsimo.contextgate` for the underlying machinery.
|
|
7
|
+
|
|
8
|
+
Auth rides the global ApiKeyMiddleware like every other router.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, HTTPException
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from apsimo.contextgate import GateConfig, prepare_context
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/v1/context", tags=["context"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DocumentIn(BaseModel):
|
|
27
|
+
name: str = ""
|
|
28
|
+
content: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PrepareRequest(BaseModel):
|
|
32
|
+
content: Optional[str] = Field(
|
|
33
|
+
default=None, description="Raw text to prepare (or use `documents`)."
|
|
34
|
+
)
|
|
35
|
+
documents: Optional[List[DocumentIn]] = Field(
|
|
36
|
+
default=None, description="Named documents; concatenated with headers."
|
|
37
|
+
)
|
|
38
|
+
query: str = Field(
|
|
39
|
+
default="", description="The task/question the context will serve."
|
|
40
|
+
)
|
|
41
|
+
budget_tokens: int = Field(
|
|
42
|
+
default=0,
|
|
43
|
+
ge=0,
|
|
44
|
+
description=(
|
|
45
|
+
"Token budget. 0 = derive from `model_tier`'s usefulContextTokens, "
|
|
46
|
+
"else the COLONY_CONTEXT_GATE_BUDGET default."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
model_tier: str = Field(
|
|
50
|
+
default="",
|
|
51
|
+
description="small|medium|large — derive budget from this tier's config.",
|
|
52
|
+
)
|
|
53
|
+
task_kind: Optional[str] = Field(
|
|
54
|
+
default=None, description="retrieval | holistic (overrides the heuristic)."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PrepareResponse(BaseModel):
|
|
59
|
+
text: str
|
|
60
|
+
decision: str
|
|
61
|
+
task_kind: str
|
|
62
|
+
est_tokens_in: int
|
|
63
|
+
est_tokens_out: int
|
|
64
|
+
budget_tokens: int
|
|
65
|
+
chunks_total: int
|
|
66
|
+
chunks_used: int
|
|
67
|
+
coverage: float
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _tier_budget(tier_name: str) -> int:
|
|
71
|
+
"""Budget from the live LLMRouter's tier config (0 when unknown)."""
|
|
72
|
+
try:
|
|
73
|
+
from apsimo.api.routers.host import get_llm_router
|
|
74
|
+
from apsimo.router.tiers import ModelTier
|
|
75
|
+
|
|
76
|
+
llm_router = get_llm_router()
|
|
77
|
+
if llm_router is None:
|
|
78
|
+
return 0
|
|
79
|
+
cfg = llm_router.tier_config(ModelTier(tier_name))
|
|
80
|
+
return cfg.useful_context_tokens if cfg else 0
|
|
81
|
+
except (ValueError, ImportError):
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.post("/prepare", response_model=PrepareResponse)
|
|
86
|
+
async def prepare(body: PrepareRequest) -> PrepareResponse:
|
|
87
|
+
"""Prepare content to fit a model's useful context window."""
|
|
88
|
+
if body.content is None and not body.documents:
|
|
89
|
+
raise HTTPException(status_code=422, detail="Provide `content` or `documents`.")
|
|
90
|
+
|
|
91
|
+
if body.documents:
|
|
92
|
+
parts = []
|
|
93
|
+
for i, doc in enumerate(body.documents):
|
|
94
|
+
name = doc.name or f"document {i + 1}"
|
|
95
|
+
parts.append(f"# {name}\n\n{doc.content}")
|
|
96
|
+
content = "\n\n".join(parts)
|
|
97
|
+
if body.content:
|
|
98
|
+
content = body.content + "\n\n" + content
|
|
99
|
+
else:
|
|
100
|
+
content = body.content or ""
|
|
101
|
+
|
|
102
|
+
budget = body.budget_tokens
|
|
103
|
+
if budget <= 0 and body.model_tier:
|
|
104
|
+
budget = _tier_budget(body.model_tier.strip().lower())
|
|
105
|
+
|
|
106
|
+
prepared = await prepare_context(
|
|
107
|
+
content=content,
|
|
108
|
+
query=body.query,
|
|
109
|
+
budget_tokens=budget,
|
|
110
|
+
task_kind=body.task_kind,
|
|
111
|
+
config=GateConfig.from_env(),
|
|
112
|
+
)
|
|
113
|
+
return PrepareResponse(
|
|
114
|
+
text=prepared.text,
|
|
115
|
+
decision=prepared.decision.value,
|
|
116
|
+
task_kind=prepared.task_kind,
|
|
117
|
+
est_tokens_in=prepared.est_tokens_in,
|
|
118
|
+
est_tokens_out=prepared.est_tokens_out,
|
|
119
|
+
budget_tokens=prepared.budget_tokens,
|
|
120
|
+
chunks_total=prepared.chunks_total,
|
|
121
|
+
chunks_used=prepared.chunks_used,
|
|
122
|
+
coverage=prepared.coverage,
|
|
123
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Execution observations reuse turn-writer and context-reader authority."""
|
|
2
|
+
import os
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from apsimo.api.authority import request_authority, resolve_request_person
|
|
9
|
+
from apsimo.turns.executions import registry
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/v1/host/executions", tags=["executions"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExecutionRuntimeObservation(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="forbid")
|
|
16
|
+
event: Literal['start', 'response', 'error']
|
|
17
|
+
request_id: str = Field(default='', max_length=256, pattern=r'^[^\x00-\x1f]*$')
|
|
18
|
+
requested_model: str = Field(default='', max_length=256, pattern=r'^[^\x00-\x1f]*$')
|
|
19
|
+
provider: str = Field(default='', max_length=256, pattern=r'^[^\x00-\x1f]*$')
|
|
20
|
+
response_model: str = Field(default='', max_length=256, pattern=r'^[^\x00-\x1f]*$')
|
|
21
|
+
api_mode: str = Field(default='', max_length=256, pattern=r'^[^\x00-\x1f]*$')
|
|
22
|
+
profile_id: str = Field(default='', pattern=r'^(?:[a-f0-9]{64})?$')
|
|
23
|
+
runtime_kind: Literal['turn', 'cron', 'kanban_worker', 'delegated_child', 'unknown'] = 'unknown'
|
|
24
|
+
approx_input_tokens: int | None = Field(default=None, ge=0, le=2147483647)
|
|
25
|
+
max_tokens: int | None = Field(default=None, ge=0, le=2147483647)
|
|
26
|
+
output_limit_kind: Literal['request', 'provider_default', 'unknown'] = 'unknown'
|
|
27
|
+
tool_count: int | None = Field(default=None, ge=0, le=2147483647)
|
|
28
|
+
api_call_count: int | None = Field(default=None, ge=0, le=2147483647)
|
|
29
|
+
retry_count: int | None = Field(default=None, ge=0, le=2147483647)
|
|
30
|
+
started_at: float | None = Field(default=None, gt=0, allow_inf_nan=False)
|
|
31
|
+
ended_at: float | None = Field(default=None, gt=0, allow_inf_nan=False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExecutionInputReference(BaseModel):
|
|
35
|
+
model_config = ConfigDict(extra="forbid")
|
|
36
|
+
source_id: str = Field(min_length=1, max_length=256)
|
|
37
|
+
input_message_hash: str = Field(pattern=r'^[a-f0-9]{64}$')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExecutionObservation(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="forbid")
|
|
42
|
+
execution_id: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
43
|
+
contact_id: str = Field(min_length=1, max_length=256)
|
|
44
|
+
session_id: str = Field(min_length=1, max_length=256, pattern=r"^[^\x00-\x1f]+$")
|
|
45
|
+
turn_id: str = Field(min_length=1, max_length=256)
|
|
46
|
+
parent_execution_id: str = Field(default="", pattern=r"^(?:[a-f0-9]{64})?$")
|
|
47
|
+
platform: str = Field(min_length=1, max_length=64, pattern=r"^[a-z0-9_.-]+$")
|
|
48
|
+
state: Literal["observed", "completed", "failed", "interrupted", "ended"] = "observed"
|
|
49
|
+
phase: Literal["turn", "model", "tool", "between_calls", "ended"] = "turn"
|
|
50
|
+
tool_name: str = Field(default="", max_length=128, pattern=r"^[a-zA-Z0-9_.:-]*$")
|
|
51
|
+
sequence: int = Field(ge=1, le=2147483647)
|
|
52
|
+
runtime: ExecutionRuntimeObservation | None = None
|
|
53
|
+
# Exact already-admitted human inputs, never native task wrappers or titles.
|
|
54
|
+
input_refs: list[ExecutionInputReference] | None = Field(default=None, min_length=1, max_length=64)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def authorized_viewer(request: Request, contact_id: str, *, scope: str) -> tuple[str, bool]:
|
|
58
|
+
authority = request_authority(request)
|
|
59
|
+
# Legacy body-selected identity is deliberately not sufficient for this new
|
|
60
|
+
# shared surface. Use the existing exact person grants, never an owner flag.
|
|
61
|
+
if not authority.authenticated or authority.anonymous or authority.legacy or not authority.has_scope(scope):
|
|
62
|
+
raise HTTPException(403, detail={"code": "scoped_execution_authority_required"})
|
|
63
|
+
person = resolve_request_person(request, claimed_person_id=contact_id)
|
|
64
|
+
owner = os.environ.get("COLONY_OWNER_PERSON_ID", "").strip() or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
|
|
65
|
+
return person, bool(owner and person == owner)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/observe")
|
|
69
|
+
def observe(body: ExecutionObservation, request: Request):
|
|
70
|
+
person, _ = authorized_viewer(request, body.contact_id, scope="turns:write")
|
|
71
|
+
try:
|
|
72
|
+
return registry().observe(body.model_dump(), principal_id=request_authority(request).principal_id, contact_id=person)
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise HTTPException(409, detail={"code": str(exc)}) from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get("")
|
|
78
|
+
async def active(request: Request, contact_id: str, session_id: str = "", limit: int = Query(20, ge=1, le=100),
|
|
79
|
+
projection: Literal['full', 'request'] = 'full', input_context: bool = False):
|
|
80
|
+
person, owner = authorized_viewer(request, contact_id, scope="context:read")
|
|
81
|
+
if projection == 'request' and not owner:
|
|
82
|
+
raise HTTPException(403, detail='owner_work_context_required')
|
|
83
|
+
if projection == 'request':
|
|
84
|
+
limit = min(limit, 8)
|
|
85
|
+
view = registry().view(contact_id=person, owner=owner, session_id=session_id, limit=limit,
|
|
86
|
+
include_ancestors=projection == 'request',
|
|
87
|
+
include_inputs=projection == 'full' or input_context)
|
|
88
|
+
view = await with_queue_work(view, owner=owner, limit=limit)
|
|
89
|
+
if projection == 'request':
|
|
90
|
+
from apsimo.turns.executions import request_work_context
|
|
91
|
+
return request_work_context(view, limit=limit, session_id=session_id)
|
|
92
|
+
return view
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def with_queue_work(view, *, owner, limit=8):
|
|
96
|
+
if not owner:
|
|
97
|
+
return view
|
|
98
|
+
import asyncio
|
|
99
|
+
from apsimo.turns.hermes_work import cron_view
|
|
100
|
+
from apsimo.turns.local_work import local_work_view
|
|
101
|
+
from apsimo.turns.hermes_kanban import kanban_view
|
|
102
|
+
from apsimo.turns.reported_workers import reported_worker_view
|
|
103
|
+
from apsimo.api.routers import host
|
|
104
|
+
from apsimo.turns.executions import work_source_coverage
|
|
105
|
+
import time
|
|
106
|
+
|
|
107
|
+
queue = getattr(host._task_queue, 'queue', host._task_queue)
|
|
108
|
+
|
|
109
|
+
async def read(reader, *, asynchronous=False, **reader_kwargs):
|
|
110
|
+
# These are independent read-only snapshots. One unavailable ledger
|
|
111
|
+
# must not hide work observed by all the other readers. Cancellation of
|
|
112
|
+
# a thread await does not stop its read; native readers bound SQLite work.
|
|
113
|
+
try:
|
|
114
|
+
operation = reader(limit=limit, **reader_kwargs) if asynchronous else asyncio.to_thread(reader, limit=limit, **reader_kwargs)
|
|
115
|
+
value = await asyncio.wait_for(operation, timeout=.2)
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
return {**value, 'observed_at': time.time()}
|
|
119
|
+
except Exception:
|
|
120
|
+
return {'items': [], 'recent': [], 'available': False, 'unavailable': True,
|
|
121
|
+
'reason': 'work_source_unavailable', 'observed_at': time.time()}
|
|
122
|
+
|
|
123
|
+
async def queue_view():
|
|
124
|
+
if queue is None or not callable(getattr(queue, 'current_work', None)):
|
|
125
|
+
return {'items': [], 'available': False, 'unavailable': True,
|
|
126
|
+
'reason': 'queue_not_attached', 'source': 'canonical_task_queue'}
|
|
127
|
+
return await read(queue.current_work, asynchronous=True)
|
|
128
|
+
|
|
129
|
+
# All sources share the existing request budget, rather than each taking
|
|
130
|
+
# another sequential budget after the native ledgers finish.
|
|
131
|
+
view['native_cron'], view['local_work'], view['native_kanban'], reported, view['worker_work'] = await asyncio.gather(
|
|
132
|
+
# Leave executor/return slack for the multi-board reader to retain
|
|
133
|
+
# faster boards when its own partial-read budget is exhausted.
|
|
134
|
+
read(cron_view), read(local_work_view), read(kanban_view, read_budget=.15),
|
|
135
|
+
read(reported_worker_view), queue_view())
|
|
136
|
+
if reported is not None:
|
|
137
|
+
view['reported_worker'] = reported
|
|
138
|
+
view['work_sources'] = work_source_coverage(view)
|
|
139
|
+
view['coverage'] = 'registered Hermes turns and selected work ledgers; see work_sources for read coverage'
|
|
140
|
+
return view
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Exact prepared task configuration; permission remains at the private outbox."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import closing
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from apsimo.api.authority import request_authority
|
|
12
|
+
from apsimo.api.routers import temporal_followups as temporal
|
|
13
|
+
from apsimo.commitments.work import CommitmentWork
|
|
14
|
+
from apsimo.initiatives.temporal_followup import TemporalFollowups
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix='/v1/host/temporal-followups', tags=['commitments'])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FollowupPlan(BaseModel):
|
|
20
|
+
model_config = ConfigDict(extra='forbid', allow_inf_nan=False)
|
|
21
|
+
wait_id: str = Field(min_length=1, max_length=128)
|
|
22
|
+
commitment_id: str = Field(min_length=1, max_length=128)
|
|
23
|
+
work_id: str = Field(min_length=1, max_length=128)
|
|
24
|
+
source_id: str = Field(min_length=1, max_length=256)
|
|
25
|
+
source_version: str = Field(pattern='^[a-f0-9]{64}$')
|
|
26
|
+
recipient_id: str = Field(min_length=1, max_length=128)
|
|
27
|
+
channel: Literal['whatsapp', 'sms', 'rcs']
|
|
28
|
+
purpose: str = Field(min_length=1, max_length=512)
|
|
29
|
+
expires_at: float = Field(gt=0)
|
|
30
|
+
max_followups: Literal[1]
|
|
31
|
+
message: str = Field(min_length=1, max_length=8000)
|
|
32
|
+
message_sha256: str = Field(pattern='^[a-f0-9]{64}$')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def digest(plan):
|
|
36
|
+
return hashlib.sha256(json.dumps(plan, sort_keys=True, separators=(',', ':'),
|
|
37
|
+
ensure_ascii=False).encode()).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BindPlan(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra='forbid')
|
|
42
|
+
contact_id: str
|
|
43
|
+
session_id: str
|
|
44
|
+
turn_id: str
|
|
45
|
+
claim_id: str
|
|
46
|
+
plan: FollowupPlan
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CheckPlan(BaseModel):
|
|
50
|
+
model_config = ConfigDict(extra='forbid')
|
|
51
|
+
plan: FollowupPlan
|
|
52
|
+
outbound_ref: str = Field(min_length=1, max_length=256)
|
|
53
|
+
target: dict
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _consistent(row, plan):
|
|
57
|
+
if any(row[key] != plan[key] for key in ('wait_id', 'commitment_id', 'work_id')):
|
|
58
|
+
raise ValueError('prepared_followup_task_mismatch')
|
|
59
|
+
if (row['contact_id'] != plan['recipient_id'] or row['expires_at'] != plan['expires_at']
|
|
60
|
+
or row['source_versions'].get(plan['source_id']) != plan['source_version']
|
|
61
|
+
or hashlib.sha256(plan['message'].encode()).hexdigest() != plan['message_sha256']):
|
|
62
|
+
raise ValueError('prepared_followup_evidence_mismatch')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.post('/{wait_id}/bind-plan')
|
|
66
|
+
def bind(wait_id: str, body: BindPlan, request: Request):
|
|
67
|
+
store, owner = temporal.ledger(request, body.contact_id, write=True)
|
|
68
|
+
def register():
|
|
69
|
+
row = temporal.owned(store, wait_id, owner)
|
|
70
|
+
plan = body.plan.model_dump()
|
|
71
|
+
_consistent(row, plan)
|
|
72
|
+
if not plan['source_id'].startswith('task-instruction:'):
|
|
73
|
+
raise ValueError('direct_owner_task_instruction_required')
|
|
74
|
+
if row['source_session_id'] != body.session_id:
|
|
75
|
+
raise ValueError('owner_task_session_mismatch')
|
|
76
|
+
existing = row['authority_scope']
|
|
77
|
+
if existing:
|
|
78
|
+
if existing.get('plan') != plan:
|
|
79
|
+
raise ValueError('task authority configuration is immutable')
|
|
80
|
+
return {'bound': True, 'plan_digest': digest(plan), 'effect_authorized': False}
|
|
81
|
+
current = CommitmentWork(store.store).operate(plan['commitment_id'], operation='renew',
|
|
82
|
+
principal_id=request_authority(request).principal_id, contact_id=owner,
|
|
83
|
+
session_id=body.session_id, task_id=plan['work_id'], turn_id=body.turn_id,
|
|
84
|
+
claim_id=body.claim_id)
|
|
85
|
+
if not current['accepted']:
|
|
86
|
+
raise ValueError('current_parent_work_claim_required')
|
|
87
|
+
store.bind_authority_scope(wait_id, scope={'plan': plan, 'plan_digest': digest(plan),
|
|
88
|
+
'owner_contact_id': owner, 'owner_evidence_ref': plan['source_id'],
|
|
89
|
+
'registered_at': time.time()}, evidence_ref=plan['source_id'])
|
|
90
|
+
return {'bound': True, 'plan_digest': digest(plan), 'effect_authorized': False}
|
|
91
|
+
return temporal.guarded(register)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check(wait_id, body, request, *, require_coverage=False):
|
|
95
|
+
authority = request_authority(request)
|
|
96
|
+
if (not authority.authenticated or authority.anonymous or authority.legacy
|
|
97
|
+
or not authority.has_scope('transport:write')):
|
|
98
|
+
raise HTTPException(403, detail='trusted_transport_producer_required')
|
|
99
|
+
from apsimo.api.routers import host
|
|
100
|
+
if host._commitment_store is None:
|
|
101
|
+
raise HTTPException(503, detail='commitment_store_unavailable')
|
|
102
|
+
store = TemporalFollowups(host._commitment_store)
|
|
103
|
+
row = store.get(wait_id)
|
|
104
|
+
with closing(store.store._connect()) as db:
|
|
105
|
+
parent = db.execute('SELECT person_id FROM commitments WHERE id=?', (row['commitment_id'],)).fetchone()
|
|
106
|
+
if not parent:
|
|
107
|
+
raise ValueError('unknown_owner_task')
|
|
108
|
+
row = temporal.refresh_source_bindings(store, row, parent['person_id'])
|
|
109
|
+
if host._comms_log is not None:
|
|
110
|
+
from apsimo.api.routers.transport import reconcile_receipts
|
|
111
|
+
row = reconcile_receipts(store, host._comms_log, row)
|
|
112
|
+
plan = body.plan.model_dump()
|
|
113
|
+
_consistent(row, plan)
|
|
114
|
+
scope = row['authority_scope']
|
|
115
|
+
if (scope.get('plan') != plan or scope.get('plan_digest') != digest(plan)
|
|
116
|
+
or row['outbound_ref'] != body.outbound_ref
|
|
117
|
+
or body.target.get('channel') != plan['channel']):
|
|
118
|
+
raise ValueError('prepared_followup_binding_mismatch')
|
|
119
|
+
# Actual provider recipient/account/thread resolution and permission belong
|
|
120
|
+
# to the private policy/outbox. This proves the canonical task configuration.
|
|
121
|
+
check = store.preflight(wait_id)
|
|
122
|
+
current_sources = not temporal.source_bindings(row['source_refs'], row['source_versions'],
|
|
123
|
+
person=parent['person_id'], session_id=row['source_session_id'])
|
|
124
|
+
coverage = None
|
|
125
|
+
if require_coverage:
|
|
126
|
+
from .transport_ingress_api import followup_coverage
|
|
127
|
+
coverage = followup_coverage(principal=authority.principal_id, contact_id=row['contact_id'],
|
|
128
|
+
since=row['created_at'], channel=plan['channel'])
|
|
129
|
+
if not coverage['observed']:
|
|
130
|
+
check = {**check, 'review_allowed': False, 'reason': 'intake_coverage_unknown'}
|
|
131
|
+
return {'verified': current_sources and row['state'] not in {'cancelled', 'expired'},
|
|
132
|
+
'review_allowed': check['review_allowed'],
|
|
133
|
+
'reason': 'due' if check['review_allowed'] else check['reason'],
|
|
134
|
+
'plan_digest': digest(plan), 'owner_contact_id': scope['owner_contact_id'],
|
|
135
|
+
'owner_evidence_ref': scope['owner_evidence_ref'],
|
|
136
|
+
'registered_at': scope['registered_at'], 'resolved_contact_id': row['contact_id'],
|
|
137
|
+
**({'transport_coverage': coverage} if coverage is not None else {})}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post('/{wait_id}/verify-plan')
|
|
141
|
+
async def verify(wait_id: str, body: CheckPlan, request: Request):
|
|
142
|
+
return temporal.guarded(lambda: _check(wait_id, body, request))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@router.post('/{wait_id}/check-plan')
|
|
146
|
+
async def check(wait_id: str, body: CheckPlan, request: Request):
|
|
147
|
+
return temporal.guarded(lambda: _check(wait_id, body, request, require_coverage=True))
|