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.
Files changed (439) hide show
  1. package/README.md +91 -148
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +75 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  5. package/docs/DEVELOPMENT.md +8 -6
  6. package/docs/ENTERPRISE.md +2 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +12 -5
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -3
  10. package/docs/REALTIME_COLLABORATION.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/WORKFLOW_DESIGNER.md +3 -2
  14. package/docs/kg-schema.md +1 -1
  15. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  16. package/lattice_brain/__init__.py +42 -79
  17. package/lattice_brain/graph/__init__.py +9 -24
  18. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  19. package/lattice_brain/ingestion/__init__.py +19 -58
  20. package/lattice_brain/ingestion/pipeline.py +20 -398
  21. package/lattice_brain/multimodal/__init__.py +7 -18
  22. package/lattice_brain/multimodal/images.py +6 -264
  23. package/lattice_brain/multimodal/video.py +8 -247
  24. package/lattice_brain/runtime/__init__.py +8 -79
  25. package/lattice_brain/runtime/hooks.py +22 -584
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/agent_worker_seam.py +7 -80
  28. package/latticeai/api/health.py +6 -20
  29. package/latticeai/api/local_files.py +16 -613
  30. package/latticeai/api/models.py +12 -87
  31. package/latticeai/api/search.py +17 -255
  32. package/latticeai/api/tools.py +69 -750
  33. package/latticeai/api/voice_capture.py +8 -70
  34. package/latticeai/api/worker_compute.py +842 -0
  35. package/latticeai/api/worker_seams.py +216 -0
  36. package/latticeai/app_factory.py +23 -232
  37. package/latticeai/cli/entrypoint.py +36 -249
  38. package/latticeai/core/agent_permission.py +16 -91
  39. package/latticeai/core/messages.py +89 -531
  40. package/latticeai/runtime/access_runtime.py +0 -14
  41. package/latticeai/runtime/bootstrap.py +10 -21
  42. package/latticeai/runtime/brain_runtime.py +35 -43
  43. package/latticeai/runtime/build_phases/__init__.py +21 -37
  44. package/latticeai/runtime/build_phases/features.py +99 -389
  45. package/latticeai/runtime/build_phases/foundation.py +82 -392
  46. package/latticeai/runtime/build_phases/web.py +91 -408
  47. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  48. package/latticeai/runtime/lifespan_runtime.py +7 -16
  49. package/latticeai/runtime/platform_services_runtime.py +1 -15
  50. package/latticeai/runtime/runtime_context.py +13 -130
  51. package/latticeai/runtime/security_runtime.py +10 -103
  52. package/latticeai/services/architecture_readiness.py +82 -43
  53. package/latticeai/services/model_runtime/__init__.py +2 -11
  54. package/latticeai/services/model_runtime/service.py +4 -28
  55. package/latticeai/services/p_reinforce.py +10 -261
  56. package/latticeai/services/product_readiness.py +31 -38
  57. package/latticeai/services/search_service.py +37 -795
  58. package/latticeai/services/tool_dispatch.py +30 -386
  59. package/latticeai/services/voice_capture.py +13 -107
  60. package/latticeai/tools/__init__.py +22 -49
  61. package/latticeai/tools/commands.py +0 -163
  62. package/latticeai/tools/computer.py +0 -39
  63. package/latticeai/tools/documents.py +1 -134
  64. package/latticeai/tools/filesystem.py +1 -247
  65. package/latticeai/tools/knowledge.py +1 -52
  66. package/latticeai/tools/local_files.py +0 -20
  67. package/latticeai/worker_app.py +75 -0
  68. package/package.json +2 -3
  69. package/requirements.txt +0 -5
  70. package/scripts/agent_eval.py +16 -28
  71. package/scripts/brain_quality_eval.py +20 -183
  72. package/scripts/bump_version.py +5 -5
  73. package/scripts/check_current_release_docs.mjs +7 -4
  74. package/scripts/check_openapi_drift.mjs +10 -0
  75. package/scripts/check_server_i18n.mjs +2 -18
  76. package/scripts/compose_openapi.py +377 -0
  77. package/scripts/export_openapi.py +32 -4
  78. package/scripts/gen_messages_catalog_fixture.py +302 -0
  79. package/scripts/gen_openapi_fragments.py +365 -0
  80. package/scripts/gen_redact_fixture.py +196 -0
  81. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  82. package/scripts/generate_agent_parity_fixtures.py +33 -14
  83. package/scripts/openapi_route_families.json +2161 -0
  84. package/scripts/release_screen_claims.json +21 -0
  85. package/scripts/run_integration_tests.mjs +134 -29
  86. package/scripts/run_sidecar_e2e.mjs +104 -11
  87. package/scripts/wheel_smoke.py +32 -22
  88. package/src-tauri/Cargo.lock +375 -9
  89. package/src-tauri/Cargo.toml +1 -1
  90. package/src-tauri/src/backend.rs +151 -45
  91. package/src-tauri/src/main.rs +18 -18
  92. package/src-tauri/src/topology.rs +58 -88
  93. package/src-tauri/tauri.conf.json +1 -2
  94. package/static/app/asset-manifest.json +41 -41
  95. package/static/app/assets/{Act-DcQizkl1.js → Act-CToZnOHz.js} +1 -1
  96. package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
  97. package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
  98. package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
  99. package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
  100. package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
  101. package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
  102. package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
  103. package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
  104. package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
  105. package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
  106. package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
  107. package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
  108. package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
  109. package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
  110. package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
  111. package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
  112. package/static/app/assets/circle-check-CY20XMCh.js +1 -0
  113. package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
  114. package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
  115. package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
  116. package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
  117. package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
  118. package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
  119. package/static/app/assets/index-BovSWRmQ.css +2 -0
  120. package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
  121. package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
  122. package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
  123. package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
  124. package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
  125. package/static/app/assets/search-DTxtjQ07.js +1 -0
  126. package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
  127. package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
  128. package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
  129. package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
  130. package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
  131. package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
  132. package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
  133. package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.js} +1 -1
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/archive.py +0 -522
  137. package/lattice_brain/context.py +0 -325
  138. package/lattice_brain/conversations.py +0 -382
  139. package/lattice_brain/core.py +0 -82
  140. package/lattice_brain/graph/_kg_contract.py +0 -249
  141. package/lattice_brain/graph/curator.py +0 -676
  142. package/lattice_brain/graph/discovery.py +0 -595
  143. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  144. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  145. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  146. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  147. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  148. package/lattice_brain/graph/documents.py +0 -380
  149. package/lattice_brain/graph/fusion.py +0 -395
  150. package/lattice_brain/graph/identity.py +0 -175
  151. package/lattice_brain/graph/image_vectors.py +0 -230
  152. package/lattice_brain/graph/ingest.py +0 -829
  153. package/lattice_brain/graph/network.py +0 -205
  154. package/lattice_brain/graph/proactive.py +0 -724
  155. package/lattice_brain/graph/projection/__init__.py +0 -42
  156. package/lattice_brain/graph/projection/curation.py +0 -500
  157. package/lattice_brain/graph/projection/v2_schema.py +0 -518
  158. package/lattice_brain/graph/provenance.py +0 -524
  159. package/lattice_brain/graph/rerank.py +0 -163
  160. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  161. package/lattice_brain/graph/retrieval/context.py +0 -197
  162. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  163. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  164. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  165. package/lattice_brain/graph/retrieval/signals.py +0 -95
  166. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  167. package/lattice_brain/graph/retrieval_policy.py +0 -180
  168. package/lattice_brain/graph/retrieval_reads.py +0 -769
  169. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  170. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  171. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  172. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  173. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  174. package/lattice_brain/graph/schema.py +0 -792
  175. package/lattice_brain/graph/store.py +0 -268
  176. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  177. package/lattice_brain/graph/vector_index/base.py +0 -167
  178. package/lattice_brain/graph/vector_index/brute_force.py +0 -110
  179. package/lattice_brain/graph/vector_index/hnsw.py +0 -290
  180. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  181. package/lattice_brain/graph/vector_index/quantized.py +0 -148
  182. package/lattice_brain/graph/vector_index/selector.py +0 -161
  183. package/lattice_brain/graph/write_master.py +0 -308
  184. package/lattice_brain/ingestion/_contract.py +0 -90
  185. package/lattice_brain/ingestion/folder_scan.py +0 -57
  186. package/lattice_brain/ingestion/folders.py +0 -258
  187. package/lattice_brain/ingestion/jobs_api.py +0 -107
  188. package/lattice_brain/ingestion/routing.py +0 -295
  189. package/lattice_brain/ingestion_jobs.py +0 -380
  190. package/lattice_brain/memory.py +0 -75
  191. package/lattice_brain/portability/__init__.py +0 -90
  192. package/lattice_brain/portability/_contract.py +0 -42
  193. package/lattice_brain/portability/backups.py +0 -338
  194. package/lattice_brain/portability/bundles.py +0 -136
  195. package/lattice_brain/portability/constants.py +0 -93
  196. package/lattice_brain/portability/fsops.py +0 -138
  197. package/lattice_brain/portability/service.py +0 -41
  198. package/lattice_brain/portability/sharing.py +0 -710
  199. package/lattice_brain/quality.py +0 -543
  200. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  201. package/lattice_brain/runtime/agent_runtime.py +0 -859
  202. package/lattice_brain/runtime/contracts.py +0 -460
  203. package/lattice_brain/runtime/multi_agent.py +0 -942
  204. package/lattice_brain/runtime/statuses.py +0 -10
  205. package/lattice_brain/sealed_box.py +0 -240
  206. package/lattice_brain/self_model.py +0 -675
  207. package/lattice_brain/sensitivity.py +0 -94
  208. package/lattice_brain/storage/__init__.py +0 -22
  209. package/lattice_brain/storage/base.py +0 -100
  210. package/lattice_brain/storage/docker.py +0 -105
  211. package/lattice_brain/storage/factory.py +0 -31
  212. package/lattice_brain/storage/migration.py +0 -191
  213. package/lattice_brain/storage/postgres.py +0 -123
  214. package/lattice_brain/storage/sqlite.py +0 -143
  215. package/lattice_brain/synthesis.py +0 -824
  216. package/lattice_brain/workflow.py +0 -497
  217. package/latticeai/api/admin.py +0 -471
  218. package/latticeai/api/agent_registry.py +0 -105
  219. package/latticeai/api/agents.py +0 -228
  220. package/latticeai/api/auth.py +0 -383
  221. package/latticeai/api/automation_intelligence.py +0 -401
  222. package/latticeai/api/brain_intelligence.py +0 -199
  223. package/latticeai/api/browser.py +0 -493
  224. package/latticeai/api/change_proposals.py +0 -89
  225. package/latticeai/api/chat.py +0 -572
  226. package/latticeai/api/chat_agent_http.py +0 -892
  227. package/latticeai/api/chat_contracts.py +0 -73
  228. package/latticeai/api/chat_documents.py +0 -276
  229. package/latticeai/api/chat_helpers.py +0 -460
  230. package/latticeai/api/chat_history.py +0 -101
  231. package/latticeai/api/chat_hybrid.py +0 -113
  232. package/latticeai/api/chat_intents.py +0 -646
  233. package/latticeai/api/chat_stream.py +0 -216
  234. package/latticeai/api/chronicle.py +0 -63
  235. package/latticeai/api/command_center.py +0 -51
  236. package/latticeai/api/computer_use.py +0 -474
  237. package/latticeai/api/evidence_actions.py +0 -48
  238. package/latticeai/api/features.py +0 -70
  239. package/latticeai/api/funnel_metrics.py +0 -31
  240. package/latticeai/api/garden.py +0 -34
  241. package/latticeai/api/hooks.py +0 -165
  242. package/latticeai/api/index_jobs.py +0 -145
  243. package/latticeai/api/invitations.py +0 -100
  244. package/latticeai/api/knowledge_graph.py +0 -536
  245. package/latticeai/api/marketplace.py +0 -105
  246. package/latticeai/api/mcp.py +0 -482
  247. package/latticeai/api/memory.py +0 -270
  248. package/latticeai/api/network.py +0 -81
  249. package/latticeai/api/network_boundary.py +0 -225
  250. package/latticeai/api/permission_mode.py +0 -61
  251. package/latticeai/api/permissions.py +0 -436
  252. package/latticeai/api/plugins.py +0 -126
  253. package/latticeai/api/portability.py +0 -391
  254. package/latticeai/api/project_sessions.py +0 -114
  255. package/latticeai/api/realtime.py +0 -118
  256. package/latticeai/api/review_queue.py +0 -364
  257. package/latticeai/api/security_dashboard.py +0 -604
  258. package/latticeai/api/setup.py +0 -319
  259. package/latticeai/api/static_routes.py +0 -354
  260. package/latticeai/api/ui_redirects.py +0 -26
  261. package/latticeai/api/workflow_designer.py +0 -394
  262. package/latticeai/api/workspace.py +0 -856
  263. package/latticeai/api/workspace_scope.py +0 -125
  264. package/latticeai/core/agent/__init__.py +0 -93
  265. package/latticeai/core/agent/_contract.py +0 -79
  266. package/latticeai/core/agent/context.py +0 -57
  267. package/latticeai/core/agent/deps.py +0 -125
  268. package/latticeai/core/agent/execution.py +0 -622
  269. package/latticeai/core/agent/planning.py +0 -145
  270. package/latticeai/core/agent/recovery.py +0 -157
  271. package/latticeai/core/agent/runtime.py +0 -210
  272. package/latticeai/core/agent/verification.py +0 -231
  273. package/latticeai/core/agent_eval.py +0 -739
  274. package/latticeai/core/agent_helpers.py +0 -493
  275. package/latticeai/core/agent_profiles.py +0 -110
  276. package/latticeai/core/agent_prompts.py +0 -171
  277. package/latticeai/core/agent_registry.py +0 -232
  278. package/latticeai/core/agent_state.py +0 -41
  279. package/latticeai/core/agent_trace.py +0 -104
  280. package/latticeai/core/artifact_ledger.py +0 -109
  281. package/latticeai/core/audit.py +0 -260
  282. package/latticeai/core/builtin_hooks.py +0 -105
  283. package/latticeai/core/context_builder.py +0 -394
  284. package/latticeai/core/document_generator.py +0 -103
  285. package/latticeai/core/enterprise.py +0 -154
  286. package/latticeai/core/enterprise_admin.py +0 -158
  287. package/latticeai/core/file_generation/__init__.py +0 -115
  288. package/latticeai/core/file_generation/bundles.py +0 -76
  289. package/latticeai/core/file_generation/extraction.py +0 -154
  290. package/latticeai/core/file_generation/inference.py +0 -235
  291. package/latticeai/core/file_generation/orchestration.py +0 -152
  292. package/latticeai/core/file_generation/prompting.py +0 -117
  293. package/latticeai/core/file_generation/repair.py +0 -114
  294. package/latticeai/core/file_generation/sanitize.py +0 -61
  295. package/latticeai/core/file_generation/validation.py +0 -201
  296. package/latticeai/core/invitations.py +0 -132
  297. package/latticeai/core/legacy_compatibility.py +0 -243
  298. package/latticeai/core/logging_safety.py +0 -46
  299. package/latticeai/core/marketplace.py +0 -293
  300. package/latticeai/core/mcp_catalog.py +0 -452
  301. package/latticeai/core/mcp_registry.py +0 -506
  302. package/latticeai/core/network_boundary.py +0 -168
  303. package/latticeai/core/oidc.py +0 -208
  304. package/latticeai/core/plugins.py +0 -432
  305. package/latticeai/core/product_hardening.py +0 -218
  306. package/latticeai/core/project_sessions.py +0 -337
  307. package/latticeai/core/realtime.py +0 -238
  308. package/latticeai/core/run_explain.py +0 -426
  309. package/latticeai/core/run_store.py +0 -252
  310. package/latticeai/core/timezones.py +0 -80
  311. package/latticeai/core/workspace_computer_memory.py +0 -84
  312. package/latticeai/core/workspace_graph_trace.py +0 -155
  313. package/latticeai/core/workspace_indexing.py +0 -102
  314. package/latticeai/core/workspace_memory.py +0 -77
  315. package/latticeai/core/workspace_onboarding.py +0 -104
  316. package/latticeai/core/workspace_os.py +0 -978
  317. package/latticeai/core/workspace_os_constants.py +0 -126
  318. package/latticeai/core/workspace_os_state.py +0 -180
  319. package/latticeai/core/workspace_os_utils.py +0 -103
  320. package/latticeai/core/workspace_permissions.py +0 -101
  321. package/latticeai/core/workspace_plugins.py +0 -97
  322. package/latticeai/core/workspace_relationships.py +0 -99
  323. package/latticeai/core/workspace_reorganization.py +0 -335
  324. package/latticeai/core/workspace_review_items.py +0 -112
  325. package/latticeai/core/workspace_runs.py +0 -726
  326. package/latticeai/core/workspace_skills.py +0 -109
  327. package/latticeai/core/workspace_snapshots.py +0 -198
  328. package/latticeai/core/workspace_timeline.py +0 -110
  329. package/latticeai/integrations/__init__.py +0 -0
  330. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  331. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  332. package/latticeai/integrations/telegram_bot/config.py +0 -86
  333. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  334. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  335. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  336. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  337. package/latticeai/runtime/audit_runtime.py +0 -76
  338. package/latticeai/runtime/automation_runtime.py +0 -81
  339. package/latticeai/runtime/chat_wiring.py +0 -141
  340. package/latticeai/runtime/context_runtime.py +0 -66
  341. package/latticeai/runtime/feature_toggle_wiring.py +0 -157
  342. package/latticeai/runtime/history_runtime.py +0 -163
  343. package/latticeai/runtime/history_writer.py +0 -138
  344. package/latticeai/runtime/hooks_runtime.py +0 -77
  345. package/latticeai/runtime/model_wiring.py +0 -68
  346. package/latticeai/runtime/namespace_runtime.py +0 -163
  347. package/latticeai/runtime/network_boundary_wiring.py +0 -117
  348. package/latticeai/runtime/network_config_runtime.py +0 -56
  349. package/latticeai/runtime/permission_mode_wiring.py +0 -112
  350. package/latticeai/runtime/persistence_runtime.py +0 -159
  351. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  352. package/latticeai/runtime/review_wiring.py +0 -42
  353. package/latticeai/runtime/router_registration.py +0 -693
  354. package/latticeai/runtime/service_singletons.py +0 -55
  355. package/latticeai/runtime/sso_config_runtime.py +0 -128
  356. package/latticeai/runtime/user_key_runtime.py +0 -106
  357. package/latticeai/runtime/web_runtime.py +0 -92
  358. package/latticeai/server_app.py +0 -51
  359. package/latticeai/services/app_context.py +0 -130
  360. package/latticeai/services/automation_execution.py +0 -266
  361. package/latticeai/services/automation_intelligence.py +0 -614
  362. package/latticeai/services/brain_automation.py +0 -191
  363. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  364. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  365. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  366. package/latticeai/services/brain_intelligence/constants.py +0 -47
  367. package/latticeai/services/brain_intelligence/digest.py +0 -258
  368. package/latticeai/services/brain_intelligence/health.py +0 -331
  369. package/latticeai/services/brain_intelligence/proposals.py +0 -259
  370. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  371. package/latticeai/services/brain_intelligence/service.py +0 -48
  372. package/latticeai/services/change_proposals.py +0 -471
  373. package/latticeai/services/chat_service.py +0 -243
  374. package/latticeai/services/chronicle.py +0 -555
  375. package/latticeai/services/cloud_egress_audit.py +0 -85
  376. package/latticeai/services/cloud_extraction.py +0 -129
  377. package/latticeai/services/cloud_streaming.py +0 -268
  378. package/latticeai/services/cloud_token_guard.py +0 -84
  379. package/latticeai/services/command_center.py +0 -548
  380. package/latticeai/services/evidence_actions.py +0 -258
  381. package/latticeai/services/feature_toggles.py +0 -502
  382. package/latticeai/services/folder_watch.py +0 -520
  383. package/latticeai/services/funnel_metrics.py +0 -307
  384. package/latticeai/services/hybrid_chat.py +0 -316
  385. package/latticeai/services/hybrid_context.py +0 -228
  386. package/latticeai/services/hybrid_policy.py +0 -129
  387. package/latticeai/services/interop_bridges.py +0 -978
  388. package/latticeai/services/local_knowledge.py +0 -465
  389. package/latticeai/services/memory_service/__init__.py +0 -52
  390. package/latticeai/services/memory_service/_contract.py +0 -100
  391. package/latticeai/services/memory_service/brief.py +0 -431
  392. package/latticeai/services/memory_service/constants.py +0 -57
  393. package/latticeai/services/memory_service/maintenance.py +0 -138
  394. package/latticeai/services/memory_service/manager.py +0 -186
  395. package/latticeai/services/memory_service/proof.py +0 -136
  396. package/latticeai/services/memory_service/recall.py +0 -225
  397. package/latticeai/services/memory_service/service.py +0 -48
  398. package/latticeai/services/memory_service/stores.py +0 -110
  399. package/latticeai/services/mode_store.py +0 -132
  400. package/latticeai/services/model_recommendation.py +0 -224
  401. package/latticeai/services/model_runtime/cloud.py +0 -87
  402. package/latticeai/services/network_boundary_service.py +0 -117
  403. package/latticeai/services/obsidian_bridge.py +0 -609
  404. package/latticeai/services/openai_compatible_adapter.py +0 -101
  405. package/latticeai/services/permission_mode_service.py +0 -122
  406. package/latticeai/services/platform_runtime.py +0 -366
  407. package/latticeai/services/review_queue.py +0 -380
  408. package/latticeai/services/router_context.py +0 -59
  409. package/latticeai/services/run_executor.py +0 -387
  410. package/latticeai/services/self_model_service.py +0 -171
  411. package/latticeai/services/setup_detection.py +0 -147
  412. package/latticeai/services/triggers.py +0 -378
  413. package/latticeai/services/upload_service.py +0 -172
  414. package/latticeai/services/workspace_service.py +0 -165
  415. package/latticeai/setup/__init__.py +0 -25
  416. package/latticeai/setup/auto_setup.py +0 -846
  417. package/latticeai/setup/demo_corpus.py +0 -98
  418. package/latticeai/setup/wizard/__init__.py +0 -126
  419. package/latticeai/setup/wizard/catalog.py +0 -175
  420. package/latticeai/setup/wizard/detect.py +0 -302
  421. package/latticeai/setup/wizard/install.py +0 -348
  422. package/latticeai/setup/wizard/paths.py +0 -165
  423. package/latticeai/setup/wizard/plans.py +0 -74
  424. package/latticeai/setup/wizard/recommend.py +0 -320
  425. package/scripts/bench_agent_smoke.py +0 -409
  426. package/scripts/bench_models.py +0 -540
  427. package/scripts/bench_vector_index.py +0 -295
  428. package/scripts/funnel_soft_gate.py +0 -192
  429. package/scripts/generate_agent_loop_fixtures.py +0 -994
  430. package/scripts/generate_rust_parity_fixtures.py +0 -908
  431. package/scripts/migrate_brain_storage.py +0 -57
  432. package/scripts/parity_fixture_corpus_context.py +0 -162
  433. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  434. package/scripts/profile_kg.py +0 -355
  435. package/server.py +0 -30
  436. package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
  437. package/static/app/assets/circle-check-qqLug9nU.js +0 -1
  438. package/static/app/assets/index-DxmOfNRi.css +0 -2
  439. 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"]