ltcai 11.5.2 → 11.6.0
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.
- package/README.md +91 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +75 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +21 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-CToZnOHz.js} +1 -1
- package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
- package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
- package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
- package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
- package/static/app/assets/circle-check-CY20XMCh.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
- package/static/app/assets/index-BovSWRmQ.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
- package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
- package/static/app/assets/search-DTxtjQ07.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
"""Shared setup/environment detection helpers."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
8
|
-
|
|
9
|
-
from latticeai.core.quiet import quiet
|
|
10
|
-
|
|
11
|
-
WhichFn = Callable[[str], Optional[str]]
|
|
12
|
-
RunFn = Callable[[List[str]], str]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def parse_windows_video_controllers(raw: str) -> List[Dict[str, Any]]:
|
|
16
|
-
controllers: List[Dict[str, Any]] = []
|
|
17
|
-
if not raw:
|
|
18
|
-
return controllers
|
|
19
|
-
try:
|
|
20
|
-
data = json.loads(raw)
|
|
21
|
-
if isinstance(data, dict):
|
|
22
|
-
data = [data]
|
|
23
|
-
if isinstance(data, list):
|
|
24
|
-
for item in data:
|
|
25
|
-
name = str(item.get("Name") or "").strip()
|
|
26
|
-
if not name:
|
|
27
|
-
continue
|
|
28
|
-
try:
|
|
29
|
-
ram_mb = int(item.get("AdapterRAM") or 0) // (1024 * 1024)
|
|
30
|
-
except Exception:
|
|
31
|
-
ram_mb = 0
|
|
32
|
-
controllers.append({"name": name, "vram_mb": ram_mb})
|
|
33
|
-
if controllers:
|
|
34
|
-
return controllers
|
|
35
|
-
except Exception:
|
|
36
|
-
quiet()
|
|
37
|
-
current: Dict[str, Any] = {}
|
|
38
|
-
for line in raw.splitlines():
|
|
39
|
-
if line.startswith("Name="):
|
|
40
|
-
if current:
|
|
41
|
-
controllers.append(current)
|
|
42
|
-
current = {"name": line.split("=", 1)[-1].strip(), "vram_mb": 0}
|
|
43
|
-
elif line.startswith("AdapterRAM=") and current:
|
|
44
|
-
try:
|
|
45
|
-
current["vram_mb"] = int(line.split("=", 1)[-1].strip()) // (1024 * 1024)
|
|
46
|
-
except ValueError:
|
|
47
|
-
current["vram_mb"] = 0
|
|
48
|
-
if current:
|
|
49
|
-
controllers.append(current)
|
|
50
|
-
return controllers
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def parse_windows_cpu_info(
|
|
55
|
-
raw: str,
|
|
56
|
-
*,
|
|
57
|
-
model: str,
|
|
58
|
-
physical_cores: int,
|
|
59
|
-
logical_cores: int,
|
|
60
|
-
) -> Tuple[str, int, int]:
|
|
61
|
-
"""Read ``wmic cpu get Name,NumberOfCores,NumberOfLogicalProcessors``.
|
|
62
|
-
|
|
63
|
-
Whatever the output does not say keeps the value the caller already had —
|
|
64
|
-
``wmic`` is absent on modern Windows images and prints nothing there, and
|
|
65
|
-
"0 cores" would be a worse answer than ``os.cpu_count()``'s guess.
|
|
66
|
-
"""
|
|
67
|
-
for line in raw.splitlines():
|
|
68
|
-
key, _, value = line.partition("=")
|
|
69
|
-
if key == "Name" and value.strip():
|
|
70
|
-
model = value.strip()
|
|
71
|
-
elif key == "NumberOfCores" and value.strip():
|
|
72
|
-
try:
|
|
73
|
-
physical_cores = int(value.strip())
|
|
74
|
-
except ValueError:
|
|
75
|
-
quiet()
|
|
76
|
-
elif key == "NumberOfLogicalProcessors" and value.strip():
|
|
77
|
-
try:
|
|
78
|
-
logical_cores = int(value.strip())
|
|
79
|
-
except ValueError:
|
|
80
|
-
quiet()
|
|
81
|
-
return model, physical_cores, logical_cores
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#: ``IsProcessorFeaturePresent`` codes worth reporting, by instruction name.
|
|
85
|
-
WINDOWS_PROCESSOR_FEATURES: Dict[int, str] = {
|
|
86
|
-
6: "sse", 10: "sse2", 13: "sse3", 19: "neon", 28: "rdrand",
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def windows_processor_features() -> List[str]:
|
|
91
|
-
"""Instruction-set flags from the Win32 API, or none anywhere else.
|
|
92
|
-
|
|
93
|
-
``ctypes.windll`` exists only on Windows, so on every other platform this
|
|
94
|
-
raises and answers "nothing known" — which is the honest reading, since the
|
|
95
|
-
flags are unobtainable rather than absent.
|
|
96
|
-
"""
|
|
97
|
-
try:
|
|
98
|
-
import ctypes
|
|
99
|
-
|
|
100
|
-
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined] # Windows-only
|
|
101
|
-
return [
|
|
102
|
-
name
|
|
103
|
-
for code, name in WINDOWS_PROCESSOR_FEATURES.items()
|
|
104
|
-
if kernel32.IsProcessorFeaturePresent(code)
|
|
105
|
-
]
|
|
106
|
-
except Exception:
|
|
107
|
-
quiet()
|
|
108
|
-
return []
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def detect_cuda(which: WhichFn, run: RunFn) -> Tuple[bool, str, Optional[str], Optional[str]]:
|
|
112
|
-
nvidia_smi = which("nvidia-smi")
|
|
113
|
-
nvcc = which("nvcc")
|
|
114
|
-
version = ""
|
|
115
|
-
if nvidia_smi:
|
|
116
|
-
raw = run([nvidia_smi, "--query-gpu=driver_version", "--format=csv,noheader"])
|
|
117
|
-
version = raw.splitlines()[0].strip() if raw.splitlines() else ""
|
|
118
|
-
if nvcc:
|
|
119
|
-
raw = run([nvcc, "--version"])
|
|
120
|
-
match = re.search(r"release\s+([\d.]+)", raw)
|
|
121
|
-
if match:
|
|
122
|
-
version = match.group(1)
|
|
123
|
-
return bool(nvidia_smi or nvcc), version, nvidia_smi, nvcc
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def detect_wsl_from_text(prof_os: str, raw: str) -> Tuple[bool, str]:
|
|
127
|
-
if prof_os.lower() != "linux":
|
|
128
|
-
return False, ""
|
|
129
|
-
lowered = (raw or "").lower()
|
|
130
|
-
is_wsl = "microsoft" in lowered or "wsl" in lowered
|
|
131
|
-
version = "2" if "microsoft-standard" in lowered or "wsl2" in lowered else ("1" if is_wsl else "")
|
|
132
|
-
return is_wsl, version
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def detect_tools(which: WhichFn, binaries: Iterable[str]) -> Dict[str, Optional[str]]:
|
|
136
|
-
return {binary: which(binary) for binary in binaries}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
__all__ = [
|
|
140
|
-
"WINDOWS_PROCESSOR_FEATURES",
|
|
141
|
-
"detect_cuda",
|
|
142
|
-
"detect_tools",
|
|
143
|
-
"detect_wsl_from_text",
|
|
144
|
-
"parse_windows_cpu_info",
|
|
145
|
-
"parse_windows_video_controllers",
|
|
146
|
-
"windows_processor_features",
|
|
147
|
-
]
|
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
"""Trigger system (T7d) — workflows fire beyond 'manual'.
|
|
2
|
-
|
|
3
|
-
Two real trigger types:
|
|
4
|
-
|
|
5
|
-
* **interval** — a supervised scheduler loop fires the workflow every
|
|
6
|
-
``interval_seconds``. Firings missed while the server was down are
|
|
7
|
-
SKIPPED with a recorded skip event (design-review amendment: no silent
|
|
8
|
-
gaps, no thundering catch-up).
|
|
9
|
-
* **brain_event** — the killer Digital Brain feature: "when new knowledge
|
|
10
|
-
enters the brain, run this workflow". Wired through the existing hooks
|
|
11
|
-
bus (the ingestion pipeline fires ``post_tool`` on ``kg_ingest.<source>``);
|
|
12
|
-
an optional ``source_type`` filter narrows it.
|
|
13
|
-
|
|
14
|
-
Trigger-fired runs carry provenance: their inputs include ``__trigger__``
|
|
15
|
-
describing what fired them, persisted in the run record like any other
|
|
16
|
-
input.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import logging
|
|
23
|
-
import os
|
|
24
|
-
import threading
|
|
25
|
-
import time
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
from zoneinfo import ZoneInfo
|
|
31
|
-
except Exception: # pragma: no cover
|
|
32
|
-
ZoneInfo = None # type: ignore
|
|
33
|
-
|
|
34
|
-
DEFAULT_TICK_SECONDS = 5.0
|
|
35
|
-
MIN_INTERVAL_SECONDS = 60
|
|
36
|
-
|
|
37
|
-
TRIGGER_HOOK_NAME = "brain-event-triggers"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class TriggerService:
|
|
41
|
-
"""Scans workflow definitions for non-manual triggers and fires them."""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
*,
|
|
46
|
-
store: Any,
|
|
47
|
-
run_workflow: Callable[[str, Dict[str, Any]], Dict[str, Any]],
|
|
48
|
-
data_dir: Path,
|
|
49
|
-
clock: Callable[[], float] = time.time,
|
|
50
|
-
tick_seconds: float = DEFAULT_TICK_SECONDS,
|
|
51
|
-
review_sink: Optional[Any] = None,
|
|
52
|
-
tz_name: Optional[str] = None,
|
|
53
|
-
) -> None:
|
|
54
|
-
self._store = store
|
|
55
|
-
self._run_workflow = run_workflow
|
|
56
|
-
# Optional review-queue seam (5.6.0). When wired, fired runs can drop a
|
|
57
|
-
# reviewable suggestion into the queue. Default None keeps legacy behavior.
|
|
58
|
-
self._review_sink = review_sink
|
|
59
|
-
self._state_file = Path(data_dir) / "triggers_state.json"
|
|
60
|
-
self._clock = clock
|
|
61
|
-
self._tick = float(tick_seconds)
|
|
62
|
-
self._stop_event = threading.Event()
|
|
63
|
-
self._thread: Optional[threading.Thread] = None
|
|
64
|
-
self._lock = threading.Lock()
|
|
65
|
-
# LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
|
|
66
|
-
# describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
|
|
67
|
-
# Prefer explicit tz_name (from Config) for DI; fall back to env for legacy/compat.
|
|
68
|
-
self._tz_name = (tz_name or os.environ.get("LATTICE_TZ") or "UTC").strip() or "UTC"
|
|
69
|
-
self._tz = None
|
|
70
|
-
if ZoneInfo is not None:
|
|
71
|
-
try:
|
|
72
|
-
self._tz = ZoneInfo(self._tz_name)
|
|
73
|
-
except Exception:
|
|
74
|
-
self._tz = ZoneInfo("UTC")
|
|
75
|
-
|
|
76
|
-
# ── durable state ──────────────────────────────────────────────────────
|
|
77
|
-
def _load_state(self) -> Dict[str, Any]:
|
|
78
|
-
if not self._state_file.exists():
|
|
79
|
-
return {}
|
|
80
|
-
try:
|
|
81
|
-
return json.loads(self._state_file.read_text(encoding="utf-8"))
|
|
82
|
-
except Exception:
|
|
83
|
-
return {}
|
|
84
|
-
|
|
85
|
-
def _save_state(self, state: Dict[str, Any]) -> None:
|
|
86
|
-
self._state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
-
tmp = self._state_file.with_suffix(".tmp")
|
|
88
|
-
tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
89
|
-
tmp.replace(self._state_file)
|
|
90
|
-
|
|
91
|
-
def _record_event(self, state: Dict[str, Any], workflow_id: str, event: Dict[str, Any]) -> None:
|
|
92
|
-
entry = state.setdefault(workflow_id, {})
|
|
93
|
-
events = entry.setdefault("events", [])
|
|
94
|
-
events.append({**event, "at": self._clock()})
|
|
95
|
-
entry["events"] = events[-50:]
|
|
96
|
-
|
|
97
|
-
# ── definition scanning ────────────────────────────────────────────────
|
|
98
|
-
def _triggered_workflows(self) -> List[Dict[str, Any]]:
|
|
99
|
-
found = []
|
|
100
|
-
try:
|
|
101
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
102
|
-
except Exception:
|
|
103
|
-
return []
|
|
104
|
-
for wf in workflows:
|
|
105
|
-
for node in wf.get("nodes") or []:
|
|
106
|
-
if node.get("type") != "trigger":
|
|
107
|
-
continue
|
|
108
|
-
cfg = node.get("config") or {}
|
|
109
|
-
kind = str(cfg.get("trigger") or "manual")
|
|
110
|
-
if cfg.get("enabled") is False:
|
|
111
|
-
continue
|
|
112
|
-
if kind in ("interval", "brain_event"):
|
|
113
|
-
found.append({"workflow": wf, "node": node, "kind": kind, "config": cfg})
|
|
114
|
-
return found
|
|
115
|
-
|
|
116
|
-
def describe(self) -> Dict[str, Any]:
|
|
117
|
-
"""Honest status surface: what is armed, when it last fired/skipped.
|
|
118
|
-
Includes LATTICE_TZ, per-trigger status (armed|degraded), consecutive_failures.
|
|
119
|
-
"""
|
|
120
|
-
state = self._load_state()
|
|
121
|
-
armed = []
|
|
122
|
-
for item in self._triggered_workflows():
|
|
123
|
-
wf_id = item["workflow"].get("id")
|
|
124
|
-
entry = state.get(wf_id) or {}
|
|
125
|
-
fails = int(entry.get("consecutive_failures", 0))
|
|
126
|
-
status = "degraded" if fails >= 3 else "armed"
|
|
127
|
-
armed.append({
|
|
128
|
-
"workflow_id": wf_id,
|
|
129
|
-
"name": item["workflow"].get("name"),
|
|
130
|
-
"kind": item["kind"],
|
|
131
|
-
"config": {k: v for k, v in item["config"].items() if k != "trigger"},
|
|
132
|
-
"last_fired_at": entry.get("last_fired_at"),
|
|
133
|
-
"status": status,
|
|
134
|
-
"consecutive_failures": fails,
|
|
135
|
-
"recent_events": entry.get("events", [])[-5:],
|
|
136
|
-
})
|
|
137
|
-
return {
|
|
138
|
-
"running": bool(self._thread and self._thread.is_alive()),
|
|
139
|
-
"tick_seconds": self._tick,
|
|
140
|
-
"tz": self._tz_name,
|
|
141
|
-
"armed": armed,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
# ── interval scheduling ────────────────────────────────────────────────
|
|
145
|
-
def reconcile_missed(self) -> int:
|
|
146
|
-
"""Startup pass: record (never replay) firings missed while down."""
|
|
147
|
-
now = self._clock()
|
|
148
|
-
skipped = 0
|
|
149
|
-
with self._lock:
|
|
150
|
-
state = self._load_state()
|
|
151
|
-
for item in self._triggered_workflows():
|
|
152
|
-
if item["kind"] != "interval":
|
|
153
|
-
continue
|
|
154
|
-
wf_id = item["workflow"].get("id")
|
|
155
|
-
interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
|
|
156
|
-
entry = state.setdefault(wf_id, {})
|
|
157
|
-
last = entry.get("last_fired_at")
|
|
158
|
-
if last is not None and now - float(last) > interval:
|
|
159
|
-
missed = int((now - float(last)) // interval)
|
|
160
|
-
self._record_event(state, wf_id, {
|
|
161
|
-
"type": "skipped",
|
|
162
|
-
"reason": f"{missed} interval firing(s) missed while the server was down",
|
|
163
|
-
})
|
|
164
|
-
skipped += missed
|
|
165
|
-
# Reset the cadence from now — no catch-up storm.
|
|
166
|
-
entry["last_fired_at"] = now if last is not None else entry.get("last_fired_at")
|
|
167
|
-
entry["last_attempt_at"] = now
|
|
168
|
-
self._save_state(state)
|
|
169
|
-
return skipped
|
|
170
|
-
|
|
171
|
-
def tick_intervals(self) -> int:
|
|
172
|
-
"""One scheduler pass; returns how many workflows fired."""
|
|
173
|
-
now = self._clock()
|
|
174
|
-
fired = 0
|
|
175
|
-
with self._lock:
|
|
176
|
-
state = self._load_state()
|
|
177
|
-
for item in self._triggered_workflows():
|
|
178
|
-
if item["kind"] != "interval":
|
|
179
|
-
continue
|
|
180
|
-
wf_id = item["workflow"].get("id")
|
|
181
|
-
interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
|
|
182
|
-
entry = state.setdefault(wf_id, {})
|
|
183
|
-
last = entry.get("last_fired_at")
|
|
184
|
-
if last is None:
|
|
185
|
-
# First sighting arms the schedule; it fires one interval later.
|
|
186
|
-
entry["last_fired_at"] = now
|
|
187
|
-
entry["last_attempt_at"] = now
|
|
188
|
-
continue
|
|
189
|
-
if now - float(last) < interval:
|
|
190
|
-
continue
|
|
191
|
-
# Dedup guard (edge case): short cooldown + last_attempt prevents rapid re-fire on
|
|
192
|
-
# clock skew, tick jitter, or restart races. Interval 자체 + 이 가드로 중복 실행 방지.
|
|
193
|
-
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
194
|
-
if now - last_attempt < 10:
|
|
195
|
-
continue
|
|
196
|
-
entry["last_attempt_at"] = now
|
|
197
|
-
entry["last_fired_at"] = now
|
|
198
|
-
self._record_event(state, wf_id, {"type": "fired", "trigger": "interval"})
|
|
199
|
-
fired += 1
|
|
200
|
-
self._fire(wf_id, {
|
|
201
|
-
"type": "interval",
|
|
202
|
-
"interval_seconds": interval,
|
|
203
|
-
"fired_at": now,
|
|
204
|
-
})
|
|
205
|
-
self._save_state(state)
|
|
206
|
-
return fired
|
|
207
|
-
|
|
208
|
-
# ── brain events ───────────────────────────────────────────────────────
|
|
209
|
-
def on_brain_event(self, event: str, payload: Optional[Dict[str, Any]] = None) -> int:
|
|
210
|
-
"""Fire workflows whose brain_event trigger matches this ingestion."""
|
|
211
|
-
payload = payload or {}
|
|
212
|
-
source_type = str(payload.get("source_type") or _ingestion_source_type(event, payload) or "")
|
|
213
|
-
event_workspace = str(payload.get("workspace_id") or "personal").strip()
|
|
214
|
-
event_user = str(payload.get("user_email") or "").strip().lower()
|
|
215
|
-
fired = 0
|
|
216
|
-
with self._lock:
|
|
217
|
-
state = self._load_state()
|
|
218
|
-
for item in self._triggered_workflows():
|
|
219
|
-
if item["kind"] != "brain_event":
|
|
220
|
-
continue
|
|
221
|
-
workflow_workspace = str(item["workflow"].get("workspace_id") or "personal").strip()
|
|
222
|
-
workflow_user = str(item["workflow"].get("user_email") or "").strip().lower()
|
|
223
|
-
if event_workspace != workflow_workspace:
|
|
224
|
-
continue
|
|
225
|
-
# Owned workflows only respond to their owner's ingestion.
|
|
226
|
-
# Ownerless legacy workflows remain compatible in personal.
|
|
227
|
-
if workflow_user and workflow_user != event_user:
|
|
228
|
-
continue
|
|
229
|
-
wanted = str(item["config"].get("source_type") or "").strip()
|
|
230
|
-
if wanted and wanted != source_type:
|
|
231
|
-
continue
|
|
232
|
-
wf_id = item["workflow"].get("id")
|
|
233
|
-
entry = state.setdefault(wf_id, {})
|
|
234
|
-
now = self._clock()
|
|
235
|
-
# Dedup guard for brain_event too (rapid ingest burst 등).
|
|
236
|
-
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
237
|
-
if now - last_attempt < 5:
|
|
238
|
-
continue
|
|
239
|
-
entry["last_attempt_at"] = now
|
|
240
|
-
entry["last_fired_at"] = now
|
|
241
|
-
self._record_event(state, wf_id, {
|
|
242
|
-
"type": "fired", "trigger": "brain_event", "source_type": source_type,
|
|
243
|
-
})
|
|
244
|
-
fired += 1
|
|
245
|
-
self._fire(wf_id, {
|
|
246
|
-
"type": "brain_event",
|
|
247
|
-
"event": event,
|
|
248
|
-
"source_type": source_type,
|
|
249
|
-
"node_id": payload.get("node_id"),
|
|
250
|
-
"user_email": payload.get("user_email"),
|
|
251
|
-
"workspace_id": payload.get("workspace_id"),
|
|
252
|
-
})
|
|
253
|
-
self._save_state(state)
|
|
254
|
-
return fired
|
|
255
|
-
|
|
256
|
-
def hook_runner(self):
|
|
257
|
-
"""A post_tool hook runner: ingestion events fan into triggers."""
|
|
258
|
-
def runner(context):
|
|
259
|
-
event = str(getattr(context, "event", "") or "")
|
|
260
|
-
payload = context.payload if isinstance(context.payload, dict) else {}
|
|
261
|
-
source_type = _ingestion_source_type(event, payload)
|
|
262
|
-
if not source_type:
|
|
263
|
-
return {"status": "ok", "output": "not an ingestion event"}
|
|
264
|
-
if str(payload.get("status") or "ok") != "ok":
|
|
265
|
-
return {"status": "ok", "output": "ignored failed ingestion event"}
|
|
266
|
-
fired = self.on_brain_event(
|
|
267
|
-
f"kg_ingest.{source_type}",
|
|
268
|
-
{
|
|
269
|
-
**payload,
|
|
270
|
-
"source_type": source_type,
|
|
271
|
-
"user_email": getattr(context, "user_email", None) or payload.get("user_email"),
|
|
272
|
-
"workspace_id": getattr(context, "workspace_id", None) or payload.get("workspace_id"),
|
|
273
|
-
},
|
|
274
|
-
)
|
|
275
|
-
return {"status": "ok", "output": f"fired {fired} workflow trigger(s)"}
|
|
276
|
-
return runner
|
|
277
|
-
|
|
278
|
-
# ── execution + lifecycle ──────────────────────────────────────────────
|
|
279
|
-
def _fire(self, workflow_id: str, trigger_info: Dict[str, Any]) -> None:
|
|
280
|
-
try:
|
|
281
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
282
|
-
workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None) or {}
|
|
283
|
-
except Exception:
|
|
284
|
-
workflow = {}
|
|
285
|
-
trigger_info = dict(trigger_info)
|
|
286
|
-
if not trigger_info.get("user_email"):
|
|
287
|
-
trigger_info["user_email"] = workflow.get("user_email")
|
|
288
|
-
if not trigger_info.get("workspace_id"):
|
|
289
|
-
trigger_info["workspace_id"] = workflow.get("workspace_id")
|
|
290
|
-
|
|
291
|
-
def _run():
|
|
292
|
-
try:
|
|
293
|
-
result = self._run_workflow(workflow_id, {"__trigger__": trigger_info})
|
|
294
|
-
self._record_fire_outcome(workflow_id, ok=True)
|
|
295
|
-
self._maybe_enqueue_review(workflow_id, trigger_info, result)
|
|
296
|
-
except Exception as exc:
|
|
297
|
-
logging.warning("trigger run failed for %s: %s", workflow_id, exc)
|
|
298
|
-
self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
|
|
299
|
-
|
|
300
|
-
threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
|
|
301
|
-
|
|
302
|
-
def _maybe_enqueue_review(
|
|
303
|
-
self, workflow_id: str, trigger_info: Dict[str, Any], result: Dict[str, Any],
|
|
304
|
-
) -> None:
|
|
305
|
-
if self._review_sink is None:
|
|
306
|
-
return
|
|
307
|
-
try:
|
|
308
|
-
from latticeai.services.review_queue import enqueue_from_automation
|
|
309
|
-
|
|
310
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
311
|
-
workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None)
|
|
312
|
-
if workflow is None:
|
|
313
|
-
return
|
|
314
|
-
trigger_type = str(trigger_info.get("type") or "")
|
|
315
|
-
source = "kg_change_digest" if trigger_type == "brain_event" else "trigger"
|
|
316
|
-
enqueue_from_automation(
|
|
317
|
-
self._review_sink,
|
|
318
|
-
workflow=workflow,
|
|
319
|
-
source=source,
|
|
320
|
-
run_result=result,
|
|
321
|
-
trigger_info=trigger_info,
|
|
322
|
-
user_email=trigger_info.get("user_email") or workflow.get("user_email"),
|
|
323
|
-
workspace_id=trigger_info.get("workspace_id") or workflow.get("workspace_id"),
|
|
324
|
-
)
|
|
325
|
-
except Exception as exc:
|
|
326
|
-
logging.warning("review_sink enqueue failed for %s: %s", workflow_id, exc)
|
|
327
|
-
|
|
328
|
-
def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
|
|
329
|
-
"""Track consecutive launch failures for degraded status in describe().
|
|
330
|
-
(Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
|
|
331
|
-
"""
|
|
332
|
-
with self._lock:
|
|
333
|
-
state = self._load_state()
|
|
334
|
-
entry = state.setdefault(wf_id, {})
|
|
335
|
-
if ok:
|
|
336
|
-
entry["consecutive_failures"] = 0
|
|
337
|
-
else:
|
|
338
|
-
fails = int(entry.get("consecutive_failures", 0)) + 1
|
|
339
|
-
entry["consecutive_failures"] = fails
|
|
340
|
-
self._record_event(state, wf_id, {"type": "failed", "detail": detail[:200]})
|
|
341
|
-
self._save_state(state)
|
|
342
|
-
|
|
343
|
-
def start(self) -> None:
|
|
344
|
-
if self._thread and self._thread.is_alive():
|
|
345
|
-
return
|
|
346
|
-
self.reconcile_missed()
|
|
347
|
-
self._stop_event.clear()
|
|
348
|
-
|
|
349
|
-
def _loop():
|
|
350
|
-
while not self._stop_event.wait(self._tick):
|
|
351
|
-
try:
|
|
352
|
-
self.tick_intervals()
|
|
353
|
-
except Exception as exc:
|
|
354
|
-
logging.warning("trigger scheduler tick failed: %s", exc)
|
|
355
|
-
|
|
356
|
-
self._thread = threading.Thread(target=_loop, name="trigger-scheduler", daemon=True)
|
|
357
|
-
self._thread.start()
|
|
358
|
-
|
|
359
|
-
def stop(self) -> None:
|
|
360
|
-
self._stop_event.set()
|
|
361
|
-
if self._thread:
|
|
362
|
-
self._thread.join(timeout=2)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _ingestion_source_type(event: str, payload: Dict[str, Any]) -> str:
|
|
366
|
-
"""Normalize the real dispatch event (``tool.kg_ingest.*``) and legacy form."""
|
|
367
|
-
tool = str(payload.get("tool") or "").strip()
|
|
368
|
-
if tool.startswith("kg_ingest."):
|
|
369
|
-
return tool.removeprefix("kg_ingest.").strip()
|
|
370
|
-
normalized = str(event or "").strip()
|
|
371
|
-
if normalized.startswith("tool.kg_ingest."):
|
|
372
|
-
return normalized.removeprefix("tool.kg_ingest.").strip()
|
|
373
|
-
if normalized.startswith("kg_ingest."):
|
|
374
|
-
return normalized.removeprefix("kg_ingest.").strip()
|
|
375
|
-
return ""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
__all__ = ["TriggerService", "TRIGGER_HOOK_NAME", "MIN_INTERVAL_SECONDS"]
|