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,1128 @@
|
|
|
1
|
+
"""Durable, scoped host-event to cognitive-concern reduction.
|
|
2
|
+
|
|
3
|
+
The host event journal is the source of truth. This reducer never consumes
|
|
4
|
+
transient WebSocket buffers and never asks a model to guess scope, salience, or
|
|
5
|
+
deduplication. Concern mutation, the idempotency receipt, and the durable
|
|
6
|
+
cursor are committed together by :class:`ConcernStore`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Callable, Dict, Mapping, Optional
|
|
18
|
+
|
|
19
|
+
from apsimo.events.journal import current_sequence, replay_events
|
|
20
|
+
from apsimo.self_model.workspace import ConcernStore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_CONSUMER_ID = "workspace-concerns-v1"
|
|
24
|
+
_EXTERNAL_CONSUMER_ID = "workspace-external-concerns-v1"
|
|
25
|
+
_TURN_CONSUMER_ID = "workspace-turn-concerns-v1"
|
|
26
|
+
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,127}$")
|
|
27
|
+
_SAFE_EVENT_TYPE = re.compile(r"^[a-z0-9][a-z0-9_.:\-]{0,127}$")
|
|
28
|
+
_SAFE_CHANNEL_LANE = re.compile(r"^[a-z0-9][a-z0-9_.\-]{0,63}$")
|
|
29
|
+
_SAFE_SESSION_PREFIX = re.compile(r"^[a-z0-9][a-z0-9_.:@/+\-]{0,127}$")
|
|
30
|
+
_SHAREABILITY = {"owner_private", "subject_private", "shared", "public"}
|
|
31
|
+
_SUMMARY_FIELDS = ("summary", "description", "title", "reason", "observation", "service")
|
|
32
|
+
_REF_FIELDS = (
|
|
33
|
+
"commitment_id", "project_id", "work_order_id", "step_id", "approval_id",
|
|
34
|
+
"expectation_id", "surprise_id", "anomaly_id", "service_id", "source_ref",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_INTERNAL_TURN_CHANNELS = frozenset({
|
|
38
|
+
"agent", "api", "cognition", "cron", "event", "health", "internal",
|
|
39
|
+
"service", "system", "tool", "worker",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def event_concern_mode() -> str:
|
|
44
|
+
value = os.environ.get("COLONY_EVENT_CONCERNS", "off").strip().lower()
|
|
45
|
+
return value if value in {"off", "shadow", "live"} else "off"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def event_concerns_enabled() -> bool:
|
|
49
|
+
return event_concern_mode() in {"shadow", "live"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def external_event_concern_mode() -> str:
|
|
53
|
+
"""External-report concern producer mode; invalid/unset fails off."""
|
|
54
|
+
|
|
55
|
+
value = os.environ.get(
|
|
56
|
+
"COLONY_EXTERNAL_EVENT_CONCERNS", "off",
|
|
57
|
+
).strip().lower()
|
|
58
|
+
return value if value in {"off", "shadow", "live"} else "off"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def external_event_concerns_enabled() -> bool:
|
|
62
|
+
return external_event_concern_mode() in {"shadow", "live"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def turn_concern_mode() -> str:
|
|
66
|
+
"""Completed-conversation concern producer mode; invalid/unset is off."""
|
|
67
|
+
|
|
68
|
+
value = os.environ.get("COLONY_TURN_CONCERNS", "off").strip().lower()
|
|
69
|
+
return value if value in {"off", "shadow", "live"} else "off"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def turn_concerns_enabled() -> bool:
|
|
73
|
+
return turn_concern_mode() in {"shadow", "live"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _turn_channel_config() -> tuple[tuple[str, ...], bool]:
|
|
77
|
+
"""Return the exact configured channel-lane allowlist.
|
|
78
|
+
|
|
79
|
+
Channel IDs are ``lane:conversation``. Configuration names only the lane
|
|
80
|
+
because per-conversation identifiers are dynamic. One malformed entry
|
|
81
|
+
invalidates the complete allowlist rather than broadening a typo.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
raw = os.environ.get("COLONY_TURN_CONCERNS_CHANNELS", "")
|
|
85
|
+
if not raw.strip():
|
|
86
|
+
return (), False
|
|
87
|
+
parts = raw.split(",")
|
|
88
|
+
if any(not item.strip() for item in parts):
|
|
89
|
+
return (), False
|
|
90
|
+
values = tuple(item.strip().lower() for item in parts)
|
|
91
|
+
if any(not _SAFE_CHANNEL_LANE.fullmatch(item) for item in values):
|
|
92
|
+
return (), False
|
|
93
|
+
return tuple(dict.fromkeys(values)), True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def turn_concern_channels() -> tuple[str, ...]:
|
|
97
|
+
return _turn_channel_config()[0]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _turn_excluded_platform_config() -> tuple[tuple[str, ...], bool]:
|
|
101
|
+
raw = os.environ.get(
|
|
102
|
+
"COLONY_TURN_CONCERNS_EXCLUDED_PLATFORMS", "",
|
|
103
|
+
)
|
|
104
|
+
if not raw.strip():
|
|
105
|
+
return (), True
|
|
106
|
+
parts = raw.split(",")
|
|
107
|
+
if any(not item.strip() for item in parts):
|
|
108
|
+
return (), False
|
|
109
|
+
values = tuple(item.strip().lower() for item in parts)
|
|
110
|
+
if any(not _SAFE_CHANNEL_LANE.fullmatch(item) for item in values):
|
|
111
|
+
return (), False
|
|
112
|
+
return tuple(dict.fromkeys(values)), True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def turn_concern_excluded_platforms() -> tuple[str, ...]:
|
|
116
|
+
return _turn_excluded_platform_config()[0]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _turn_excluded_session_prefix_config() -> tuple[tuple[str, ...], bool]:
|
|
120
|
+
raw = os.environ.get(
|
|
121
|
+
"COLONY_TURN_CONCERNS_EXCLUDED_SESSION_PREFIXES", "",
|
|
122
|
+
)
|
|
123
|
+
if not raw.strip():
|
|
124
|
+
return (), True
|
|
125
|
+
parts = raw.split(",")
|
|
126
|
+
if any(not item.strip() for item in parts):
|
|
127
|
+
return (), False
|
|
128
|
+
values = tuple(item.strip().lower() for item in parts)
|
|
129
|
+
if any(not _SAFE_SESSION_PREFIX.fullmatch(item) for item in values):
|
|
130
|
+
return (), False
|
|
131
|
+
return tuple(dict.fromkeys(values)), True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def turn_concern_excluded_session_prefixes() -> tuple[str, ...]:
|
|
135
|
+
return _turn_excluded_session_prefix_config()[0]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _bootstrap_mode() -> str:
|
|
139
|
+
value = os.environ.get("COLONY_EVENT_CONCERNS_BOOTSTRAP", "tail").strip().lower()
|
|
140
|
+
return value if value in {"tail", "replay"} else "tail"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _external_bootstrap_mode() -> str:
|
|
144
|
+
# External intake already made these reports durable while this consumer
|
|
145
|
+
# was off. First enablement therefore replays retained history instead of
|
|
146
|
+
# silently losing the off-period backlog.
|
|
147
|
+
return "replay"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _turn_bootstrap_mode() -> str:
|
|
151
|
+
value = os.environ.get(
|
|
152
|
+
"COLONY_TURN_CONCERNS_BOOTSTRAP", "tail",
|
|
153
|
+
).strip().lower()
|
|
154
|
+
return value if value in {"tail", "replay"} else "tail"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _owner_person_id() -> str:
|
|
158
|
+
return (
|
|
159
|
+
os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
|
|
160
|
+
or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
|
|
161
|
+
or "owner"
|
|
162
|
+
)[:128]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _clean_id(value: Any) -> str:
|
|
166
|
+
candidate = str(value or "").strip()
|
|
167
|
+
return candidate if _SAFE_ID.fullmatch(candidate) else ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _clean_text(value: Any, maximum: int = 300) -> str:
|
|
171
|
+
raw = str(value or "")
|
|
172
|
+
cleaned = " ".join(
|
|
173
|
+
"".join(char if 32 <= ord(char) < 127 else " " for char in raw).split()
|
|
174
|
+
)
|
|
175
|
+
return cleaned[:maximum]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _digest(value: Any) -> str:
|
|
179
|
+
encoded = json.dumps(
|
|
180
|
+
value,
|
|
181
|
+
ensure_ascii=False,
|
|
182
|
+
separators=(",", ":"),
|
|
183
|
+
sort_keys=True,
|
|
184
|
+
default=str,
|
|
185
|
+
).encode("utf-8")
|
|
186
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class EventProjection:
|
|
191
|
+
operation: str
|
|
192
|
+
kind: str
|
|
193
|
+
summary: str
|
|
194
|
+
salience: float
|
|
195
|
+
dedup_key: str
|
|
196
|
+
sources: tuple[str, ...]
|
|
197
|
+
subject_person_id: str
|
|
198
|
+
viewer_scope: str
|
|
199
|
+
shareability: str
|
|
200
|
+
occurred_at: str
|
|
201
|
+
material_digest: str
|
|
202
|
+
note: str = ""
|
|
203
|
+
producer_name: str = "event_concerns"
|
|
204
|
+
producer_mode: str = ""
|
|
205
|
+
producer_revision: str = "event-concern-reducer-v2"
|
|
206
|
+
external_event_projection: bool = False
|
|
207
|
+
|
|
208
|
+
def store_payload(self) -> Dict[str, Any]:
|
|
209
|
+
return {
|
|
210
|
+
"operation": self.operation,
|
|
211
|
+
"kind": self.kind,
|
|
212
|
+
"summary": self.summary,
|
|
213
|
+
"salience": self.salience,
|
|
214
|
+
"dedup_key": self.dedup_key,
|
|
215
|
+
"sources": list(self.sources),
|
|
216
|
+
"subject_person_id": self.subject_person_id,
|
|
217
|
+
"viewer_scope": self.viewer_scope,
|
|
218
|
+
"shareability": self.shareability,
|
|
219
|
+
"occurred_at": self.occurred_at,
|
|
220
|
+
"note": self.note,
|
|
221
|
+
"producer_name": self.producer_name,
|
|
222
|
+
"producer_mode": self.producer_mode or event_concern_mode(),
|
|
223
|
+
"producer_revision": self.producer_revision,
|
|
224
|
+
"external_event_projection": self.external_event_projection,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _event_identity(event: Mapping[str, Any]) -> tuple[int, str, str, str, Mapping[str, Any]]:
|
|
229
|
+
sequence = int(event.get("seq") or 0)
|
|
230
|
+
event_id = _clean_id(event.get("ulid")) or f"journal-seq-{sequence}"
|
|
231
|
+
event_type = str(event.get("type") or "").strip().lower()
|
|
232
|
+
occurred_at = _clean_text(
|
|
233
|
+
event.get("occurredAt") or event.get("recordedAt") or "", 64,
|
|
234
|
+
)
|
|
235
|
+
data = event.get("data")
|
|
236
|
+
if sequence < 1 or not _SAFE_EVENT_TYPE.fullmatch(event_type):
|
|
237
|
+
raise ValueError("malformed journal event identity")
|
|
238
|
+
if not isinstance(data, Mapping):
|
|
239
|
+
raise ValueError("malformed journal event data")
|
|
240
|
+
return sequence, event_id, event_type, occurred_at, data
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _subject_and_scope(data: Mapping[str, Any]) -> tuple[str, str, str]:
|
|
244
|
+
subject = ""
|
|
245
|
+
for key in ("subject_person_id", "person_id", "contact_id"):
|
|
246
|
+
subject = _clean_id(data.get(key))
|
|
247
|
+
if subject:
|
|
248
|
+
break
|
|
249
|
+
subject = subject or _owner_person_id()
|
|
250
|
+
|
|
251
|
+
requested = str(
|
|
252
|
+
data.get("shareability") or data.get("privacy_scope") or "owner_private"
|
|
253
|
+
).strip().lower()
|
|
254
|
+
shareability = requested if requested in _SHAREABILITY else "owner_private"
|
|
255
|
+
# A payload can originate at a transport boundary. Shared/public is
|
|
256
|
+
# accepted only when a server-side boundary explicitly attested it.
|
|
257
|
+
if shareability in {"shared", "public"} and data.get("boundary_attested") is not True:
|
|
258
|
+
shareability = "owner_private"
|
|
259
|
+
if shareability == "subject_private":
|
|
260
|
+
viewer_scope = f"person:{subject}"
|
|
261
|
+
elif shareability in {"shared", "public"}:
|
|
262
|
+
viewer_scope = shareability
|
|
263
|
+
else:
|
|
264
|
+
viewer_scope = "owner"
|
|
265
|
+
return subject, viewer_scope, shareability
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _entity(data: Mapping[str, Any], event_type: str, summary: str) -> str:
|
|
269
|
+
if event_type.startswith(("work_order.", "work.")):
|
|
270
|
+
ordered_fields = (
|
|
271
|
+
"work_order_id", "step_id", "project_id",
|
|
272
|
+
*tuple(key for key in _REF_FIELDS if key not in {
|
|
273
|
+
"work_order_id", "step_id", "project_id",
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
elif event_type.startswith("project."):
|
|
277
|
+
ordered_fields = (
|
|
278
|
+
"project_id",
|
|
279
|
+
*tuple(key for key in _REF_FIELDS if key != "project_id"),
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
ordered_fields = _REF_FIELDS
|
|
283
|
+
for key in ordered_fields:
|
|
284
|
+
value = _clean_id(data.get(key))
|
|
285
|
+
if value:
|
|
286
|
+
return value
|
|
287
|
+
generic = _clean_id(data.get("id"))
|
|
288
|
+
if generic:
|
|
289
|
+
return generic
|
|
290
|
+
return _digest({"type": event_type, "summary": summary})[:24]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _summary(event_type: str, data: Mapping[str, Any]) -> str:
|
|
294
|
+
for key in _SUMMARY_FIELDS:
|
|
295
|
+
value = _clean_text(data.get(key))
|
|
296
|
+
if value:
|
|
297
|
+
return value
|
|
298
|
+
return event_type.replace(".", " ").replace("_", " ")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _source_refs(sequence: int, event_id: str, data: Mapping[str, Any]) -> tuple[str, ...]:
|
|
302
|
+
values = [f"journal:{sequence}:{event_id}"]
|
|
303
|
+
for key in _REF_FIELDS:
|
|
304
|
+
value = _clean_id(data.get(key))
|
|
305
|
+
if value:
|
|
306
|
+
values.append(f"{key.removesuffix('_id')}:{value}")
|
|
307
|
+
return tuple(dict.fromkeys(values))[:20]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def project_event(event: Mapping[str, Any]) -> tuple[Optional[EventProjection], str, str]:
|
|
311
|
+
"""Return ``(projection, skip_reason, raw_digest)`` for one host event."""
|
|
312
|
+
|
|
313
|
+
sequence, event_id, event_type, occurred_at, data = _event_identity(event)
|
|
314
|
+
raw_digest = _digest({
|
|
315
|
+
"type": event_type,
|
|
316
|
+
"occurred_at": occurred_at,
|
|
317
|
+
"data": data,
|
|
318
|
+
})
|
|
319
|
+
summary = _summary(event_type, data)
|
|
320
|
+
subject, viewer_scope, shareability = _subject_and_scope(data)
|
|
321
|
+
sources = _source_refs(sequence, event_id, data)
|
|
322
|
+
|
|
323
|
+
category = ""
|
|
324
|
+
operation = "upsert"
|
|
325
|
+
kind = "thread"
|
|
326
|
+
salience = 0.5
|
|
327
|
+
|
|
328
|
+
if event_type == "commitment.overdue":
|
|
329
|
+
category, kind, salience = "commitment", "goal", 0.75
|
|
330
|
+
elif event_type in {"commitment.fulfilled", "commitment.cancelled"}:
|
|
331
|
+
category, operation, kind = "commitment", "resolve", "goal"
|
|
332
|
+
elif event_type in {"surprise.high", "surprise.accumulation", "expectation.missed"}:
|
|
333
|
+
category, kind, salience = "expectation", "question", 0.72
|
|
334
|
+
elif event_type in {"expectation.hit", "expectation.resolved"}:
|
|
335
|
+
category, operation, kind = "expectation", "resolve", "question"
|
|
336
|
+
elif "anomaly" in event_type or event_type.startswith("health.")\
|
|
337
|
+
or event_type.startswith("service."):
|
|
338
|
+
category = "service" if event_type.startswith(("health.", "service.")) else "anomaly"
|
|
339
|
+
kind = "maintenance" if category == "service" else "anomaly"
|
|
340
|
+
terminal = event_type.endswith((".recovered", ".resolved", ".healthy"))
|
|
341
|
+
operation = "resolve" if terminal else "upsert"
|
|
342
|
+
salience = 0.78 if operation == "upsert" else 0.0
|
|
343
|
+
elif event_type.startswith(("project.", "work_order.", "work.")):
|
|
344
|
+
category, kind = "project", "goal"
|
|
345
|
+
status = str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower()
|
|
346
|
+
if status in {
|
|
347
|
+
"blocked", "failed", "overdue", "stalled", "unverified",
|
|
348
|
+
"verification_pending", "abandoned",
|
|
349
|
+
}:
|
|
350
|
+
operation, salience = "upsert", 0.8
|
|
351
|
+
elif status in {"completed", "verified", "cancelled", "resolved"}:
|
|
352
|
+
operation, salience = "resolve", 0.0
|
|
353
|
+
else:
|
|
354
|
+
return None, "non_material_project_event", raw_digest
|
|
355
|
+
elif event_type.startswith("approval."):
|
|
356
|
+
category, kind = "approval", "maintenance"
|
|
357
|
+
status = str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower()
|
|
358
|
+
if status in {"pending", "required", "awaiting_approval"}:
|
|
359
|
+
operation, salience = "upsert", 0.7
|
|
360
|
+
elif status in {"approved", "denied", "expired", "cancelled", "consumed"}:
|
|
361
|
+
operation, salience = "resolve", 0.0
|
|
362
|
+
else:
|
|
363
|
+
return None, "non_material_approval_event", raw_digest
|
|
364
|
+
elif event_type in {"relationship.follow_up_due", "contact.cadence_overdue"}:
|
|
365
|
+
category, kind, salience = "relationship", "thread", 0.58
|
|
366
|
+
elif event_type in {"relationship.followed_up", "contact.cadence_satisfied"}:
|
|
367
|
+
category, operation, kind = "relationship", "resolve", "thread"
|
|
368
|
+
else:
|
|
369
|
+
return None, "unmapped_event_type", raw_digest
|
|
370
|
+
|
|
371
|
+
entity = _entity(data, event_type, summary)
|
|
372
|
+
dedup_key = f"event:{category}:{subject}:{entity}"[:200]
|
|
373
|
+
material = {
|
|
374
|
+
"operation": operation,
|
|
375
|
+
"category": category,
|
|
376
|
+
"entity": entity,
|
|
377
|
+
"subject": subject,
|
|
378
|
+
"summary": summary if operation == "upsert" else "",
|
|
379
|
+
"shareability": shareability,
|
|
380
|
+
"status": str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower(),
|
|
381
|
+
}
|
|
382
|
+
projection = EventProjection(
|
|
383
|
+
operation=operation,
|
|
384
|
+
kind=kind,
|
|
385
|
+
summary=summary,
|
|
386
|
+
salience=salience,
|
|
387
|
+
dedup_key=dedup_key,
|
|
388
|
+
sources=sources,
|
|
389
|
+
subject_person_id=subject,
|
|
390
|
+
viewer_scope=viewer_scope,
|
|
391
|
+
shareability=shareability,
|
|
392
|
+
occurred_at=occurred_at,
|
|
393
|
+
material_digest=_digest(material),
|
|
394
|
+
note=(f"resolved by {event_type}" if operation == "resolve" else ""),
|
|
395
|
+
)
|
|
396
|
+
return projection, "", raw_digest
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _turn_text(value: Any, maximum: int = 300) -> str:
|
|
400
|
+
if type(value) is not str:
|
|
401
|
+
return ""
|
|
402
|
+
return " ".join(
|
|
403
|
+
"".join(
|
|
404
|
+
char if char >= " " and char != "\x7f" else " "
|
|
405
|
+
for char in value
|
|
406
|
+
).split()
|
|
407
|
+
)[:maximum]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _turn_id(value: Any) -> str:
|
|
411
|
+
if (
|
|
412
|
+
type(value) is not str
|
|
413
|
+
or not value
|
|
414
|
+
or value != value.strip()
|
|
415
|
+
or len(value) > 256
|
|
416
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in value)
|
|
417
|
+
):
|
|
418
|
+
return ""
|
|
419
|
+
return value
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _turn_session(value: Any) -> str:
|
|
423
|
+
if (
|
|
424
|
+
type(value) is not str
|
|
425
|
+
or not value
|
|
426
|
+
or value != value.strip()
|
|
427
|
+
or len(value) > 256
|
|
428
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in value)
|
|
429
|
+
):
|
|
430
|
+
return ""
|
|
431
|
+
return value
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _turn_channel(value: Any) -> tuple[str, str]:
|
|
435
|
+
if (
|
|
436
|
+
type(value) is not str
|
|
437
|
+
or not value
|
|
438
|
+
or value != value.strip()
|
|
439
|
+
or len(value) > 512
|
|
440
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in value)
|
|
441
|
+
):
|
|
442
|
+
return "", ""
|
|
443
|
+
lane = value.split(":", 1)[0].lower()
|
|
444
|
+
if not _SAFE_CHANNEL_LANE.fullmatch(lane):
|
|
445
|
+
return "", ""
|
|
446
|
+
return value, lane
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _channel_lane_matches(lane: str, blocked: frozenset[str]) -> bool:
|
|
450
|
+
return any(
|
|
451
|
+
lane == value
|
|
452
|
+
or any(lane.startswith(value + separator) for separator in ("-", "_", "."))
|
|
453
|
+
for value in blocked
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _configured_turn_owner() -> str:
|
|
458
|
+
value = (
|
|
459
|
+
os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
|
|
460
|
+
or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
|
|
461
|
+
)
|
|
462
|
+
return value if _SAFE_ID.fullmatch(value) else ""
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def project_turn_concern_hold_reason(project: Any, concern_store: Any) -> str:
|
|
466
|
+
"""Return the runtime hold for exact cognition-spine turn provenance."""
|
|
467
|
+
|
|
468
|
+
if str(getattr(project, "source", "") or "") != "cognition_spine":
|
|
469
|
+
return ""
|
|
470
|
+
concern_id = str(getattr(project, "concern_id", "") or "")
|
|
471
|
+
if not concern_id:
|
|
472
|
+
return ""
|
|
473
|
+
if concern_store is None:
|
|
474
|
+
return "project_hold_reason_unavailable"
|
|
475
|
+
concern = concern_store.get(concern_id)
|
|
476
|
+
if concern is None:
|
|
477
|
+
return "project_hold_reason_unavailable"
|
|
478
|
+
if str(getattr(concern, "producer_name", "") or "") != "turn_concerns":
|
|
479
|
+
return ""
|
|
480
|
+
if turn_concern_mode() != "live":
|
|
481
|
+
return "turn_concerns_current_mode_not_live"
|
|
482
|
+
return ""
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def project_conversation_turn(
|
|
486
|
+
event: Mapping[str, Any],
|
|
487
|
+
) -> tuple[Optional[EventProjection], str, str]:
|
|
488
|
+
"""Project one server-attested completed human turn into a concern.
|
|
489
|
+
|
|
490
|
+
Conversation prose is untrusted evidence. This adapter accepts no action,
|
|
491
|
+
capability, approval, receipt, or effect fields; it only preserves the
|
|
492
|
+
server-produced subject and private viewer lane for the existing policy
|
|
493
|
+
spine to evaluate later.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
sequence, event_id, event_type, occurred_at, data = _event_identity(event)
|
|
497
|
+
raw_digest = _digest({
|
|
498
|
+
"type": event_type,
|
|
499
|
+
"occurred_at": occurred_at,
|
|
500
|
+
"data": data,
|
|
501
|
+
})
|
|
502
|
+
if event_type != "conversation.turn":
|
|
503
|
+
return None, "not_conversation_turn", raw_digest
|
|
504
|
+
if data.get("turn_scope_schema") != "ConversationTurnJournalScopeV1":
|
|
505
|
+
return None, "missing_turn_scope_envelope", raw_digest
|
|
506
|
+
|
|
507
|
+
session_id = _turn_session(data.get("session_id"))
|
|
508
|
+
if not session_id:
|
|
509
|
+
return None, "missing_turn_session", raw_digest
|
|
510
|
+
excluded_prefixes, session_config_valid = (
|
|
511
|
+
_turn_excluded_session_prefix_config()
|
|
512
|
+
)
|
|
513
|
+
if not session_config_valid:
|
|
514
|
+
return None, "session_prefix_config_invalid", raw_digest
|
|
515
|
+
if any(session_id.lower().startswith(prefix) for prefix in excluded_prefixes):
|
|
516
|
+
return None, "excluded_session_prefix", raw_digest
|
|
517
|
+
|
|
518
|
+
turn_id = _turn_id(data.get("turn_id"))
|
|
519
|
+
if not turn_id:
|
|
520
|
+
return None, "missing_turn_id", raw_digest
|
|
521
|
+
turn_id_source = data.get("turn_id_source")
|
|
522
|
+
if (
|
|
523
|
+
data.get("turn_id_attested") is not True
|
|
524
|
+
or turn_id_source not in {"client_idempotency_key", "server_digest"}
|
|
525
|
+
):
|
|
526
|
+
return None, "turn_id_not_attested", raw_digest
|
|
527
|
+
|
|
528
|
+
channel_id, channel_lane = _turn_channel(data.get("channel_id"))
|
|
529
|
+
if not channel_lane:
|
|
530
|
+
return None, "missing_channel", raw_digest
|
|
531
|
+
if _channel_lane_matches(channel_lane, _INTERNAL_TURN_CHANNELS):
|
|
532
|
+
return None, "internal_channel", raw_digest
|
|
533
|
+
if channel_lane not in turn_concern_channels():
|
|
534
|
+
return None, "channel_not_allowed", raw_digest
|
|
535
|
+
|
|
536
|
+
source_platform = data.get("source_platform")
|
|
537
|
+
if (
|
|
538
|
+
type(source_platform) is not str
|
|
539
|
+
or not _SAFE_CHANNEL_LANE.fullmatch(source_platform)
|
|
540
|
+
or data.get("source_platform_attested") is not True
|
|
541
|
+
):
|
|
542
|
+
return None, "source_platform_not_attested", raw_digest
|
|
543
|
+
excluded_platforms, platform_config_valid = (
|
|
544
|
+
_turn_excluded_platform_config()
|
|
545
|
+
)
|
|
546
|
+
if not platform_config_valid:
|
|
547
|
+
return None, "excluded_platform_config_invalid", raw_digest
|
|
548
|
+
if source_platform in excluded_platforms:
|
|
549
|
+
return None, "excluded_source_platform", raw_digest
|
|
550
|
+
|
|
551
|
+
contact = data.get("contact_id")
|
|
552
|
+
subject_value = data.get("subject_person_id")
|
|
553
|
+
subject = (
|
|
554
|
+
subject_value
|
|
555
|
+
if type(subject_value) is str and _SAFE_ID.fullmatch(subject_value)
|
|
556
|
+
else ""
|
|
557
|
+
)
|
|
558
|
+
if (
|
|
559
|
+
not subject
|
|
560
|
+
or type(contact) is not str
|
|
561
|
+
or contact != subject
|
|
562
|
+
or subject == "system"
|
|
563
|
+
):
|
|
564
|
+
return None, "missing_attested_subject", raw_digest
|
|
565
|
+
if data.get("identity_attested") is not True:
|
|
566
|
+
return None, "turn_identity_not_attested", raw_digest
|
|
567
|
+
if data.get("scope_attested") is not True:
|
|
568
|
+
return None, "turn_scope_not_attested", raw_digest
|
|
569
|
+
|
|
570
|
+
principal = data.get("source_principal_id")
|
|
571
|
+
attribution = data.get("attribution_method")
|
|
572
|
+
if (
|
|
573
|
+
type(principal) is not str
|
|
574
|
+
or not _SAFE_ID.fullmatch(principal)
|
|
575
|
+
or type(attribution) is not str
|
|
576
|
+
or not _SAFE_ID.fullmatch(attribution)
|
|
577
|
+
):
|
|
578
|
+
return None, "missing_sender_attestation", raw_digest
|
|
579
|
+
|
|
580
|
+
owner = _configured_turn_owner()
|
|
581
|
+
if not owner:
|
|
582
|
+
return None, "owner_identity_unconfigured", raw_digest
|
|
583
|
+
expected_scope = "owner" if subject == owner else f"person:{subject}"
|
|
584
|
+
expected_sharing = (
|
|
585
|
+
"owner_private" if subject == owner else "subject_private"
|
|
586
|
+
)
|
|
587
|
+
viewer_scope = data.get("viewer_scope")
|
|
588
|
+
shareability = data.get("shareability")
|
|
589
|
+
if (
|
|
590
|
+
viewer_scope != expected_scope
|
|
591
|
+
or shareability != expected_sharing
|
|
592
|
+
or data.get("boundary_attested") is not False
|
|
593
|
+
):
|
|
594
|
+
return None, "unsafe_turn_scope", raw_digest
|
|
595
|
+
|
|
596
|
+
summary = _turn_text(data.get("summary"))
|
|
597
|
+
if not summary:
|
|
598
|
+
return None, "missing_turn_summary", raw_digest
|
|
599
|
+
turn_digest = hashlib.sha256(turn_id.encode("utf-8")).hexdigest()
|
|
600
|
+
sources = [
|
|
601
|
+
f"journal:{sequence}:{event_id}",
|
|
602
|
+
f"turn:{turn_digest}",
|
|
603
|
+
f"channel:{channel_lane}",
|
|
604
|
+
f"platform:{source_platform}",
|
|
605
|
+
f"principal:{principal}",
|
|
606
|
+
]
|
|
607
|
+
session_digest = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
|
|
608
|
+
sources.append(f"conversation:{session_digest}")
|
|
609
|
+
material = {
|
|
610
|
+
"namespace": "turn_concerns",
|
|
611
|
+
"turn_digest": turn_digest,
|
|
612
|
+
"turn_id_source": turn_id_source,
|
|
613
|
+
"session_digest": session_digest,
|
|
614
|
+
"channel_lane": channel_lane,
|
|
615
|
+
"source_platform": source_platform,
|
|
616
|
+
"subject_person_id": subject,
|
|
617
|
+
"viewer_scope": viewer_scope,
|
|
618
|
+
"shareability": shareability,
|
|
619
|
+
"summary": summary,
|
|
620
|
+
"source_principal_id": principal,
|
|
621
|
+
"attribution_method": attribution,
|
|
622
|
+
"evidence_status": "reported/unverified",
|
|
623
|
+
}
|
|
624
|
+
return EventProjection(
|
|
625
|
+
operation="upsert",
|
|
626
|
+
kind="thread",
|
|
627
|
+
summary=summary,
|
|
628
|
+
salience=0.5,
|
|
629
|
+
dedup_key=f"turn:{subject}:{turn_digest}"[:200],
|
|
630
|
+
sources=tuple(dict.fromkeys(sources)),
|
|
631
|
+
subject_person_id=subject,
|
|
632
|
+
viewer_scope=str(viewer_scope),
|
|
633
|
+
shareability=str(shareability),
|
|
634
|
+
occurred_at=occurred_at,
|
|
635
|
+
material_digest=_digest(material),
|
|
636
|
+
producer_name="turn_concerns",
|
|
637
|
+
producer_mode=turn_concern_mode(),
|
|
638
|
+
producer_revision="conversation-turn-concern-reducer-v1",
|
|
639
|
+
), "", raw_digest
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _external_occurred_at(value: Any) -> str:
|
|
643
|
+
if type(value) is not str or not value or value != value.strip():
|
|
644
|
+
raise ValueError("external cognition occurredAt is not canonical text")
|
|
645
|
+
text = value
|
|
646
|
+
candidate = text[:-1] + "+00:00" if text.endswith("Z") else text
|
|
647
|
+
try:
|
|
648
|
+
parsed = datetime.fromisoformat(candidate)
|
|
649
|
+
except ValueError as exc:
|
|
650
|
+
raise ValueError("external cognition occurredAt is invalid") from exc
|
|
651
|
+
if parsed.tzinfo is None:
|
|
652
|
+
raise ValueError("external cognition occurredAt requires timezone authority")
|
|
653
|
+
return text
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def project_external_event(
|
|
657
|
+
event: Mapping[str, Any],
|
|
658
|
+
) -> tuple[Optional[EventProjection], str, str]:
|
|
659
|
+
"""Project one strictly validated external report into its own namespace.
|
|
660
|
+
|
|
661
|
+
Terminal reports may only close the concern with the exact deterministic
|
|
662
|
+
subject/producer/kind/entity identity. They never join a Project, action,
|
|
663
|
+
grant, effect, or competence ledger.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
if type(event.get("seq")) is not int or event.get("seq") < 1:
|
|
667
|
+
raise ValueError("external cognition journal sequence is not canonical")
|
|
668
|
+
from apsimo.cognition.external_events import (
|
|
669
|
+
validate_external_journal_event_id,
|
|
670
|
+
validate_external_journal_projection,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
validate_external_journal_event_id(event.get("ulid"))
|
|
674
|
+
sequence, event_id, event_type, _occurred, data = _event_identity(event)
|
|
675
|
+
raw_digest = _digest({
|
|
676
|
+
"type": event_type,
|
|
677
|
+
"occurred_at": event.get("occurredAt"),
|
|
678
|
+
"data": data,
|
|
679
|
+
})
|
|
680
|
+
if not event_type.startswith("cognition.external."):
|
|
681
|
+
return None, "not_external_cognition_event", raw_digest
|
|
682
|
+
if type(event.get("ulid")) is not str or event.get("ulid") != event_id:
|
|
683
|
+
raise ValueError("external cognition journal ID is not canonical")
|
|
684
|
+
if type(event.get("type")) is not str or event.get("type") != event_type:
|
|
685
|
+
raise ValueError("external cognition journal type is not canonical")
|
|
686
|
+
|
|
687
|
+
projected = validate_external_journal_projection(event_type, data)
|
|
688
|
+
occurred_at = _external_occurred_at(event.get("occurredAt"))
|
|
689
|
+
if occurred_at != projected["external_occurred_at"]:
|
|
690
|
+
raise ValueError(
|
|
691
|
+
"external cognition host time differs from its server projection"
|
|
692
|
+
)
|
|
693
|
+
kind = projected["kind"]
|
|
694
|
+
attributes = dict(projected["attributes"])
|
|
695
|
+
producer = projected["producer_principal_id"]
|
|
696
|
+
subject = projected["subject_person_id"]
|
|
697
|
+
sharing = projected["shareability"]
|
|
698
|
+
viewer_scope = projected["viewer_scope"]
|
|
699
|
+
summary = (
|
|
700
|
+
attributes["observation"]
|
|
701
|
+
if kind == "text_turn_observation"
|
|
702
|
+
else projected["summary"]
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
entity_fields = {
|
|
706
|
+
"action_outcome": ("action_id",),
|
|
707
|
+
"delivery_outcome": ("delivery_ref", "message_ref"),
|
|
708
|
+
"service_state": ("service",),
|
|
709
|
+
"approval_state": ("request_id",),
|
|
710
|
+
"operator_reaction": ("target_ref",),
|
|
711
|
+
"text_turn_observation": ("turn_id",),
|
|
712
|
+
}
|
|
713
|
+
entity = next(
|
|
714
|
+
str(attributes.get(field) or "")
|
|
715
|
+
for field in entity_fields[kind]
|
|
716
|
+
if attributes.get(field)
|
|
717
|
+
)
|
|
718
|
+
state_field = {
|
|
719
|
+
"action_outcome": "outcome",
|
|
720
|
+
"delivery_outcome": "outcome",
|
|
721
|
+
"service_state": "state",
|
|
722
|
+
"approval_state": "state",
|
|
723
|
+
"operator_reaction": "reaction",
|
|
724
|
+
"text_turn_observation": "channel",
|
|
725
|
+
}[kind]
|
|
726
|
+
state = str(attributes[state_field])
|
|
727
|
+
resolve_states = {
|
|
728
|
+
"action_outcome": {"succeeded", "cancelled"},
|
|
729
|
+
"delivery_outcome": {"delivered", "read", "acknowledged"},
|
|
730
|
+
"service_state": {"healthy"},
|
|
731
|
+
"approval_state": {"approved", "rejected", "expired", "cancelled"},
|
|
732
|
+
"operator_reaction": {"positive", "neutral", "dismissed"},
|
|
733
|
+
"text_turn_observation": set(),
|
|
734
|
+
}
|
|
735
|
+
operation = "resolve" if state in resolve_states[kind] else "upsert"
|
|
736
|
+
concern_kind = (
|
|
737
|
+
"thread"
|
|
738
|
+
if kind in {"operator_reaction", "text_turn_observation"}
|
|
739
|
+
else "maintenance"
|
|
740
|
+
)
|
|
741
|
+
salience = {
|
|
742
|
+
"action_outcome": 0.78,
|
|
743
|
+
"delivery_outcome": 0.66,
|
|
744
|
+
"service_state": 0.76,
|
|
745
|
+
"approval_state": 0.62,
|
|
746
|
+
"operator_reaction": 0.58,
|
|
747
|
+
"text_turn_observation": 0.5,
|
|
748
|
+
}[kind] if operation == "upsert" else 0.0
|
|
749
|
+
identity = {
|
|
750
|
+
"namespace": "external_event_concerns",
|
|
751
|
+
"subject_person_id": subject,
|
|
752
|
+
"producer_principal_id": producer,
|
|
753
|
+
"kind": kind,
|
|
754
|
+
"entity": entity,
|
|
755
|
+
}
|
|
756
|
+
dedup_key = f"external:{kind}:{_digest(identity)}"
|
|
757
|
+
sources = (
|
|
758
|
+
f"journal:{sequence}:{event_id}",
|
|
759
|
+
f"xevent:{projected['external_event_id']}",
|
|
760
|
+
f"xdigest:{projected['external_event_digest']}",
|
|
761
|
+
f"external_producer:{producer}",
|
|
762
|
+
f"external_revision:{projected['producer_revision']}",
|
|
763
|
+
f"external_kind:{kind}",
|
|
764
|
+
f"xentity:{entity}",
|
|
765
|
+
)
|
|
766
|
+
material = {
|
|
767
|
+
**identity,
|
|
768
|
+
"operation": operation,
|
|
769
|
+
"state": state,
|
|
770
|
+
"summary": summary if operation == "upsert" else "",
|
|
771
|
+
"attributes": attributes,
|
|
772
|
+
"external_event_digest": projected["external_event_digest"],
|
|
773
|
+
"external_producer_revision": projected["producer_revision"],
|
|
774
|
+
"reducer_revision": "external-event-concern-reducer-v1",
|
|
775
|
+
"scope_digest": projected["scope_digest"],
|
|
776
|
+
"boundary_attested": False,
|
|
777
|
+
"evidence_status": "reported/unverified",
|
|
778
|
+
}
|
|
779
|
+
return EventProjection(
|
|
780
|
+
operation=operation,
|
|
781
|
+
kind=concern_kind,
|
|
782
|
+
summary=summary,
|
|
783
|
+
salience=salience,
|
|
784
|
+
dedup_key=dedup_key,
|
|
785
|
+
sources=sources,
|
|
786
|
+
subject_person_id=subject,
|
|
787
|
+
viewer_scope=viewer_scope,
|
|
788
|
+
shareability=sharing,
|
|
789
|
+
occurred_at=occurred_at,
|
|
790
|
+
material_digest=_digest(material),
|
|
791
|
+
note=(
|
|
792
|
+
f"resolved by reported external {kind} state {state}"
|
|
793
|
+
if operation == "resolve" else ""
|
|
794
|
+
),
|
|
795
|
+
producer_name="external_event_concerns",
|
|
796
|
+
producer_mode=external_event_concern_mode(),
|
|
797
|
+
producer_revision="external-event-concern-reducer-v1",
|
|
798
|
+
external_event_projection=True,
|
|
799
|
+
), "", raw_digest
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
class EventConcernReducer:
|
|
803
|
+
"""Idempotently consume the durable host journal into ``ConcernStore``."""
|
|
804
|
+
|
|
805
|
+
def __init__(
|
|
806
|
+
self,
|
|
807
|
+
store: ConcernStore,
|
|
808
|
+
*,
|
|
809
|
+
consumer_id: str = _CONSUMER_ID,
|
|
810
|
+
replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
|
|
811
|
+
current_sequence_fn: Callable[[], int] = current_sequence,
|
|
812
|
+
projection_fn: Callable[
|
|
813
|
+
[Mapping[str, Any]],
|
|
814
|
+
tuple[Optional[EventProjection], str, str],
|
|
815
|
+
] = project_event,
|
|
816
|
+
mode_fn: Callable[[], str] = event_concern_mode,
|
|
817
|
+
bootstrap_mode_fn: Callable[[], str] = _bootstrap_mode,
|
|
818
|
+
gap_policy_env: str = "COLONY_EVENT_CONCERNS_GAP_POLICY",
|
|
819
|
+
malformed_skip_reason: str = "",
|
|
820
|
+
) -> None:
|
|
821
|
+
self.store = store
|
|
822
|
+
self.consumer_id = consumer_id
|
|
823
|
+
self._replay = replay_fn
|
|
824
|
+
self._current_sequence = current_sequence_fn
|
|
825
|
+
self._project = projection_fn
|
|
826
|
+
self._mode = mode_fn
|
|
827
|
+
self._bootstrap = bootstrap_mode_fn
|
|
828
|
+
self._gap_policy_env = str(gap_policy_env)
|
|
829
|
+
self._malformed_skip_reason = str(malformed_skip_reason or "")[:100]
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def mode(self) -> str:
|
|
833
|
+
return self._mode()
|
|
834
|
+
|
|
835
|
+
def status(self) -> Dict[str, Any]:
|
|
836
|
+
state = self.store.event_reducer_status(self.consumer_id)
|
|
837
|
+
state.update({
|
|
838
|
+
"enabled": self.mode != "off",
|
|
839
|
+
"mode": self.mode,
|
|
840
|
+
"consumer_id": self.consumer_id,
|
|
841
|
+
"healthy": not bool(state.get("last_error")),
|
|
842
|
+
"journal_high_water": int(self._current_sequence()),
|
|
843
|
+
})
|
|
844
|
+
return state
|
|
845
|
+
|
|
846
|
+
@staticmethod
|
|
847
|
+
def _replay_integrity_error(snapshot: Mapping[str, Any]) -> str:
|
|
848
|
+
if snapshot.get("replayError"):
|
|
849
|
+
return "event_journal_unavailable"
|
|
850
|
+
try:
|
|
851
|
+
corrupt = int(snapshot.get("corruptCount") or 0)
|
|
852
|
+
first = int(snapshot.get("firstAvailableSeq") or 0)
|
|
853
|
+
high = int(snapshot.get("journalLastSeq") or 0)
|
|
854
|
+
except (TypeError, ValueError):
|
|
855
|
+
return "event_journal_metadata_invalid"
|
|
856
|
+
if corrupt < 0 or first < 0 or high < 0 or (first and first > high):
|
|
857
|
+
return "event_journal_metadata_invalid"
|
|
858
|
+
if corrupt:
|
|
859
|
+
return f"journal_corruption_detected:{corrupt}"
|
|
860
|
+
if not isinstance(snapshot.get("events") or (), (list, tuple)):
|
|
861
|
+
return "event_journal_metadata_invalid"
|
|
862
|
+
return ""
|
|
863
|
+
|
|
864
|
+
def _gap_policy(self) -> str:
|
|
865
|
+
return os.environ.get(
|
|
866
|
+
self._gap_policy_env, "stop",
|
|
867
|
+
).strip().lower()
|
|
868
|
+
|
|
869
|
+
def _stop(self, error: str, *, processed: int = 0,
|
|
870
|
+
cursor: Optional[int] = None) -> Dict[str, Any]:
|
|
871
|
+
self.store.set_event_error(self.consumer_id, error)
|
|
872
|
+
result: Dict[str, Any] = {
|
|
873
|
+
"enabled": True, "processed": int(processed), "error": error,
|
|
874
|
+
}
|
|
875
|
+
if cursor is not None:
|
|
876
|
+
result["cursor"] = int(cursor)
|
|
877
|
+
return result
|
|
878
|
+
|
|
879
|
+
def _initialize(self) -> Dict[str, Any]:
|
|
880
|
+
snapshot = self._replay(after_seq=0, limit=1)
|
|
881
|
+
bootstrap = self._bootstrap()
|
|
882
|
+
error = self._replay_integrity_error(snapshot)
|
|
883
|
+
if error:
|
|
884
|
+
initial = 0
|
|
885
|
+
else:
|
|
886
|
+
first = int(snapshot.get("firstAvailableSeq") or 0)
|
|
887
|
+
high_water = int(snapshot.get("journalLastSeq") or 0)
|
|
888
|
+
initial = high_water if bootstrap == "tail" else max(0, first - 1)
|
|
889
|
+
cursor = self.store.initialize_event_cursor(
|
|
890
|
+
self.consumer_id,
|
|
891
|
+
initial,
|
|
892
|
+
bootstrap_mode=bootstrap,
|
|
893
|
+
)
|
|
894
|
+
result = {
|
|
895
|
+
"bootstrapped": True, "bootstrap_mode": bootstrap, "cursor": cursor,
|
|
896
|
+
}
|
|
897
|
+
if error:
|
|
898
|
+
self.store.set_event_error(self.consumer_id, error)
|
|
899
|
+
result["error"] = error
|
|
900
|
+
return result
|
|
901
|
+
|
|
902
|
+
def run_once(self, *, limit: int = 100) -> Dict[str, Any]:
|
|
903
|
+
if self.mode == "off":
|
|
904
|
+
return {"enabled": False, "processed": 0}
|
|
905
|
+
cursor = self.store.event_cursor(self.consumer_id)
|
|
906
|
+
if cursor is None:
|
|
907
|
+
initialized = self._initialize()
|
|
908
|
+
cursor = int(initialized["cursor"])
|
|
909
|
+
if initialized.get("error"):
|
|
910
|
+
return self._stop(str(initialized["error"]), cursor=cursor)
|
|
911
|
+
if initialized["bootstrap_mode"] == "tail":
|
|
912
|
+
return {**initialized, "enabled": True, "processed": 0}
|
|
913
|
+
|
|
914
|
+
batch = self._replay(after_seq=cursor, limit=max(1, min(500, int(limit))))
|
|
915
|
+
integrity_error = self._replay_integrity_error(batch)
|
|
916
|
+
if integrity_error:
|
|
917
|
+
return self._stop(integrity_error, cursor=cursor)
|
|
918
|
+
journal_high = int(batch.get("journalLastSeq") or 0)
|
|
919
|
+
if cursor > journal_high:
|
|
920
|
+
return self._stop(
|
|
921
|
+
f"event_journal_rewind:{cursor}:{journal_high}",
|
|
922
|
+
cursor=cursor,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
first = int(batch.get("firstAvailableSeq") or 0)
|
|
926
|
+
if first and cursor < first - 1:
|
|
927
|
+
message = f"journal_retention_gap:{cursor}:{first}"
|
|
928
|
+
if self._gap_policy() == "acknowledge":
|
|
929
|
+
cursor = self.store.acknowledge_event_gap(
|
|
930
|
+
self.consumer_id,
|
|
931
|
+
prior_cursor=cursor,
|
|
932
|
+
resume_after=first - 1,
|
|
933
|
+
reason=message,
|
|
934
|
+
)
|
|
935
|
+
batch = self._replay(
|
|
936
|
+
after_seq=cursor, limit=max(1, min(500, int(limit))),
|
|
937
|
+
)
|
|
938
|
+
integrity_error = self._replay_integrity_error(batch)
|
|
939
|
+
if integrity_error:
|
|
940
|
+
return self._stop(integrity_error, cursor=cursor)
|
|
941
|
+
journal_high = int(batch.get("journalLastSeq") or 0)
|
|
942
|
+
if cursor > journal_high:
|
|
943
|
+
return self._stop(
|
|
944
|
+
f"event_journal_rewind:{cursor}:{journal_high}",
|
|
945
|
+
cursor=cursor,
|
|
946
|
+
)
|
|
947
|
+
else:
|
|
948
|
+
return self._stop(message, cursor=cursor)
|
|
949
|
+
|
|
950
|
+
counts: Dict[str, int] = {}
|
|
951
|
+
last_seq = cursor
|
|
952
|
+
for raw in batch.get("events") or []:
|
|
953
|
+
try:
|
|
954
|
+
if not isinstance(raw, Mapping):
|
|
955
|
+
raise ValueError("journal event is not an object")
|
|
956
|
+
sequence = int(raw.get("seq") or 0)
|
|
957
|
+
if sequence <= last_seq:
|
|
958
|
+
raise ValueError("journal event sequence is not increasing")
|
|
959
|
+
if sequence != last_seq + 1:
|
|
960
|
+
message = f"journal_sequence_gap:{last_seq}:{sequence}"
|
|
961
|
+
if self._gap_policy() != "acknowledge":
|
|
962
|
+
return self._stop(
|
|
963
|
+
message, processed=sum(counts.values()),
|
|
964
|
+
cursor=last_seq,
|
|
965
|
+
)
|
|
966
|
+
last_seq = self.store.acknowledge_event_gap(
|
|
967
|
+
self.consumer_id,
|
|
968
|
+
prior_cursor=last_seq,
|
|
969
|
+
resume_after=sequence - 1,
|
|
970
|
+
reason=message,
|
|
971
|
+
)
|
|
972
|
+
sequence, event_id, event_type, _, _ = _event_identity(raw)
|
|
973
|
+
projection, skip_reason, raw_digest = self._project(raw)
|
|
974
|
+
result = self.store.apply_event(
|
|
975
|
+
consumer_id=self.consumer_id,
|
|
976
|
+
event_seq=sequence,
|
|
977
|
+
event_id=event_id,
|
|
978
|
+
event_type=event_type,
|
|
979
|
+
material_digest=(
|
|
980
|
+
projection.material_digest if projection is not None else raw_digest
|
|
981
|
+
),
|
|
982
|
+
projection=(projection.store_payload() if projection is not None else None),
|
|
983
|
+
skip_reason=skip_reason,
|
|
984
|
+
)
|
|
985
|
+
except Exception as exc:
|
|
986
|
+
# If the journal supplied a usable sequence, record malformed
|
|
987
|
+
# content as skipped and advance. Identity-less records stop
|
|
988
|
+
# the batch because advancing would guess a cursor.
|
|
989
|
+
sequence = int(raw.get("seq") or 0) if isinstance(raw, Mapping) else 0
|
|
990
|
+
if sequence < 1:
|
|
991
|
+
self.store.set_event_error(self.consumer_id, "malformed_event_without_sequence")
|
|
992
|
+
return {
|
|
993
|
+
"enabled": True,
|
|
994
|
+
"processed": sum(counts.values()),
|
|
995
|
+
"error": "malformed_event_without_sequence",
|
|
996
|
+
}
|
|
997
|
+
event_id = _clean_id(raw.get("ulid")) or f"journal-seq-{sequence}"
|
|
998
|
+
event_type = str(raw.get("type") or "malformed")[:128]
|
|
999
|
+
result = self.store.apply_event(
|
|
1000
|
+
consumer_id=self.consumer_id,
|
|
1001
|
+
event_seq=sequence,
|
|
1002
|
+
event_id=event_id,
|
|
1003
|
+
event_type=(event_type if _SAFE_EVENT_TYPE.fullmatch(event_type) else "malformed"),
|
|
1004
|
+
material_digest=_digest({"malformed": raw}),
|
|
1005
|
+
projection=None,
|
|
1006
|
+
skip_reason=(
|
|
1007
|
+
self._malformed_skip_reason
|
|
1008
|
+
or f"malformed_event:{type(exc).__name__}"
|
|
1009
|
+
),
|
|
1010
|
+
)
|
|
1011
|
+
disposition = str(result.get("disposition") or result.get("status") or "unknown")
|
|
1012
|
+
counts[disposition] = counts.get(disposition, 0) + 1
|
|
1013
|
+
last_seq = max(last_seq, int(result.get("cursor") or last_seq))
|
|
1014
|
+
|
|
1015
|
+
journal_high = int(batch.get("journalLastSeq") or 0)
|
|
1016
|
+
if not bool(batch.get("hasMore")) and journal_high > last_seq:
|
|
1017
|
+
message = f"journal_sequence_gap:{last_seq}:{journal_high + 1}"
|
|
1018
|
+
if self._gap_policy() != "acknowledge":
|
|
1019
|
+
return self._stop(
|
|
1020
|
+
message, processed=sum(counts.values()), cursor=last_seq,
|
|
1021
|
+
)
|
|
1022
|
+
last_seq = self.store.acknowledge_event_gap(
|
|
1023
|
+
self.consumer_id,
|
|
1024
|
+
prior_cursor=last_seq,
|
|
1025
|
+
resume_after=journal_high,
|
|
1026
|
+
reason=message,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
return {
|
|
1030
|
+
"enabled": True,
|
|
1031
|
+
"processed": sum(counts.values()),
|
|
1032
|
+
"dispositions": counts,
|
|
1033
|
+
"cursor": last_seq,
|
|
1034
|
+
"has_more": bool(batch.get("hasMore")),
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
class ExternalEventConcernReducer(EventConcernReducer):
|
|
1039
|
+
"""Independent durable consumer for reported external cognition events."""
|
|
1040
|
+
|
|
1041
|
+
def __init__(
|
|
1042
|
+
self,
|
|
1043
|
+
store: ConcernStore,
|
|
1044
|
+
*,
|
|
1045
|
+
consumer_id: str = _EXTERNAL_CONSUMER_ID,
|
|
1046
|
+
replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
|
|
1047
|
+
current_sequence_fn: Callable[[], int] = current_sequence,
|
|
1048
|
+
) -> None:
|
|
1049
|
+
super().__init__(
|
|
1050
|
+
store,
|
|
1051
|
+
consumer_id=consumer_id,
|
|
1052
|
+
replay_fn=replay_fn,
|
|
1053
|
+
current_sequence_fn=current_sequence_fn,
|
|
1054
|
+
projection_fn=project_external_event,
|
|
1055
|
+
mode_fn=external_event_concern_mode,
|
|
1056
|
+
bootstrap_mode_fn=_external_bootstrap_mode,
|
|
1057
|
+
gap_policy_env="COLONY_EXTERNAL_EVENT_CONCERNS_GAP_POLICY",
|
|
1058
|
+
malformed_skip_reason="malformed_event:ValueError",
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
class ConversationTurnConcernReducer(EventConcernReducer):
|
|
1063
|
+
"""Independent cursor for eligible, server-attested completed turns."""
|
|
1064
|
+
|
|
1065
|
+
def __init__(
|
|
1066
|
+
self,
|
|
1067
|
+
store: ConcernStore,
|
|
1068
|
+
*,
|
|
1069
|
+
consumer_id: str = _TURN_CONSUMER_ID,
|
|
1070
|
+
replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
|
|
1071
|
+
current_sequence_fn: Callable[[], int] = current_sequence,
|
|
1072
|
+
) -> None:
|
|
1073
|
+
super().__init__(
|
|
1074
|
+
store,
|
|
1075
|
+
consumer_id=consumer_id,
|
|
1076
|
+
replay_fn=replay_fn,
|
|
1077
|
+
current_sequence_fn=current_sequence_fn,
|
|
1078
|
+
projection_fn=project_conversation_turn,
|
|
1079
|
+
mode_fn=turn_concern_mode,
|
|
1080
|
+
bootstrap_mode_fn=_turn_bootstrap_mode,
|
|
1081
|
+
gap_policy_env="COLONY_TURN_CONCERNS_GAP_POLICY",
|
|
1082
|
+
malformed_skip_reason="malformed_turn_event",
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
@staticmethod
|
|
1086
|
+
def _config_error() -> str:
|
|
1087
|
+
_channels, channels_valid = _turn_channel_config()
|
|
1088
|
+
_prefixes, prefixes_valid = _turn_excluded_session_prefix_config()
|
|
1089
|
+
_platforms, platforms_valid = _turn_excluded_platform_config()
|
|
1090
|
+
if not channels_valid:
|
|
1091
|
+
return "turn_concern_channels_config_invalid"
|
|
1092
|
+
if not prefixes_valid:
|
|
1093
|
+
return "turn_concern_session_prefix_config_invalid"
|
|
1094
|
+
if not platforms_valid:
|
|
1095
|
+
return "turn_concern_excluded_platform_config_invalid"
|
|
1096
|
+
if not _configured_turn_owner():
|
|
1097
|
+
return "turn_concern_owner_identity_config_invalid"
|
|
1098
|
+
return ""
|
|
1099
|
+
|
|
1100
|
+
def run_once(self, *, limit: int = 100) -> Dict[str, Any]:
|
|
1101
|
+
if self.mode == "off":
|
|
1102
|
+
return super().run_once(limit=limit)
|
|
1103
|
+
error = self._config_error()
|
|
1104
|
+
if error:
|
|
1105
|
+
return self._stop(error)
|
|
1106
|
+
# A corrected configuration immediately releases a prior config stop;
|
|
1107
|
+
# the generic reducer then initializes/replays from the retained event.
|
|
1108
|
+
self.store.set_event_error(self.consumer_id, "")
|
|
1109
|
+
return super().run_once(limit=limit)
|
|
1110
|
+
|
|
1111
|
+
def status(self) -> Dict[str, Any]:
|
|
1112
|
+
state = super().status()
|
|
1113
|
+
channels, channels_valid = _turn_channel_config()
|
|
1114
|
+
state["allowed_channels"] = list(channels)
|
|
1115
|
+
state["channel_config_valid"] = channels_valid
|
|
1116
|
+
prefixes, valid = _turn_excluded_session_prefix_config()
|
|
1117
|
+
state["excluded_session_prefixes"] = list(prefixes)
|
|
1118
|
+
state["session_prefix_config_valid"] = valid
|
|
1119
|
+
platforms, platforms_valid = _turn_excluded_platform_config()
|
|
1120
|
+
state["excluded_platforms"] = list(platforms)
|
|
1121
|
+
state["excluded_platform_config_valid"] = platforms_valid
|
|
1122
|
+
state["owner_identity_configured"] = bool(_configured_turn_owner())
|
|
1123
|
+
config_error = self._config_error() if self.mode != "off" else ""
|
|
1124
|
+
state["config_error"] = config_error
|
|
1125
|
+
if config_error:
|
|
1126
|
+
state["last_error"] = config_error
|
|
1127
|
+
state["healthy"] = False
|
|
1128
|
+
return state
|