ltcai 11.5.2 → 11.7.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 +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -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 +54 -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-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.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/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- 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/primitives-SNp0LRJz.js +0 -1
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,978 +0,0 @@
|
|
|
1
|
-
"""Workspace OS persistence and orchestration primitives.
|
|
2
|
-
|
|
3
|
-
This module keeps the 1.0 Workspace OS surface intentionally local-first:
|
|
4
|
-
state is stored as JSON under the configured LatticeAI data directory, graph
|
|
5
|
-
operations are additive, and snapshots are immutable files that can be
|
|
6
|
-
exported or compared without mutating the live knowledge graph.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import shutil
|
|
13
|
-
import sqlite3
|
|
14
|
-
from contextlib import contextmanager
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional
|
|
17
|
-
|
|
18
|
-
from latticeai.core.quiet import quiet
|
|
19
|
-
|
|
20
|
-
from .timeutil import now_iso as _now
|
|
21
|
-
from .workspace_computer_memory import WorkspaceComputerMemory
|
|
22
|
-
from .workspace_graph_trace import WorkspaceGraphTrace
|
|
23
|
-
from .workspace_indexing import WorkspaceIndexing
|
|
24
|
-
from .workspace_memory import WorkspaceMemory
|
|
25
|
-
from .workspace_onboarding import WorkspaceOnboarding
|
|
26
|
-
from .workspace_os_constants import (
|
|
27
|
-
DEFAULT_WORKSPACE_ID,
|
|
28
|
-
EXECUTION_EVENT_TYPES,
|
|
29
|
-
MEMORY_KINDS,
|
|
30
|
-
ONBOARDING_STEPS,
|
|
31
|
-
ROLE_PERMISSIONS,
|
|
32
|
-
WORKSPACE_AREAS,
|
|
33
|
-
WORKSPACE_OS_VERSION,
|
|
34
|
-
WORKSPACE_PERMISSIONS,
|
|
35
|
-
WORKSPACE_ROLES,
|
|
36
|
-
WORKSPACE_TYPES,
|
|
37
|
-
)
|
|
38
|
-
from .workspace_os_state import default_state, migrate_workspaces, new_workspace_record
|
|
39
|
-
|
|
40
|
-
# Extracted pure helpers (keeps this module smaller and focused on the store).
|
|
41
|
-
from .workspace_os_utils import (
|
|
42
|
-
_atomic_write_json,
|
|
43
|
-
_deep_merge,
|
|
44
|
-
_json_hash,
|
|
45
|
-
_listify,
|
|
46
|
-
_safe_slug,
|
|
47
|
-
remove_skill_directory,
|
|
48
|
-
)
|
|
49
|
-
from .workspace_permissions import ( # type: ignore
|
|
50
|
-
WorkspacePermissionManager,
|
|
51
|
-
_member_role,
|
|
52
|
-
)
|
|
53
|
-
from .workspace_plugins import WorkspacePluginManager
|
|
54
|
-
from .workspace_relationships import WorkspaceRelationships
|
|
55
|
-
from .workspace_relationships import shortest_path as _shortest_path
|
|
56
|
-
from .workspace_review_items import WorkspaceReviewItems
|
|
57
|
-
from .workspace_runs import WorkspaceRuns
|
|
58
|
-
from .workspace_skills import WorkspaceSkills
|
|
59
|
-
from .workspace_snapshots import WorkspaceSnapshots
|
|
60
|
-
from .workspace_timeline import WorkspaceTimeline
|
|
61
|
-
|
|
62
|
-
__all__ = [
|
|
63
|
-
"WORKSPACE_OS_VERSION",
|
|
64
|
-
"WORKSPACE_TYPES",
|
|
65
|
-
"DEFAULT_WORKSPACE_ID",
|
|
66
|
-
"WORKSPACE_ROLES",
|
|
67
|
-
"WORKSPACE_PERMISSIONS",
|
|
68
|
-
"ROLE_PERMISSIONS",
|
|
69
|
-
"WORKSPACE_AREAS",
|
|
70
|
-
"ONBOARDING_STEPS",
|
|
71
|
-
"MEMORY_KINDS",
|
|
72
|
-
"EXECUTION_EVENT_TYPES",
|
|
73
|
-
"WorkspaceOSStore",
|
|
74
|
-
"remove_skill_directory",
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class WorkspaceOSStore:
|
|
90
|
-
"""Local-first state store for Workspace OS APIs."""
|
|
91
|
-
|
|
92
|
-
def __init__(self, data_dir: Path | str, *, event_sink: Optional[Callable[[Dict[str, Any]], Any]] = None):
|
|
93
|
-
self.data_dir = Path(data_dir).expanduser()
|
|
94
|
-
self.state_path = self.data_dir / "workspace_os.json"
|
|
95
|
-
self.sqlite_path = self.data_dir / "knowledge_graph.sqlite"
|
|
96
|
-
self.snapshots_dir = self.data_dir / "workspace_snapshots"
|
|
97
|
-
self.exports_dir = self.data_dir / "workspace_exports"
|
|
98
|
-
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
self.snapshots_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
-
self.exports_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
-
# Optional realtime hook: fired on every timeline event so the Realtime
|
|
102
|
-
# bus receives all workspace activity without per-call wiring.
|
|
103
|
-
# Defaults to None → zero behavior change for existing callers/tests.
|
|
104
|
-
self.event_sink = event_sink
|
|
105
|
-
# Composed managers (extracted to shrink god class per architecture rules).
|
|
106
|
-
self.permissions = WorkspacePermissionManager(self)
|
|
107
|
-
self._timeline = WorkspaceTimeline(self)
|
|
108
|
-
self.plugins = WorkspacePluginManager(self)
|
|
109
|
-
self.memory = WorkspaceMemory(self)
|
|
110
|
-
self.snapshots = WorkspaceSnapshots(self)
|
|
111
|
-
self.graph_trace = WorkspaceGraphTrace(self)
|
|
112
|
-
self.runs = WorkspaceRuns(self)
|
|
113
|
-
self.review_items = WorkspaceReviewItems(self)
|
|
114
|
-
self.skills = WorkspaceSkills(self)
|
|
115
|
-
self.indexing = WorkspaceIndexing(self)
|
|
116
|
-
self.relationships = WorkspaceRelationships(self)
|
|
117
|
-
self.computer_memory = WorkspaceComputerMemory(self)
|
|
118
|
-
self.onboarding = WorkspaceOnboarding(self)
|
|
119
|
-
|
|
120
|
-
@contextmanager
|
|
121
|
-
def _connect_state_db(self) -> Iterator[sqlite3.Connection]:
|
|
122
|
-
"""Transactional connection that is closed when the block exits.
|
|
123
|
-
|
|
124
|
-
``with sqlite3.connect(...)`` commits but never closes; see
|
|
125
|
-
:meth:`lattice_brain.storage.base.StorageEngine.session`.
|
|
126
|
-
"""
|
|
127
|
-
conn = sqlite3.connect(self.sqlite_path)
|
|
128
|
-
conn.execute("PRAGMA journal_mode=WAL")
|
|
129
|
-
conn.execute(
|
|
130
|
-
"CREATE TABLE IF NOT EXISTS workspace_os_state ("
|
|
131
|
-
"id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TEXT NOT NULL)"
|
|
132
|
-
)
|
|
133
|
-
conn.execute(
|
|
134
|
-
"CREATE TABLE IF NOT EXISTS workspace_os_meta ("
|
|
135
|
-
"key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
|
136
|
-
)
|
|
137
|
-
try:
|
|
138
|
-
with conn:
|
|
139
|
-
yield conn
|
|
140
|
-
finally:
|
|
141
|
-
conn.close()
|
|
142
|
-
|
|
143
|
-
# ── moved to workspace_os_state.py (10.2.0) ───────────────────────────────
|
|
144
|
-
# Kept as delegators: `_default_state` and `_migrate_workspaces` are called
|
|
145
|
-
# by name in tests and by subclasses, and `_new_workspace_record` is used
|
|
146
|
-
# across the manager classes. The behaviour lives in one place now; these
|
|
147
|
-
# keep every existing call site working.
|
|
148
|
-
@staticmethod
|
|
149
|
-
def _new_workspace_record(**kwargs) -> Dict[str, Any]:
|
|
150
|
-
return new_workspace_record(**kwargs)
|
|
151
|
-
|
|
152
|
-
def _migrate_workspaces(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
153
|
-
return migrate_workspaces(state)
|
|
154
|
-
|
|
155
|
-
def _default_state(self) -> Dict[str, Any]:
|
|
156
|
-
return default_state()
|
|
157
|
-
|
|
158
|
-
def _load_sqlite_state(self) -> Optional[Dict[str, Any]]:
|
|
159
|
-
try:
|
|
160
|
-
with self._connect_state_db() as conn:
|
|
161
|
-
row = conn.execute(
|
|
162
|
-
"SELECT state_json FROM workspace_os_state WHERE id='current'"
|
|
163
|
-
).fetchone()
|
|
164
|
-
if not row:
|
|
165
|
-
return None
|
|
166
|
-
data = json.loads(row[0])
|
|
167
|
-
return data if isinstance(data, dict) else None
|
|
168
|
-
except Exception:
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
def _save_sqlite_state(self, state: Dict[str, Any]) -> None:
|
|
172
|
-
payload = json.dumps(state, ensure_ascii=False)
|
|
173
|
-
with self._connect_state_db() as conn:
|
|
174
|
-
conn.execute(
|
|
175
|
-
"INSERT OR REPLACE INTO workspace_os_state(id, state_json, updated_at) VALUES('current', ?, ?)",
|
|
176
|
-
(payload, state.get("updated_at") or _now()),
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
def _import_json_state_once(self, default: Dict[str, Any]) -> Dict[str, Any]:
|
|
180
|
-
if not self.state_path.exists():
|
|
181
|
-
return default
|
|
182
|
-
try:
|
|
183
|
-
loaded = json.loads(self.state_path.read_text(encoding="utf-8"))
|
|
184
|
-
if not isinstance(loaded, dict):
|
|
185
|
-
return default
|
|
186
|
-
except Exception:
|
|
187
|
-
return default
|
|
188
|
-
try:
|
|
189
|
-
backup = self.state_path.with_name(
|
|
190
|
-
f"{self.state_path.name}.pre-sqlite.{_now().replace(':', '-')}.json"
|
|
191
|
-
)
|
|
192
|
-
if not any(self.state_path.parent.glob(f"{self.state_path.name}.pre-sqlite.*.json")):
|
|
193
|
-
shutil.copy2(self.state_path, backup)
|
|
194
|
-
except Exception:
|
|
195
|
-
quiet()
|
|
196
|
-
return _deep_merge(default, loaded)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def migrate_workspace_identities(self, email_to_id: Dict[str, str]) -> int:
|
|
201
|
-
"""Rewrite workspace membership identities from legacy emails to UUIDs.
|
|
202
|
-
|
|
203
|
-
The migration is additive and in-place: workspace records, memberships,
|
|
204
|
-
and owner fields keep their shape, only identity string values change.
|
|
205
|
-
"""
|
|
206
|
-
if not email_to_id:
|
|
207
|
-
return 0
|
|
208
|
-
normalized = {str(email).strip().lower(): user_id for email, user_id in email_to_id.items() if user_id}
|
|
209
|
-
state = self.load_state()
|
|
210
|
-
changed = 0
|
|
211
|
-
for ws in (state.get("workspaces") or {}).values():
|
|
212
|
-
owner = str(ws.get("owner_user_id") or "").strip().lower()
|
|
213
|
-
if owner in normalized and ws.get("owner_user_id") != normalized[owner]:
|
|
214
|
-
ws["owner_user_id"] = normalized[owner]
|
|
215
|
-
changed += 1
|
|
216
|
-
for member in _listify(ws.get("members")):
|
|
217
|
-
member_id = str(member.get("user_id") or "").strip().lower()
|
|
218
|
-
if member_id in normalized and member.get("user_id") != normalized[member_id]:
|
|
219
|
-
member["user_id"] = normalized[member_id]
|
|
220
|
-
member["updated_at"] = _now()
|
|
221
|
-
changed += 1
|
|
222
|
-
members = _listify(ws.get("members"))
|
|
223
|
-
deduped = []
|
|
224
|
-
seen_members = set()
|
|
225
|
-
for member in members:
|
|
226
|
-
member_id = member.get("user_id")
|
|
227
|
-
if member_id and member_id in seen_members:
|
|
228
|
-
changed += 1
|
|
229
|
-
continue
|
|
230
|
-
if member_id:
|
|
231
|
-
seen_members.add(member_id)
|
|
232
|
-
deduped.append(member)
|
|
233
|
-
if len(deduped) != len(members):
|
|
234
|
-
ws["members"] = deduped
|
|
235
|
-
if changed:
|
|
236
|
-
state["updated_at"] = _now()
|
|
237
|
-
self.save_state(state)
|
|
238
|
-
self.record_timeline_event("workspace", "identity_uuid_migrated", {"records": changed})
|
|
239
|
-
return changed
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def load_state(self) -> Dict[str, Any]:
|
|
243
|
-
default = self._default_state()
|
|
244
|
-
loaded = self._load_sqlite_state()
|
|
245
|
-
imported = loaded is None
|
|
246
|
-
if loaded is None:
|
|
247
|
-
loaded = self._import_json_state_once(default)
|
|
248
|
-
state = _deep_merge(default, loaded)
|
|
249
|
-
state["version"] = WORKSPACE_OS_VERSION
|
|
250
|
-
self._migrate_workspaces(state)
|
|
251
|
-
if imported:
|
|
252
|
-
self.save_state(state)
|
|
253
|
-
return state
|
|
254
|
-
|
|
255
|
-
def save_state(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
256
|
-
state["version"] = WORKSPACE_OS_VERSION
|
|
257
|
-
state["updated_at"] = _now()
|
|
258
|
-
self._save_sqlite_state(state)
|
|
259
|
-
_atomic_write_json(self.state_path, state)
|
|
260
|
-
return state
|
|
261
|
-
|
|
262
|
-
def record_timeline_event(self, area: str, event_type: str, payload: Dict[str, Any], workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
263
|
-
return self._timeline.record_timeline_event(area, event_type, payload, workspace_id=workspace_id)
|
|
264
|
-
|
|
265
|
-
def _emit_execution_event(
|
|
266
|
-
self,
|
|
267
|
-
*,
|
|
268
|
-
area: str,
|
|
269
|
-
event_type: str,
|
|
270
|
-
payload: Dict[str, Any],
|
|
271
|
-
workspace_id: Optional[str],
|
|
272
|
-
) -> None:
|
|
273
|
-
"""Best-effort execution observability event for the realtime feed."""
|
|
274
|
-
if event_type not in EXECUTION_EVENT_TYPES:
|
|
275
|
-
return
|
|
276
|
-
try:
|
|
277
|
-
self.record_timeline_event(area, event_type, payload, workspace_id=workspace_id)
|
|
278
|
-
except Exception:
|
|
279
|
-
quiet()
|
|
280
|
-
|
|
281
|
-
def _emit_replayable_timeline_events(
|
|
282
|
-
self,
|
|
283
|
-
*,
|
|
284
|
-
area: str,
|
|
285
|
-
run_id: str,
|
|
286
|
-
timeline: List[Dict[str, Any]],
|
|
287
|
-
workspace_id: Optional[str],
|
|
288
|
-
) -> None:
|
|
289
|
-
for index, item in enumerate(timeline or []):
|
|
290
|
-
event_type = item.get("event") or item.get("event_type")
|
|
291
|
-
if event_type in EXECUTION_EVENT_TYPES:
|
|
292
|
-
payload = {k: v for k, v in item.items() if k not in {"context_packet"}}
|
|
293
|
-
payload["run_id"] = run_id
|
|
294
|
-
payload["timeline_index"] = index
|
|
295
|
-
self._emit_execution_event(area=area, event_type=event_type, payload=payload, workspace_id=workspace_id)
|
|
296
|
-
|
|
297
|
-
def summary(self) -> Dict[str, Any]:
|
|
298
|
-
state = self.load_state()
|
|
299
|
-
return {
|
|
300
|
-
"version": WORKSPACE_OS_VERSION,
|
|
301
|
-
"identity": state.get("identity"),
|
|
302
|
-
"active_workspace": state.get("active_workspace"),
|
|
303
|
-
# The raw workspace registry (with member lists) must not leak to
|
|
304
|
-
# non-members; WorkspaceService.summary() adds a membership-filtered
|
|
305
|
-
# "workspace_registry" instead.
|
|
306
|
-
"workspace_count": len(state.get("workspaces") or {}),
|
|
307
|
-
"navigation": list(WORKSPACE_AREAS),
|
|
308
|
-
"feature_flags": state.get("feature_flags"),
|
|
309
|
-
"updated_at": state.get("updated_at"),
|
|
310
|
-
"counts": {
|
|
311
|
-
"snapshots": len(_listify(state.get("snapshots"))),
|
|
312
|
-
"traces": len(_listify(state.get("traces"))),
|
|
313
|
-
"memories": len(_listify(state.get("memories"))),
|
|
314
|
-
"memory_snapshots": len(_listify(state.get("memory_snapshots"))),
|
|
315
|
-
"agent_runs": len(_listify(state.get("agent_runs"))),
|
|
316
|
-
"handoffs": len(_listify(state.get("handoffs"))),
|
|
317
|
-
"workflows": len(_listify(state.get("workflows"))),
|
|
318
|
-
"workflow_runs": len(_listify(state.get("workflow_runs"))),
|
|
319
|
-
"skills": len(state.get("skill_registry") or {}),
|
|
320
|
-
"plugins": len(state.get("plugin_registry") or {}),
|
|
321
|
-
"templates": len(state.get("template_registry") or {}),
|
|
322
|
-
"timeline": len(_listify(state.get("timeline"))),
|
|
323
|
-
},
|
|
324
|
-
"onboarding": state.get("onboarding"),
|
|
325
|
-
"storage": {
|
|
326
|
-
"state_path": str(self.state_path),
|
|
327
|
-
"snapshots_dir": str(self.snapshots_dir),
|
|
328
|
-
"exports_dir": str(self.exports_dir),
|
|
329
|
-
},
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
# ------------------------------------------------------------------
|
|
333
|
-
# Organization workspaces, membership, and roles
|
|
334
|
-
# ------------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
def _active_workspace_id(self, state: Optional[Dict[str, Any]] = None) -> str:
|
|
337
|
-
state = state or self.load_state()
|
|
338
|
-
active = state.get("active_workspace") or DEFAULT_WORKSPACE_ID
|
|
339
|
-
if active not in (state.get("workspaces") or {}):
|
|
340
|
-
return DEFAULT_WORKSPACE_ID
|
|
341
|
-
return active
|
|
342
|
-
|
|
343
|
-
def _resolve_scope(self, workspace_id: Optional[str], state: Optional[Dict[str, Any]] = None) -> str:
|
|
344
|
-
"""Resolve the workspace a write should be tagged with.
|
|
345
|
-
|
|
346
|
-
``None`` falls back to the active workspace (Personal by default), so
|
|
347
|
-
legacy callers keep writing to the Personal workspace unchanged.
|
|
348
|
-
"""
|
|
349
|
-
if workspace_id:
|
|
350
|
-
return str(workspace_id)
|
|
351
|
-
return self._active_workspace_id(state)
|
|
352
|
-
|
|
353
|
-
@staticmethod
|
|
354
|
-
def _record_workspace(record: Dict[str, Any]) -> str:
|
|
355
|
-
"""Workspace a stored record belongs to (legacy records map to Personal)."""
|
|
356
|
-
return str(record.get("workspace_id") or DEFAULT_WORKSPACE_ID)
|
|
357
|
-
|
|
358
|
-
def _scoped(self, records: List[Dict[str, Any]], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
359
|
-
if not workspace_id:
|
|
360
|
-
return records
|
|
361
|
-
target = str(workspace_id)
|
|
362
|
-
return [item for item in records if self._record_workspace(item) == target]
|
|
363
|
-
|
|
364
|
-
def list_workspaces(self, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
365
|
-
state = self.load_state()
|
|
366
|
-
workspaces = state.get("workspaces") or {}
|
|
367
|
-
items = []
|
|
368
|
-
for ws in workspaces.values():
|
|
369
|
-
if user_id and ws.get("type") == "organization":
|
|
370
|
-
role = self._member_role(ws, user_id)
|
|
371
|
-
if role is None:
|
|
372
|
-
continue
|
|
373
|
-
items.append(self._workspace_public(ws, user_id))
|
|
374
|
-
items.sort(key=lambda w: (w.get("type") != "personal", w.get("created_at") or ""))
|
|
375
|
-
return {
|
|
376
|
-
"active_workspace": self._active_workspace_id(state),
|
|
377
|
-
"workspaces": items,
|
|
378
|
-
"roles": list(WORKSPACE_ROLES),
|
|
379
|
-
"permissions": {role: sorted(perms) for role, perms in ROLE_PERMISSIONS.items()},
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
def _workspace_public(self, ws: Dict[str, Any], user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
383
|
-
return {
|
|
384
|
-
"workspace_id": ws.get("workspace_id") or ws.get("id"),
|
|
385
|
-
"id": ws.get("workspace_id") or ws.get("id"),
|
|
386
|
-
"name": ws.get("name"),
|
|
387
|
-
"type": ws.get("type"),
|
|
388
|
-
"owner_user_id": ws.get("owner_user_id"),
|
|
389
|
-
"status": ws.get("status", "active"),
|
|
390
|
-
"member_count": len(_listify(ws.get("members"))),
|
|
391
|
-
"members": _listify(ws.get("members")),
|
|
392
|
-
"settings": ws.get("settings") or {},
|
|
393
|
-
"created_at": ws.get("created_at"),
|
|
394
|
-
"updated_at": ws.get("updated_at"),
|
|
395
|
-
"your_role": self._member_role(ws, user_id) if user_id else ("owner" if ws.get("type") == "personal" else None),
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
def get_workspace(self, workspace_id: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
399
|
-
state = self.load_state()
|
|
400
|
-
ws = (state.get("workspaces") or {}).get(workspace_id)
|
|
401
|
-
if not ws:
|
|
402
|
-
raise FileNotFoundError(workspace_id)
|
|
403
|
-
return self._workspace_public(ws, user_id)
|
|
404
|
-
|
|
405
|
-
def create_organization_workspace(
|
|
406
|
-
self,
|
|
407
|
-
*,
|
|
408
|
-
name: str,
|
|
409
|
-
owner_user_id: Optional[str],
|
|
410
|
-
settings: Optional[Dict[str, Any]] = None,
|
|
411
|
-
) -> Dict[str, Any]:
|
|
412
|
-
if not str(name or "").strip():
|
|
413
|
-
raise ValueError("workspace name is required")
|
|
414
|
-
state = self.load_state()
|
|
415
|
-
workspaces = state.setdefault("workspaces", {})
|
|
416
|
-
base = _safe_slug(f"org-{name}")
|
|
417
|
-
workspace_id = base
|
|
418
|
-
suffix = 2
|
|
419
|
-
while workspace_id in workspaces:
|
|
420
|
-
workspace_id = f"{base}-{suffix}"
|
|
421
|
-
suffix += 1
|
|
422
|
-
record = self._new_workspace_record(
|
|
423
|
-
workspace_id=workspace_id,
|
|
424
|
-
name=name.strip(),
|
|
425
|
-
workspace_type="organization",
|
|
426
|
-
owner_user_id=owner_user_id,
|
|
427
|
-
settings=settings or {},
|
|
428
|
-
)
|
|
429
|
-
workspaces[workspace_id] = record
|
|
430
|
-
self.save_state(state)
|
|
431
|
-
self.record_timeline_event("workspace", "workspace_created", {"workspace_id": workspace_id, "type": "organization"})
|
|
432
|
-
return self._workspace_public(record, owner_user_id)
|
|
433
|
-
|
|
434
|
-
@staticmethod
|
|
435
|
-
def _member_role(ws: Dict[str, Any], user_id: Optional[str]) -> Optional[str]:
|
|
436
|
-
# Delegated to extracted module (kept for any internal direct calls).
|
|
437
|
-
return _member_role(ws, user_id)
|
|
438
|
-
|
|
439
|
-
def get_member_role(self, workspace_id: str, user_id: Optional[str]) -> Optional[str]:
|
|
440
|
-
return self.permissions.get_member_role(workspace_id, user_id)
|
|
441
|
-
|
|
442
|
-
def has_permission(self, workspace_id: str, user_id: Optional[str], permission: str) -> bool:
|
|
443
|
-
return self.permissions.has_permission(workspace_id, user_id, permission)
|
|
444
|
-
|
|
445
|
-
def _require_permission(self, ws: Dict[str, Any], actor: Optional[str], permission: str) -> None:
|
|
446
|
-
self.permissions.require_permission(ws, actor, permission)
|
|
447
|
-
|
|
448
|
-
def _load_org(self, state: Dict[str, Any], workspace_id: str) -> Dict[str, Any]:
|
|
449
|
-
ws = (state.get("workspaces") or {}).get(workspace_id)
|
|
450
|
-
if not ws:
|
|
451
|
-
raise FileNotFoundError(workspace_id)
|
|
452
|
-
if ws.get("type") != "organization":
|
|
453
|
-
raise ValueError("operation only valid for organization workspaces")
|
|
454
|
-
return ws
|
|
455
|
-
|
|
456
|
-
def update_workspace(
|
|
457
|
-
self,
|
|
458
|
-
workspace_id: str,
|
|
459
|
-
*,
|
|
460
|
-
name: Optional[str] = None,
|
|
461
|
-
settings: Optional[Dict[str, Any]] = None,
|
|
462
|
-
actor: Optional[str] = None,
|
|
463
|
-
) -> Dict[str, Any]:
|
|
464
|
-
state = self.load_state()
|
|
465
|
-
ws = self._load_org(state, workspace_id)
|
|
466
|
-
self._require_permission(ws, actor, "manage_workspace")
|
|
467
|
-
if name is not None and str(name).strip():
|
|
468
|
-
ws["name"] = str(name).strip()
|
|
469
|
-
if settings is not None:
|
|
470
|
-
ws["settings"] = {**(ws.get("settings") or {}), **settings}
|
|
471
|
-
ws["updated_at"] = _now()
|
|
472
|
-
self.save_state(state)
|
|
473
|
-
self.record_timeline_event("workspace", "workspace_updated", {"workspace_id": workspace_id})
|
|
474
|
-
return self._workspace_public(ws, actor)
|
|
475
|
-
|
|
476
|
-
def archive_workspace(self, workspace_id: str, *, actor: Optional[str] = None) -> Dict[str, Any]:
|
|
477
|
-
"""Soft-archive an organization workspace. Data is never deleted."""
|
|
478
|
-
state = self.load_state()
|
|
479
|
-
ws = self._load_org(state, workspace_id)
|
|
480
|
-
self._require_permission(ws, actor, "manage_workspace")
|
|
481
|
-
ws["status"] = "archived"
|
|
482
|
-
ws["updated_at"] = _now()
|
|
483
|
-
if state.get("active_workspace") == workspace_id:
|
|
484
|
-
state["active_workspace"] = DEFAULT_WORKSPACE_ID
|
|
485
|
-
self.save_state(state)
|
|
486
|
-
self.record_timeline_event("workspace", "workspace_archived", {"workspace_id": workspace_id})
|
|
487
|
-
return self._workspace_public(ws, actor)
|
|
488
|
-
|
|
489
|
-
def add_member(self, workspace_id: str, *, user_id: str, role: str = "member", actor: Optional[str] = None) -> Dict[str, Any]:
|
|
490
|
-
return self.permissions.add_member(workspace_id, user_id=user_id, role=role, actor=actor)
|
|
491
|
-
|
|
492
|
-
def update_member_role(self, workspace_id: str, *, user_id: str, role: str, actor: Optional[str] = None) -> Dict[str, Any]:
|
|
493
|
-
# delegate or keep simple for now (full impl in permissions if expanded)
|
|
494
|
-
if role not in WORKSPACE_ROLES:
|
|
495
|
-
raise ValueError(f"unknown role: {role}")
|
|
496
|
-
state = self.load_state()
|
|
497
|
-
ws = self._load_org(state, workspace_id)
|
|
498
|
-
self._require_permission(ws, actor, "manage_members")
|
|
499
|
-
if user_id == ws.get("owner_user_id") and role != "owner":
|
|
500
|
-
raise ValueError("cannot demote the workspace owner")
|
|
501
|
-
member = next((m for m in _listify(ws.get("members")) if m.get("user_id") == user_id), None)
|
|
502
|
-
if not member:
|
|
503
|
-
raise FileNotFoundError(user_id)
|
|
504
|
-
member["role"] = role
|
|
505
|
-
member["updated_at"] = _now()
|
|
506
|
-
ws["updated_at"] = _now()
|
|
507
|
-
self.save_state(state)
|
|
508
|
-
self.record_timeline_event("workspace", "member_role_updated", {"workspace_id": workspace_id, "user_id": user_id, "role": role})
|
|
509
|
-
return self._workspace_public(ws, actor)
|
|
510
|
-
|
|
511
|
-
def remove_member(self, workspace_id: str, *, user_id: str, actor: Optional[str] = None) -> Dict[str, Any]:
|
|
512
|
-
state = self.load_state()
|
|
513
|
-
ws = self._load_org(state, workspace_id)
|
|
514
|
-
self._require_permission(ws, actor, "manage_members")
|
|
515
|
-
if user_id == ws.get("owner_user_id"):
|
|
516
|
-
raise ValueError("cannot remove the workspace owner")
|
|
517
|
-
members = _listify(ws.get("members"))
|
|
518
|
-
kept = [m for m in members if m.get("user_id") != user_id]
|
|
519
|
-
if len(kept) == len(members):
|
|
520
|
-
raise FileNotFoundError(user_id)
|
|
521
|
-
ws["members"] = kept
|
|
522
|
-
ws["updated_at"] = _now()
|
|
523
|
-
self.save_state(state)
|
|
524
|
-
self.record_timeline_event("workspace", "member_removed", {"workspace_id": workspace_id, "user_id": user_id})
|
|
525
|
-
return self._workspace_public(ws, actor)
|
|
526
|
-
|
|
527
|
-
def set_active_workspace(self, workspace_id: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
528
|
-
state = self.load_state()
|
|
529
|
-
ws = (state.get("workspaces") or {}).get(workspace_id)
|
|
530
|
-
if not ws:
|
|
531
|
-
raise FileNotFoundError(workspace_id)
|
|
532
|
-
if ws.get("type") == "organization" and self._member_role(ws, user_id) is None:
|
|
533
|
-
raise PermissionError(f"'{user_id or 'anonymous'}' is not a member of '{workspace_id}'")
|
|
534
|
-
state["active_workspace"] = workspace_id
|
|
535
|
-
self.save_state(state)
|
|
536
|
-
self.record_timeline_event("workspace", "workspace_activated", {"workspace_id": workspace_id})
|
|
537
|
-
return self._workspace_public(ws, user_id)
|
|
538
|
-
|
|
539
|
-
def workspace_summary(self, workspace_id: str, user_id: Optional[str] = None) -> Dict[str, Any]:
|
|
540
|
-
state = self.load_state()
|
|
541
|
-
ws = (state.get("workspaces") or {}).get(workspace_id)
|
|
542
|
-
if not ws:
|
|
543
|
-
raise FileNotFoundError(workspace_id)
|
|
544
|
-
public = self._workspace_public(ws, user_id)
|
|
545
|
-
public["counts"] = {
|
|
546
|
-
"snapshots": len(self._scoped(_listify(state.get("snapshots")), workspace_id)),
|
|
547
|
-
"memories": len(self._scoped(_listify(state.get("memories")), workspace_id)),
|
|
548
|
-
"memory_snapshots": len(self._scoped(_listify(state.get("memory_snapshots")), workspace_id)),
|
|
549
|
-
"agent_runs": len(self._scoped(_listify(state.get("agent_runs")), workspace_id)),
|
|
550
|
-
"handoffs": len(self._scoped(_listify(state.get("handoffs")), workspace_id)),
|
|
551
|
-
"workflows": len(self._scoped(_listify(state.get("workflows")), workspace_id)),
|
|
552
|
-
"workflow_runs": len(self._scoped(_listify(state.get("workflow_runs")), workspace_id)),
|
|
553
|
-
"traces": len(self._scoped(_listify(state.get("traces")), workspace_id)),
|
|
554
|
-
"timeline": len(self._scoped(_listify(state.get("timeline")), workspace_id)),
|
|
555
|
-
}
|
|
556
|
-
return public
|
|
557
|
-
|
|
558
|
-
# ------------------------------------------------------------------
|
|
559
|
-
# Onboarding
|
|
560
|
-
# ------------------------------------------------------------------
|
|
561
|
-
|
|
562
|
-
def onboarding_status(self, users: Optional[Dict[str, Any]] = None, graph_stats: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
563
|
-
return self.onboarding.status(users=users, graph_stats=graph_stats)
|
|
564
|
-
|
|
565
|
-
def update_onboarding_step(
|
|
566
|
-
self,
|
|
567
|
-
step: str,
|
|
568
|
-
*,
|
|
569
|
-
status: str = "complete",
|
|
570
|
-
data: Optional[Dict[str, Any]] = None,
|
|
571
|
-
error: str = "",
|
|
572
|
-
user_email: Optional[str] = None,
|
|
573
|
-
) -> Dict[str, Any]:
|
|
574
|
-
return self.onboarding.update_step(
|
|
575
|
-
step, status=status, data=data, error=error, user_email=user_email
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
def complete_onboarding(self, data: Optional[Dict[str, Any]] = None, user_email: Optional[str] = None) -> Dict[str, Any]:
|
|
579
|
-
return self.onboarding.complete(data=data, user_email=user_email)
|
|
580
|
-
|
|
581
|
-
# ------------------------------------------------------------------
|
|
582
|
-
# Graph answer traces
|
|
583
|
-
# ------------------------------------------------------------------
|
|
584
|
-
|
|
585
|
-
def build_graph_trace(
|
|
586
|
-
self,
|
|
587
|
-
question: str,
|
|
588
|
-
graph: Any,
|
|
589
|
-
context: str = "",
|
|
590
|
-
*,
|
|
591
|
-
limit: int = 8,
|
|
592
|
-
allowed_workspaces=None,
|
|
593
|
-
) -> Dict[str, Any]:
|
|
594
|
-
return self.graph_trace.build_graph_trace(
|
|
595
|
-
question,
|
|
596
|
-
graph,
|
|
597
|
-
context,
|
|
598
|
-
limit=limit,
|
|
599
|
-
allowed_workspaces=allowed_workspaces,
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
def record_trace(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
603
|
-
return self.graph_trace.record_trace(*args, **kwargs)
|
|
604
|
-
|
|
605
|
-
def list_traces(self, conversation_id: Optional[str] = None, limit: int = 50, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
606
|
-
return self.graph_trace.list_traces(conversation_id=conversation_id, limit=limit, workspace_id=workspace_id)
|
|
607
|
-
|
|
608
|
-
# ------------------------------------------------------------------
|
|
609
|
-
# Indexing dashboard
|
|
610
|
-
# ------------------------------------------------------------------
|
|
611
|
-
|
|
612
|
-
def build_indexing_dashboard(self, graph: Any, watcher_status: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
613
|
-
return self.indexing.build_dashboard(graph, watcher_status)
|
|
614
|
-
|
|
615
|
-
def pause_indexing(self, graph: Any, source_id: str, watcher: Any = None) -> Dict[str, Any]:
|
|
616
|
-
return self.indexing.pause(graph, source_id, watcher)
|
|
617
|
-
|
|
618
|
-
def resume_indexing(self, graph: Any, source_id: str, watcher: Any = None) -> Dict[str, Any]:
|
|
619
|
-
return self.indexing.resume(graph, source_id, watcher)
|
|
620
|
-
|
|
621
|
-
def remove_index_source(self, graph: Any, source_id: str, watcher: Any = None) -> Dict[str, Any]:
|
|
622
|
-
return self.indexing.remove_source(graph, source_id, watcher)
|
|
623
|
-
|
|
624
|
-
# ------------------------------------------------------------------
|
|
625
|
-
# Snapshots, Time Machine, and diffs
|
|
626
|
-
# ------------------------------------------------------------------
|
|
627
|
-
|
|
628
|
-
def create_snapshot(
|
|
629
|
-
self,
|
|
630
|
-
*,
|
|
631
|
-
name: str,
|
|
632
|
-
graph: Any,
|
|
633
|
-
history: Iterable[Dict[str, Any]],
|
|
634
|
-
settings: Dict[str, Any],
|
|
635
|
-
models: Dict[str, Any],
|
|
636
|
-
workspace_id: Optional[str] = None,
|
|
637
|
-
) -> Dict[str, Any]:
|
|
638
|
-
return self.snapshots.create_snapshot(name=name, graph=graph, history=history, settings=settings, models=models, workspace_id=workspace_id)
|
|
639
|
-
|
|
640
|
-
def list_snapshots(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
641
|
-
return self.snapshots.list_snapshots(workspace_id=workspace_id)
|
|
642
|
-
|
|
643
|
-
def get_snapshot(self, snapshot_id: str) -> Dict[str, Any]:
|
|
644
|
-
return self.snapshots.get_snapshot(snapshot_id)
|
|
645
|
-
|
|
646
|
-
def snapshot_view(self, snapshot_id: str, area: str) -> Dict[str, Any]:
|
|
647
|
-
return self.snapshots.snapshot_view(snapshot_id, area)
|
|
648
|
-
|
|
649
|
-
def export_snapshot(self, snapshot_id: str) -> Dict[str, Any]:
|
|
650
|
-
return self.snapshots.export_snapshot(snapshot_id)
|
|
651
|
-
|
|
652
|
-
def restore_snapshot(
|
|
653
|
-
self,
|
|
654
|
-
snapshot_id: str,
|
|
655
|
-
*,
|
|
656
|
-
graph: Any,
|
|
657
|
-
workspace_id: Optional[str] = None,
|
|
658
|
-
user_email: Optional[str] = None,
|
|
659
|
-
) -> Dict[str, Any]:
|
|
660
|
-
return self.snapshots.restore_snapshot(snapshot_id, graph=graph, workspace_id=workspace_id, user_email=user_email)
|
|
661
|
-
|
|
662
|
-
def compare_snapshots(self, before_id: str, after_id: str) -> Dict[str, Any]:
|
|
663
|
-
return self.snapshots.compare_snapshots(before_id, after_id)
|
|
664
|
-
|
|
665
|
-
# ------------------------------------------------------------------
|
|
666
|
-
# Personal memory
|
|
667
|
-
# ------------------------------------------------------------------
|
|
668
|
-
|
|
669
|
-
def upsert_memory(
|
|
670
|
-
self,
|
|
671
|
-
*,
|
|
672
|
-
kind: str,
|
|
673
|
-
content: str,
|
|
674
|
-
user_email: Optional[str],
|
|
675
|
-
tags: Optional[List[str]] = None,
|
|
676
|
-
memory_id: Optional[str] = None,
|
|
677
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
678
|
-
graph: Any = None,
|
|
679
|
-
workspace_id: Optional[str] = None,
|
|
680
|
-
) -> Dict[str, Any]:
|
|
681
|
-
return self.memory.upsert_memory(
|
|
682
|
-
kind=kind, content=content, user_email=user_email, tags=tags,
|
|
683
|
-
memory_id=memory_id, metadata=metadata, graph=graph, workspace_id=workspace_id
|
|
684
|
-
)
|
|
685
|
-
|
|
686
|
-
def list_memories(self, user_email: Optional[str] = None, kind: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
687
|
-
return self.memory.list_memories(user_email=user_email, kind=kind, workspace_id=workspace_id)
|
|
688
|
-
|
|
689
|
-
def search_memories(self, query: str, user_email: Optional[str] = None, limit: int = 20, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
690
|
-
q = str(query or "").lower().strip()
|
|
691
|
-
memories = self.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", [])
|
|
692
|
-
if q:
|
|
693
|
-
memories = [
|
|
694
|
-
item for item in memories
|
|
695
|
-
if q in str(item.get("content") or "").lower()
|
|
696
|
-
or q in " ".join(item.get("tags") or []).lower()
|
|
697
|
-
or q in str(item.get("kind") or "").lower()
|
|
698
|
-
]
|
|
699
|
-
return {"query": query, "memories": memories[: max(1, min(limit, 100))]}
|
|
700
|
-
|
|
701
|
-
def get_memory(self, memory_id: str) -> Dict[str, Any]:
|
|
702
|
-
record = next(
|
|
703
|
-
(item for item in _listify(self.load_state().get("memories")) if item.get("id") == memory_id),
|
|
704
|
-
None,
|
|
705
|
-
)
|
|
706
|
-
if record is None:
|
|
707
|
-
raise FileNotFoundError(memory_id)
|
|
708
|
-
return record
|
|
709
|
-
|
|
710
|
-
def delete_memory(self, memory_id: str) -> Dict[str, Any]:
|
|
711
|
-
state = self.load_state()
|
|
712
|
-
memories = _listify(state.get("memories"))
|
|
713
|
-
target = next((item for item in memories if item.get("id") == memory_id), None)
|
|
714
|
-
if target is None:
|
|
715
|
-
raise FileNotFoundError(memory_id)
|
|
716
|
-
state["memories"] = [item for item in memories if item.get("id") != memory_id]
|
|
717
|
-
self.save_state(state)
|
|
718
|
-
self.record_timeline_event(
|
|
719
|
-
"memory", "memory_deleted", {"memory_id": memory_id}, workspace_id=target.get("workspace_id")
|
|
720
|
-
)
|
|
721
|
-
return {"status": "ok", "memory_id": memory_id}
|
|
722
|
-
|
|
723
|
-
def create_memory_snapshot(
|
|
724
|
-
self,
|
|
725
|
-
*,
|
|
726
|
-
label: str = "memory snapshot",
|
|
727
|
-
user_email: Optional[str] = None,
|
|
728
|
-
workspace_id: Optional[str] = None,
|
|
729
|
-
memory_ids: Optional[List[str]] = None,
|
|
730
|
-
reason: str = "",
|
|
731
|
-
) -> Dict[str, Any]:
|
|
732
|
-
"""Persist a replayable point-in-time memory view without mutation."""
|
|
733
|
-
state = self.load_state()
|
|
734
|
-
scope = self._resolve_scope(workspace_id, state)
|
|
735
|
-
memories = self._scoped(_listify(state.get("memories")), scope)
|
|
736
|
-
if user_email:
|
|
737
|
-
memories = [item for item in memories if item.get("user_email") in {None, user_email}]
|
|
738
|
-
if memory_ids:
|
|
739
|
-
allowed = set(memory_ids)
|
|
740
|
-
memories = [item for item in memories if item.get("id") in allowed]
|
|
741
|
-
snapshot = {
|
|
742
|
-
"id": f"memory-snapshot-{_json_hash([label, scope, memories, _now()])[:16]}",
|
|
743
|
-
"label": label,
|
|
744
|
-
"reason": reason,
|
|
745
|
-
"workspace_id": scope,
|
|
746
|
-
"user_email": user_email,
|
|
747
|
-
"memory_count": len(memories),
|
|
748
|
-
"memories": memories,
|
|
749
|
-
"created_at": _now(),
|
|
750
|
-
}
|
|
751
|
-
state.setdefault("memory_snapshots", []).append(snapshot)
|
|
752
|
-
self.save_state(state)
|
|
753
|
-
self.record_timeline_event("memory", "memory_snapshot", {"snapshot_id": snapshot["id"], "memory_count": len(memories)}, workspace_id=scope)
|
|
754
|
-
return snapshot
|
|
755
|
-
|
|
756
|
-
def list_memory_snapshots(self, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|
|
757
|
-
snapshots = self._scoped(_listify(self.load_state().get("memory_snapshots")), workspace_id)
|
|
758
|
-
return {"snapshots": list(reversed(snapshots[-max(1, min(limit, 200)):]))}
|
|
759
|
-
|
|
760
|
-
# ------------------------------------------------------------------
|
|
761
|
-
# Agent and workflow graph
|
|
762
|
-
# ------------------------------------------------------------------
|
|
763
|
-
|
|
764
|
-
def list_agents(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
765
|
-
return self.runs.list_agents(*args, **kwargs)
|
|
766
|
-
|
|
767
|
-
def record_agent_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
768
|
-
return self.runs.record_agent_run(*args, **kwargs)
|
|
769
|
-
|
|
770
|
-
def update_agent_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
771
|
-
return self.runs.update_agent_run(*args, **kwargs)
|
|
772
|
-
|
|
773
|
-
def get_agent_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
774
|
-
return self.runs.get_agent_run(*args, **kwargs)
|
|
775
|
-
|
|
776
|
-
def list_handoffs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
777
|
-
return self.runs.list_handoffs(*args, **kwargs)
|
|
778
|
-
|
|
779
|
-
def create_workflow(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
780
|
-
return self.runs.create_workflow(*args, **kwargs)
|
|
781
|
-
|
|
782
|
-
def record_workflow_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
783
|
-
return self.runs.record_workflow_run(*args, **kwargs)
|
|
784
|
-
|
|
785
|
-
def update_workflow_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
786
|
-
return self.runs.update_workflow_run(*args, **kwargs)
|
|
787
|
-
|
|
788
|
-
def list_workflow_runs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
789
|
-
return self.runs.list_workflow_runs(*args, **kwargs)
|
|
790
|
-
|
|
791
|
-
def list_combined_runs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
792
|
-
return self.runs.list_combined_runs(*args, **kwargs)
|
|
793
|
-
|
|
794
|
-
def mark_workflow_run_resolved(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
795
|
-
return self.runs.mark_workflow_run_resolved(*args, **kwargs)
|
|
796
|
-
|
|
797
|
-
def get_workflow_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
798
|
-
return self.runs.get_workflow_run(*args, **kwargs)
|
|
799
|
-
|
|
800
|
-
def reconcile_interrupted_runs(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
801
|
-
return self.runs.reconcile_interrupted_runs(*args, **kwargs)
|
|
802
|
-
|
|
803
|
-
@staticmethod
|
|
804
|
-
def _replay_frames(run: Dict[str, Any], *, kind: str) -> List[Dict[str, Any]]:
|
|
805
|
-
return WorkspaceRuns._replay_frames(run, kind=kind)
|
|
806
|
-
|
|
807
|
-
def replay_agent_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
808
|
-
return self.runs.replay_agent_run(*args, **kwargs)
|
|
809
|
-
|
|
810
|
-
def replay_workflow_run(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
811
|
-
return self.runs.replay_workflow_run(*args, **kwargs)
|
|
812
|
-
|
|
813
|
-
def get_workflow(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
814
|
-
return self.runs.get_workflow(*args, **kwargs)
|
|
815
|
-
|
|
816
|
-
def update_workflow_definition(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
817
|
-
return self.runs.update_workflow_definition(*args, **kwargs)
|
|
818
|
-
|
|
819
|
-
def list_workflows(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
820
|
-
return self.runs.list_workflows(*args, **kwargs)
|
|
821
|
-
|
|
822
|
-
def record_workflow_event(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
823
|
-
return self.runs.record_workflow_event(*args, **kwargs)
|
|
824
|
-
|
|
825
|
-
# ── review queue (5.6.0) ─────────────────────────────────────────────
|
|
826
|
-
# Workspace-scoped suggestion inbox. Persistence moved to
|
|
827
|
-
# ``workspace_review_items.WorkspaceReviewItems`` (v9.9.6); the transition
|
|
828
|
-
# policy still lives in ReviewQueueService (services/review_queue.py).
|
|
829
|
-
|
|
830
|
-
def create_review_item(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
831
|
-
return self.review_items.create_review_item(*args, **kwargs)
|
|
832
|
-
|
|
833
|
-
def list_review_items(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
834
|
-
return self.review_items.list_review_items(*args, **kwargs)
|
|
835
|
-
|
|
836
|
-
def get_review_item(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
837
|
-
return self.review_items.get_review_item(*args, **kwargs)
|
|
838
|
-
|
|
839
|
-
def update_review_item(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
840
|
-
return self.review_items.update_review_item(*args, **kwargs)
|
|
841
|
-
|
|
842
|
-
# ------------------------------------------------------------------
|
|
843
|
-
# Agent-native workspace: folder reorganization (v11.1.0)
|
|
844
|
-
# ------------------------------------------------------------------
|
|
845
|
-
|
|
846
|
-
def propose_reorganization(
|
|
847
|
-
self, *, root: str = "", change_proposals: Any, graph: Any = None,
|
|
848
|
-
user_email: Optional[str] = None, workspace_id: Optional[str] = None,
|
|
849
|
-
max_moves: int = 20,
|
|
850
|
-
) -> Dict[str, Any]:
|
|
851
|
-
"""Stage a folder reorganization for review and record it on the timeline.
|
|
852
|
-
|
|
853
|
-
The staging itself belongs to ``ChangeProposalService`` (one apply path
|
|
854
|
-
for every staged change); this seam exists so a long-running agent
|
|
855
|
-
session asking the Workspace OS to tidy a project leaves a trace of
|
|
856
|
-
having asked, whether or not there was anything to propose.
|
|
857
|
-
"""
|
|
858
|
-
result = change_proposals.propose_reorganization(
|
|
859
|
-
root=root, graph=graph, user_email=user_email,
|
|
860
|
-
workspace_id=workspace_id, max_moves=max_moves,
|
|
861
|
-
)
|
|
862
|
-
proposed = result.get("proposed") or {}
|
|
863
|
-
self.record_timeline_event(
|
|
864
|
-
"workspace",
|
|
865
|
-
"workspace_reorg_proposed",
|
|
866
|
-
{
|
|
867
|
-
"root": root,
|
|
868
|
-
"moves": (result.get("plan") or {}).get("move_count", 0),
|
|
869
|
-
"proposal_id": proposed.get("id"),
|
|
870
|
-
},
|
|
871
|
-
workspace_id=workspace_id,
|
|
872
|
-
)
|
|
873
|
-
return result
|
|
874
|
-
|
|
875
|
-
# ------------------------------------------------------------------
|
|
876
|
-
# Relationship explorer
|
|
877
|
-
# ------------------------------------------------------------------
|
|
878
|
-
|
|
879
|
-
def relationship_explorer(self, graph: Any, node_id: str, target_id: Optional[str] = None, limit: int = 500) -> Dict[str, Any]:
|
|
880
|
-
return self.relationships.explore(graph, node_id, target_id=target_id, limit=limit)
|
|
881
|
-
|
|
882
|
-
@staticmethod
|
|
883
|
-
def _shortest_path(edges: List[Dict[str, Any]], start: str, target: Optional[str]) -> List[str]:
|
|
884
|
-
return _shortest_path(edges, start, target)
|
|
885
|
-
|
|
886
|
-
# ------------------------------------------------------------------
|
|
887
|
-
# Local Computer Memory
|
|
888
|
-
# ------------------------------------------------------------------
|
|
889
|
-
|
|
890
|
-
def configure_computer_memory(
|
|
891
|
-
self,
|
|
892
|
-
*,
|
|
893
|
-
enabled: bool,
|
|
894
|
-
approved_by: Optional[str],
|
|
895
|
-
consent: Optional[Dict[str, Any]] = None,
|
|
896
|
-
scopes: Optional[List[str]] = None,
|
|
897
|
-
) -> Dict[str, Any]:
|
|
898
|
-
return self.computer_memory.configure(
|
|
899
|
-
enabled=enabled, approved_by=approved_by, consent=consent, scopes=scopes
|
|
900
|
-
)
|
|
901
|
-
|
|
902
|
-
def record_computer_activity(self, activity: Dict[str, Any], graph: Any = None) -> Dict[str, Any]:
|
|
903
|
-
return self.computer_memory.record_activity(activity, graph)
|
|
904
|
-
|
|
905
|
-
# ------------------------------------------------------------------
|
|
906
|
-
# Skills
|
|
907
|
-
# ------------------------------------------------------------------
|
|
908
|
-
|
|
909
|
-
def list_skill_registry(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
910
|
-
return self.skills.list_skill_registry(*args, **kwargs)
|
|
911
|
-
|
|
912
|
-
def set_skill_enabled(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
913
|
-
return self.skills.set_skill_enabled(*args, **kwargs)
|
|
914
|
-
|
|
915
|
-
def mark_skill_installed(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
916
|
-
return self.skills.mark_skill_installed(*args, **kwargs)
|
|
917
|
-
|
|
918
|
-
def mark_skill_uninstalled(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
919
|
-
return self.skills.mark_skill_uninstalled(*args, **kwargs)
|
|
920
|
-
|
|
921
|
-
# ------------------------------------------------------------------
|
|
922
|
-
# Plugin SDK registry — mirrors the skill registry contract.
|
|
923
|
-
# ------------------------------------------------------------------
|
|
924
|
-
|
|
925
|
-
def list_plugin_registry(self) -> Dict[str, Any]:
|
|
926
|
-
return self.plugins.list_plugin_registry()
|
|
927
|
-
|
|
928
|
-
def set_plugin_enabled(self, plugin_id: str, enabled: bool) -> Dict[str, Any]:
|
|
929
|
-
return self.plugins.set_plugin_enabled(plugin_id, enabled)
|
|
930
|
-
|
|
931
|
-
def mark_plugin_installed(self, plugin_id: str, *, version: str = "0.0.0", metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
932
|
-
return self.plugins.mark_plugin_installed(plugin_id, version=version, metadata=metadata)
|
|
933
|
-
|
|
934
|
-
def mark_plugin_uninstalled(self, plugin_id: str) -> Dict[str, Any]:
|
|
935
|
-
return self.plugins.mark_plugin_uninstalled(plugin_id)
|
|
936
|
-
|
|
937
|
-
# ------------------------------------------------------------------
|
|
938
|
-
# Marketplace template registry (v2.1 foundation)
|
|
939
|
-
# ------------------------------------------------------------------
|
|
940
|
-
|
|
941
|
-
@staticmethod
|
|
942
|
-
def _template_registry_key(kind: str, template_id: str, workspace_id: str) -> str:
|
|
943
|
-
base = f"{kind}:{template_id}"
|
|
944
|
-
return base if workspace_id == DEFAULT_WORKSPACE_ID else f"{workspace_id}:{base}"
|
|
945
|
-
|
|
946
|
-
def list_template_registry(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
947
|
-
return self.plugins.list_template_registry(workspace_id)
|
|
948
|
-
|
|
949
|
-
def mark_template_installed(
|
|
950
|
-
self,
|
|
951
|
-
*,
|
|
952
|
-
kind: str,
|
|
953
|
-
template_id: str,
|
|
954
|
-
version: str = "1.0.0",
|
|
955
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
956
|
-
workspace_id: Optional[str] = None,
|
|
957
|
-
) -> Dict[str, Any]:
|
|
958
|
-
return self.plugins.mark_template_installed(kind=kind, template_id=template_id, version=version, metadata=metadata, workspace_id=workspace_id)
|
|
959
|
-
|
|
960
|
-
# ------------------------------------------------------------------
|
|
961
|
-
# Audit timeline
|
|
962
|
-
# ------------------------------------------------------------------
|
|
963
|
-
|
|
964
|
-
def filter_audit_timeline(
|
|
965
|
-
self,
|
|
966
|
-
audit_events: Iterable[Dict[str, Any]],
|
|
967
|
-
*,
|
|
968
|
-
user: Optional[str] = None,
|
|
969
|
-
event_type: Optional[str] = None,
|
|
970
|
-
model: Optional[str] = None,
|
|
971
|
-
since: Optional[str] = None,
|
|
972
|
-
until: Optional[str] = None,
|
|
973
|
-
limit: int = 100,
|
|
974
|
-
) -> Dict[str, Any]:
|
|
975
|
-
return self._timeline.filter_audit_timeline(audit_events, user=user, event_type=event_type, model=model, since=since, until=until, limit=limit)
|
|
976
|
-
|
|
977
|
-
def timeline(self, audit_events: Optional[Iterable[Dict[str, Any]]] = None, limit: int = 100, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
978
|
-
return self._timeline.timeline(audit_events=audit_events, limit=limit, workspace_id=workspace_id)
|