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,859 +0,0 @@
|
|
|
1
|
-
"""AgentRuntime — the single boundary for agent execution and observability.
|
|
2
|
-
|
|
3
|
-
(lattice_brain/runtime/agent_runtime.py)
|
|
4
|
-
책임: 퍼사드. store/orchestrator/hooks/audit 주입 받아 start/reserve/complete,
|
|
5
|
-
status/health/config/events/replay/stop, pre/post_run hook firing.
|
|
6
|
-
RunExecutor와 /agents 라우터의 유일한 의존 대상.
|
|
7
|
-
의존: .multi_agent (orchestrator), .hooks, store (WORKSPACE_OS).
|
|
8
|
-
진입점: app_factory.py:AGENT_RUNTIME (wiring root), api/agents.py, RunExecutor.
|
|
9
|
-
|
|
10
|
-
Before this module the agent concern was spread across three places: the
|
|
11
|
-
:class:`~lattice_brain.runtime.multi_agent.MultiAgentOrchestrator` (role pipeline),
|
|
12
|
-
the :class:`~latticeai.services.platform_runtime.PlatformRuntime` (cross-system
|
|
13
|
-
wiring + an ad-hoc ``run_agent``), and ``api/agents.py`` (HTTP transport that
|
|
14
|
-
also owned orchestration + persistence + audit). The frontend reached past all
|
|
15
|
-
of them into the workspace store via ``/workspace/agents``.
|
|
16
|
-
|
|
17
|
-
``AgentRuntime`` collapses that into one façade with a small, stable surface:
|
|
18
|
-
|
|
19
|
-
* **configuration** — :meth:`config`, :meth:`roles`
|
|
20
|
-
* **status / health** — :meth:`status`, :meth:`health`
|
|
21
|
-
* **execution** — :meth:`start`, :meth:`stop`
|
|
22
|
-
* **events / state** — :meth:`list_runs`, :meth:`get_run`, :meth:`events`, :meth:`replay`
|
|
23
|
-
|
|
24
|
-
It *wraps* the existing orchestrator and run store rather than reimplementing
|
|
25
|
-
them — execution semantics are unchanged, but every caller (HTTP router and, via
|
|
26
|
-
it, the frontend) now depends on this boundary instead of internal paths.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
from __future__ import annotations
|
|
30
|
-
|
|
31
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
32
|
-
|
|
33
|
-
from ..quiet import quiet
|
|
34
|
-
from ..utils import now_iso as _now
|
|
35
|
-
from .contracts import (
|
|
36
|
-
contract_view,
|
|
37
|
-
contract_views,
|
|
38
|
-
extract_contract,
|
|
39
|
-
multi_agent_contract,
|
|
40
|
-
run_record_contract,
|
|
41
|
-
runtime_boundary_contract,
|
|
42
|
-
)
|
|
43
|
-
from .multi_agent import (
|
|
44
|
-
AGENT_ROLES,
|
|
45
|
-
CORE_PIPELINE,
|
|
46
|
-
MULTI_AGENT_VERSION,
|
|
47
|
-
ROLE_AGENT_IDS,
|
|
48
|
-
)
|
|
49
|
-
from .statuses import (
|
|
50
|
-
RUN_ACTIVE_STATUSES as _ACTIVE_STATUSES,
|
|
51
|
-
)
|
|
52
|
-
from .statuses import (
|
|
53
|
-
RUN_TERMINAL_STATUSES as _TERMINAL_STATUSES,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
ROLE_DESCRIPTIONS = {
|
|
57
|
-
"researcher": "Gathers workspace context and memory for the goal.",
|
|
58
|
-
"planner": "Decomposes the goal into an ordered, bounded plan.",
|
|
59
|
-
"executor": "Executes each planned step, invoking tools and workflows.",
|
|
60
|
-
"reviewer": "Reviews the executed work and approves, rejects, or retries.",
|
|
61
|
-
"release": "Finalizes and summarizes the approved outcome.",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Run statuses the orchestrator can emit that mean "still working". The default
|
|
65
|
-
# orchestrator runs synchronously, so persisted runs are always terminal; this
|
|
66
|
-
# set lets the runtime report live work if a future async runner lands.
|
|
67
|
-
def _compact_text(value: Any, *, limit: int) -> str:
|
|
68
|
-
return " ".join(str(value or "").split())[:limit]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class AgentRuntimeUnavailable(RuntimeError):
|
|
72
|
-
"""Raised when a product run would otherwise persist simulation output."""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class AgentRuntime:
|
|
76
|
-
def __init__(
|
|
77
|
-
self,
|
|
78
|
-
*,
|
|
79
|
-
store: Any,
|
|
80
|
-
orchestrator_factory: Callable[[Optional[str], Optional[str]], Any],
|
|
81
|
-
workspace_graph: Callable[[], Any],
|
|
82
|
-
append_audit_event: Callable[..., None],
|
|
83
|
-
max_retries_cap: int = 5,
|
|
84
|
-
hooks: Any = None,
|
|
85
|
-
allow_simulation_runs: bool = False,
|
|
86
|
-
memory_ingest: Optional[Callable[..., Dict[str, Any]]] = None,
|
|
87
|
-
review_sink: Any = None,
|
|
88
|
-
):
|
|
89
|
-
self._store = store
|
|
90
|
-
self._orchestrator_factory = orchestrator_factory
|
|
91
|
-
self._workspace_graph = workspace_graph
|
|
92
|
-
self._append_audit_event = append_audit_event
|
|
93
|
-
self._max_retries_cap = int(max_retries_cap)
|
|
94
|
-
# Lifecycle hooks registry (optional). When present, ``start`` fires the
|
|
95
|
-
# ``pre_run`` / ``post_run`` hooks; a blocking ``pre_run`` aborts the run.
|
|
96
|
-
self._hooks = hooks
|
|
97
|
-
self._allow_simulation_runs = bool(allow_simulation_runs)
|
|
98
|
-
self._run_executor: Any = None
|
|
99
|
-
# Optional memory synthesis: successful agent runs produce durable
|
|
100
|
-
# Brain memories (long_term / workspace tier) so users *feel* the results
|
|
101
|
-
# in BrainBrief, MemoryRings, search and graph immediately.
|
|
102
|
-
self._memory_ingest = memory_ingest
|
|
103
|
-
self._review_sink = review_sink
|
|
104
|
-
|
|
105
|
-
def attach_executor(self, executor: Any) -> None:
|
|
106
|
-
self._run_executor = executor
|
|
107
|
-
|
|
108
|
-
def _execution_mode(self) -> str:
|
|
109
|
-
return "async" if self._run_executor is not None else "synchronous"
|
|
110
|
-
|
|
111
|
-
def boundary(self) -> Dict[str, Any]:
|
|
112
|
-
return runtime_boundary_contract(
|
|
113
|
-
name="AgentRuntime",
|
|
114
|
-
runtime="multi_agent",
|
|
115
|
-
entrypoint="lattice_brain.runtime.agent_runtime.AgentRuntime",
|
|
116
|
-
surface="/agents",
|
|
117
|
-
owns="product agent execution, observability, status, health, events, replay, and stop",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# ── configuration ─────────────────────────────────────────────────────
|
|
121
|
-
def config(self) -> Dict[str, Any]:
|
|
122
|
-
return {
|
|
123
|
-
"version": MULTI_AGENT_VERSION,
|
|
124
|
-
"boundary": self.boundary(),
|
|
125
|
-
"roles": list(AGENT_ROLES),
|
|
126
|
-
"default_pipeline": list(CORE_PIPELINE),
|
|
127
|
-
"max_retries_cap": self._max_retries_cap,
|
|
128
|
-
"execution_mode": self._execution_mode(),
|
|
129
|
-
"simulation_runs_allowed": self._allow_simulation_runs,
|
|
130
|
-
"cancellation": (
|
|
131
|
-
"cooperative; running synchronous model/tool calls finish their current step before a cancelled status is persisted"
|
|
132
|
-
if self._run_executor is not None else
|
|
133
|
-
"not supported for the synchronous runtime"
|
|
134
|
-
),
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
def roles(self) -> List[Dict[str, Any]]:
|
|
138
|
-
return [
|
|
139
|
-
{
|
|
140
|
-
"role": role,
|
|
141
|
-
"agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
|
|
142
|
-
"description": ROLE_DESCRIPTIONS.get(role, ""),
|
|
143
|
-
"terminal": role not in {"researcher", "planner", "executor", "reviewer"},
|
|
144
|
-
}
|
|
145
|
-
for role in AGENT_ROLES
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
# ── health ────────────────────────────────────────────────────────────
|
|
149
|
-
def health(self) -> Dict[str, Any]:
|
|
150
|
-
checks: Dict[str, Any] = {}
|
|
151
|
-
ok = True
|
|
152
|
-
ready = True
|
|
153
|
-
try:
|
|
154
|
-
self._store.list_agents(workspace_id=None)
|
|
155
|
-
checks["run_store"] = {"status": "ok"}
|
|
156
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
157
|
-
ok = False
|
|
158
|
-
checks["run_store"] = {"status": "error", "detail": str(exc)}
|
|
159
|
-
try:
|
|
160
|
-
orchestrator = self._orchestrator_factory(None, None)
|
|
161
|
-
mode = getattr(orchestrator, "mode", "simulation")
|
|
162
|
-
if mode == "simulation":
|
|
163
|
-
if self._allow_simulation_runs:
|
|
164
|
-
checks["orchestrator"] = {
|
|
165
|
-
"status": "ok",
|
|
166
|
-
"mode": mode,
|
|
167
|
-
"detail": "Simulation runs are explicitly enabled for this non-product runtime.",
|
|
168
|
-
}
|
|
169
|
-
else:
|
|
170
|
-
ready = False
|
|
171
|
-
checks["orchestrator"] = {
|
|
172
|
-
"status": "unavailable",
|
|
173
|
-
"mode": mode,
|
|
174
|
-
"detail": "No LLM-backed model is loaded; product execution API refuses simulation runs.",
|
|
175
|
-
}
|
|
176
|
-
else:
|
|
177
|
-
checks["orchestrator"] = {"status": "ok", "mode": mode}
|
|
178
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
179
|
-
ok = False
|
|
180
|
-
checks["orchestrator"] = {"status": "error", "detail": str(exc)}
|
|
181
|
-
return {
|
|
182
|
-
"status": "ok" if ok and ready else "unavailable" if ok else "degraded",
|
|
183
|
-
"ready": bool(ok and ready),
|
|
184
|
-
"checks": checks,
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
def _live_orchestrator(self, user_email: Optional[str], scope: Optional[str]) -> Any:
|
|
188
|
-
orchestrator = self._orchestrator_factory(user_email or None, scope)
|
|
189
|
-
mode = getattr(orchestrator, "mode", "simulation")
|
|
190
|
-
if mode == "simulation" and not self._allow_simulation_runs:
|
|
191
|
-
raise AgentRuntimeUnavailable(
|
|
192
|
-
"Agent execution is unavailable because no LLM-backed model is loaded. "
|
|
193
|
-
"Simulation mode is disabled in the product execution API so it cannot be recorded as real success."
|
|
194
|
-
)
|
|
195
|
-
return orchestrator
|
|
196
|
-
|
|
197
|
-
# ── roster + status ───────────────────────────────────────────────────
|
|
198
|
-
def _roster(self, runs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
199
|
-
"""Canonical role roster enriched with real run statistics."""
|
|
200
|
-
by_agent: Dict[str, Dict[str, Any]] = {}
|
|
201
|
-
for run in runs:
|
|
202
|
-
aid = str(run.get("agent_id") or "")
|
|
203
|
-
entry = by_agent.setdefault(aid, {"runs": 0, "last_status": None, "last_at": None})
|
|
204
|
-
entry["runs"] += 1
|
|
205
|
-
if entry["last_at"] is None: # runs are newest-first
|
|
206
|
-
entry["last_status"] = run.get("status")
|
|
207
|
-
entry["last_at"] = run.get("created_at") or run.get("completed_at")
|
|
208
|
-
|
|
209
|
-
roster: List[Dict[str, Any]] = []
|
|
210
|
-
order = list(CORE_PIPELINE) # planner, executor, reviewer first
|
|
211
|
-
ordered_roles = order + [r for r in AGENT_ROLES if r not in order]
|
|
212
|
-
for role in ordered_roles:
|
|
213
|
-
agent_id = ROLE_AGENT_IDS.get(role, f"agent:{role}")
|
|
214
|
-
stats = by_agent.get(agent_id, {"runs": 0, "last_status": None, "last_at": None})
|
|
215
|
-
handoffs = []
|
|
216
|
-
if role == "planner":
|
|
217
|
-
handoffs = [ROLE_AGENT_IDS["executor"]]
|
|
218
|
-
elif role == "executor":
|
|
219
|
-
handoffs = [ROLE_AGENT_IDS["reviewer"]]
|
|
220
|
-
roster.append({
|
|
221
|
-
"id": agent_id,
|
|
222
|
-
"name": role.capitalize(),
|
|
223
|
-
"role": ROLE_DESCRIPTIONS.get(role, ""),
|
|
224
|
-
"state": "available" if role != "release" else "idle",
|
|
225
|
-
"runs": stats["runs"],
|
|
226
|
-
"last_status": stats["last_status"],
|
|
227
|
-
"last_at": stats["last_at"],
|
|
228
|
-
"handoffs": handoffs,
|
|
229
|
-
})
|
|
230
|
-
return roster
|
|
231
|
-
|
|
232
|
-
def status(self, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
233
|
-
try:
|
|
234
|
-
listing = self._store.list_agents(workspace_id=scope)
|
|
235
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
236
|
-
listing = {"agents": [], "runs": [], "error": str(exc)}
|
|
237
|
-
runs = list(listing.get("runs") or [])
|
|
238
|
-
active = sum(1 for r in runs if str(r.get("status")) in _ACTIVE_STATUSES)
|
|
239
|
-
health = self.health()
|
|
240
|
-
orchestrator_status = (health.get("checks") or {}).get("orchestrator") or {}
|
|
241
|
-
ready = bool(health.get("ready"))
|
|
242
|
-
return {
|
|
243
|
-
"runtime": {
|
|
244
|
-
"ready": ready,
|
|
245
|
-
"version": MULTI_AGENT_VERSION,
|
|
246
|
-
"execution_mode": self._execution_mode(),
|
|
247
|
-
"mode": orchestrator_status.get("mode", "unknown"),
|
|
248
|
-
"unavailable_reason": None if ready else orchestrator_status.get("detail"),
|
|
249
|
-
"default_pipeline": list(CORE_PIPELINE),
|
|
250
|
-
"total_runs": len(runs),
|
|
251
|
-
"active_runs": active,
|
|
252
|
-
},
|
|
253
|
-
"health": health,
|
|
254
|
-
"roles": self.roles(),
|
|
255
|
-
"agents": self._roster(runs),
|
|
256
|
-
"runs": runs[:25],
|
|
257
|
-
"contracts": contract_views(runs[:25]),
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
def preview(
|
|
261
|
-
self,
|
|
262
|
-
goal: str,
|
|
263
|
-
*,
|
|
264
|
-
roles: Optional[List[str]] = None,
|
|
265
|
-
inputs: Optional[Dict[str, Any]] = None,
|
|
266
|
-
max_retries: int = 2,
|
|
267
|
-
scope: Optional[str] = None,
|
|
268
|
-
) -> Dict[str, Any]:
|
|
269
|
-
"""Return the execution contract without reserving or starting a run.
|
|
270
|
-
|
|
271
|
-
The preview is the product-facing readiness gate for agent execution:
|
|
272
|
-
clients can show whether a real LLM-backed run can start, which roles
|
|
273
|
-
will execute, how retry limits are clamped, and why the run is blocked.
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
requested_roles = list(roles or CORE_PIPELINE)
|
|
277
|
-
unknown_roles = [role for role in requested_roles if role not in AGENT_ROLES]
|
|
278
|
-
health = self.health()
|
|
279
|
-
goal_ready = bool(str(goal or "").strip())
|
|
280
|
-
retry_budget = self._clamp_retries(max_retries)
|
|
281
|
-
blocking_reasons: List[str] = []
|
|
282
|
-
if not goal_ready:
|
|
283
|
-
blocking_reasons.append("goal is required")
|
|
284
|
-
if unknown_roles:
|
|
285
|
-
blocking_reasons.append(f"unknown roles: {', '.join(unknown_roles)}")
|
|
286
|
-
if not health.get("ready"):
|
|
287
|
-
orchestrator = (health.get("checks") or {}).get("orchestrator") or {}
|
|
288
|
-
blocking_reasons.append(str(orchestrator.get("detail") or "agent runtime is unavailable"))
|
|
289
|
-
can_start = not blocking_reasons
|
|
290
|
-
return {
|
|
291
|
-
"ready": can_start,
|
|
292
|
-
"can_start": can_start,
|
|
293
|
-
"blocking_reasons": blocking_reasons,
|
|
294
|
-
"goal": str(goal or "").strip(),
|
|
295
|
-
"roles": requested_roles,
|
|
296
|
-
"unknown_roles": unknown_roles,
|
|
297
|
-
"inputs_keys": sorted((inputs or {}).keys()),
|
|
298
|
-
"max_retries": retry_budget,
|
|
299
|
-
"max_retries_requested": max_retries,
|
|
300
|
-
"scope": scope,
|
|
301
|
-
"execution_mode": self._execution_mode(),
|
|
302
|
-
"runtime": {
|
|
303
|
-
"version": MULTI_AGENT_VERSION,
|
|
304
|
-
"default_pipeline": list(CORE_PIPELINE),
|
|
305
|
-
"max_retries_cap": self._max_retries_cap,
|
|
306
|
-
"simulation_runs_allowed": self._allow_simulation_runs,
|
|
307
|
-
},
|
|
308
|
-
"health": health,
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
# ── events / state ────────────────────────────────────────────────────
|
|
312
|
-
def list_runs(self, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
313
|
-
listing = self._store.list_agents(workspace_id=scope)
|
|
314
|
-
runs = list(listing.get("runs") or [])
|
|
315
|
-
payload = dict(listing)
|
|
316
|
-
payload["contracts"] = contract_views(runs)
|
|
317
|
-
return payload
|
|
318
|
-
|
|
319
|
-
def get_run(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
320
|
-
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
321
|
-
payload = {"run": run}
|
|
322
|
-
contract = self._ensure_contract(run)
|
|
323
|
-
if contract is not None:
|
|
324
|
-
payload["contract"] = contract_view(contract)
|
|
325
|
-
return payload
|
|
326
|
-
|
|
327
|
-
def replay(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
328
|
-
replay = self._store.replay_agent_run(run_id, workspace_id=scope)
|
|
329
|
-
payload = {"replay": replay}
|
|
330
|
-
contract = extract_contract(replay)
|
|
331
|
-
if contract is not None:
|
|
332
|
-
payload["contract"] = contract_view(contract)
|
|
333
|
-
return payload
|
|
334
|
-
|
|
335
|
-
def events(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
336
|
-
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
337
|
-
status = str(run.get("status") or "")
|
|
338
|
-
contract = self._ensure_contract(run)
|
|
339
|
-
return {
|
|
340
|
-
"run_id": run_id,
|
|
341
|
-
"status": status,
|
|
342
|
-
"is_final": status in _TERMINAL_STATUSES or status not in _ACTIVE_STATUSES,
|
|
343
|
-
"current_role": run.get("current_role"),
|
|
344
|
-
"timeline": run.get("timeline") or [],
|
|
345
|
-
"handoffs": run.get("handoffs") or [],
|
|
346
|
-
"contract": contract_view(contract) if contract is not None else None,
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
# ── execution ─────────────────────────────────────────────────────────
|
|
350
|
-
def _fire_pre_run(
|
|
351
|
-
self,
|
|
352
|
-
*,
|
|
353
|
-
goal: str,
|
|
354
|
-
roles: Optional[List[str]],
|
|
355
|
-
max_retries: int,
|
|
356
|
-
user_email: Optional[str],
|
|
357
|
-
scope: Optional[str],
|
|
358
|
-
) -> Optional[Dict[str, Any]]:
|
|
359
|
-
pre_dispatch: Optional[Dict[str, Any]] = None
|
|
360
|
-
if self._hooks is not None:
|
|
361
|
-
pre_dispatch = self._hooks.fire_hook(
|
|
362
|
-
"pre_run", "agent.run",
|
|
363
|
-
payload={"goal": goal, "roles": roles or None, "max_retries": max_retries},
|
|
364
|
-
user_email=user_email, workspace_id=scope,
|
|
365
|
-
)
|
|
366
|
-
if pre_dispatch.get("blocked"):
|
|
367
|
-
self._append_audit_event(
|
|
368
|
-
"multi_agent_run_blocked",
|
|
369
|
-
user_email=user_email,
|
|
370
|
-
reason=pre_dispatch.get("block_reason"),
|
|
371
|
-
)
|
|
372
|
-
raise PermissionError(pre_dispatch.get("block_reason") or "Agent run blocked by a pre_run hook.")
|
|
373
|
-
return pre_dispatch
|
|
374
|
-
|
|
375
|
-
def _clamp_retries(self, max_retries: int) -> int:
|
|
376
|
-
return max(0, min(int(max_retries or 0), self._max_retries_cap))
|
|
377
|
-
|
|
378
|
-
def _validate_roles(self, roles: Optional[List[str]]) -> Optional[List[str]]:
|
|
379
|
-
"""Reject unknown roles at the boundary instead of deep in orchestration.
|
|
380
|
-
|
|
381
|
-
``preview`` reports unknown roles as a blocking reason; execution paths
|
|
382
|
-
must enforce the same contract so a run can never be recorded with a
|
|
383
|
-
role the runtime does not own.
|
|
384
|
-
"""
|
|
385
|
-
if not roles:
|
|
386
|
-
return None
|
|
387
|
-
unknown = [role for role in roles if role not in AGENT_ROLES]
|
|
388
|
-
if unknown:
|
|
389
|
-
raise ValueError(f"unknown roles: {', '.join(unknown)}")
|
|
390
|
-
return list(roles)
|
|
391
|
-
|
|
392
|
-
@staticmethod
|
|
393
|
-
def _ensure_contract(record: Any) -> Optional[Dict[str, Any]]:
|
|
394
|
-
"""Return the record's family contract, synthesizing one for legacy rows.
|
|
395
|
-
|
|
396
|
-
Every read surface (get_run/events/replay) must expose the
|
|
397
|
-
``agent-run-contract/v1`` envelope even for runs persisted before the
|
|
398
|
-
contract family existed, so consumers never need a legacy branch.
|
|
399
|
-
"""
|
|
400
|
-
contract = extract_contract(record)
|
|
401
|
-
if contract is not None:
|
|
402
|
-
return contract
|
|
403
|
-
if isinstance(record, dict) and record.get("id"):
|
|
404
|
-
return run_record_contract(record)
|
|
405
|
-
return None
|
|
406
|
-
|
|
407
|
-
@staticmethod
|
|
408
|
-
def _result_patch(result: Any, goal: str) -> Dict[str, Any]:
|
|
409
|
-
return {
|
|
410
|
-
"agent_id": result.agent_id,
|
|
411
|
-
"status": result.status,
|
|
412
|
-
"input": goal,
|
|
413
|
-
"output_text": result.output,
|
|
414
|
-
"timeline": result.timeline,
|
|
415
|
-
"relationships": [ROLE_AGENT_IDS.get(r, f"agent:{r}") for r in result.roles_run],
|
|
416
|
-
"handoffs": result.handoffs,
|
|
417
|
-
"context_packets": result.context_packets,
|
|
418
|
-
"plan": result.plan,
|
|
419
|
-
"plan_review": result.plan_review,
|
|
420
|
-
"review_history": result.review_history,
|
|
421
|
-
"retry_history": result.retry_history,
|
|
422
|
-
"memory_snapshots": result.memory_snapshots,
|
|
423
|
-
"mode": getattr(result, "mode", "simulation"),
|
|
424
|
-
"contract": multi_agent_contract(result=result, goal=goal),
|
|
425
|
-
"current_role": None,
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
def _synthesize_brain_memory(self, *, goal: str, result: Any, user_email: Optional[str], scope: Optional[str]) -> None:
|
|
429
|
-
"""Turn a successful agent run into durable Brain memory + graph nodes.
|
|
430
|
-
|
|
431
|
-
This is the key to users *strongly feeling* the scale of agent work:
|
|
432
|
-
delegated goals don't disappear into Act tab; they become part of the
|
|
433
|
-
Living Brain (searchable, visible in rings/brief, connected in graph).
|
|
434
|
-
"""
|
|
435
|
-
if not self._memory_ingest:
|
|
436
|
-
return
|
|
437
|
-
if getattr(result, "status", None) not in ("ok", "retried_ok"):
|
|
438
|
-
return
|
|
439
|
-
try:
|
|
440
|
-
output = _compact_text(getattr(result, "output", ""), limit=2200)
|
|
441
|
-
plan_steps = getattr(result, "plan", None) or []
|
|
442
|
-
sections = self._agent_synthesis_sections(goal=goal, output=output, plan_steps=plan_steps, result=result)
|
|
443
|
-
plan_summary = "; ".join(sections["plan_steps"][:4])
|
|
444
|
-
content = "\n\n".join([
|
|
445
|
-
f"[Agent synthesis] Goal: {_compact_text(goal, limit=240)}",
|
|
446
|
-
f"Outcome: {output}",
|
|
447
|
-
self._format_synthesis_section("Key facts", sections["facts"]),
|
|
448
|
-
self._format_synthesis_section("Decisions", sections["decisions"]),
|
|
449
|
-
self._format_synthesis_section("Follow-ups", sections["followups"]),
|
|
450
|
-
f"Plan: {plan_summary}" if plan_summary else "",
|
|
451
|
-
]).strip()
|
|
452
|
-
if not content or len(content) < 20:
|
|
453
|
-
return # pragma: no cover — the "[Agent synthesis] Goal: " prefix is always >= 20 chars
|
|
454
|
-
tags = ["agent-synthesis", "delegated", "auto"]
|
|
455
|
-
self._memory_ingest(
|
|
456
|
-
kind="long_term",
|
|
457
|
-
content=content,
|
|
458
|
-
user_email=user_email,
|
|
459
|
-
tags=tags,
|
|
460
|
-
metadata={
|
|
461
|
-
"source": "agent_runtime",
|
|
462
|
-
"synthesis_version": 2,
|
|
463
|
-
"goal": _compact_text(goal, limit=200),
|
|
464
|
-
"roles": getattr(result, "roles_run", None),
|
|
465
|
-
"facts": sections["facts"],
|
|
466
|
-
"decisions": sections["decisions"],
|
|
467
|
-
"followups": sections["followups"],
|
|
468
|
-
},
|
|
469
|
-
graph=self._workspace_graph() if hasattr(self, "_workspace_graph") else None,
|
|
470
|
-
workspace_id=scope,
|
|
471
|
-
)
|
|
472
|
-
if sections["decisions"]:
|
|
473
|
-
self._memory_ingest(
|
|
474
|
-
kind="decisions",
|
|
475
|
-
content=f"Agent decision for {_compact_text(goal, limit=120)}: {'; '.join(sections['decisions'][:3])}",
|
|
476
|
-
user_email=user_email,
|
|
477
|
-
tags=["agent", "outcome", "decision"],
|
|
478
|
-
metadata={"source": "agent_runtime_synthesis", "synthesis_version": 2, "goal": _compact_text(goal, limit=200)},
|
|
479
|
-
workspace_id=scope,
|
|
480
|
-
)
|
|
481
|
-
if sections["followups"]:
|
|
482
|
-
self._memory_ingest(
|
|
483
|
-
kind="workspace",
|
|
484
|
-
content=f"Agent follow-ups for {_compact_text(goal, limit=120)}: {'; '.join(sections['followups'][:5])}",
|
|
485
|
-
user_email=user_email,
|
|
486
|
-
tags=["agent", "follow-up", "next-action"],
|
|
487
|
-
metadata={"source": "agent_runtime_followups", "synthesis_version": 2, "goal": _compact_text(goal, limit=200)},
|
|
488
|
-
workspace_id=scope,
|
|
489
|
-
)
|
|
490
|
-
self._enqueue_agent_followups(
|
|
491
|
-
goal=goal,
|
|
492
|
-
followups=sections["followups"],
|
|
493
|
-
result=result,
|
|
494
|
-
user_email=user_email,
|
|
495
|
-
scope=scope,
|
|
496
|
-
output=output,
|
|
497
|
-
)
|
|
498
|
-
except Exception:
|
|
499
|
-
# Synthesis must never break the run record.
|
|
500
|
-
quiet()
|
|
501
|
-
|
|
502
|
-
@staticmethod
|
|
503
|
-
def _format_synthesis_section(title: str, items: List[str]) -> str:
|
|
504
|
-
if not items:
|
|
505
|
-
return ""
|
|
506
|
-
return f"{title}:\n" + "\n".join(f"- {item}" for item in items[:5])
|
|
507
|
-
|
|
508
|
-
@staticmethod
|
|
509
|
-
def _agent_synthesis_sections(*, goal: str, output: str, plan_steps: List[Dict[str, Any]], result: Any) -> Dict[str, List[str]]:
|
|
510
|
-
plan_descriptions = [
|
|
511
|
-
_compact_text(step.get("description") or step.get("name") or step.get("id"), limit=120)
|
|
512
|
-
for step in plan_steps
|
|
513
|
-
if isinstance(step, dict) and (step.get("description") or step.get("name") or step.get("id"))
|
|
514
|
-
]
|
|
515
|
-
sentences = [
|
|
516
|
-
sentence.strip(" -•\t")
|
|
517
|
-
for sentence in output.replace("\n", ". ").split(".")
|
|
518
|
-
if sentence.strip(" -•\t")
|
|
519
|
-
]
|
|
520
|
-
facts = [_compact_text(sentence, limit=180) for sentence in sentences[:4]]
|
|
521
|
-
# `or {}` rather than the isinstance ternary: the ternary could still
|
|
522
|
-
# yield None when the attribute was a dict-typed None, and the next
|
|
523
|
-
# line calls .get on both.
|
|
524
|
-
review = getattr(result, "review", None)
|
|
525
|
-
review = review if isinstance(review, dict) else {}
|
|
526
|
-
plan_review = getattr(result, "plan_review", None)
|
|
527
|
-
plan_review = plan_review if isinstance(plan_review, dict) else {}
|
|
528
|
-
decision_seed = review.get("decision") or review.get("status") or plan_review.get("decision") or getattr(result, "status", "")
|
|
529
|
-
decisions = [
|
|
530
|
-
_compact_text(f"Run finished with {decision_seed}", limit=160),
|
|
531
|
-
_compact_text(f"Goal accepted: {goal}", limit=180),
|
|
532
|
-
]
|
|
533
|
-
followups = [
|
|
534
|
-
item for item in plan_descriptions
|
|
535
|
-
if any(token in item.lower() for token in ("next", "follow", "review", "verify", "test", "ship", "publish", "document", "implement"))
|
|
536
|
-
][:5]
|
|
537
|
-
if not followups:
|
|
538
|
-
followups = plan_descriptions[:3]
|
|
539
|
-
return {
|
|
540
|
-
"facts": [item for item in facts if item],
|
|
541
|
-
"decisions": [item for item in decisions if item],
|
|
542
|
-
"followups": [item for item in followups if item],
|
|
543
|
-
"plan_steps": plan_descriptions,
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
def _enqueue_agent_followups(
|
|
547
|
-
self,
|
|
548
|
-
*,
|
|
549
|
-
goal: str,
|
|
550
|
-
followups: List[str],
|
|
551
|
-
result: Any,
|
|
552
|
-
user_email: Optional[str],
|
|
553
|
-
scope: Optional[str],
|
|
554
|
-
output: str,
|
|
555
|
-
) -> None:
|
|
556
|
-
if self._review_sink is None:
|
|
557
|
-
return
|
|
558
|
-
for index, followup in enumerate(followups[:5], start=1):
|
|
559
|
-
try:
|
|
560
|
-
self._review_sink.create(
|
|
561
|
-
title=_compact_text(followup, limit=96) or f"Agent follow-up {index}",
|
|
562
|
-
summary=_compact_text(f"From goal: {goal}", limit=420),
|
|
563
|
-
source="agent_followup",
|
|
564
|
-
kind="task_draft",
|
|
565
|
-
payload={
|
|
566
|
-
"goal": _compact_text(goal, limit=300),
|
|
567
|
-
"followup": _compact_text(followup, limit=300),
|
|
568
|
-
"output_preview": _compact_text(output, limit=800),
|
|
569
|
-
"roles": getattr(result, "roles_run", None),
|
|
570
|
-
},
|
|
571
|
-
provenance={
|
|
572
|
-
"agent_id": getattr(result, "agent_id", ""),
|
|
573
|
-
"source_detail": "agent_runtime_followup",
|
|
574
|
-
"status": getattr(result, "status", ""),
|
|
575
|
-
},
|
|
576
|
-
user_email=user_email,
|
|
577
|
-
workspace_id=scope,
|
|
578
|
-
)
|
|
579
|
-
except Exception:
|
|
580
|
-
quiet()
|
|
581
|
-
continue
|
|
582
|
-
|
|
583
|
-
def _post_run_hooks(
|
|
584
|
-
self,
|
|
585
|
-
*,
|
|
586
|
-
run_id: Optional[str],
|
|
587
|
-
result: Any,
|
|
588
|
-
user_email: Optional[str],
|
|
589
|
-
scope: Optional[str],
|
|
590
|
-
status: Optional[str] = None,
|
|
591
|
-
) -> Optional[Dict[str, Any]]:
|
|
592
|
-
if self._hooks is None:
|
|
593
|
-
return None
|
|
594
|
-
return self._hooks.fire_hook(
|
|
595
|
-
"post_run", "agent.run",
|
|
596
|
-
payload={
|
|
597
|
-
"run_id": run_id,
|
|
598
|
-
"agent_id": result.agent_id,
|
|
599
|
-
"status": status or result.status,
|
|
600
|
-
"retries": result.retries,
|
|
601
|
-
},
|
|
602
|
-
user_email=user_email, workspace_id=scope,
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
def reserve_run(
|
|
606
|
-
self,
|
|
607
|
-
goal: str,
|
|
608
|
-
*,
|
|
609
|
-
user_email: Optional[str],
|
|
610
|
-
scope: Optional[str],
|
|
611
|
-
roles: Optional[List[str]] = None,
|
|
612
|
-
inputs: Optional[Dict[str, Any]] = None,
|
|
613
|
-
max_retries: int = 2,
|
|
614
|
-
) -> Dict[str, Any]:
|
|
615
|
-
"""Create the durable queued row used by the async executor."""
|
|
616
|
-
if not str(goal or "").strip():
|
|
617
|
-
raise ValueError("goal is required")
|
|
618
|
-
roles = self._validate_roles(roles)
|
|
619
|
-
pre_dispatch = self._fire_pre_run(
|
|
620
|
-
goal=goal,
|
|
621
|
-
roles=roles,
|
|
622
|
-
max_retries=max_retries,
|
|
623
|
-
user_email=user_email,
|
|
624
|
-
scope=scope,
|
|
625
|
-
)
|
|
626
|
-
orchestrator = self._live_orchestrator(user_email, scope)
|
|
627
|
-
mode = getattr(orchestrator, "mode", "llm")
|
|
628
|
-
run = self._store.record_agent_run(
|
|
629
|
-
agent_id=ROLE_AGENT_IDS.get("executor", "agent:executor"),
|
|
630
|
-
status="queued",
|
|
631
|
-
input_text=goal,
|
|
632
|
-
output_text="",
|
|
633
|
-
timeline=[{"event": "agent_started", "status": "queued", "timestamp": _now()}],
|
|
634
|
-
relationships=[],
|
|
635
|
-
handoffs=[],
|
|
636
|
-
context_packets=[],
|
|
637
|
-
plan=[],
|
|
638
|
-
plan_review={},
|
|
639
|
-
review_history=[],
|
|
640
|
-
retry_history=[],
|
|
641
|
-
memory_snapshots=[],
|
|
642
|
-
user_email=user_email or None,
|
|
643
|
-
graph=None,
|
|
644
|
-
workspace_id=scope,
|
|
645
|
-
mode=mode,
|
|
646
|
-
)
|
|
647
|
-
run = self._store.update_agent_run(
|
|
648
|
-
run.get("id"),
|
|
649
|
-
workspace_id=scope,
|
|
650
|
-
execution_mode="async",
|
|
651
|
-
requested_roles=roles or None,
|
|
652
|
-
inputs=inputs or {},
|
|
653
|
-
# Persist the clamped budget so the durable row reflects what the
|
|
654
|
-
# executor will actually honor, not the raw client request.
|
|
655
|
-
max_retries=self._clamp_retries(max_retries),
|
|
656
|
-
)
|
|
657
|
-
payload: Dict[str, Any] = {"run": run}
|
|
658
|
-
if pre_dispatch is not None:
|
|
659
|
-
payload["pre_run_hooks"] = pre_dispatch
|
|
660
|
-
return payload
|
|
661
|
-
|
|
662
|
-
def complete_reserved_run(
|
|
663
|
-
self,
|
|
664
|
-
run_id: str,
|
|
665
|
-
goal: str,
|
|
666
|
-
*,
|
|
667
|
-
user_email: Optional[str],
|
|
668
|
-
scope: Optional[str],
|
|
669
|
-
roles: Optional[List[str]] = None,
|
|
670
|
-
inputs: Optional[Dict[str, Any]] = None,
|
|
671
|
-
max_retries: int = 2,
|
|
672
|
-
pre_dispatch: Optional[Dict[str, Any]] = None,
|
|
673
|
-
cancel_requested: Optional[Callable[[], bool]] = None,
|
|
674
|
-
) -> Dict[str, Any]:
|
|
675
|
-
"""Execute orchestration and update an existing durable async row."""
|
|
676
|
-
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
677
|
-
base_timeline = list(run.get("timeline") or [])
|
|
678
|
-
self._store.update_agent_run(
|
|
679
|
-
run_id,
|
|
680
|
-
workspace_id=scope,
|
|
681
|
-
status="running",
|
|
682
|
-
current_role=(roles or list(CORE_PIPELINE))[0] if (roles or CORE_PIPELINE) else None,
|
|
683
|
-
started_at=run.get("started_at") or _now(),
|
|
684
|
-
)
|
|
685
|
-
try:
|
|
686
|
-
orchestrator = self._live_orchestrator(user_email, scope)
|
|
687
|
-
result = orchestrator.run(
|
|
688
|
-
goal,
|
|
689
|
-
user_email=user_email or None,
|
|
690
|
-
workspace_id=scope,
|
|
691
|
-
inputs=inputs or {},
|
|
692
|
-
roles=roles or None,
|
|
693
|
-
max_retries=self._clamp_retries(max_retries),
|
|
694
|
-
)
|
|
695
|
-
except Exception as exc:
|
|
696
|
-
failed = self._store.update_agent_run(
|
|
697
|
-
run_id,
|
|
698
|
-
workspace_id=scope,
|
|
699
|
-
graph=self._workspace_graph(),
|
|
700
|
-
status="failed",
|
|
701
|
-
current_role=None,
|
|
702
|
-
error=str(exc),
|
|
703
|
-
output_text=str(exc),
|
|
704
|
-
timeline=base_timeline + [{
|
|
705
|
-
"event": "execution_failed",
|
|
706
|
-
"status": "failed",
|
|
707
|
-
"detail": str(exc),
|
|
708
|
-
"timestamp": _now(),
|
|
709
|
-
}],
|
|
710
|
-
)
|
|
711
|
-
self._append_audit_event("multi_agent_run", user_email=user_email, agent_id=failed.get("agent_id"), status="failed", retries=0)
|
|
712
|
-
return {"run": failed, "result": {"status": "failed", "error": str(exc)}}
|
|
713
|
-
|
|
714
|
-
patch = self._result_patch(result, goal)
|
|
715
|
-
patch["timeline"] = base_timeline + list(result.timeline or [])
|
|
716
|
-
if cancel_requested is not None and cancel_requested():
|
|
717
|
-
patch["status"] = "cancelled"
|
|
718
|
-
patch["current_role"] = None
|
|
719
|
-
patch["cancel_reason"] = "cancelled after the current synchronous step completed"
|
|
720
|
-
patch["cancelled_at"] = _now()
|
|
721
|
-
patch["timeline"] = patch["timeline"] + [{
|
|
722
|
-
"event": "execution_cancelled",
|
|
723
|
-
"status": "cancelled",
|
|
724
|
-
"reason": patch["cancel_reason"],
|
|
725
|
-
"timestamp": _now(),
|
|
726
|
-
}]
|
|
727
|
-
updated = self._store.update_agent_run(
|
|
728
|
-
run_id,
|
|
729
|
-
workspace_id=scope,
|
|
730
|
-
graph=self._workspace_graph(),
|
|
731
|
-
patch=patch,
|
|
732
|
-
)
|
|
733
|
-
self._append_audit_event(
|
|
734
|
-
"multi_agent_run",
|
|
735
|
-
user_email=user_email,
|
|
736
|
-
agent_id=result.agent_id,
|
|
737
|
-
status=updated.get("status") or result.status,
|
|
738
|
-
retries=result.retries,
|
|
739
|
-
)
|
|
740
|
-
# Large-scale user-visible impact: successful runs enrich the Brain permanently.
|
|
741
|
-
if (updated.get("status") or result.status) in ("ok", "retried_ok"):
|
|
742
|
-
self._synthesize_brain_memory(goal=goal, result=result, user_email=user_email, scope=scope)
|
|
743
|
-
post_dispatch = self._post_run_hooks(
|
|
744
|
-
run_id=run_id,
|
|
745
|
-
result=result,
|
|
746
|
-
user_email=user_email,
|
|
747
|
-
scope=scope,
|
|
748
|
-
status=updated.get("status") or result.status,
|
|
749
|
-
)
|
|
750
|
-
result_payload = result.as_dict()
|
|
751
|
-
result_payload["contract"] = multi_agent_contract(
|
|
752
|
-
result=result,
|
|
753
|
-
goal=goal,
|
|
754
|
-
run_id=run_id,
|
|
755
|
-
current_role=updated.get("current_role") if isinstance(updated, dict) else None,
|
|
756
|
-
)
|
|
757
|
-
if updated.get("status") == "cancelled":
|
|
758
|
-
result_payload = {"status": "cancelled", "reason": updated.get("cancel_reason"), "completed_result": result_payload}
|
|
759
|
-
payload = {"run": updated, "result": result_payload}
|
|
760
|
-
if pre_dispatch is not None:
|
|
761
|
-
payload["pre_run_hooks"] = pre_dispatch
|
|
762
|
-
if post_dispatch is not None:
|
|
763
|
-
payload["post_run_hooks"] = post_dispatch
|
|
764
|
-
return payload
|
|
765
|
-
|
|
766
|
-
def start(
|
|
767
|
-
self,
|
|
768
|
-
goal: str,
|
|
769
|
-
*,
|
|
770
|
-
user_email: Optional[str],
|
|
771
|
-
scope: Optional[str],
|
|
772
|
-
roles: Optional[List[str]] = None,
|
|
773
|
-
inputs: Optional[Dict[str, Any]] = None,
|
|
774
|
-
max_retries: int = 2,
|
|
775
|
-
) -> Dict[str, Any]:
|
|
776
|
-
if not str(goal or "").strip():
|
|
777
|
-
raise ValueError("goal is required")
|
|
778
|
-
roles = self._validate_roles(roles)
|
|
779
|
-
|
|
780
|
-
pre_dispatch = self._fire_pre_run(
|
|
781
|
-
goal=goal,
|
|
782
|
-
roles=roles,
|
|
783
|
-
max_retries=max_retries,
|
|
784
|
-
user_email=user_email,
|
|
785
|
-
scope=scope,
|
|
786
|
-
)
|
|
787
|
-
orchestrator = self._live_orchestrator(user_email, scope)
|
|
788
|
-
result = orchestrator.run(
|
|
789
|
-
goal,
|
|
790
|
-
user_email=user_email or None,
|
|
791
|
-
workspace_id=scope,
|
|
792
|
-
inputs=inputs or {},
|
|
793
|
-
roles=roles or None,
|
|
794
|
-
max_retries=self._clamp_retries(max_retries),
|
|
795
|
-
)
|
|
796
|
-
run = self._store.record_agent_run(
|
|
797
|
-
agent_id=result.agent_id,
|
|
798
|
-
status=result.status,
|
|
799
|
-
input_text=goal,
|
|
800
|
-
output_text=result.output,
|
|
801
|
-
timeline=result.timeline,
|
|
802
|
-
relationships=[ROLE_AGENT_IDS.get(r, f"agent:{r}") for r in result.roles_run],
|
|
803
|
-
handoffs=result.handoffs,
|
|
804
|
-
context_packets=result.context_packets,
|
|
805
|
-
plan=result.plan,
|
|
806
|
-
plan_review=result.plan_review,
|
|
807
|
-
review_history=result.review_history,
|
|
808
|
-
retry_history=result.retry_history,
|
|
809
|
-
memory_snapshots=result.memory_snapshots,
|
|
810
|
-
user_email=user_email or None,
|
|
811
|
-
graph=self._workspace_graph(),
|
|
812
|
-
workspace_id=scope,
|
|
813
|
-
mode=getattr(result, "mode", "simulation"),
|
|
814
|
-
)
|
|
815
|
-
self._append_audit_event(
|
|
816
|
-
"multi_agent_run",
|
|
817
|
-
user_email=user_email,
|
|
818
|
-
agent_id=result.agent_id,
|
|
819
|
-
status=result.status,
|
|
820
|
-
retries=result.retries,
|
|
821
|
-
)
|
|
822
|
-
# Large-scale user-visible impact: successful runs enrich the Brain permanently.
|
|
823
|
-
if result.status in ("ok", "retried_ok"):
|
|
824
|
-
self._synthesize_brain_memory(goal=goal, result=result, user_email=user_email, scope=scope)
|
|
825
|
-
|
|
826
|
-
run_id = run.get("id") or run.get("run_id") if isinstance(run, dict) else None
|
|
827
|
-
post_dispatch = self._post_run_hooks(
|
|
828
|
-
run_id=run_id,
|
|
829
|
-
result=result,
|
|
830
|
-
user_email=user_email,
|
|
831
|
-
scope=scope,
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
result_payload = result.as_dict()
|
|
835
|
-
result_payload["contract"] = multi_agent_contract(result=result, goal=goal, run_id=run.get("id") if isinstance(run, dict) else None)
|
|
836
|
-
payload = {"run": run, "result": result_payload}
|
|
837
|
-
if pre_dispatch is not None:
|
|
838
|
-
payload["pre_run_hooks"] = pre_dispatch
|
|
839
|
-
if post_dispatch is not None:
|
|
840
|
-
payload["post_run_hooks"] = post_dispatch
|
|
841
|
-
return payload
|
|
842
|
-
|
|
843
|
-
def stop(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
844
|
-
"""Best-effort stop.
|
|
845
|
-
|
|
846
|
-
The default runtime executes synchronously, so by the time a run id
|
|
847
|
-
exists the run has already completed. Report that honestly rather than
|
|
848
|
-
pretending a cancellation occurred.
|
|
849
|
-
"""
|
|
850
|
-
if self._run_executor is not None:
|
|
851
|
-
return self._run_executor.cancel(run_id, kind="agent", scope=scope)
|
|
852
|
-
try:
|
|
853
|
-
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
854
|
-
except FileNotFoundError:
|
|
855
|
-
return {"stopped": False, "reason": "run not found", "run_id": run_id}
|
|
856
|
-
status = str(run.get("status") or "")
|
|
857
|
-
if status in _ACTIVE_STATUSES:
|
|
858
|
-
return {"stopped": False, "reason": "asynchronous cancellation is not supported by the synchronous runtime", "run_id": run_id, "status": status}
|
|
859
|
-
return {"stopped": False, "reason": "run already finished", "run_id": run_id, "status": status}
|