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,942 +0,0 @@
|
|
|
1
|
-
"""Multi-Agent Runtime 2.1. (lattice_brain/runtime/multi_agent.py)
|
|
2
|
-
|
|
3
|
-
책임: 역할 파이프라인 실행, handoff/context_packet/timeline/review/retry 기록 생성,
|
|
4
|
-
OrchestrationContext, default/llm_role_runner, MultiAgentOrchestrator.
|
|
5
|
-
의존성: 없음 (순수). runner 주입으로 simulation vs llm 분리.
|
|
6
|
-
상위 호출자: agent_runtime.py (orchestrator_factory), platform_runtime.
|
|
7
|
-
|
|
8
|
-
The runtime remains a small, dependency-injected orchestrator, but v2.1 makes
|
|
9
|
-
the operational objects first-class: handoffs, context packets, review/retry
|
|
10
|
-
history, replayable timeline events, and explicit planning records. The default
|
|
11
|
-
runner is still deterministic and LLM-free so tests, local demos, and Community
|
|
12
|
-
installations can exercise the full Planner -> Executor -> Reviewer loop.
|
|
13
|
-
|
|
14
|
-
Consistency with the single-agent harness (latticeai.core.agent)
|
|
15
|
-
----------------------------------------------------------------
|
|
16
|
-
Both runtimes expose the shared ``agent-run-contract/v1`` envelope
|
|
17
|
-
(:mod:`.contracts`), and every terminal status this orchestrator emits
|
|
18
|
-
(``ok`` / ``retried_ok`` / ``failed``) is a member of
|
|
19
|
-
``statuses.RUN_TERMINAL_STATUSES``. Three differences are intentional design,
|
|
20
|
-
not drift:
|
|
21
|
-
|
|
22
|
-
* **Tool dispatch** — this orchestrator is pure and never executes tools
|
|
23
|
-
itself. Tool work reachable from a multi-agent step flows through the
|
|
24
|
-
*injected* ``workflow_runner`` / ``plugin_runner`` seams (wired in
|
|
25
|
-
``latticeai.services.platform_runtime``), and those seams route every call
|
|
26
|
-
through the same shared ``hooks.dispatch_tool`` pre_tool/post_tool lifecycle
|
|
27
|
-
the single-agent loop uses.
|
|
28
|
-
* **Change governance** — the single-agent loop stages mutations of existing
|
|
29
|
-
content as review proposals (``AgentDeps.change_governor``); the injected
|
|
30
|
-
workflow tool node instead pauses non-auto-approve tools into
|
|
31
|
-
``awaiting_approval`` (``ApprovalRequired``). Different mechanisms, same
|
|
32
|
-
fail-closed outcome: no unapproved mutation executes from either runtime.
|
|
33
|
-
* **Tracing** — the single-agent loop records a ``LoopTrace`` event stream;
|
|
34
|
-
this runtime records replayable ``timeline`` events. Both surface uniformly
|
|
35
|
-
as the contract's ``timeline``.
|
|
36
|
-
|
|
37
|
-
Run-level ``pre_run`` / ``post_run`` hooks fire in
|
|
38
|
-
``lattice_brain.runtime.agent_runtime.AgentRuntime``, which wraps this
|
|
39
|
-
orchestrator for the product ``/agents`` surface.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
from __future__ import annotations
|
|
43
|
-
|
|
44
|
-
import json
|
|
45
|
-
import re
|
|
46
|
-
from dataclasses import dataclass, field
|
|
47
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
48
|
-
|
|
49
|
-
from ..utils import now_iso as _now
|
|
50
|
-
from .contracts import multi_agent_contract
|
|
51
|
-
|
|
52
|
-
MULTI_AGENT_VERSION = "11.5.2"
|
|
53
|
-
|
|
54
|
-
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
55
|
-
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
56
|
-
|
|
57
|
-
ROLE_AGENT_IDS = {
|
|
58
|
-
"researcher": "agent:researcher",
|
|
59
|
-
"planner": "agent:planner",
|
|
60
|
-
"executor": "agent:executor",
|
|
61
|
-
"reviewer": "agent:reviewer",
|
|
62
|
-
"release": "agent:release",
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
HANDOFF_STATUSES = (
|
|
66
|
-
"created",
|
|
67
|
-
"accepted",
|
|
68
|
-
"running",
|
|
69
|
-
"blocked",
|
|
70
|
-
"completed",
|
|
71
|
-
"rejected",
|
|
72
|
-
"retry_requested",
|
|
73
|
-
"cancelled",
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
REVIEW_OUTCOMES = ("approve", "reject", "retry")
|
|
77
|
-
|
|
78
|
-
_SECRET_KEYS = ("secret", "token", "password", "api_key", "apikey", "credential")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _redact(value: Any) -> Any:
|
|
82
|
-
"""Return a JSON-safe value with obvious secret fields redacted."""
|
|
83
|
-
if isinstance(value, dict):
|
|
84
|
-
clean: Dict[str, Any] = {}
|
|
85
|
-
for key, item in value.items():
|
|
86
|
-
if any(part in str(key).lower() for part in _SECRET_KEYS):
|
|
87
|
-
clean[key] = "[redacted]"
|
|
88
|
-
else:
|
|
89
|
-
clean[key] = _redact(item)
|
|
90
|
-
return clean
|
|
91
|
-
if isinstance(value, list):
|
|
92
|
-
return [_redact(item) for item in value[:100]]
|
|
93
|
-
if isinstance(value, tuple):
|
|
94
|
-
return [_redact(item) for item in value[:100]]
|
|
95
|
-
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
96
|
-
return value
|
|
97
|
-
return str(value)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _review_outcome(review: Dict[str, Any]) -> str:
|
|
101
|
-
raw = str(review.get("outcome") or review.get("verdict") or "").lower().strip()
|
|
102
|
-
if raw in {"approve", "approved", "pass", "passed", "ok"}:
|
|
103
|
-
return "approve"
|
|
104
|
-
if raw in {"reject", "rejected", "fail", "failed"}:
|
|
105
|
-
return "reject"
|
|
106
|
-
if raw == "retry":
|
|
107
|
-
return "retry"
|
|
108
|
-
return "approve"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@dataclass
|
|
112
|
-
class AgentContextPacket:
|
|
113
|
-
"""Structured, replay-safe context transferred between agent roles."""
|
|
114
|
-
|
|
115
|
-
packet_id: str
|
|
116
|
-
objective: str
|
|
117
|
-
task_summary: str
|
|
118
|
-
workspace_context: Dict[str, Any] = field(default_factory=dict)
|
|
119
|
-
graph_context: Dict[str, Any] = field(default_factory=dict)
|
|
120
|
-
memory_context: List[Any] = field(default_factory=list)
|
|
121
|
-
workflow_context: Dict[str, Any] = field(default_factory=dict)
|
|
122
|
-
plugin_outputs: List[Any] = field(default_factory=list)
|
|
123
|
-
constraints: List[str] = field(default_factory=list)
|
|
124
|
-
reviewer_notes: List[str] = field(default_factory=list)
|
|
125
|
-
retry_metadata: Dict[str, Any] = field(default_factory=dict)
|
|
126
|
-
created_at: str = field(default_factory=_now)
|
|
127
|
-
|
|
128
|
-
def as_dict(self) -> Dict[str, Any]:
|
|
129
|
-
return _redact({
|
|
130
|
-
"packet_id": self.packet_id,
|
|
131
|
-
"objective": self.objective,
|
|
132
|
-
"task_summary": self.task_summary,
|
|
133
|
-
"workspace_context": self.workspace_context,
|
|
134
|
-
"graph_context": self.graph_context,
|
|
135
|
-
"memory_context": self.memory_context,
|
|
136
|
-
"workflow_context": self.workflow_context,
|
|
137
|
-
"plugin_outputs": self.plugin_outputs,
|
|
138
|
-
"constraints": self.constraints,
|
|
139
|
-
"reviewer_notes": self.reviewer_notes,
|
|
140
|
-
"retry_metadata": self.retry_metadata,
|
|
141
|
-
"created_at": self.created_at,
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@dataclass
|
|
146
|
-
class AgentHandoff:
|
|
147
|
-
"""Inspectable handoff between two agent roles."""
|
|
148
|
-
|
|
149
|
-
handoff_id: str
|
|
150
|
-
source_agent: str
|
|
151
|
-
target_agent: str
|
|
152
|
-
reason: str
|
|
153
|
-
task_summary: str
|
|
154
|
-
context_packet: Dict[str, Any]
|
|
155
|
-
status: str = "created"
|
|
156
|
-
created_at: str = field(default_factory=_now)
|
|
157
|
-
accepted_at: Optional[str] = None
|
|
158
|
-
started_at: Optional[str] = None
|
|
159
|
-
completed_at: Optional[str] = None
|
|
160
|
-
|
|
161
|
-
def as_dict(self) -> Dict[str, Any]:
|
|
162
|
-
return {
|
|
163
|
-
"handoff_id": self.handoff_id,
|
|
164
|
-
"source_agent": self.source_agent,
|
|
165
|
-
"target_agent": self.target_agent,
|
|
166
|
-
"reason": self.reason,
|
|
167
|
-
"task_summary": self.task_summary,
|
|
168
|
-
"context_packet": self.context_packet,
|
|
169
|
-
"status": self.status,
|
|
170
|
-
"created_at": self.created_at,
|
|
171
|
-
"accepted_at": self.accepted_at,
|
|
172
|
-
"started_at": self.started_at,
|
|
173
|
-
"completed_at": self.completed_at,
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@dataclass
|
|
178
|
-
class OrchestrationContext:
|
|
179
|
-
"""Mutable carrier threaded through every role stage."""
|
|
180
|
-
|
|
181
|
-
goal: str
|
|
182
|
-
user_email: Optional[str] = None
|
|
183
|
-
workspace_id: Optional[str] = None
|
|
184
|
-
inputs: Dict[str, Any] = field(default_factory=dict)
|
|
185
|
-
plan: List[Dict[str, Any]] = field(default_factory=list)
|
|
186
|
-
plan_id: str = ""
|
|
187
|
-
plan_review: Dict[str, Any] = field(default_factory=dict)
|
|
188
|
-
research: List[str] = field(default_factory=list)
|
|
189
|
-
executed: List[Dict[str, Any]] = field(default_factory=list)
|
|
190
|
-
plugin_outputs: List[Any] = field(default_factory=list)
|
|
191
|
-
workflow_outputs: List[Any] = field(default_factory=list)
|
|
192
|
-
review: Dict[str, Any] = field(default_factory=dict)
|
|
193
|
-
review_history: List[Dict[str, Any]] = field(default_factory=list)
|
|
194
|
-
retry_history: List[Dict[str, Any]] = field(default_factory=list)
|
|
195
|
-
timeline: List[Dict[str, Any]] = field(default_factory=list)
|
|
196
|
-
handoffs: List[Dict[str, Any]] = field(default_factory=list)
|
|
197
|
-
context_packets: List[Dict[str, Any]] = field(default_factory=list)
|
|
198
|
-
memory_snapshots: List[Dict[str, Any]] = field(default_factory=list)
|
|
199
|
-
retries: int = 0
|
|
200
|
-
output: str = ""
|
|
201
|
-
|
|
202
|
-
def build_context_packet(
|
|
203
|
-
self,
|
|
204
|
-
*,
|
|
205
|
-
target_agent: Optional[str] = None,
|
|
206
|
-
reviewer_notes: Optional[List[str]] = None,
|
|
207
|
-
retry_metadata: Optional[Dict[str, Any]] = None,
|
|
208
|
-
) -> Dict[str, Any]:
|
|
209
|
-
packet = AgentContextPacket(
|
|
210
|
-
packet_id=f"context-packet-{len(self.context_packets) + 1}",
|
|
211
|
-
objective=self.goal,
|
|
212
|
-
task_summary=(self.output or self.goal or "Agent task")[:500],
|
|
213
|
-
workspace_context={
|
|
214
|
-
"workspace_id": self.workspace_id,
|
|
215
|
-
"user_email": self.user_email,
|
|
216
|
-
"target_agent": target_agent,
|
|
217
|
-
},
|
|
218
|
-
graph_context=_redact(self.inputs.get("graph_context") or {}),
|
|
219
|
-
memory_context=list(self.research[:20]),
|
|
220
|
-
workflow_context={
|
|
221
|
-
"requested_workflow": self.inputs.get("workflow"),
|
|
222
|
-
"workflow_outputs": self.workflow_outputs[-10:],
|
|
223
|
-
},
|
|
224
|
-
plugin_outputs=self.plugin_outputs[-10:],
|
|
225
|
-
constraints=list(self.inputs.get("constraints") or []),
|
|
226
|
-
reviewer_notes=reviewer_notes or [],
|
|
227
|
-
retry_metadata=retry_metadata or {"retry_count": self.retries},
|
|
228
|
-
).as_dict()
|
|
229
|
-
self.context_packets.append(packet)
|
|
230
|
-
return packet
|
|
231
|
-
|
|
232
|
-
def handoff(self, frm: str, to: str, note: str = "", *, status: str = "completed") -> Dict[str, Any]:
|
|
233
|
-
if status not in HANDOFF_STATUSES:
|
|
234
|
-
status = "completed"
|
|
235
|
-
handoff_id = f"handoff-{len(self.handoffs) + 1}"
|
|
236
|
-
packet = self.build_context_packet(target_agent=to)
|
|
237
|
-
now = _now()
|
|
238
|
-
record = AgentHandoff(
|
|
239
|
-
handoff_id=handoff_id,
|
|
240
|
-
source_agent=ROLE_AGENT_IDS.get(frm, f"agent:{frm}"),
|
|
241
|
-
target_agent=ROLE_AGENT_IDS.get(to, f"agent:{to}"),
|
|
242
|
-
reason=note or f"{frm} completed work for {to}",
|
|
243
|
-
task_summary=(self.output or self.goal or "Agent handoff")[:500],
|
|
244
|
-
context_packet=packet,
|
|
245
|
-
status=status,
|
|
246
|
-
created_at=now,
|
|
247
|
-
accepted_at=now if status in {"accepted", "running", "completed", "retry_requested"} else None,
|
|
248
|
-
started_at=now if status in {"running", "completed", "retry_requested"} else None,
|
|
249
|
-
completed_at=now if status in {"completed", "retry_requested"} else None,
|
|
250
|
-
).as_dict()
|
|
251
|
-
self.handoffs.append(record)
|
|
252
|
-
|
|
253
|
-
self.timeline.append({
|
|
254
|
-
"event": "handoff_created",
|
|
255
|
-
"handoff_id": handoff_id,
|
|
256
|
-
"from": frm,
|
|
257
|
-
"to": to,
|
|
258
|
-
"source_agent": record["source_agent"],
|
|
259
|
-
"target_agent": record["target_agent"],
|
|
260
|
-
"reason": record["reason"],
|
|
261
|
-
"context_packet": packet,
|
|
262
|
-
"status": "created",
|
|
263
|
-
"timestamp": now,
|
|
264
|
-
})
|
|
265
|
-
if record["accepted_at"]:
|
|
266
|
-
self.timeline.append({
|
|
267
|
-
"event": "handoff_accepted",
|
|
268
|
-
"handoff_id": handoff_id,
|
|
269
|
-
"from": frm,
|
|
270
|
-
"to": to,
|
|
271
|
-
"status": "accepted",
|
|
272
|
-
"timestamp": record["accepted_at"],
|
|
273
|
-
})
|
|
274
|
-
if status in {"completed", "retry_requested"}:
|
|
275
|
-
self.timeline.append({
|
|
276
|
-
"event": "handoff_completed",
|
|
277
|
-
"handoff_id": handoff_id,
|
|
278
|
-
"from": frm,
|
|
279
|
-
"to": to,
|
|
280
|
-
"status": status,
|
|
281
|
-
"timestamp": record["completed_at"],
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
# Backward-compatible compact event used by v2.0 UI/tests.
|
|
285
|
-
self.timeline.append({
|
|
286
|
-
"event": "handoff",
|
|
287
|
-
"handoff_id": handoff_id,
|
|
288
|
-
"from": frm,
|
|
289
|
-
"to": to,
|
|
290
|
-
"note": note,
|
|
291
|
-
"status": status,
|
|
292
|
-
"timestamp": now,
|
|
293
|
-
})
|
|
294
|
-
return record
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
@dataclass
|
|
298
|
-
class AgentRunResult:
|
|
299
|
-
agent_id: str
|
|
300
|
-
status: str # ok | failed | retried_ok
|
|
301
|
-
output: str
|
|
302
|
-
timeline: List[Dict[str, Any]]
|
|
303
|
-
plan: List[Dict[str, Any]]
|
|
304
|
-
review: Dict[str, Any]
|
|
305
|
-
roles_run: List[str]
|
|
306
|
-
retries: int = 0
|
|
307
|
-
handoffs: List[Dict[str, Any]] = field(default_factory=list)
|
|
308
|
-
context_packets: List[Dict[str, Any]] = field(default_factory=list)
|
|
309
|
-
review_history: List[Dict[str, Any]] = field(default_factory=list)
|
|
310
|
-
retry_history: List[Dict[str, Any]] = field(default_factory=list)
|
|
311
|
-
plan_review: Dict[str, Any] = field(default_factory=dict)
|
|
312
|
-
memory_snapshots: List[Dict[str, Any]] = field(default_factory=list)
|
|
313
|
-
goal: str = ""
|
|
314
|
-
# "simulation" = deterministic LLM-free runner; "llm" = model-driven (v4 runtime).
|
|
315
|
-
mode: str = "simulation"
|
|
316
|
-
|
|
317
|
-
def as_dict(self) -> Dict[str, Any]:
|
|
318
|
-
payload = {
|
|
319
|
-
"agent_id": self.agent_id,
|
|
320
|
-
"mode": self.mode,
|
|
321
|
-
"status": self.status,
|
|
322
|
-
"output": self.output,
|
|
323
|
-
"timeline": self.timeline,
|
|
324
|
-
"plan": self.plan,
|
|
325
|
-
"review": self.review,
|
|
326
|
-
"roles_run": self.roles_run,
|
|
327
|
-
"retries": self.retries,
|
|
328
|
-
"handoffs": self.handoffs,
|
|
329
|
-
"context_packets": self.context_packets,
|
|
330
|
-
"review_history": self.review_history,
|
|
331
|
-
"retry_history": self.retry_history,
|
|
332
|
-
"plan_review": self.plan_review,
|
|
333
|
-
"memory_snapshots": self.memory_snapshots,
|
|
334
|
-
"goal": self.goal,
|
|
335
|
-
}
|
|
336
|
-
payload["contract"] = multi_agent_contract(result=self, goal=self.goal or self.output)
|
|
337
|
-
return payload
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def default_role_runner(
|
|
341
|
-
*,
|
|
342
|
-
workflow_runner: Optional[Callable[..., Any]] = None,
|
|
343
|
-
plugin_runner: Optional[Callable[..., Any]] = None,
|
|
344
|
-
context_provider: Optional[Callable[[str], List[str]]] = None,
|
|
345
|
-
) -> Callable[[str, OrchestrationContext], Dict[str, Any]]:
|
|
346
|
-
"""Build a deterministic, dependency-free role runner."""
|
|
347
|
-
|
|
348
|
-
def runner(role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
|
|
349
|
-
if role == "researcher":
|
|
350
|
-
found = context_provider(ctx.goal) if context_provider else []
|
|
351
|
-
ctx.research = list(found)
|
|
352
|
-
snapshot = {
|
|
353
|
-
"snapshot_id": f"memory-snapshot-{len(ctx.memory_snapshots) + 1}",
|
|
354
|
-
"scope": "short_term",
|
|
355
|
-
"items": ctx.research[:10],
|
|
356
|
-
"created_at": _now(),
|
|
357
|
-
}
|
|
358
|
-
ctx.memory_snapshots.append(snapshot)
|
|
359
|
-
return {"role": role, "context_items": len(ctx.research), "items": ctx.research[:10], "memory_snapshot": snapshot}
|
|
360
|
-
|
|
361
|
-
if role == "planner":
|
|
362
|
-
goal = ctx.goal.strip() or "Complete the requested task"
|
|
363
|
-
requested = ctx.inputs.get("steps")
|
|
364
|
-
steps: List[Dict[str, Any]]
|
|
365
|
-
if isinstance(requested, list) and requested:
|
|
366
|
-
steps = []
|
|
367
|
-
for i, step in enumerate(requested):
|
|
368
|
-
if isinstance(step, dict):
|
|
369
|
-
item = dict(step)
|
|
370
|
-
item.setdefault("index", i)
|
|
371
|
-
item.setdefault("description", str(step.get("description") or step.get("name") or f"Step {i + 1}"))
|
|
372
|
-
item.setdefault("status", "planned")
|
|
373
|
-
else:
|
|
374
|
-
item = {"index": i, "description": str(step), "status": "planned"}
|
|
375
|
-
steps.append(item)
|
|
376
|
-
else:
|
|
377
|
-
steps = [
|
|
378
|
-
{"index": 0, "description": f"Analyze: {goal}", "status": "planned"},
|
|
379
|
-
{"index": 1, "description": f"Execute: {goal}", "status": "planned"},
|
|
380
|
-
{"index": 2, "description": "Verify the result", "status": "planned"},
|
|
381
|
-
]
|
|
382
|
-
if ctx.inputs.get("workflow") and steps:
|
|
383
|
-
steps[0]["workflow"] = ctx.inputs.get("workflow")
|
|
384
|
-
if ctx.inputs.get("plugin") and steps:
|
|
385
|
-
steps[0]["plugin"] = ctx.inputs.get("plugin")
|
|
386
|
-
ctx.plan = steps
|
|
387
|
-
ctx.plan_id = f"plan-{abs(hash((ctx.goal, len(steps)))) % 10_000_000}"
|
|
388
|
-
ctx.plan_review = {
|
|
389
|
-
"plan_id": ctx.plan_id,
|
|
390
|
-
"outcome": "approve",
|
|
391
|
-
"reason": "deterministic plan is bounded and executable",
|
|
392
|
-
"reviewed_at": _now(),
|
|
393
|
-
}
|
|
394
|
-
return {"role": role, "plan_id": ctx.plan_id, "steps": len(steps), "plan": steps, "plan_review": ctx.plan_review}
|
|
395
|
-
|
|
396
|
-
if role == "executor":
|
|
397
|
-
results = []
|
|
398
|
-
for step in ctx.plan:
|
|
399
|
-
outcome: Dict[str, Any] = {"index": step["index"], "description": step["description"]}
|
|
400
|
-
wf = step.get("workflow") or (ctx.inputs.get("workflow") if step["index"] == 0 else None)
|
|
401
|
-
pl = step.get("plugin") or (ctx.inputs.get("plugin") if step["index"] == 0 else None)
|
|
402
|
-
if wf and workflow_runner is not None:
|
|
403
|
-
try:
|
|
404
|
-
workflow_result = workflow_runner(wf, ctx)
|
|
405
|
-
outcome["workflow_result"] = workflow_result
|
|
406
|
-
ctx.workflow_outputs.append(workflow_result)
|
|
407
|
-
except Exception as exc:
|
|
408
|
-
outcome["workflow_error"] = str(exc)
|
|
409
|
-
if pl and plugin_runner is not None:
|
|
410
|
-
try:
|
|
411
|
-
plugin_result = plugin_runner(pl, ctx)
|
|
412
|
-
outcome["plugin_result"] = plugin_result
|
|
413
|
-
ctx.plugin_outputs.append(plugin_result)
|
|
414
|
-
except Exception as exc:
|
|
415
|
-
outcome["plugin_error"] = str(exc)
|
|
416
|
-
if outcome.get("workflow_error") or outcome.get("plugin_error"):
|
|
417
|
-
step["status"] = "failed"
|
|
418
|
-
outcome["status"] = "error"
|
|
419
|
-
else:
|
|
420
|
-
step["status"] = "done"
|
|
421
|
-
outcome["status"] = "done"
|
|
422
|
-
results.append(outcome)
|
|
423
|
-
ctx.executed = results
|
|
424
|
-
done = sum(1 for item in results if item.get("status") == "done")
|
|
425
|
-
ctx.output = f"Completed {done}/{len(results)} planned step(s) for: {ctx.goal}"
|
|
426
|
-
return {"role": role, "executed": len(results), "results": results, "plugin_outputs": ctx.plugin_outputs[-10:]}
|
|
427
|
-
|
|
428
|
-
if role == "reviewer":
|
|
429
|
-
ok = bool(ctx.executed) and all(r.get("status") == "done" for r in ctx.executed)
|
|
430
|
-
ctx.review = {
|
|
431
|
-
"outcome": "approve" if ok else "retry",
|
|
432
|
-
"verdict": "pass" if ok else "retry",
|
|
433
|
-
"reason": "all steps completed" if ok else "one or more steps failed or no steps executed",
|
|
434
|
-
"confidence": 0.9 if ok else 0.3,
|
|
435
|
-
"notes": [] if ok else ["executor should retry with preserved context"],
|
|
436
|
-
"reviewed_at": _now(),
|
|
437
|
-
}
|
|
438
|
-
return {"role": role, **ctx.review}
|
|
439
|
-
|
|
440
|
-
if role == "release":
|
|
441
|
-
ctx.output = ctx.output or f"Released outcome for: {ctx.goal}"
|
|
442
|
-
return {"role": role, "released": True, "summary": ctx.output}
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
"role": role,
|
|
446
|
-
"status": "skipped",
|
|
447
|
-
"reason": "this role has no deterministic behaviour (custom agents require a loaded model)",
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return runner
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
def _json_object_candidates(text: str) -> List[str]:
|
|
454
|
-
"""Every balanced ``{...}`` span in ``text``, outermost first.
|
|
455
|
-
|
|
456
|
-
``find("{")`` to ``rfind("}")`` is one span, and it is the wrong one
|
|
457
|
-
whenever the model wrote anything after its object — a closing remark, a
|
|
458
|
-
second example, a stray ``}`` in prose — because the slice then contains
|
|
459
|
-
the object *and* the noise, and parses as neither. Scanning for balance
|
|
460
|
-
yields the actual objects, in the order they appear.
|
|
461
|
-
|
|
462
|
-
String literals are tracked so a brace inside ``"a } b"`` does not close a
|
|
463
|
-
span early.
|
|
464
|
-
"""
|
|
465
|
-
spans: List[str] = []
|
|
466
|
-
depth = 0
|
|
467
|
-
start = -1
|
|
468
|
-
in_string = False
|
|
469
|
-
escaped = False
|
|
470
|
-
for index, char in enumerate(text):
|
|
471
|
-
if in_string:
|
|
472
|
-
if escaped:
|
|
473
|
-
escaped = False
|
|
474
|
-
elif char == "\\":
|
|
475
|
-
escaped = True
|
|
476
|
-
elif char == '"':
|
|
477
|
-
in_string = False
|
|
478
|
-
continue
|
|
479
|
-
if char == '"':
|
|
480
|
-
in_string = True
|
|
481
|
-
elif char == "{":
|
|
482
|
-
if depth == 0:
|
|
483
|
-
start = index
|
|
484
|
-
depth += 1
|
|
485
|
-
elif char == "}":
|
|
486
|
-
if depth:
|
|
487
|
-
depth -= 1
|
|
488
|
-
if depth == 0 and start >= 0:
|
|
489
|
-
spans.append(text[start : index + 1])
|
|
490
|
-
return spans
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
#: A comma directly before a closing brace/bracket. Small local models emit
|
|
494
|
-
#: these constantly — it is the single most common reason an otherwise correct
|
|
495
|
-
#: plan is thrown away.
|
|
496
|
-
_TRAILING_COMMA_RE = re.compile(r",(\s*[}\]])")
|
|
497
|
-
|
|
498
|
-
#: Reasoning models wrap their scratchpad in these, and the scratchpad is full
|
|
499
|
-
#: of braces. Removed before anything else so it cannot poison a span.
|
|
500
|
-
_THINK_BLOCK_RE = re.compile(
|
|
501
|
-
r"<(think|thinking|reasoning|reflection)>.*?</\1>",
|
|
502
|
-
re.DOTALL | re.IGNORECASE,
|
|
503
|
-
)
|
|
504
|
-
_THINK_OPEN_RE = re.compile(r"<(think|thinking|reasoning)>.*\Z", re.DOTALL | re.IGNORECASE)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def _extract_json_object(raw: str) -> Dict[str, Any]:
|
|
508
|
-
"""Parse one JSON object out of an LLM response (fences/prose tolerated).
|
|
509
|
-
|
|
510
|
-
Recovery only — never invention. Every strategy here returns something the
|
|
511
|
-
model actually wrote; when none of them parse, this still raises and the
|
|
512
|
-
run fails loudly with the raw output preserved, exactly as before. The
|
|
513
|
-
point is that a 1–4B model's plan should not be discarded over a trailing
|
|
514
|
-
comma or a farewell sentence.
|
|
515
|
-
"""
|
|
516
|
-
text = str(raw or "").strip()
|
|
517
|
-
text = _THINK_BLOCK_RE.sub("", text)
|
|
518
|
-
text = _THINK_OPEN_RE.sub("", text).strip()
|
|
519
|
-
|
|
520
|
-
candidates: List[str] = []
|
|
521
|
-
for fenced in re.findall(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL):
|
|
522
|
-
candidates.append(fenced)
|
|
523
|
-
candidates.extend(_json_object_candidates(text))
|
|
524
|
-
if text.startswith("{"):
|
|
525
|
-
candidates.append(text)
|
|
526
|
-
|
|
527
|
-
last_error: Optional[Exception] = None
|
|
528
|
-
empty_fallback: Optional[Dict[str, Any]] = None
|
|
529
|
-
for candidate in candidates:
|
|
530
|
-
for attempt in (candidate, _TRAILING_COMMA_RE.sub(r"\1", candidate)):
|
|
531
|
-
try:
|
|
532
|
-
parsed = json.loads(attempt)
|
|
533
|
-
except ValueError as exc:
|
|
534
|
-
last_error = exc
|
|
535
|
-
continue
|
|
536
|
-
if not isinstance(parsed, dict):
|
|
537
|
-
last_error = ValueError("model returned JSON that is not an object")
|
|
538
|
-
continue
|
|
539
|
-
if parsed:
|
|
540
|
-
return parsed
|
|
541
|
-
# A prose aside like "I considered using { } notation" is a
|
|
542
|
-
# syntactically valid empty object, and taking the first thing that
|
|
543
|
-
# parses handed that back as the plan. An empty object is only ever
|
|
544
|
-
# the answer when nothing else in the reply is one.
|
|
545
|
-
empty_fallback = parsed
|
|
546
|
-
if empty_fallback is not None:
|
|
547
|
-
return empty_fallback
|
|
548
|
-
raise last_error or ValueError("model returned no JSON object")
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def llm_role_runner(
|
|
552
|
-
*,
|
|
553
|
-
generate: Callable[..., str],
|
|
554
|
-
planner_prompt: str,
|
|
555
|
-
critic_prompt: str,
|
|
556
|
-
context_provider: Optional[Callable[[str], List[str]]] = None,
|
|
557
|
-
workflow_runner: Optional[Callable[..., Any]] = None,
|
|
558
|
-
plugin_runner: Optional[Callable[..., Any]] = None,
|
|
559
|
-
custom_agents: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
560
|
-
) -> Callable[[str, OrchestrationContext], Dict[str, Any]]:
|
|
561
|
-
"""Model-driven role runner — the real Multi-Agent Runtime (T7b).
|
|
562
|
-
|
|
563
|
-
``generate(message, context, max_tokens, temperature) -> str`` is a
|
|
564
|
-
synchronous bridge to the loaded model. Honesty contract (design-review
|
|
565
|
-
amendment): when the model responds but its plan/critique cannot be
|
|
566
|
-
parsed, the RUN FAILS with the raw output preserved in the records —
|
|
567
|
-
it never silently falls back to fabricated deterministic artifacts.
|
|
568
|
-
"""
|
|
569
|
-
base = default_role_runner(
|
|
570
|
-
workflow_runner=workflow_runner,
|
|
571
|
-
plugin_runner=plugin_runner,
|
|
572
|
-
context_provider=context_provider,
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
def _fail(ctx: OrchestrationContext, role: str, reason: str, raw: str) -> Dict[str, Any]:
|
|
576
|
-
ctx.inputs["__llm_failure__"] = {"role": role, "reason": reason, "raw": raw[:2000]}
|
|
577
|
-
ctx.review = {
|
|
578
|
-
"outcome": "reject",
|
|
579
|
-
"verdict": "fail",
|
|
580
|
-
"reason": f"{role}: {reason}",
|
|
581
|
-
"raw_output": raw[:2000],
|
|
582
|
-
"reviewed_at": _now(),
|
|
583
|
-
}
|
|
584
|
-
return {"role": role, "status": "error", "reason": reason, "raw": raw[:2000]}
|
|
585
|
-
|
|
586
|
-
def runner(role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
|
|
587
|
-
failure = ctx.inputs.get("__llm_failure__")
|
|
588
|
-
|
|
589
|
-
custom = (custom_agents or {}).get(role)
|
|
590
|
-
if custom is not None:
|
|
591
|
-
# Executable registry entry (T7e): the agent's persisted config
|
|
592
|
-
# (system_prompt, max_tokens, temperature) is actually loaded.
|
|
593
|
-
cfg = custom.get("config") or {}
|
|
594
|
-
system = str(
|
|
595
|
-
cfg.get("system_prompt")
|
|
596
|
-
or custom.get("description")
|
|
597
|
-
or f"You are {custom.get('name') or role}."
|
|
598
|
-
)
|
|
599
|
-
try:
|
|
600
|
-
out = str(generate(
|
|
601
|
-
ctx.output or ctx.goal,
|
|
602
|
-
context=system,
|
|
603
|
-
max_tokens=int(cfg.get("max_tokens") or 1024),
|
|
604
|
-
temperature=float(cfg.get("temperature") or 0.2),
|
|
605
|
-
))
|
|
606
|
-
except Exception as exc:
|
|
607
|
-
return _fail(ctx, role, f"custom agent generation failed ({exc})", "")
|
|
608
|
-
ctx.output = out
|
|
609
|
-
return {"role": role, "agent": custom.get("name"), "status": "ok",
|
|
610
|
-
"output": out[:2000]}
|
|
611
|
-
|
|
612
|
-
if role == "planner":
|
|
613
|
-
research = "\n".join(f"- {item}" for item in (ctx.research or [])[:8])
|
|
614
|
-
raw = generate(
|
|
615
|
-
"Produce a JSON execution plan for this goal. Respond with one JSON "
|
|
616
|
-
'object: {"goal": str, "steps": [{"description": str}, ...]} and nothing else.',
|
|
617
|
-
context=f"{planner_prompt}\n\nGoal: {ctx.goal}\n\nKnown context:\n{research}",
|
|
618
|
-
max_tokens=1024,
|
|
619
|
-
temperature=0.1,
|
|
620
|
-
)
|
|
621
|
-
try:
|
|
622
|
-
parsed = _extract_json_object(str(raw))
|
|
623
|
-
except Exception as exc:
|
|
624
|
-
return _fail(ctx, role, f"plan output unparseable ({exc})", str(raw))
|
|
625
|
-
steps = []
|
|
626
|
-
for i, step in enumerate(parsed.get("steps") or []):
|
|
627
|
-
description = step.get("description") if isinstance(step, dict) else str(step)
|
|
628
|
-
steps.append({"index": i, "description": str(description or f"Step {i + 1}"), "status": "planned"})
|
|
629
|
-
if not steps:
|
|
630
|
-
return _fail(ctx, role, "model returned a plan with no steps", str(raw))
|
|
631
|
-
if ctx.inputs.get("workflow"):
|
|
632
|
-
steps[0]["workflow"] = ctx.inputs.get("workflow")
|
|
633
|
-
if ctx.inputs.get("plugin"):
|
|
634
|
-
steps[0]["plugin"] = ctx.inputs.get("plugin")
|
|
635
|
-
ctx.plan = steps
|
|
636
|
-
ctx.plan_id = f"plan-{abs(hash((ctx.goal, len(steps)))) % 10_000_000}"
|
|
637
|
-
ctx.plan_review = {
|
|
638
|
-
"plan_id": ctx.plan_id,
|
|
639
|
-
"outcome": "approve",
|
|
640
|
-
"reason": "model-generated plan parsed and bounded",
|
|
641
|
-
"reviewed_at": _now(),
|
|
642
|
-
}
|
|
643
|
-
return {"role": role, "plan_id": ctx.plan_id, "steps": len(steps), "plan": steps, "plan_review": ctx.plan_review}
|
|
644
|
-
|
|
645
|
-
if role == "executor":
|
|
646
|
-
if failure:
|
|
647
|
-
return {"role": role, "status": "error", "reason": f"skipped — {failure['role']} failed"}
|
|
648
|
-
results = []
|
|
649
|
-
for step in ctx.plan:
|
|
650
|
-
outcome: Dict[str, Any] = {"index": step["index"], "description": step["description"]}
|
|
651
|
-
wf = step.get("workflow")
|
|
652
|
-
pl = step.get("plugin")
|
|
653
|
-
if wf and workflow_runner is not None:
|
|
654
|
-
try:
|
|
655
|
-
outcome["workflow_result"] = workflow_runner(wf, ctx)
|
|
656
|
-
ctx.workflow_outputs.append(outcome["workflow_result"])
|
|
657
|
-
except Exception as exc:
|
|
658
|
-
outcome["workflow_error"] = str(exc)
|
|
659
|
-
if pl and plugin_runner is not None:
|
|
660
|
-
try:
|
|
661
|
-
outcome["plugin_result"] = plugin_runner(pl, ctx)
|
|
662
|
-
ctx.plugin_outputs.append(outcome["plugin_result"])
|
|
663
|
-
except Exception as exc:
|
|
664
|
-
outcome["plugin_error"] = str(exc)
|
|
665
|
-
try:
|
|
666
|
-
outcome["result"] = str(generate(
|
|
667
|
-
f"Execute this step and return the concrete result only.\n"
|
|
668
|
-
f"Goal: {ctx.goal}\nStep: {step['description']}",
|
|
669
|
-
context="",
|
|
670
|
-
max_tokens=1024,
|
|
671
|
-
temperature=0.2,
|
|
672
|
-
))[:4000]
|
|
673
|
-
except Exception as exc:
|
|
674
|
-
outcome["error"] = str(exc)
|
|
675
|
-
if outcome.get("workflow_error") or outcome.get("plugin_error") or outcome.get("error"):
|
|
676
|
-
step["status"] = "failed"
|
|
677
|
-
outcome["status"] = "error"
|
|
678
|
-
else:
|
|
679
|
-
step["status"] = "done"
|
|
680
|
-
outcome["status"] = "done"
|
|
681
|
-
results.append(outcome)
|
|
682
|
-
ctx.executed = results
|
|
683
|
-
done = [r for r in results if r.get("status") == "done"]
|
|
684
|
-
ctx.output = "\n\n".join(str(r.get("result") or "") for r in done).strip() or (
|
|
685
|
-
f"Completed {len(done)}/{len(results)} step(s) for: {ctx.goal}"
|
|
686
|
-
)
|
|
687
|
-
return {"role": role, "executed": len(results), "results": results}
|
|
688
|
-
|
|
689
|
-
if role == "reviewer":
|
|
690
|
-
if failure:
|
|
691
|
-
# Fail-closed: an upstream unparseable model output means this
|
|
692
|
-
# run is failed, with the raw output preserved — never rescued
|
|
693
|
-
# by a rubber-stamp review.
|
|
694
|
-
ctx.review = {
|
|
695
|
-
"outcome": "reject",
|
|
696
|
-
"verdict": "fail",
|
|
697
|
-
"reason": f"{failure['role']} output unparseable",
|
|
698
|
-
"raw_output": failure.get("raw"),
|
|
699
|
-
"reviewed_at": _now(),
|
|
700
|
-
}
|
|
701
|
-
return {"role": role, **ctx.review}
|
|
702
|
-
raw = generate(
|
|
703
|
-
"Review this execution. Respond with one JSON object: "
|
|
704
|
-
'{"approve": bool, "reason": str} and nothing else.',
|
|
705
|
-
context=(
|
|
706
|
-
f"{critic_prompt}\n\nGoal: {ctx.goal}\n\n"
|
|
707
|
-
f"Steps: {[s.get('status') for s in ctx.plan]}\n\nOutput:\n{(ctx.output or '')[:3000]}"
|
|
708
|
-
),
|
|
709
|
-
max_tokens=512,
|
|
710
|
-
temperature=0.1,
|
|
711
|
-
)
|
|
712
|
-
try:
|
|
713
|
-
parsed = _extract_json_object(str(raw))
|
|
714
|
-
approve = bool(parsed.get("approve"))
|
|
715
|
-
reason = str(parsed.get("reason") or "")
|
|
716
|
-
except Exception as exc:
|
|
717
|
-
ctx.review = {
|
|
718
|
-
"outcome": "reject",
|
|
719
|
-
"verdict": "fail",
|
|
720
|
-
"reason": f"critic output unparseable ({exc})",
|
|
721
|
-
"raw_output": str(raw)[:2000],
|
|
722
|
-
"reviewed_at": _now(),
|
|
723
|
-
}
|
|
724
|
-
return {"role": role, **ctx.review}
|
|
725
|
-
ctx.review = {
|
|
726
|
-
"outcome": "approve" if approve else "retry",
|
|
727
|
-
"verdict": "pass" if approve else "retry",
|
|
728
|
-
"reason": reason or ("model approved the result" if approve else "model requested a retry"),
|
|
729
|
-
"confidence": 0.9 if approve else 0.4,
|
|
730
|
-
"notes": [],
|
|
731
|
-
"reviewed_at": _now(),
|
|
732
|
-
}
|
|
733
|
-
return {"role": role, **ctx.review}
|
|
734
|
-
|
|
735
|
-
# researcher / release / anything else: the deterministic behaviour is
|
|
736
|
-
# real work (memory recall, bookkeeping) — reuse it.
|
|
737
|
-
return base(role, ctx)
|
|
738
|
-
|
|
739
|
-
return runner
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
class MultiAgentOrchestrator:
|
|
743
|
-
"""Drives a role pipeline with handoff, planning, review, and retry."""
|
|
744
|
-
|
|
745
|
-
def __init__(
|
|
746
|
-
self,
|
|
747
|
-
role_runner: Optional[Callable[[str, OrchestrationContext], Dict[str, Any]]] = None,
|
|
748
|
-
mode: str = "simulation",
|
|
749
|
-
custom_agents: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
750
|
-
):
|
|
751
|
-
self.role_runner = role_runner or default_role_runner()
|
|
752
|
-
# Executable registry entries (T7e): a requested role may be a
|
|
753
|
-
# registered custom agent id; its config (system_prompt, …) is
|
|
754
|
-
# actually loaded at run time — registration is no longer a UI illusion.
|
|
755
|
-
self.custom_agents = dict(custom_agents or {})
|
|
756
|
-
# Honest execution-mode label persisted on every run record. The
|
|
757
|
-
# built-in runner never calls a model, so the default is "simulation";
|
|
758
|
-
# an LLM-backed runner must declare mode="llm" explicitly.
|
|
759
|
-
self.mode = mode
|
|
760
|
-
|
|
761
|
-
def _run_role(self, role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
|
|
762
|
-
started = _now()
|
|
763
|
-
if role == "reviewer":
|
|
764
|
-
ctx.timeline.append({
|
|
765
|
-
"event": "review_requested",
|
|
766
|
-
"role": role,
|
|
767
|
-
"agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
|
|
768
|
-
"timestamp": started,
|
|
769
|
-
})
|
|
770
|
-
try:
|
|
771
|
-
result = self.role_runner(role, ctx) or {}
|
|
772
|
-
status = result.get("status", "ok")
|
|
773
|
-
except Exception as exc:
|
|
774
|
-
result = {"status": "error", "error": str(exc)}
|
|
775
|
-
status = "error"
|
|
776
|
-
if role == "reviewer":
|
|
777
|
-
review = dict(ctx.review or result)
|
|
778
|
-
outcome = _review_outcome(review)
|
|
779
|
-
event = {
|
|
780
|
-
"approve": "review_approved",
|
|
781
|
-
"reject": "review_rejected",
|
|
782
|
-
"retry": "retry_requested",
|
|
783
|
-
}[outcome]
|
|
784
|
-
ctx.timeline.append({
|
|
785
|
-
"event": event,
|
|
786
|
-
"role": role,
|
|
787
|
-
"agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
|
|
788
|
-
"outcome": outcome,
|
|
789
|
-
"reason": review.get("reason", ""),
|
|
790
|
-
"review": review,
|
|
791
|
-
"timestamp": _now(),
|
|
792
|
-
})
|
|
793
|
-
ctx.timeline.append({
|
|
794
|
-
"event": "role",
|
|
795
|
-
"role": role,
|
|
796
|
-
"agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
|
|
797
|
-
"status": status,
|
|
798
|
-
"result": result,
|
|
799
|
-
"started_at": started,
|
|
800
|
-
"timestamp": _now(),
|
|
801
|
-
})
|
|
802
|
-
return result
|
|
803
|
-
|
|
804
|
-
def run(
|
|
805
|
-
self,
|
|
806
|
-
goal: str,
|
|
807
|
-
*,
|
|
808
|
-
user_email: Optional[str] = None,
|
|
809
|
-
workspace_id: Optional[str] = None,
|
|
810
|
-
inputs: Optional[Dict[str, Any]] = None,
|
|
811
|
-
roles: Optional[List[str]] = None,
|
|
812
|
-
max_retries: int = 2,
|
|
813
|
-
) -> AgentRunResult:
|
|
814
|
-
ctx = OrchestrationContext(
|
|
815
|
-
goal=goal or "",
|
|
816
|
-
user_email=user_email,
|
|
817
|
-
workspace_id=workspace_id,
|
|
818
|
-
inputs=inputs or {},
|
|
819
|
-
)
|
|
820
|
-
pipeline = [
|
|
821
|
-
r for r in (roles or list(CORE_PIPELINE))
|
|
822
|
-
if r in AGENT_ROLES or r in self.custom_agents
|
|
823
|
-
]
|
|
824
|
-
if not pipeline:
|
|
825
|
-
pipeline = list(CORE_PIPELINE)
|
|
826
|
-
max_retries = max(0, int(max_retries or 0))
|
|
827
|
-
|
|
828
|
-
ctx.timeline.append({"event": "start", "goal": ctx.goal, "pipeline": pipeline, "timestamp": _now()})
|
|
829
|
-
ctx.timeline.append({
|
|
830
|
-
"event": "agent_started",
|
|
831
|
-
"agent_id": ROLE_AGENT_IDS.get(pipeline[0], "agent:planner"),
|
|
832
|
-
"goal": ctx.goal,
|
|
833
|
-
"pipeline": pipeline,
|
|
834
|
-
"workspace_id": workspace_id,
|
|
835
|
-
"timestamp": _now(),
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
roles_run: List[str] = []
|
|
839
|
-
previous: Optional[str] = None
|
|
840
|
-
index = 0
|
|
841
|
-
while index < len(pipeline):
|
|
842
|
-
role = pipeline[index]
|
|
843
|
-
if previous is not None:
|
|
844
|
-
ctx.handoff(previous, role)
|
|
845
|
-
role_result = self._run_role(role, ctx)
|
|
846
|
-
roles_run.append(role)
|
|
847
|
-
|
|
848
|
-
# A role exception is terminal for this attempt. Continuing would
|
|
849
|
-
# let downstream roles review stale or missing output and could
|
|
850
|
-
# incorrectly convert a failed run into an approved one.
|
|
851
|
-
if str(role_result.get("status") or "").lower() == "error":
|
|
852
|
-
# Both role-error result shapes are honored: a raised exception
|
|
853
|
-
# (``_run_role``) carries ``error``; an llm_role_runner failure
|
|
854
|
-
# carries ``reason`` (with the raw model output preserved in
|
|
855
|
-
# ``ctx.review``). Either way the terminal timeline event names
|
|
856
|
-
# the real cause instead of a generic placeholder.
|
|
857
|
-
reason = str(
|
|
858
|
-
role_result.get("error")
|
|
859
|
-
or role_result.get("reason")
|
|
860
|
-
or f"{role} role failed"
|
|
861
|
-
)
|
|
862
|
-
existing_review = dict(ctx.review or {})
|
|
863
|
-
ctx.review = existing_review or {
|
|
864
|
-
"outcome": "reject",
|
|
865
|
-
"verdict": "fail",
|
|
866
|
-
"reason": reason,
|
|
867
|
-
"notes": [f"{role} did not complete"],
|
|
868
|
-
"reviewed_at": _now(),
|
|
869
|
-
}
|
|
870
|
-
ctx.timeline.append(
|
|
871
|
-
{
|
|
872
|
-
"event": "execution_failed",
|
|
873
|
-
"role": role,
|
|
874
|
-
"reason": reason,
|
|
875
|
-
"timestamp": _now(),
|
|
876
|
-
}
|
|
877
|
-
)
|
|
878
|
-
break
|
|
879
|
-
|
|
880
|
-
if role == "reviewer":
|
|
881
|
-
review = dict(ctx.review or {})
|
|
882
|
-
outcome = _review_outcome(review)
|
|
883
|
-
review_entry = {
|
|
884
|
-
"index": len(ctx.review_history),
|
|
885
|
-
"outcome": outcome,
|
|
886
|
-
"verdict": review.get("verdict") or ("pass" if outcome == "approve" else outcome),
|
|
887
|
-
"reason": review.get("reason", ""),
|
|
888
|
-
"notes": review.get("notes") or review.get("reviewer_notes") or [],
|
|
889
|
-
"retry_count": ctx.retries,
|
|
890
|
-
"timestamp": _now(),
|
|
891
|
-
}
|
|
892
|
-
ctx.review_history.append(review_entry)
|
|
893
|
-
if outcome == "retry" and ctx.retries < max_retries:
|
|
894
|
-
ctx.retries += 1
|
|
895
|
-
retry_entry = {
|
|
896
|
-
"retry": ctx.retries,
|
|
897
|
-
"limit": max_retries,
|
|
898
|
-
"reason": review_entry["reason"],
|
|
899
|
-
"reviewer_notes": review_entry["notes"],
|
|
900
|
-
"timestamp": _now(),
|
|
901
|
-
}
|
|
902
|
-
ctx.retry_history.append(retry_entry)
|
|
903
|
-
exec_index = pipeline.index("executor") if "executor" in pipeline else None
|
|
904
|
-
if exec_index is not None:
|
|
905
|
-
ctx.handoff("reviewer", "executor", note=f"retry #{ctx.retries}: {review_entry['reason']}", status="retry_requested")
|
|
906
|
-
index = exec_index
|
|
907
|
-
previous = "reviewer"
|
|
908
|
-
continue
|
|
909
|
-
if outcome == "reject":
|
|
910
|
-
ctx.timeline.append({"event": "execution_failed", "reason": review_entry["reason"], "timestamp": _now()})
|
|
911
|
-
break
|
|
912
|
-
|
|
913
|
-
previous = role
|
|
914
|
-
index += 1
|
|
915
|
-
|
|
916
|
-
final_outcome = _review_outcome(ctx.review or {})
|
|
917
|
-
if final_outcome == "approve":
|
|
918
|
-
status = "retried_ok" if ctx.retries else "ok"
|
|
919
|
-
else:
|
|
920
|
-
status = "failed"
|
|
921
|
-
if status == "failed":
|
|
922
|
-
ctx.timeline.append({"event": "execution_failed", "status": status, "retries": ctx.retries, "timestamp": _now()})
|
|
923
|
-
ctx.timeline.append({"event": "end", "status": status, "retries": ctx.retries, "timestamp": _now()})
|
|
924
|
-
|
|
925
|
-
return AgentRunResult(
|
|
926
|
-
agent_id=ROLE_AGENT_IDS.get("executor", "agent:executor"),
|
|
927
|
-
status=status,
|
|
928
|
-
output=ctx.output or f"Processed goal: {ctx.goal}",
|
|
929
|
-
timeline=ctx.timeline,
|
|
930
|
-
plan=ctx.plan,
|
|
931
|
-
review=ctx.review,
|
|
932
|
-
roles_run=roles_run,
|
|
933
|
-
retries=ctx.retries,
|
|
934
|
-
handoffs=ctx.handoffs,
|
|
935
|
-
context_packets=ctx.context_packets,
|
|
936
|
-
review_history=ctx.review_history,
|
|
937
|
-
retry_history=ctx.retry_history,
|
|
938
|
-
plan_review=ctx.plan_review,
|
|
939
|
-
memory_snapshots=ctx.memory_snapshots,
|
|
940
|
-
goal=ctx.goal,
|
|
941
|
-
mode=self.mode,
|
|
942
|
-
)
|