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,892 +0,0 @@
|
|
|
1
|
-
"""HTTP adapter for the local chat agent runtime."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
import re
|
|
9
|
-
import secrets
|
|
10
|
-
import threading
|
|
11
|
-
import time
|
|
12
|
-
from datetime import datetime, timedelta, timezone
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any, Callable, Dict, Optional
|
|
15
|
-
|
|
16
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
17
|
-
from fastapi.responses import StreamingResponse
|
|
18
|
-
|
|
19
|
-
from lattice_brain.runtime.hooks import dispatch_tool
|
|
20
|
-
from latticeai.api.chat_contracts import (
|
|
21
|
-
AgentEvalRequest,
|
|
22
|
-
AgentRequest,
|
|
23
|
-
AgentResumeRequest,
|
|
24
|
-
)
|
|
25
|
-
from latticeai.api.chat_helpers import _LANG_HINT, detect_language
|
|
26
|
-
from latticeai.api.chat_stream import agent_live_stream
|
|
27
|
-
from latticeai.api.workspace_scope import requested_workspace
|
|
28
|
-
from latticeai.core.agent import AgentRunContext, AgentState, normalize_plan
|
|
29
|
-
from latticeai.core.messages import resolve_language
|
|
30
|
-
from latticeai.core.quiet import quiet
|
|
31
|
-
from latticeai.core.run_explain import explain_run
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _bind_case(
|
|
35
|
-
execute_tool: Callable[[str, Dict[str, Any]], Any],
|
|
36
|
-
action_name: str,
|
|
37
|
-
case_input: Dict[str, Any],
|
|
38
|
-
) -> Callable[[], Any]:
|
|
39
|
-
"""Freeze this iteration's action and input into a zero-arg thunk.
|
|
40
|
-
|
|
41
|
-
Previously a lambda with default-argument binding, which is correct but
|
|
42
|
-
invisible to a type checker (`Cannot infer type of lambda`) and one
|
|
43
|
-
refactor away from every case running the last input.
|
|
44
|
-
"""
|
|
45
|
-
return lambda: execute_tool(action_name, case_input)
|
|
46
|
-
from latticeai.core.run_store import (
|
|
47
|
-
AgentRunStore,
|
|
48
|
-
hash_approval_token,
|
|
49
|
-
restore_run_context,
|
|
50
|
-
)
|
|
51
|
-
from latticeai.services.tool_dispatch import collect_artifacts, collect_created_files
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class AgentHTTPController:
|
|
55
|
-
def __init__(
|
|
56
|
-
self,
|
|
57
|
-
*,
|
|
58
|
-
runtime: Any,
|
|
59
|
-
model_router: Any,
|
|
60
|
-
require_user: Any,
|
|
61
|
-
require_admin: Any,
|
|
62
|
-
enforce_rate_limit: Any,
|
|
63
|
-
authenticated_identity: Any,
|
|
64
|
-
write_workspace: Any,
|
|
65
|
-
save_to_history: Any,
|
|
66
|
-
workspace_store: Any,
|
|
67
|
-
workspace_graph: Any,
|
|
68
|
-
hooks: Any,
|
|
69
|
-
execute_tool: Any,
|
|
70
|
-
base_dir: Path,
|
|
71
|
-
agent_root: Path,
|
|
72
|
-
ensure_agent_root: Any,
|
|
73
|
-
funnel_metrics: Any = None,
|
|
74
|
-
run_store: Any = None,
|
|
75
|
-
project_sessions: Any = None,
|
|
76
|
-
artifact_ledger: Any = None,
|
|
77
|
-
) -> None:
|
|
78
|
-
self.runtime = runtime
|
|
79
|
-
self.model_router = model_router
|
|
80
|
-
self.require_user = require_user
|
|
81
|
-
self.require_admin = require_admin
|
|
82
|
-
self.enforce_rate_limit = enforce_rate_limit
|
|
83
|
-
self.authenticated_identity = authenticated_identity
|
|
84
|
-
self.write_workspace = write_workspace
|
|
85
|
-
self.save_to_history = save_to_history
|
|
86
|
-
self.workspace_store = workspace_store
|
|
87
|
-
self.workspace_graph = workspace_graph
|
|
88
|
-
self.hooks = hooks
|
|
89
|
-
self.execute_tool = execute_tool
|
|
90
|
-
self.base_dir = Path(base_dir)
|
|
91
|
-
self.agent_root = Path(agent_root)
|
|
92
|
-
self.ensure_agent_root = ensure_agent_root
|
|
93
|
-
self.funnel_metrics = funnel_metrics
|
|
94
|
-
# Multi-turn project loop (v9.9.6). Optional: without a store the
|
|
95
|
-
# agent behaves exactly as before — single runs, no project memory.
|
|
96
|
-
self.project_sessions = project_sessions
|
|
97
|
-
# Re-search loop (v9.9.6): the next turn must see files this run just
|
|
98
|
-
# wrote, even before asynchronous indexing catches up.
|
|
99
|
-
self.artifact_ledger = artifact_ledger
|
|
100
|
-
self._pending_lock = threading.Lock()
|
|
101
|
-
# awaiting_approval runs: run_id -> paused run state + approval token.
|
|
102
|
-
# Fail-closed: governed steps only ever execute after resume presents
|
|
103
|
-
# the matching, unexpired token for this run (or via the legacy
|
|
104
|
-
# explicit human-in-loop context flow above).
|
|
105
|
-
self._approvals: Dict[str, Dict[str, Any]] = {}
|
|
106
|
-
self._approval_ttl_seconds = 10 * 60
|
|
107
|
-
# Durable side of the approval loop (review Wave 0.1): paused runs are
|
|
108
|
-
# mirrored to disk so a valid token still resumes after a restart or
|
|
109
|
-
# from another worker process. In-memory stays the fast path.
|
|
110
|
-
self.run_store = run_store if run_store is not None else AgentRunStore(
|
|
111
|
-
Path(base_dir) / "data" / "agent_runs"
|
|
112
|
-
)
|
|
113
|
-
try:
|
|
114
|
-
self.run_store.sweep_expired()
|
|
115
|
-
except Exception as exc: # noqa: BLE001 — hygiene must not break startup
|
|
116
|
-
logging.warning("agent run store sweep failed: %s", exc)
|
|
117
|
-
self._background_tasks: set[asyncio.Task] = set()
|
|
118
|
-
|
|
119
|
-
def register_routes(self, router: APIRouter) -> None:
|
|
120
|
-
@router.post("/agent/eval")
|
|
121
|
-
async def agent_eval(req: AgentEvalRequest, request: Request):
|
|
122
|
-
return await self.eval(req, request)
|
|
123
|
-
|
|
124
|
-
@router.post("/agent")
|
|
125
|
-
async def agent(req: AgentRequest, request: Request):
|
|
126
|
-
if req.stream:
|
|
127
|
-
# Surface parity (v9.9.7): the web app already watches the loop
|
|
128
|
-
# live through the chat route. `/agent` clients (VS Code) get
|
|
129
|
-
# the same named `agent_step` frames here, followed by the
|
|
130
|
-
# exact terminal payload the JSON response returns — a client
|
|
131
|
-
# that ignores named events sees the historical shape.
|
|
132
|
-
async def start(observer):
|
|
133
|
-
return await self.agent(req, request, on_step=observer)
|
|
134
|
-
|
|
135
|
-
return StreamingResponse(
|
|
136
|
-
agent_live_stream(
|
|
137
|
-
start,
|
|
138
|
-
router=self.model_router,
|
|
139
|
-
model_id=req.executing_model,
|
|
140
|
-
),
|
|
141
|
-
media_type="text/event-stream",
|
|
142
|
-
headers={"X-Routed-To": "agent"},
|
|
143
|
-
)
|
|
144
|
-
return await self.agent(req, request)
|
|
145
|
-
|
|
146
|
-
@router.post("/agent/resume")
|
|
147
|
-
async def agent_resume(req: AgentResumeRequest, request: Request):
|
|
148
|
-
return await self.resume(req, request)
|
|
149
|
-
|
|
150
|
-
@router.get("/agent/approvals")
|
|
151
|
-
async def agent_approvals(request: Request):
|
|
152
|
-
return self.pending_approvals(request)
|
|
153
|
-
|
|
154
|
-
def _schedule_background_task(self, coro: Any) -> None:
|
|
155
|
-
task = asyncio.create_task(coro)
|
|
156
|
-
self._background_tasks.add(task)
|
|
157
|
-
|
|
158
|
-
def finish(done: asyncio.Task) -> None:
|
|
159
|
-
self._background_tasks.discard(done)
|
|
160
|
-
try:
|
|
161
|
-
done.result()
|
|
162
|
-
except asyncio.CancelledError:
|
|
163
|
-
quiet()
|
|
164
|
-
except Exception as exc:
|
|
165
|
-
logging.warning("background chat task failed: %s", exc)
|
|
166
|
-
|
|
167
|
-
task.add_done_callback(finish)
|
|
168
|
-
|
|
169
|
-
async def eval(self, req: AgentEvalRequest, request: Request) -> Dict[str, Any]:
|
|
170
|
-
"""Run a skill's schema.json eval cases."""
|
|
171
|
-
if self.require_admin is not None:
|
|
172
|
-
self.require_admin(request)
|
|
173
|
-
else:
|
|
174
|
-
self.require_user(request)
|
|
175
|
-
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", req.skill):
|
|
176
|
-
raise HTTPException(status_code=400, detail="Invalid skill name.")
|
|
177
|
-
skills_root = (self.base_dir / "skills").resolve()
|
|
178
|
-
skill_dir = (skills_root / req.skill).resolve()
|
|
179
|
-
if skill_dir.parent != skills_root:
|
|
180
|
-
raise HTTPException(status_code=400, detail="Invalid skill path.")
|
|
181
|
-
schema_path = skill_dir / "schema.json"
|
|
182
|
-
if not schema_path.exists():
|
|
183
|
-
raise HTTPException(
|
|
184
|
-
status_code=404,
|
|
185
|
-
detail=f"Skill '{req.skill}' not found or missing schema.json",
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
189
|
-
eval_cases = schema.get("evals", [])
|
|
190
|
-
if req.case_id:
|
|
191
|
-
eval_cases = [case for case in eval_cases if case.get("id") == req.case_id]
|
|
192
|
-
if not eval_cases:
|
|
193
|
-
return {
|
|
194
|
-
"skill": req.skill,
|
|
195
|
-
"total": 0,
|
|
196
|
-
"passed": 0,
|
|
197
|
-
"failed": 0,
|
|
198
|
-
"results": [],
|
|
199
|
-
"message": "No eval cases defined in schema.json",
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
action_name = schema.get("action", req.skill)
|
|
203
|
-
results = []
|
|
204
|
-
for case in eval_cases:
|
|
205
|
-
case_id = case.get("id", "?")
|
|
206
|
-
try:
|
|
207
|
-
case_input = case.get("input", {})
|
|
208
|
-
result = dispatch_tool(
|
|
209
|
-
self.hooks,
|
|
210
|
-
action_name,
|
|
211
|
-
case_input,
|
|
212
|
-
# Bound as defaults: the lambda is invoked inside this
|
|
213
|
-
# iteration today, so late binding is harmless — until
|
|
214
|
-
# someone defers it and every case runs the last input.
|
|
215
|
-
_bind_case(self.execute_tool, action_name, case_input),
|
|
216
|
-
source="eval",
|
|
217
|
-
)
|
|
218
|
-
criteria = case.get("pass_criteria", "")
|
|
219
|
-
if "success == true" in criteria:
|
|
220
|
-
passed = result.get("success") is True
|
|
221
|
-
elif "success == false" in criteria:
|
|
222
|
-
passed = result.get("success") is False
|
|
223
|
-
else:
|
|
224
|
-
passed = True
|
|
225
|
-
results.append(
|
|
226
|
-
{
|
|
227
|
-
"id": case_id,
|
|
228
|
-
"description": case.get("description", ""),
|
|
229
|
-
"passed": passed,
|
|
230
|
-
"result": result,
|
|
231
|
-
"pass_criteria": criteria,
|
|
232
|
-
}
|
|
233
|
-
)
|
|
234
|
-
except Exception as exc:
|
|
235
|
-
results.append(
|
|
236
|
-
{
|
|
237
|
-
"id": case_id,
|
|
238
|
-
"description": case.get("description", ""),
|
|
239
|
-
"passed": False,
|
|
240
|
-
"error": str(exc),
|
|
241
|
-
"pass_criteria": case.get("pass_criteria", ""),
|
|
242
|
-
}
|
|
243
|
-
)
|
|
244
|
-
passed_count = sum(1 for result in results if result.get("passed") is True)
|
|
245
|
-
return {
|
|
246
|
-
"skill": req.skill,
|
|
247
|
-
"action": action_name,
|
|
248
|
-
"total": len(results),
|
|
249
|
-
"passed": passed_count,
|
|
250
|
-
"failed": len(results) - passed_count,
|
|
251
|
-
"results": results,
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async def agent(
|
|
255
|
-
self,
|
|
256
|
-
req: AgentRequest,
|
|
257
|
-
request: Request,
|
|
258
|
-
on_step: Any = None,
|
|
259
|
-
) -> Dict[str, Any]:
|
|
260
|
-
"""Plan and execute a natural-language local agent run.
|
|
261
|
-
|
|
262
|
-
``on_step`` (optional) is a per-run step observer — the live SSE
|
|
263
|
-
route attaches one so the client sees progress while EXECUTING.
|
|
264
|
-
"""
|
|
265
|
-
current_user = self.require_user(request)
|
|
266
|
-
self.enforce_rate_limit(current_user, "agent")
|
|
267
|
-
effective_email = self.authenticated_identity(current_user, req.user_email, resolve_language(request))
|
|
268
|
-
req.workspace_id = self.write_workspace(
|
|
269
|
-
requested_workspace(request, body_workspace=req.workspace_id),
|
|
270
|
-
current_user,
|
|
271
|
-
)
|
|
272
|
-
req.user_email = effective_email
|
|
273
|
-
if not self.model_router.current_model_id:
|
|
274
|
-
raise HTTPException(
|
|
275
|
-
status_code=400,
|
|
276
|
-
detail="No model loaded. Call /models/load first.",
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
self.ensure_agent_root()
|
|
280
|
-
language_hint = _LANG_HINT[detect_language(req.message)]
|
|
281
|
-
max_steps = max(1, min(req.max_steps, 50))
|
|
282
|
-
max_retry = 3
|
|
283
|
-
ctx = AgentRunContext()
|
|
284
|
-
if on_step is not None:
|
|
285
|
-
ctx.on_step = on_step
|
|
286
|
-
ctx.executing_model = req.executing_model
|
|
287
|
-
ctx.reviewing_model = req.reviewing_model
|
|
288
|
-
ctx.project_context = self._project_summary(req, current_user)
|
|
289
|
-
# Autonomy dial (v9.9.8): resolve the user/workspace-scoped mode once so
|
|
290
|
-
# the plan gate and every per-tool gate in this run agree, even if the
|
|
291
|
-
# stored preference changes mid-run.
|
|
292
|
-
mode_probe = getattr(self.runtime, "resolve_permission_mode", None)
|
|
293
|
-
if callable(mode_probe):
|
|
294
|
-
try:
|
|
295
|
-
ctx.permission_mode = mode_probe(
|
|
296
|
-
user_email=current_user, workspace_id=req.workspace_id,
|
|
297
|
-
).value
|
|
298
|
-
except Exception: # noqa: BLE001 — never fail a run over the dial
|
|
299
|
-
ctx.permission_mode = None
|
|
300
|
-
ctx.state = AgentState.PLANNING
|
|
301
|
-
ctx.state_history.append(ctx.state.value)
|
|
302
|
-
await self.runtime.plan(
|
|
303
|
-
ctx,
|
|
304
|
-
req,
|
|
305
|
-
language_hint,
|
|
306
|
-
current_user,
|
|
307
|
-
model_id=req.planning_model,
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
requirements_probe = getattr(self.runtime, "approval_requirements", None)
|
|
311
|
-
|
|
312
|
-
if req.human_in_loop:
|
|
313
|
-
# Deprecated explicit pause (L1 unification, 9.9.5): the legacy
|
|
314
|
-
# ``human_in_loop`` flow now rides the same durable approval
|
|
315
|
-
# store as ``awaiting_approval`` — one pause path, one resume
|
|
316
|
-
# path, restart-safe. The legacy wire contract is preserved:
|
|
317
|
-
# ``status="waiting_approval"`` + ``context_id`` (token-less,
|
|
318
|
-
# same-user resume).
|
|
319
|
-
requirements = (
|
|
320
|
-
requirements_probe(ctx)
|
|
321
|
-
if callable(requirements_probe)
|
|
322
|
-
else {"requires_approval": True, "non_auto_steps": [], "plan_summary": ""}
|
|
323
|
-
)
|
|
324
|
-
return self._pause_for_approval(
|
|
325
|
-
ctx, req, language_hint, current_user, requirements,
|
|
326
|
-
legacy_context=True,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
# Interactive approval: when the plan needs human approval, pause the
|
|
330
|
-
# run as awaiting_approval (short-TTL token) instead of failing it.
|
|
331
|
-
# ``getattr`` keeps injected fake runtimes without the preview method
|
|
332
|
-
# on the historical fail-closed path.
|
|
333
|
-
if callable(requirements_probe):
|
|
334
|
-
requirements = requirements_probe(ctx)
|
|
335
|
-
if requirements.get("requires_approval"):
|
|
336
|
-
return self._pause_for_approval(
|
|
337
|
-
ctx, req, language_hint, current_user, requirements,
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
self.runtime.approve(ctx, current_user, approved_by_human=False)
|
|
341
|
-
return await self._finish(
|
|
342
|
-
ctx,
|
|
343
|
-
req,
|
|
344
|
-
language_hint,
|
|
345
|
-
current_user,
|
|
346
|
-
max_steps,
|
|
347
|
-
max_retry,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
def _project_summary(self, req: AgentRequest, current_user: str) -> str:
|
|
351
|
-
"""Where this project stands, for the planner/executor prompt.
|
|
352
|
-
|
|
353
|
-
Advisory: a missing store, an unknown project id, or a read failure
|
|
354
|
-
yields "" and the run proceeds as a standalone run.
|
|
355
|
-
"""
|
|
356
|
-
project_id = getattr(req, "project_id", None)
|
|
357
|
-
if not project_id or self.project_sessions is None:
|
|
358
|
-
return ""
|
|
359
|
-
try:
|
|
360
|
-
return self.project_sessions.summary(
|
|
361
|
-
str(project_id),
|
|
362
|
-
user_email=current_user or None,
|
|
363
|
-
workspace_id=req.workspace_id,
|
|
364
|
-
)
|
|
365
|
-
except Exception as exc: # noqa: BLE001 — project context never gates a run
|
|
366
|
-
logging.warning("project session summary failed: %s", exc)
|
|
367
|
-
return ""
|
|
368
|
-
|
|
369
|
-
def _record_project_run(
|
|
370
|
-
self,
|
|
371
|
-
req: AgentRequest,
|
|
372
|
-
current_user: str,
|
|
373
|
-
ctx: AgentRunContext,
|
|
374
|
-
payload: Dict[str, Any],
|
|
375
|
-
) -> None:
|
|
376
|
-
"""Fold a finished run into its project session (best effort).
|
|
377
|
-
|
|
378
|
-
The verification result is stored exactly as the run reported it — a
|
|
379
|
-
NEEDS_REVIEW run never becomes a project's "done".
|
|
380
|
-
"""
|
|
381
|
-
project_id = getattr(req, "project_id", None)
|
|
382
|
-
if not project_id or self.project_sessions is None:
|
|
383
|
-
return
|
|
384
|
-
try:
|
|
385
|
-
self.project_sessions.record_run(
|
|
386
|
-
str(project_id),
|
|
387
|
-
run_id=str(payload.get("run_id") or ""),
|
|
388
|
-
goal=str(ctx.plan.get("goal") or req.message),
|
|
389
|
-
status=str(payload.get("status") or ""),
|
|
390
|
-
final_state=ctx.state.value,
|
|
391
|
-
files=payload.get("created_files") or [],
|
|
392
|
-
explanation=payload.get("explanation"),
|
|
393
|
-
user_email=current_user or None,
|
|
394
|
-
workspace_id=req.workspace_id,
|
|
395
|
-
)
|
|
396
|
-
except Exception as exc: # noqa: BLE001 — bookkeeping never fails a run
|
|
397
|
-
logging.warning("project session record failed: %s", exc)
|
|
398
|
-
|
|
399
|
-
def _pause_for_approval(
|
|
400
|
-
self,
|
|
401
|
-
ctx: AgentRunContext,
|
|
402
|
-
req: AgentRequest,
|
|
403
|
-
language_hint: str,
|
|
404
|
-
current_user: str,
|
|
405
|
-
requirements: Dict[str, Any],
|
|
406
|
-
*,
|
|
407
|
-
legacy_context: bool = False,
|
|
408
|
-
) -> Dict[str, Any]:
|
|
409
|
-
"""Park a plan that needs approval and hand the user a resume token.
|
|
410
|
-
|
|
411
|
-
``legacy_context=True`` marks a deprecated ``human_in_loop`` pause:
|
|
412
|
-
same durable storage, but the response speaks the historical
|
|
413
|
-
``waiting_approval``/``context_id`` contract and resume may present
|
|
414
|
-
the ``context_id`` instead of the approval token (same user only).
|
|
415
|
-
"""
|
|
416
|
-
run_id = secrets.token_urlsafe(16)
|
|
417
|
-
approval_token = secrets.token_urlsafe(32)
|
|
418
|
-
now_monotonic = time.monotonic()
|
|
419
|
-
expires_at = (
|
|
420
|
-
datetime.now(timezone.utc) + timedelta(seconds=self._approval_ttl_seconds)
|
|
421
|
-
).isoformat(timespec="seconds")
|
|
422
|
-
with self._pending_lock:
|
|
423
|
-
self._purge_expired_approvals_locked(now_monotonic)
|
|
424
|
-
self._approvals[run_id] = {
|
|
425
|
-
"ctx": ctx,
|
|
426
|
-
"req": req,
|
|
427
|
-
"language_hint": language_hint,
|
|
428
|
-
"user": current_user,
|
|
429
|
-
"token": approval_token,
|
|
430
|
-
"expires_monotonic": now_monotonic + self._approval_ttl_seconds,
|
|
431
|
-
"expires_at": expires_at,
|
|
432
|
-
"legacy_context": legacy_context,
|
|
433
|
-
}
|
|
434
|
-
# Durable mirror: a restart between pause and resume must not orphan
|
|
435
|
-
# the run. Best-effort — the in-memory pause still answers on failure.
|
|
436
|
-
try:
|
|
437
|
-
self.run_store.save(
|
|
438
|
-
run_id,
|
|
439
|
-
ctx=ctx,
|
|
440
|
-
req_payload=req.model_dump(),
|
|
441
|
-
language_hint=language_hint,
|
|
442
|
-
user=current_user,
|
|
443
|
-
token=approval_token,
|
|
444
|
-
expires_epoch=time.time() + self._approval_ttl_seconds,
|
|
445
|
-
expires_at=expires_at,
|
|
446
|
-
legacy_context=legacy_context,
|
|
447
|
-
)
|
|
448
|
-
except Exception as exc: # noqa: BLE001
|
|
449
|
-
logging.warning("agent run store persist failed: %s", exc)
|
|
450
|
-
if self.funnel_metrics is not None:
|
|
451
|
-
try:
|
|
452
|
-
self.funnel_metrics.increment("approval_pauses")
|
|
453
|
-
except Exception as exc: # noqa: BLE001 — advisory only
|
|
454
|
-
logging.warning("funnel metrics increment failed: %s", exc)
|
|
455
|
-
ctx.state_history.append(AgentState.WAITING_APPROVAL.value)
|
|
456
|
-
message = (
|
|
457
|
-
"이 작업에는 승인이 필요한 단계가 있어 실행을 잠시 멈췄습니다. "
|
|
458
|
-
"계획을 확인한 뒤 승인하면 이어서 실행합니다."
|
|
459
|
-
)
|
|
460
|
-
payload = {
|
|
461
|
-
"status": "waiting_approval" if legacy_context else "awaiting_approval",
|
|
462
|
-
"run_id": run_id,
|
|
463
|
-
"approval": {
|
|
464
|
-
"token": approval_token,
|
|
465
|
-
"expires_at": expires_at,
|
|
466
|
-
"plan_summary": requirements.get("plan_summary", ""),
|
|
467
|
-
},
|
|
468
|
-
"response": message,
|
|
469
|
-
"plan": ctx.plan,
|
|
470
|
-
"steps": ctx.transcript,
|
|
471
|
-
"state_history": ctx.state_history,
|
|
472
|
-
"final_state": AgentState.WAITING_APPROVAL.value,
|
|
473
|
-
"non_auto_steps": requirements.get("non_auto_steps", []),
|
|
474
|
-
"planning_model": req.planning_model or self.model_router.current_model_id,
|
|
475
|
-
"executing_model": req.executing_model or self.model_router.current_model_id,
|
|
476
|
-
"reviewing_model": req.reviewing_model or self.model_router.current_model_id,
|
|
477
|
-
"loop": ctx.trace.summary(),
|
|
478
|
-
}
|
|
479
|
-
if legacy_context:
|
|
480
|
-
# Historical wire field — the run id doubles as the context id.
|
|
481
|
-
payload["context_id"] = run_id
|
|
482
|
-
return payload
|
|
483
|
-
|
|
484
|
-
def _purge_expired_approvals_locked(self, now_monotonic: float) -> None:
|
|
485
|
-
for run_id, entry in list(self._approvals.items()):
|
|
486
|
-
if now_monotonic >= entry["expires_monotonic"]:
|
|
487
|
-
self._approvals.pop(run_id, None)
|
|
488
|
-
try:
|
|
489
|
-
self.run_store.delete(run_id)
|
|
490
|
-
except Exception: # noqa: BLE001 — hygiene only
|
|
491
|
-
quiet()
|
|
492
|
-
|
|
493
|
-
def pending_approvals(self, request: Request) -> Dict[str, Any]:
|
|
494
|
-
"""Unexpired paused runs for the current user (memory ∪ disk).
|
|
495
|
-
|
|
496
|
-
Lets the UI re-surface an approval card after a reload/restart instead
|
|
497
|
-
of the run silently vanishing.
|
|
498
|
-
"""
|
|
499
|
-
current_user = self.require_user(request)
|
|
500
|
-
now_monotonic = time.monotonic()
|
|
501
|
-
pending: Dict[str, Dict[str, Any]] = {}
|
|
502
|
-
with self._pending_lock:
|
|
503
|
-
for run_id, entry in self._approvals.items():
|
|
504
|
-
if entry["user"] != current_user:
|
|
505
|
-
continue
|
|
506
|
-
if now_monotonic >= entry["expires_monotonic"]:
|
|
507
|
-
continue
|
|
508
|
-
plan = getattr(entry["ctx"], "plan", {}) or {}
|
|
509
|
-
pending[run_id] = {
|
|
510
|
-
"run_id": run_id,
|
|
511
|
-
"goal": str(plan.get("goal") or "")[:200],
|
|
512
|
-
"expires_at": entry["expires_at"],
|
|
513
|
-
}
|
|
514
|
-
try:
|
|
515
|
-
for summary in self.run_store.pending_summaries(current_user):
|
|
516
|
-
run_id = str(summary.get("run_id") or "")
|
|
517
|
-
if run_id and run_id not in pending:
|
|
518
|
-
pending[run_id] = {
|
|
519
|
-
"run_id": run_id,
|
|
520
|
-
"goal": summary.get("goal") or "",
|
|
521
|
-
"expires_at": summary.get("expires_at"),
|
|
522
|
-
}
|
|
523
|
-
except Exception as exc: # noqa: BLE001 — disk listing is best-effort
|
|
524
|
-
logging.warning("agent run store listing failed: %s", exc)
|
|
525
|
-
return {"pending": sorted(pending.values(), key=lambda p: p["run_id"])}
|
|
526
|
-
|
|
527
|
-
def _restore_persisted_approval(self, run_id: str) -> Optional[Dict[str, Any]]:
|
|
528
|
-
"""Rebuild an approval entry from disk after a restart.
|
|
529
|
-
|
|
530
|
-
Returns an entry shaped like the in-memory ones but carrying
|
|
531
|
-
``token_hash`` instead of the plaintext token. Expired records are
|
|
532
|
-
deleted and reported via the same 410 contract as the memory path.
|
|
533
|
-
"""
|
|
534
|
-
try:
|
|
535
|
-
record = self.run_store.load(run_id)
|
|
536
|
-
except Exception as exc: # noqa: BLE001 — load error == not found
|
|
537
|
-
logging.warning("agent run store load failed: %s", exc)
|
|
538
|
-
return None
|
|
539
|
-
if record is None:
|
|
540
|
-
return None
|
|
541
|
-
req_payload = record.get("req") or {}
|
|
542
|
-
if time.time() >= float(record.get("expires_epoch") or 0):
|
|
543
|
-
try:
|
|
544
|
-
self.run_store.delete(run_id)
|
|
545
|
-
except Exception: # noqa: BLE001
|
|
546
|
-
quiet()
|
|
547
|
-
raise HTTPException(
|
|
548
|
-
status_code=410,
|
|
549
|
-
detail={
|
|
550
|
-
"error": "approval_expired",
|
|
551
|
-
"message": "Approval token expired. Start a new request.",
|
|
552
|
-
"replan": {"message": str(req_payload.get("message") or "")},
|
|
553
|
-
},
|
|
554
|
-
)
|
|
555
|
-
try:
|
|
556
|
-
restored_req = AgentRequest(**req_payload)
|
|
557
|
-
except Exception as exc: # noqa: BLE001 — unreconstructable == not found
|
|
558
|
-
logging.warning("agent run store request restore failed: %s", exc)
|
|
559
|
-
return None
|
|
560
|
-
remaining = max(1.0, float(record["expires_epoch"]) - time.time())
|
|
561
|
-
return {
|
|
562
|
-
"ctx": restore_run_context(record.get("ctx") or {}),
|
|
563
|
-
"req": restored_req,
|
|
564
|
-
"language_hint": str(record.get("language_hint") or "English"),
|
|
565
|
-
"user": record.get("user"),
|
|
566
|
-
"token_hash": str(record.get("token_hash") or ""),
|
|
567
|
-
"expires_monotonic": time.monotonic() + remaining,
|
|
568
|
-
"expires_at": record.get("expires_at"),
|
|
569
|
-
"legacy_context": bool(record.get("legacy_context")),
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
@staticmethod
|
|
573
|
-
def _token_matches(entry: Dict[str, Any], supplied: str) -> bool:
|
|
574
|
-
"""Constant-time token check for both plaintext and hashed entries."""
|
|
575
|
-
if not supplied:
|
|
576
|
-
return False
|
|
577
|
-
stored = entry.get("token")
|
|
578
|
-
if stored is not None:
|
|
579
|
-
return secrets.compare_digest(str(stored), supplied)
|
|
580
|
-
stored_hash = str(entry.get("token_hash") or "")
|
|
581
|
-
return bool(stored_hash) and secrets.compare_digest(
|
|
582
|
-
stored_hash, hash_approval_token(supplied)
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
async def _finish(
|
|
586
|
-
self,
|
|
587
|
-
ctx: AgentRunContext,
|
|
588
|
-
req: AgentRequest,
|
|
589
|
-
language_hint: str,
|
|
590
|
-
current_user: str,
|
|
591
|
-
max_steps: int,
|
|
592
|
-
max_retry: int,
|
|
593
|
-
) -> Dict[str, Any]:
|
|
594
|
-
await self.runtime.run_to_completion(
|
|
595
|
-
ctx,
|
|
596
|
-
req,
|
|
597
|
-
language_hint,
|
|
598
|
-
current_user,
|
|
599
|
-
max_steps,
|
|
600
|
-
max_retry,
|
|
601
|
-
)
|
|
602
|
-
self._schedule_background_task(self.runtime.memory_update(ctx, req, current_user))
|
|
603
|
-
|
|
604
|
-
message = ctx.final_message or "작업을 완료했습니다."
|
|
605
|
-
history_user = {
|
|
606
|
-
"user_email": req.user_email or current_user or None,
|
|
607
|
-
"user_nickname": req.user_nickname,
|
|
608
|
-
}
|
|
609
|
-
await asyncio.to_thread(
|
|
610
|
-
self.save_to_history,
|
|
611
|
-
"user",
|
|
612
|
-
req.message,
|
|
613
|
-
**history_user,
|
|
614
|
-
source=req.source or "web",
|
|
615
|
-
conversation_id=req.conversation_id,
|
|
616
|
-
workspace_id=req.workspace_id,
|
|
617
|
-
)
|
|
618
|
-
await asyncio.to_thread(
|
|
619
|
-
self.save_to_history,
|
|
620
|
-
"assistant",
|
|
621
|
-
message,
|
|
622
|
-
**history_user,
|
|
623
|
-
source=req.source or "web",
|
|
624
|
-
conversation_id=req.conversation_id,
|
|
625
|
-
workspace_id=req.workspace_id,
|
|
626
|
-
)
|
|
627
|
-
try:
|
|
628
|
-
self.workspace_store.record_agent_run(
|
|
629
|
-
agent_id="agent:executor",
|
|
630
|
-
status="ok" if ctx.state == AgentState.DONE else "failed",
|
|
631
|
-
input_text=req.message,
|
|
632
|
-
output_text=message,
|
|
633
|
-
user_email=current_user or None,
|
|
634
|
-
workspace_id=req.workspace_id,
|
|
635
|
-
mode="llm",
|
|
636
|
-
timeline=ctx.transcript,
|
|
637
|
-
relationships=["agent:planner", "agent:reviewer"],
|
|
638
|
-
graph=self.workspace_graph(),
|
|
639
|
-
)
|
|
640
|
-
except Exception as exc:
|
|
641
|
-
logging.warning("workspace agent run record failed: %s", exc)
|
|
642
|
-
if self.funnel_metrics is not None:
|
|
643
|
-
# UX funnel (backlog #16): every completed agent run counts, and
|
|
644
|
-
# NEEDS_REVIEW terminals feed the needs_review_rate the review
|
|
645
|
-
# gates watch. Advisory only — a metrics failure never fails chat.
|
|
646
|
-
try:
|
|
647
|
-
self.funnel_metrics.increment("agent_runs")
|
|
648
|
-
if ctx.state == AgentState.NEEDS_REVIEW:
|
|
649
|
-
self.funnel_metrics.increment("needs_review_runs")
|
|
650
|
-
except Exception as exc: # noqa: BLE001
|
|
651
|
-
logging.warning("funnel metrics increment failed: %s", exc)
|
|
652
|
-
loop_summary = ctx.trace.summary()
|
|
653
|
-
payload = {
|
|
654
|
-
"status": "ok" if ctx.state == AgentState.DONE else "failed",
|
|
655
|
-
"response": message,
|
|
656
|
-
"workspace": str(self.agent_root),
|
|
657
|
-
"steps": ctx.transcript,
|
|
658
|
-
"state_history": ctx.state_history,
|
|
659
|
-
"final_state": ctx.state.value,
|
|
660
|
-
"created_files": collect_created_files(ctx.transcript),
|
|
661
|
-
"artifacts": collect_artifacts(ctx.transcript),
|
|
662
|
-
"loop": loop_summary,
|
|
663
|
-
# Plain-language outcome (v9.9.6): the loop counters say a weak
|
|
664
|
-
# model needed three repairs; this says it in a sentence a person
|
|
665
|
-
# can read, and never upgrades a non-success into a success.
|
|
666
|
-
"explanation": explain_run(
|
|
667
|
-
state=ctx.state,
|
|
668
|
-
loop=loop_summary,
|
|
669
|
-
transcript=ctx.transcript,
|
|
670
|
-
max_retry=max_retry,
|
|
671
|
-
),
|
|
672
|
-
}
|
|
673
|
-
# Multi-turn project loop (v9.9.6): the project remembers this run's
|
|
674
|
-
# files and its honest verdict, so the next turn continues instead of
|
|
675
|
-
# restarting.
|
|
676
|
-
self._record_project_run(req, current_user, ctx, payload)
|
|
677
|
-
if self.artifact_ledger is not None and payload.get("created_files"):
|
|
678
|
-
try:
|
|
679
|
-
self.artifact_ledger.record(
|
|
680
|
-
payload["created_files"],
|
|
681
|
-
user_email=current_user or None,
|
|
682
|
-
conversation_id=req.conversation_id,
|
|
683
|
-
workspace_id=req.workspace_id,
|
|
684
|
-
)
|
|
685
|
-
except Exception as exc: # noqa: BLE001 — advisory context only
|
|
686
|
-
logging.warning("artifact ledger record failed: %s", exc)
|
|
687
|
-
if getattr(req, "project_id", None):
|
|
688
|
-
payload["project_id"] = str(req.project_id)
|
|
689
|
-
return payload
|
|
690
|
-
|
|
691
|
-
async def resume(
|
|
692
|
-
self,
|
|
693
|
-
req: AgentResumeRequest,
|
|
694
|
-
request: Request,
|
|
695
|
-
) -> Dict[str, Any]:
|
|
696
|
-
"""Resume a paused agent after human approval of the plan.
|
|
697
|
-
|
|
698
|
-
Two entry modes share this endpoint:
|
|
699
|
-
|
|
700
|
-
* ``run_id`` + ``approval_token`` — an ``awaiting_approval`` run
|
|
701
|
-
(token-validated, short TTL, bound to the pausing user);
|
|
702
|
-
* ``context_id`` — the legacy explicit human-in-loop pause.
|
|
703
|
-
"""
|
|
704
|
-
current_user = self.require_user(request)
|
|
705
|
-
if req.run_id:
|
|
706
|
-
return await self._resume_approval(req, current_user)
|
|
707
|
-
if not req.context_id:
|
|
708
|
-
raise HTTPException(
|
|
709
|
-
status_code=400,
|
|
710
|
-
detail="run_id (with approval_token) or context_id is required.",
|
|
711
|
-
)
|
|
712
|
-
# Deprecated context_id resume (L1 unification, 9.9.5): the legacy
|
|
713
|
-
# pause lives in the same durable approval store now. Token-less
|
|
714
|
-
# resume stays allowed for it — bound to the pausing user, and only
|
|
715
|
-
# for entries that were created through the legacy pause.
|
|
716
|
-
now_monotonic = time.monotonic()
|
|
717
|
-
with self._pending_lock:
|
|
718
|
-
entry = self._approvals.get(req.context_id)
|
|
719
|
-
if entry is None:
|
|
720
|
-
try:
|
|
721
|
-
entry = self._restore_persisted_approval(req.context_id)
|
|
722
|
-
except HTTPException as exc:
|
|
723
|
-
if exc.status_code == 410:
|
|
724
|
-
# Legacy contract never had a distinct expiry answer.
|
|
725
|
-
raise HTTPException(
|
|
726
|
-
status_code=404,
|
|
727
|
-
detail="Agent context not found or expired. Start a new request.",
|
|
728
|
-
) from exc
|
|
729
|
-
raise
|
|
730
|
-
if entry is None or not entry.get("legacy_context"):
|
|
731
|
-
raise HTTPException(
|
|
732
|
-
status_code=404,
|
|
733
|
-
detail="Agent context not found or expired. Start a new request.",
|
|
734
|
-
)
|
|
735
|
-
if entry["user"] != current_user:
|
|
736
|
-
raise HTTPException(
|
|
737
|
-
status_code=403,
|
|
738
|
-
detail="Agent context belongs to another user.",
|
|
739
|
-
)
|
|
740
|
-
if now_monotonic >= entry["expires_monotonic"]:
|
|
741
|
-
self._approvals.pop(req.context_id, None)
|
|
742
|
-
try:
|
|
743
|
-
self.run_store.delete(req.context_id)
|
|
744
|
-
except Exception: # noqa: BLE001
|
|
745
|
-
quiet()
|
|
746
|
-
raise HTTPException(
|
|
747
|
-
status_code=404,
|
|
748
|
-
detail="Agent context not found or expired. Start a new request.",
|
|
749
|
-
)
|
|
750
|
-
self._approvals.pop(req.context_id, None)
|
|
751
|
-
try:
|
|
752
|
-
self.run_store.delete(req.context_id)
|
|
753
|
-
except Exception as exc: # noqa: BLE001
|
|
754
|
-
logging.warning("agent run store delete failed: %s", exc)
|
|
755
|
-
|
|
756
|
-
ctx = entry["ctx"]
|
|
757
|
-
original_request = entry["req"]
|
|
758
|
-
language_hint = entry["language_hint"]
|
|
759
|
-
if not req.approved:
|
|
760
|
-
return {"status": "cancelled", "response": "사용자가 계획을 취소했습니다."}
|
|
761
|
-
if req.modified_plan:
|
|
762
|
-
plan, plan_fixes = normalize_plan(req.modified_plan, original_request.message)
|
|
763
|
-
ctx.plan = plan
|
|
764
|
-
ctx.transcript.append({
|
|
765
|
-
"state": AgentState.WAITING_APPROVAL.value,
|
|
766
|
-
"edited_plan": True,
|
|
767
|
-
**({"plan_fixes": plan_fixes} if plan_fixes else {}),
|
|
768
|
-
})
|
|
769
|
-
ctx.executing_model = req.executing_model or ctx.executing_model
|
|
770
|
-
ctx.reviewing_model = req.reviewing_model or ctx.reviewing_model
|
|
771
|
-
# The authenticated owner of this pending context explicitly approved
|
|
772
|
-
# the plan — record it as a human approval so approval-gated steps can
|
|
773
|
-
# actually run instead of terminating the run as FAILED.
|
|
774
|
-
self.runtime.approve(ctx, current_user, approved_by_human=True)
|
|
775
|
-
return await self._finish(
|
|
776
|
-
ctx,
|
|
777
|
-
original_request,
|
|
778
|
-
language_hint,
|
|
779
|
-
current_user,
|
|
780
|
-
max(1, min(original_request.max_steps, 50)),
|
|
781
|
-
3,
|
|
782
|
-
)
|
|
783
|
-
|
|
784
|
-
async def _resume_approval(
|
|
785
|
-
self,
|
|
786
|
-
req: AgentResumeRequest,
|
|
787
|
-
current_user: str,
|
|
788
|
-
) -> Dict[str, Any]:
|
|
789
|
-
"""Validate an awaiting_approval token, then continue or cancel the run."""
|
|
790
|
-
now_monotonic = time.monotonic()
|
|
791
|
-
with self._pending_lock:
|
|
792
|
-
entry = self._approvals.get(req.run_id or "")
|
|
793
|
-
if entry is None:
|
|
794
|
-
# Restart survival (review Wave 0.1): the in-memory dict is
|
|
795
|
-
# gone but the durable record may still be valid.
|
|
796
|
-
entry = self._restore_persisted_approval(req.run_id or "")
|
|
797
|
-
if entry is None:
|
|
798
|
-
raise HTTPException(
|
|
799
|
-
status_code=404,
|
|
800
|
-
detail="Agent run not found. It may have expired — start a new request.",
|
|
801
|
-
)
|
|
802
|
-
if entry["user"] != current_user:
|
|
803
|
-
raise HTTPException(
|
|
804
|
-
status_code=403,
|
|
805
|
-
detail="Agent run belongs to another user.",
|
|
806
|
-
)
|
|
807
|
-
if now_monotonic >= entry["expires_monotonic"]:
|
|
808
|
-
self._approvals.pop(req.run_id or "", None)
|
|
809
|
-
try:
|
|
810
|
-
self.run_store.delete(req.run_id or "")
|
|
811
|
-
except Exception: # noqa: BLE001
|
|
812
|
-
quiet()
|
|
813
|
-
raise HTTPException(
|
|
814
|
-
status_code=410,
|
|
815
|
-
detail={
|
|
816
|
-
"error": "approval_expired",
|
|
817
|
-
"message": "Approval token expired. Start a new request.",
|
|
818
|
-
"replan": {"message": getattr(entry.get("req"), "message", "")},
|
|
819
|
-
},
|
|
820
|
-
)
|
|
821
|
-
if not self._token_matches(entry, req.approval_token or ""):
|
|
822
|
-
raise HTTPException(
|
|
823
|
-
status_code=403,
|
|
824
|
-
detail="Invalid approval token for this run.",
|
|
825
|
-
)
|
|
826
|
-
# Token validated — the pending run is consumed either way.
|
|
827
|
-
self._approvals.pop(req.run_id or "", None)
|
|
828
|
-
try:
|
|
829
|
-
self.run_store.delete(req.run_id or "")
|
|
830
|
-
except Exception as exc: # noqa: BLE001 — consumption must proceed
|
|
831
|
-
logging.warning("agent run store delete failed: %s", exc)
|
|
832
|
-
self._purge_expired_approvals_locked(now_monotonic)
|
|
833
|
-
|
|
834
|
-
ctx: AgentRunContext = entry["ctx"]
|
|
835
|
-
original_request: AgentRequest = entry["req"]
|
|
836
|
-
language_hint: str = entry["language_hint"]
|
|
837
|
-
|
|
838
|
-
if self.funnel_metrics is not None:
|
|
839
|
-
# approval_resume_rate = resumes / pauses (review §4.3): counted on
|
|
840
|
-
# every token-valid resume decision, approve and deny alike.
|
|
841
|
-
try:
|
|
842
|
-
self.funnel_metrics.increment("approval_resumes")
|
|
843
|
-
except Exception as exc: # noqa: BLE001 — advisory only
|
|
844
|
-
logging.warning("funnel metrics increment failed: %s", exc)
|
|
845
|
-
|
|
846
|
-
approved = req.approve if req.approve is not None else req.approved
|
|
847
|
-
if not approved:
|
|
848
|
-
message = "사용자가 계획을 취소했습니다."
|
|
849
|
-
try:
|
|
850
|
-
self.workspace_store.record_agent_run(
|
|
851
|
-
agent_id="agent:executor",
|
|
852
|
-
status="cancelled",
|
|
853
|
-
input_text=original_request.message,
|
|
854
|
-
output_text=message,
|
|
855
|
-
user_email=current_user or None,
|
|
856
|
-
workspace_id=original_request.workspace_id,
|
|
857
|
-
mode="llm",
|
|
858
|
-
timeline=ctx.transcript,
|
|
859
|
-
relationships=["agent:planner", "agent:reviewer"],
|
|
860
|
-
graph=self.workspace_graph(),
|
|
861
|
-
)
|
|
862
|
-
except Exception as exc:
|
|
863
|
-
logging.warning("workspace agent run record failed: %s", exc)
|
|
864
|
-
return {
|
|
865
|
-
"status": "cancelled",
|
|
866
|
-
"run_id": req.run_id,
|
|
867
|
-
"response": message,
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
edited_plan = req.edited_plan or req.modified_plan
|
|
871
|
-
if edited_plan:
|
|
872
|
-
plan, plan_fixes = normalize_plan(edited_plan, original_request.message)
|
|
873
|
-
ctx.plan = plan
|
|
874
|
-
ctx.transcript.append({
|
|
875
|
-
"state": AgentState.WAITING_APPROVAL.value,
|
|
876
|
-
"edited_plan": True,
|
|
877
|
-
**({"plan_fixes": plan_fixes} if plan_fixes else {}),
|
|
878
|
-
})
|
|
879
|
-
ctx.executing_model = req.executing_model or ctx.executing_model
|
|
880
|
-
ctx.reviewing_model = req.reviewing_model or ctx.reviewing_model
|
|
881
|
-
self.runtime.approve(ctx, current_user, approved_by_human=True)
|
|
882
|
-
return await self._finish(
|
|
883
|
-
ctx,
|
|
884
|
-
original_request,
|
|
885
|
-
language_hint,
|
|
886
|
-
current_user,
|
|
887
|
-
max(1, min(original_request.max_steps, 50)),
|
|
888
|
-
3,
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
__all__ = ["AgentHTTPController"]
|