langchain-agentx-python 0.1__py3-none-any.whl

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 (354) hide show
  1. langchain_agentx/__init__.py +46 -0
  2. langchain_agentx/command/__init__.py +28 -0
  3. langchain_agentx/command/builtin/__init__.py +25 -0
  4. langchain_agentx/command/builtin/clear.py +33 -0
  5. langchain_agentx/command/builtin/compact.py +33 -0
  6. langchain_agentx/command/builtin/memory.py +37 -0
  7. langchain_agentx/command/builtin/reload_plugins.py +42 -0
  8. langchain_agentx/command/context.py +30 -0
  9. langchain_agentx/command/dispatcher.py +183 -0
  10. langchain_agentx/command/registry.py +110 -0
  11. langchain_agentx/command/result.py +25 -0
  12. langchain_agentx/command/types.py +41 -0
  13. langchain_agentx/config/__init__.py +14 -0
  14. langchain_agentx/loop/__init__.py +47 -0
  15. langchain_agentx/loop/config/__init__.py +20 -0
  16. langchain_agentx/loop/config/agent_config.py +66 -0
  17. langchain_agentx/loop/config/agent_loop_config.py +72 -0
  18. langchain_agentx/loop/config/model_context_resolver.py +105 -0
  19. langchain_agentx/loop/config/runtime_settings.py +50 -0
  20. langchain_agentx/loop/config/token_estimator.py +133 -0
  21. langchain_agentx/loop/context/__init__.py +66 -0
  22. langchain_agentx/loop/context/blocking_guard.py +97 -0
  23. langchain_agentx/loop/context/compaction_service.py +60 -0
  24. langchain_agentx/loop/context/message_utils.py +56 -0
  25. langchain_agentx/loop/context/pipeline.py +127 -0
  26. langchain_agentx/loop/context/settings.py +103 -0
  27. langchain_agentx/loop/context/stages/__init__.py +29 -0
  28. langchain_agentx/loop/context/stages/autocompact.py +140 -0
  29. langchain_agentx/loop/context/stages/base.py +32 -0
  30. langchain_agentx/loop/context/stages/collapse.py +76 -0
  31. langchain_agentx/loop/context/stages/microcompact.py +76 -0
  32. langchain_agentx/loop/context/stages/noop.py +33 -0
  33. langchain_agentx/loop/context/stages/snip.py +71 -0
  34. langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
  35. langchain_agentx/loop/context/types.py +79 -0
  36. langchain_agentx/loop/exit/__init__.py +1 -0
  37. langchain_agentx/loop/exit/exit_logic.py +320 -0
  38. langchain_agentx/loop/exit/reason_codes.py +39 -0
  39. langchain_agentx/loop/graph/__init__.py +5 -0
  40. langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
  41. langchain_agentx/loop/graph/factory.py +1409 -0
  42. langchain_agentx/loop/graph/graph_edges.py +820 -0
  43. langchain_agentx/loop/hook/__init__.py +48 -0
  44. langchain_agentx/loop/hook/async_hook_runner.py +62 -0
  45. langchain_agentx/loop/hook/config.py +280 -0
  46. langchain_agentx/loop/hook/engine.py +321 -0
  47. langchain_agentx/loop/hook/executors/__init__.py +9 -0
  48. langchain_agentx/loop/hook/executors/agent.py +107 -0
  49. langchain_agentx/loop/hook/executors/command.py +230 -0
  50. langchain_agentx/loop/hook/executors/http.py +114 -0
  51. langchain_agentx/loop/hook/executors/prompt.py +92 -0
  52. langchain_agentx/loop/hook/graph_wiring.py +134 -0
  53. langchain_agentx/loop/hook/registry.py +262 -0
  54. langchain_agentx/loop/hook/trust.py +43 -0
  55. langchain_agentx/loop/hook/types.py +110 -0
  56. langchain_agentx/loop/injection/__init__.py +13 -0
  57. langchain_agentx/loop/injection/dedup.py +74 -0
  58. langchain_agentx/loop/loop_abort.py +36 -0
  59. langchain_agentx/loop/model/__init__.py +1 -0
  60. langchain_agentx/loop/model/model_node.py +648 -0
  61. langchain_agentx/loop/model/model_nodes.py +661 -0
  62. langchain_agentx/loop/model/orphan_tool_results.py +38 -0
  63. langchain_agentx/loop/model/retrier.py +307 -0
  64. langchain_agentx/loop/model/retry_bridge.py +58 -0
  65. langchain_agentx/loop/model/retry_events.py +35 -0
  66. langchain_agentx/loop/model/retry_policy.py +56 -0
  67. langchain_agentx/loop/model/schema_and_format.py +153 -0
  68. langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
  69. langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
  70. langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
  71. langchain_agentx/loop/prompt/__init__.py +95 -0
  72. langchain_agentx/loop/prompt/builder.py +61 -0
  73. langchain_agentx/loop/prompt/builtin.py +218 -0
  74. langchain_agentx/loop/prompt/compact.py +408 -0
  75. langchain_agentx/loop/prompt/sections.py +120 -0
  76. langchain_agentx/loop/runtime/__init__.py +19 -0
  77. langchain_agentx/loop/runtime/context.py +34 -0
  78. langchain_agentx/loop/runtime/context_factory.py +107 -0
  79. langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
  80. langchain_agentx/loop/subagent/__init__.py +53 -0
  81. langchain_agentx/loop/subagent/async_runner.py +215 -0
  82. langchain_agentx/loop/subagent/context.py +209 -0
  83. langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
  84. langchain_agentx/loop/subagent/graph.py +72 -0
  85. langchain_agentx/loop/subagent/orchestrator.py +391 -0
  86. langchain_agentx/loop/subagent/progress.py +30 -0
  87. langchain_agentx/loop/subagent/prompt.py +52 -0
  88. langchain_agentx/loop/subagent/runner.py +504 -0
  89. langchain_agentx/loop/subagent/transcript.py +172 -0
  90. langchain_agentx/memory/__init__.py +2 -0
  91. langchain_agentx/memory/instruction/__init__.py +12 -0
  92. langchain_agentx/memory/instruction/loader.py +325 -0
  93. langchain_agentx/memory/instruction/resolver.py +24 -0
  94. langchain_agentx/memory/instruction/runtime.py +83 -0
  95. langchain_agentx/memory/instruction/sections.py +83 -0
  96. langchain_agentx/memory/instruction/types.py +59 -0
  97. langchain_agentx/memory/memdir/__init__.py +77 -0
  98. langchain_agentx/memory/memdir/age.py +36 -0
  99. langchain_agentx/memory/memdir/agent_memory.py +380 -0
  100. langchain_agentx/memory/memdir/extractor.py +309 -0
  101. langchain_agentx/memory/memdir/loader.py +187 -0
  102. langchain_agentx/memory/memdir/paths.py +63 -0
  103. langchain_agentx/memory/memdir/recall.py +45 -0
  104. langchain_agentx/memory/memdir/runtime.py +43 -0
  105. langchain_agentx/memory/memdir/scan.py +135 -0
  106. langchain_agentx/memory/memdir/types.py +104 -0
  107. langchain_agentx/memory/session/__init__.py +76 -0
  108. langchain_agentx/memory/session/compact_bridge.py +208 -0
  109. langchain_agentx/memory/session/prompts.py +172 -0
  110. langchain_agentx/memory/session/session_memory.py +282 -0
  111. langchain_agentx/observability/__init__.py +67 -0
  112. langchain_agentx/observability/evaluation/__init__.py +17 -0
  113. langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
  114. langchain_agentx/observability/evaluation/checkers/base.py +34 -0
  115. langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
  116. langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
  117. langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
  118. langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
  119. langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
  120. langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
  121. langchain_agentx/observability/evaluation/service.py +102 -0
  122. langchain_agentx/observability/evaluation/state.py +32 -0
  123. langchain_agentx/observability/evaluation/store.py +258 -0
  124. langchain_agentx/observability/events/__init__.py +15 -0
  125. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
  126. langchain_agentx/observability/logging/__init__.py +15 -0
  127. langchain_agentx/observability/logging/debug_burst.py +95 -0
  128. langchain_agentx/observability/logging/logging_config.py +178 -0
  129. langchain_agentx/observability/logging/logging_contract.py +65 -0
  130. langchain_agentx/observability/replay/__init__.py +35 -0
  131. langchain_agentx/observability/replay/cli.py +91 -0
  132. langchain_agentx/observability/replay/service.py +83 -0
  133. langchain_agentx/observability/replay/store.py +278 -0
  134. langchain_agentx/observability/replay/ui.py +47 -0
  135. langchain_agentx/observability/trace/__init__.py +25 -0
  136. langchain_agentx/observability/trace/collector.py +560 -0
  137. langchain_agentx/observability/trace/event_emitter.py +183 -0
  138. langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
  139. langchain_agentx/observability/trace/models.py +144 -0
  140. langchain_agentx/observability/trace/sqlite_store.py +873 -0
  141. langchain_agentx/observability/trace/trace_callback.py +295 -0
  142. langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
  143. langchain_agentx/plugin/__init__.py +26 -0
  144. langchain_agentx/plugin/builtin.py +53 -0
  145. langchain_agentx/plugin/config.py +113 -0
  146. langchain_agentx/plugin/loader.py +386 -0
  147. langchain_agentx/plugin/manifest.py +154 -0
  148. langchain_agentx/plugin/registries.py +211 -0
  149. langchain_agentx/plugin/types.py +142 -0
  150. langchain_agentx/provider/__init__.py +27 -0
  151. langchain_agentx/provider/anthropic.py +121 -0
  152. langchain_agentx/provider/compatible_chat_openai.py +86 -0
  153. langchain_agentx/provider/env.py +45 -0
  154. langchain_agentx/provider/model_profile.py +156 -0
  155. langchain_agentx/provider/openai.py +89 -0
  156. langchain_agentx/session/__init__.py +17 -0
  157. langchain_agentx/session/agent_session.py +320 -0
  158. langchain_agentx/session/conversation_factory.py +87 -0
  159. langchain_agentx/session/conversation_recovery.py +156 -0
  160. langchain_agentx/session/conversation_session.py +198 -0
  161. langchain_agentx/session/factory.py +143 -0
  162. langchain_agentx/session/protocol.py +25 -0
  163. langchain_agentx/task_runtime/__init__.py +113 -0
  164. langchain_agentx/task_runtime/core/__init__.py +51 -0
  165. langchain_agentx/task_runtime/core/ids.py +33 -0
  166. langchain_agentx/task_runtime/core/interfaces.py +115 -0
  167. langchain_agentx/task_runtime/core/notification_priority.py +19 -0
  168. langchain_agentx/task_runtime/core/types.py +136 -0
  169. langchain_agentx/task_runtime/integrations/__init__.py +33 -0
  170. langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
  171. langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
  172. langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
  173. langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
  174. langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
  175. langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
  176. langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
  177. langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
  178. langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
  179. langchain_agentx/task_runtime/output/__init__.py +5 -0
  180. langchain_agentx/task_runtime/output/sink.py +64 -0
  181. langchain_agentx/task_runtime/policy/__init__.py +11 -0
  182. langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
  183. langchain_agentx/task_runtime/queue/__init__.py +5 -0
  184. langchain_agentx/task_runtime/queue/in_memory.py +55 -0
  185. langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
  186. langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
  187. langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
  188. langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
  189. langchain_agentx/task_runtime/store/__init__.py +6 -0
  190. langchain_agentx/task_runtime/store/in_memory.py +81 -0
  191. langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
  192. langchain_agentx/task_runtime/tasks/__init__.py +76 -0
  193. langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
  194. langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
  195. langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
  196. langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
  197. langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
  198. langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
  199. langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
  200. langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
  201. langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
  202. langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
  203. langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
  204. langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
  205. langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
  206. langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
  207. langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
  208. langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
  209. langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
  210. langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
  211. langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
  212. langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
  213. langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
  214. langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
  215. langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
  216. langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
  217. langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
  218. langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
  219. langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
  220. langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
  221. langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
  222. langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
  223. langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
  224. langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
  225. langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
  226. langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
  227. langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
  228. langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
  229. langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
  230. langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
  231. langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
  232. langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
  233. langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
  234. langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
  235. langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
  236. langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
  237. langchain_agentx/tool_runtime/__init__.py +90 -0
  238. langchain_agentx/tool_runtime/adapter.py +365 -0
  239. langchain_agentx/tool_runtime/base.py +319 -0
  240. langchain_agentx/tool_runtime/errors.py +190 -0
  241. langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
  242. langchain_agentx/tool_runtime/loader.py +195 -0
  243. langchain_agentx/tool_runtime/models.py +260 -0
  244. langchain_agentx/tool_runtime/permission_context.py +78 -0
  245. langchain_agentx/tool_runtime/pipeline.py +621 -0
  246. langchain_agentx/tool_runtime/policy.py +447 -0
  247. langchain_agentx/tool_runtime/registry.py +81 -0
  248. langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
  249. langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
  250. langchain_agentx/tool_runtime/resolvers/background.py +32 -0
  251. langchain_agentx/tool_runtime/resolvers/base.py +20 -0
  252. langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
  253. langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
  254. langchain_agentx/tool_runtime/session_store.py +132 -0
  255. langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
  256. langchain_agentx/tool_runtime/state_bridge.py +164 -0
  257. langchain_agentx/tools/__init__.py +26 -0
  258. langchain_agentx/tools/agent/__init__.py +9 -0
  259. langchain_agentx/tools/agent/backend.py +53 -0
  260. langchain_agentx/tools/agent/built_in/__init__.py +19 -0
  261. langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
  262. langchain_agentx/tools/agent/built_in/explore.py +80 -0
  263. langchain_agentx/tools/agent/built_in/general.py +57 -0
  264. langchain_agentx/tools/agent/built_in/plan.py +89 -0
  265. langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
  266. langchain_agentx/tools/agent/built_in/verification.py +120 -0
  267. langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
  268. langchain_agentx/tools/agent/cwd_resolution.py +119 -0
  269. langchain_agentx/tools/agent/limits.py +26 -0
  270. langchain_agentx/tools/agent/loader.py +270 -0
  271. langchain_agentx/tools/agent/models.py +85 -0
  272. langchain_agentx/tools/agent/prompt.py +120 -0
  273. langchain_agentx/tools/agent/registry/__init__.py +18 -0
  274. langchain_agentx/tools/agent/registry/config.py +29 -0
  275. langchain_agentx/tools/agent/registry/registry.py +47 -0
  276. langchain_agentx/tools/agent/scope.py +137 -0
  277. langchain_agentx/tools/agent/tool.py +256 -0
  278. langchain_agentx/tools/bash/__init__.py +9 -0
  279. langchain_agentx/tools/bash/ast_security.py +571 -0
  280. langchain_agentx/tools/bash/backend.py +1447 -0
  281. langchain_agentx/tools/bash/bash_hardening.py +734 -0
  282. langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
  283. langchain_agentx/tools/bash/cwd_reporter.py +95 -0
  284. langchain_agentx/tools/bash/limits.py +71 -0
  285. langchain_agentx/tools/bash/mode_validation.py +282 -0
  286. langchain_agentx/tools/bash/models.py +131 -0
  287. langchain_agentx/tools/bash/observability.py +148 -0
  288. langchain_agentx/tools/bash/output_utils.py +200 -0
  289. langchain_agentx/tools/bash/path_security.py +2429 -0
  290. langchain_agentx/tools/bash/prompt.py +68 -0
  291. langchain_agentx/tools/bash/read_only_validation.py +589 -0
  292. langchain_agentx/tools/bash/result_presenter.py +324 -0
  293. langchain_agentx/tools/bash/sandbox_decision.py +133 -0
  294. langchain_agentx/tools/bash/security.py +311 -0
  295. langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
  296. langchain_agentx/tools/bash/sed_validation.py +163 -0
  297. langchain_agentx/tools/bash/semantics.py +111 -0
  298. langchain_agentx/tools/bash/session_manager.py +205 -0
  299. langchain_agentx/tools/bash/session_runtime.py +290 -0
  300. langchain_agentx/tools/bash/shell_locator.py +191 -0
  301. langchain_agentx/tools/bash/task_runtime.py +91 -0
  302. langchain_agentx/tools/bash/tool.py +939 -0
  303. langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
  304. langchain_agentx/tools/glob/__init__.py +9 -0
  305. langchain_agentx/tools/glob/models.py +57 -0
  306. langchain_agentx/tools/glob/pagination.py +30 -0
  307. langchain_agentx/tools/glob/prompt.py +24 -0
  308. langchain_agentx/tools/glob/rg_list_backend.py +139 -0
  309. langchain_agentx/tools/glob/rg_pattern.py +44 -0
  310. langchain_agentx/tools/glob/tool.py +327 -0
  311. langchain_agentx/tools/grep/__init__.py +7 -0
  312. langchain_agentx/tools/grep/backend.py +375 -0
  313. langchain_agentx/tools/grep/models.py +127 -0
  314. langchain_agentx/tools/grep/prompt.py +30 -0
  315. langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
  316. langchain_agentx/tools/grep/tool.py +475 -0
  317. langchain_agentx/tools/read/__init__.py +9 -0
  318. langchain_agentx/tools/read/backend.py +415 -0
  319. langchain_agentx/tools/read/limits.py +67 -0
  320. langchain_agentx/tools/read/models.py +156 -0
  321. langchain_agentx/tools/read/prompt.py +73 -0
  322. langchain_agentx/tools/read/tool.py +494 -0
  323. langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
  324. langchain_agentx/tools/skill/__init__.py +4 -0
  325. langchain_agentx/tools/skill/argument_substitution.py +80 -0
  326. langchain_agentx/tools/skill/loader.py +196 -0
  327. langchain_agentx/tools/skill/models.py +88 -0
  328. langchain_agentx/tools/skill/policy.py +80 -0
  329. langchain_agentx/tools/skill/prompt.py +35 -0
  330. langchain_agentx/tools/skill/tool.py +222 -0
  331. langchain_agentx/utils/__init__.py +0 -0
  332. langchain_agentx/utils/cwd.py +124 -0
  333. langchain_agentx/utils/host_platform.py +112 -0
  334. langchain_agentx/utils/path_hierarchy.py +48 -0
  335. langchain_agentx/utils/path_user_input.py +66 -0
  336. langchain_agentx/utils/rg_executable.py +18 -0
  337. langchain_agentx/utils/subprocess_text.py +101 -0
  338. langchain_agentx/utils/temp_paths.py +77 -0
  339. langchain_agentx/utils/unc_path.py +25 -0
  340. langchain_agentx/utils/win_reserved_paths.py +51 -0
  341. langchain_agentx/workflow/__init__.py +7 -0
  342. langchain_agentx/workflow/base.py +97 -0
  343. langchain_agentx/workflow/batch.py +55 -0
  344. langchain_agentx/workflow/dag.py +54 -0
  345. langchain_agentx/workspace/__init__.py +13 -0
  346. langchain_agentx/workspace/config.py +140 -0
  347. langchain_agentx/workspace/path_key_normalizer.py +30 -0
  348. langchain_agentx/workspace/resolver.py +74 -0
  349. langchain_agentx/workspace/validators.py +41 -0
  350. langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
  351. langchain_agentx_python-0.1.dist-info/METADATA +513 -0
  352. langchain_agentx_python-0.1.dist-info/RECORD +354 -0
  353. langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
  354. langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,873 @@
1
+ """
2
+ SqliteTraceStore:SQLite 持久化存储。
3
+
4
+ 职责:
5
+ 1. 管理 SQLite schema(trace_sessions / trace_spans)
6
+ 2. 提供写入接口(save_session / save_spans)
7
+ 3. 提供查询接口(query_sessions / get_span_tree)
8
+
9
+ 设计要点:
10
+ - WAL 模式:并发读写安全
11
+ - 索引优化:session_id / parent_span_id / span_type
12
+ - 批量写入:减少 I/O
13
+ """
14
+
15
+ import sqlite3
16
+ import json
17
+ import os
18
+ import time
19
+ from pathlib import Path
20
+ from typing import List, Optional, Dict, Any
21
+ from contextlib import contextmanager
22
+
23
+ from langchain_agentx.workspace import resolve_agent_workspace_config
24
+
25
+ from .models import TraceSession, TraceSpan
26
+
27
+
28
+ class SqliteTraceStore:
29
+ """SQLite trace 存储"""
30
+
31
+ def __init__(self, db_path: str = None):
32
+ """
33
+ 初始化 SqliteTraceStore。
34
+
35
+ Args:
36
+ db_path: SQLite 数据库路径(默认:<workspace_root>/<agent_home>/data/db/traces.db)
37
+ """
38
+ if db_path is None:
39
+ workspace_cfg = resolve_agent_workspace_config()
40
+ db_file = workspace_cfg.traces_db_path
41
+ db_file.parent.mkdir(parents=True, exist_ok=True)
42
+ db_path = str(db_file)
43
+
44
+ self.db_path = db_path
45
+ self._init_db()
46
+
47
+ def _init_db(self) -> None:
48
+ """初始化数据库 schema(非破坏性,不清空历史数据)。"""
49
+ with self._get_connection() as conn:
50
+ current_journal_mode = str(
51
+ conn.execute("PRAGMA journal_mode").fetchone()[0]
52
+ ).lower()
53
+ if current_journal_mode != "wal":
54
+ conn.execute("PRAGMA journal_mode=WAL")
55
+
56
+ # 创建 trace_sessions 表
57
+ conn.execute("""
58
+ CREATE TABLE IF NOT EXISTS trace_sessions (
59
+ session_id TEXT PRIMARY KEY,
60
+ conversation_session_id TEXT NOT NULL,
61
+ parent_session_id TEXT,
62
+ parent_tool_call_id TEXT,
63
+ workflow_id TEXT,
64
+ container_type TEXT NOT NULL DEFAULT 'agent_session',
65
+ unit_type TEXT NOT NULL DEFAULT 'main_loop',
66
+ origin TEXT NOT NULL DEFAULT 'user',
67
+ visibility TEXT NOT NULL DEFAULT 'default',
68
+ user_query TEXT,
69
+ graph_name TEXT,
70
+ start_time REAL NOT NULL,
71
+ end_time REAL,
72
+ status TEXT NOT NULL DEFAULT 'running',
73
+ finish_reason TEXT,
74
+ exit_code TEXT,
75
+ terminal_reason TEXT,
76
+ loop_count INTEGER DEFAULT 0,
77
+ total_input_tokens INTEGER DEFAULT 0,
78
+ total_output_tokens INTEGER DEFAULT 0,
79
+ total_latency_ms REAL DEFAULT 0.0,
80
+ metadata_json TEXT
81
+ )
82
+ """)
83
+ self._ensure_session_columns(conn)
84
+
85
+ # 创建 trace_spans 表
86
+ conn.execute("""
87
+ CREATE TABLE IF NOT EXISTS trace_spans (
88
+ span_id TEXT PRIMARY KEY,
89
+ session_id TEXT NOT NULL,
90
+ parent_span_id TEXT,
91
+ span_type TEXT NOT NULL,
92
+ name TEXT NOT NULL,
93
+ start_time REAL NOT NULL,
94
+ end_time REAL,
95
+ latency_ms REAL,
96
+ status TEXT NOT NULL DEFAULT 'running',
97
+ input_json TEXT,
98
+ output_json TEXT,
99
+ error TEXT,
100
+ metadata_json TEXT,
101
+ FOREIGN KEY (session_id) REFERENCES trace_sessions(session_id)
102
+ )
103
+ """)
104
+
105
+ # 创建索引
106
+ conn.execute("""
107
+ CREATE INDEX IF NOT EXISTS idx_spans_session
108
+ ON trace_spans(session_id)
109
+ """)
110
+
111
+ conn.execute("""
112
+ CREATE INDEX IF NOT EXISTS idx_spans_parent
113
+ ON trace_spans(parent_span_id)
114
+ """)
115
+
116
+ conn.execute("""
117
+ CREATE INDEX IF NOT EXISTS idx_spans_type
118
+ ON trace_spans(span_type)
119
+ """)
120
+
121
+ conn.execute("""
122
+ CREATE INDEX IF NOT EXISTS idx_sessions_time
123
+ ON trace_sessions(start_time DESC)
124
+ """)
125
+
126
+ conn.execute("""
127
+ CREATE INDEX IF NOT EXISTS idx_sessions_status
128
+ ON trace_sessions(status)
129
+ """)
130
+ conn.execute("""
131
+ CREATE INDEX IF NOT EXISTS idx_sessions_conversation
132
+ ON trace_sessions(conversation_session_id)
133
+ """)
134
+ conn.execute("""
135
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent
136
+ ON trace_sessions(parent_session_id)
137
+ """)
138
+ conn.execute("""
139
+ CREATE INDEX IF NOT EXISTS idx_sessions_visibility
140
+ ON trace_sessions(visibility)
141
+ """)
142
+ conn.execute("""
143
+ CREATE INDEX IF NOT EXISTS idx_sessions_container_type
144
+ ON trace_sessions(container_type)
145
+ """)
146
+ conn.execute("""
147
+ CREATE INDEX IF NOT EXISTS idx_sessions_workflow
148
+ ON trace_sessions(workflow_id)
149
+ """)
150
+ conn.execute("""
151
+ CREATE INDEX IF NOT EXISTS idx_sessions_unit_type
152
+ ON trace_sessions(unit_type)
153
+ """)
154
+ self._init_views(conn)
155
+
156
+ conn.commit()
157
+
158
+ @staticmethod
159
+ def _init_views(conn: sqlite3.Connection) -> None:
160
+ conn.execute("""
161
+ CREATE VIEW IF NOT EXISTS v_session_summary AS
162
+ SELECT
163
+ s.session_id,
164
+ s.graph_name,
165
+ s.user_query,
166
+ s.start_time,
167
+ s.end_time,
168
+ s.total_latency_ms,
169
+ s.total_input_tokens,
170
+ s.total_output_tokens,
171
+ s.finish_reason,
172
+ s.status,
173
+ s.container_type,
174
+ s.unit_type,
175
+ s.visibility,
176
+ (SELECT COUNT(*) FROM trace_spans sp
177
+ WHERE sp.session_id = s.session_id AND sp.span_type = 'loop_step') AS step_count,
178
+ (SELECT COUNT(*) FROM trace_spans sp
179
+ WHERE sp.session_id = s.session_id AND sp.span_type = 'tool_call') AS tool_count
180
+ FROM trace_sessions s
181
+ ORDER BY s.start_time DESC
182
+ """)
183
+ conn.execute("""
184
+ CREATE VIEW IF NOT EXISTS v_conversation_summary AS
185
+ SELECT
186
+ conversation_session_id AS conversation_id,
187
+ COUNT(*) AS run_count,
188
+ MIN(start_time) AS first_start_time,
189
+ MAX(COALESCE(end_time, start_time)) AS last_end_time,
190
+ SUM(total_input_tokens) AS total_input_tokens,
191
+ SUM(total_output_tokens) AS total_output_tokens,
192
+ SUM(total_latency_ms) AS total_latency_ms
193
+ FROM trace_sessions
194
+ WHERE conversation_session_id IS NOT NULL
195
+ AND conversation_session_id != ''
196
+ GROUP BY conversation_session_id
197
+ ORDER BY last_end_time DESC
198
+ """)
199
+ conn.execute("""
200
+ CREATE VIEW IF NOT EXISTS v_workflow_summary AS
201
+ SELECT
202
+ workflow_id,
203
+ COUNT(*) AS total_tasks,
204
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
205
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
206
+ SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
207
+ MIN(start_time) AS workflow_start,
208
+ MAX(COALESCE(end_time, start_time)) AS workflow_end,
209
+ (MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
210
+ SUM(total_input_tokens) AS total_input_tokens,
211
+ SUM(total_output_tokens) AS total_output_tokens
212
+ FROM trace_sessions
213
+ WHERE workflow_id IS NOT NULL
214
+ AND workflow_id != ''
215
+ GROUP BY workflow_id
216
+ ORDER BY workflow_start DESC
217
+ """)
218
+
219
+ @staticmethod
220
+ def _ensure_session_columns(conn: sqlite3.Connection) -> None:
221
+ """为历史数据库补齐新增列(幂等)。"""
222
+ existing = {
223
+ str(row[1])
224
+ for row in conn.execute("PRAGMA table_info(trace_sessions)").fetchall()
225
+ }
226
+ required_columns = {
227
+ "container_type": "TEXT NOT NULL DEFAULT 'agent_session'",
228
+ "unit_type": "TEXT NOT NULL DEFAULT 'main_loop'",
229
+ "origin": "TEXT NOT NULL DEFAULT 'user'",
230
+ "visibility": "TEXT NOT NULL DEFAULT 'default'",
231
+ "workflow_id": "TEXT",
232
+ }
233
+ for column, column_type in required_columns.items():
234
+ if column in existing:
235
+ continue
236
+ conn.execute(f"ALTER TABLE trace_sessions ADD COLUMN {column} {column_type}")
237
+
238
+ @contextmanager
239
+ def _get_connection(self):
240
+ """获取数据库连接(上下文管理器)
241
+
242
+ Note:
243
+ timeout=30.0 用于并发写入场景(WAL 模式下写锁竞争)。
244
+ 详见 docs/architecture/observability-sqlite-concurrency.md
245
+ """
246
+ conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
247
+ conn.row_factory = sqlite3.Row
248
+ try:
249
+ yield conn
250
+ finally:
251
+ conn.close()
252
+
253
+ def save_session(self, session: TraceSession) -> None:
254
+ with self._get_connection() as conn:
255
+ conn.execute("""
256
+ INSERT OR REPLACE INTO trace_sessions (
257
+ session_id, conversation_session_id, parent_session_id, parent_tool_call_id,
258
+ workflow_id,
259
+ container_type, unit_type, origin, visibility,
260
+ user_query, graph_name, start_time, end_time,
261
+ status, finish_reason, exit_code, terminal_reason, loop_count,
262
+ total_input_tokens, total_output_tokens, total_latency_ms, metadata_json
263
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
264
+ """, (
265
+ session.session_id,
266
+ session.conversation_session_id or session.session_id,
267
+ session.parent_session_id,
268
+ session.parent_tool_call_id,
269
+ session.workflow_id,
270
+ session.container_type,
271
+ session.unit_type,
272
+ session.origin,
273
+ session.visibility,
274
+ session.user_query,
275
+ session.graph_name,
276
+ session.start_time,
277
+ session.end_time,
278
+ session.status,
279
+ session.finish_reason,
280
+ session.exit_code,
281
+ session.terminal_reason,
282
+ session.loop_count,
283
+ session.total_input_tokens,
284
+ session.total_output_tokens,
285
+ session.total_latency_ms,
286
+ json.dumps(session.metadata, default=str) if session.metadata else None,
287
+ ))
288
+ conn.commit()
289
+
290
+ def save_spans(self, spans: List[TraceSpan]) -> None:
291
+ """
292
+ 批量保存 spans。
293
+
294
+ Args:
295
+ spans: TraceSpan 列表
296
+ """
297
+ if not spans:
298
+ return
299
+
300
+ with self._get_connection() as conn:
301
+ conn.executemany("""
302
+ INSERT OR REPLACE INTO trace_spans (
303
+ span_id, session_id, parent_span_id, span_type, name,
304
+ start_time, end_time, latency_ms, status,
305
+ input_json, output_json, error, metadata_json
306
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
307
+ """, [
308
+ (
309
+ span.span_id,
310
+ span.session_id,
311
+ span.parent_span_id,
312
+ span.span_type.value if hasattr(span.span_type, 'value') else span.span_type,
313
+ span.name,
314
+ span.start_time,
315
+ span.end_time,
316
+ span.latency_ms,
317
+ span.status.value if hasattr(span.status, 'value') else span.status,
318
+ json.dumps(span.input_data, default=str) if span.input_data else None,
319
+ json.dumps(span.output_data, default=str) if span.output_data else None,
320
+ span.error,
321
+ json.dumps(span.metadata, default=str) if span.metadata else None,
322
+ )
323
+ for span in spans
324
+ ])
325
+ conn.commit()
326
+
327
+ def _row_to_session(self, row) -> TraceSession:
328
+ return TraceSession(
329
+ session_id=row["session_id"],
330
+ conversation_session_id=row["conversation_session_id"],
331
+ parent_session_id=row["parent_session_id"],
332
+ parent_tool_call_id=row["parent_tool_call_id"],
333
+ workflow_id=row["workflow_id"] if "workflow_id" in row.keys() else None,
334
+ container_type=row["container_type"] if "container_type" in row.keys() else "agent_session",
335
+ unit_type=row["unit_type"],
336
+ origin=row["origin"],
337
+ visibility=row["visibility"],
338
+ user_query=row["user_query"],
339
+ graph_name=row["graph_name"],
340
+ start_time=row["start_time"],
341
+ end_time=row["end_time"],
342
+ status=row["status"],
343
+ finish_reason=row["finish_reason"],
344
+ exit_code=row["exit_code"],
345
+ terminal_reason=row["terminal_reason"],
346
+ loop_count=row["loop_count"],
347
+ total_input_tokens=row["total_input_tokens"],
348
+ total_output_tokens=row["total_output_tokens"],
349
+ total_latency_ms=row["total_latency_ms"],
350
+ metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
351
+ )
352
+
353
+ def get_session(self, session_id: str) -> Optional[TraceSession]:
354
+ """
355
+ 获取 session。
356
+
357
+ Args:
358
+ session_id: session 唯一标识
359
+
360
+ Returns:
361
+ TraceSession 或 None
362
+ """
363
+ with self._get_connection() as conn:
364
+ row = conn.execute("""
365
+ SELECT * FROM trace_sessions WHERE session_id = ?
366
+ """, (session_id,)).fetchone()
367
+
368
+ if not row:
369
+ return None
370
+ return self._row_to_session(row)
371
+
372
+ def query_sessions(
373
+ self,
374
+ limit: int = 20,
375
+ offset: int = 0,
376
+ status: Optional[str] = None,
377
+ graph_name: Optional[str] = None,
378
+ visibility: Optional[str] = None,
379
+ workflow_id: Optional[str] = None,
380
+ **kwargs
381
+ ) -> List[TraceSession]:
382
+ """
383
+ 查询 session 列表。
384
+
385
+ Args:
386
+ limit: 返回条数
387
+ offset: 偏移量
388
+ status: 过滤状态
389
+ graph_name: 过滤 graph 名称
390
+
391
+ Returns:
392
+ TraceSession 列表
393
+ """
394
+ query = "SELECT * FROM trace_sessions WHERE 1=1"
395
+ params = []
396
+
397
+ if status:
398
+ query += " AND status = ?"
399
+ params.append(status)
400
+
401
+ if graph_name:
402
+ query += " AND graph_name = ?"
403
+ params.append(graph_name)
404
+
405
+ if visibility:
406
+ query += " AND visibility = ?"
407
+ params.append(visibility)
408
+
409
+ if workflow_id:
410
+ query += " AND workflow_id = ?"
411
+ params.append(workflow_id)
412
+
413
+ query += " ORDER BY COALESCE(end_time, start_time) DESC LIMIT ? OFFSET ?"
414
+ params.extend([limit, offset])
415
+
416
+ with self._get_connection() as conn:
417
+ rows = conn.execute(query, params).fetchall()
418
+ return [self._row_to_session(row) for row in rows]
419
+
420
+ def query_workflow_summary(self, limit: int = 20) -> List[Dict[str, Any]]:
421
+ """按 workflow_id 聚合查询摘要。"""
422
+ with self._get_connection() as conn:
423
+ rows = conn.execute(
424
+ """
425
+ SELECT
426
+ workflow_id,
427
+ COUNT(*) AS total_tasks,
428
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
429
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
430
+ SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
431
+ MIN(start_time) AS workflow_start,
432
+ MAX(COALESCE(end_time, start_time)) AS workflow_end,
433
+ (MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
434
+ SUM(total_input_tokens) AS total_input_tokens,
435
+ SUM(total_output_tokens) AS total_output_tokens
436
+ FROM trace_sessions
437
+ WHERE workflow_id IS NOT NULL AND workflow_id != ''
438
+ GROUP BY workflow_id
439
+ ORDER BY workflow_start DESC
440
+ LIMIT ?
441
+ """,
442
+ (limit,),
443
+ ).fetchall()
444
+ return [dict(row) for row in rows]
445
+
446
+ def get_workflow_summary(self, workflow_id: str) -> Optional[Dict[str, Any]]:
447
+ """获取单个 workflow 的聚合摘要。"""
448
+ with self._get_connection() as conn:
449
+ row = conn.execute(
450
+ """
451
+ SELECT
452
+ workflow_id,
453
+ COUNT(*) AS total_tasks,
454
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS succeeded,
455
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
456
+ SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
457
+ MIN(start_time) AS workflow_start,
458
+ MAX(COALESCE(end_time, start_time)) AS workflow_end,
459
+ (MAX(COALESCE(end_time, start_time)) - MIN(start_time)) * 1000 AS total_latency_ms,
460
+ SUM(total_input_tokens) AS total_input_tokens,
461
+ SUM(total_output_tokens) AS total_output_tokens
462
+ FROM trace_sessions
463
+ WHERE workflow_id = ?
464
+ GROUP BY workflow_id
465
+ """,
466
+ (workflow_id,),
467
+ ).fetchone()
468
+ return dict(row) if row else None
469
+
470
+ def get_sessions_by_ids(self, session_ids: List[str]) -> Dict[str, TraceSession]:
471
+ """按 session_id 批量读取 sessions。"""
472
+ if not session_ids:
473
+ return {}
474
+ placeholders = ",".join("?" for _ in session_ids)
475
+ query = f"SELECT * FROM trace_sessions WHERE session_id IN ({placeholders})"
476
+ with self._get_connection() as conn:
477
+ rows = conn.execute(query, session_ids).fetchall()
478
+ sessions = [self._row_to_session(row) for row in rows]
479
+ return {session.session_id: session for session in sessions}
480
+
481
+ def _conversation_id_of(self, session: TraceSession) -> str:
482
+ return session.conversation_session_id or session.session_id
483
+
484
+ def query_conversations(
485
+ self,
486
+ limit: int = 20,
487
+ status: Optional[str] = None,
488
+ include_hidden: bool = False,
489
+ main_loop_only: bool = False,
490
+ ) -> List[Dict[str, Any]]:
491
+ """按 conversation_session_id 聚合查询会话列表(SQL 聚合)。"""
492
+ where_clause = ""
493
+ params: List[Any] = []
494
+ if status:
495
+ where_clause = "WHERE s.status = ?"
496
+ params.append(status)
497
+
498
+ query = f"""
499
+ WITH grouped AS (
500
+ SELECT
501
+ COALESCE(s.conversation_session_id, s.session_id) AS conversation_id,
502
+ COUNT(*) AS run_count,
503
+ AVG(s.total_latency_ms) AS avg_latency_ms,
504
+ SUM(s.total_input_tokens) AS total_input_tokens,
505
+ SUM(s.total_output_tokens) AS total_output_tokens,
506
+ MAX(COALESCE(s.end_time, s.start_time)) AS latest_end_time
507
+ FROM trace_sessions s
508
+ {where_clause}
509
+ GROUP BY COALESCE(s.conversation_session_id, s.session_id)
510
+ )
511
+ SELECT
512
+ g.conversation_id AS session_id,
513
+ g.run_count,
514
+ g.avg_latency_ms,
515
+ g.total_input_tokens,
516
+ g.total_output_tokens,
517
+ g.latest_end_time,
518
+ (
519
+ SELECT s2.graph_name
520
+ FROM trace_sessions s2
521
+ WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
522
+ {f"AND s2.status = ?" if status else ""}
523
+ ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
524
+ LIMIT 1
525
+ ) AS graph_name,
526
+ (
527
+ SELECT s2.status
528
+ FROM trace_sessions s2
529
+ WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
530
+ {f"AND s2.status = ?" if status else ""}
531
+ ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
532
+ LIMIT 1
533
+ ) AS last_status
534
+ ,
535
+ (
536
+ SELECT s2.visibility
537
+ FROM trace_sessions s2
538
+ WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
539
+ {f"AND s2.status = ?" if status else ""}
540
+ ORDER BY COALESCE(s2.end_time, s2.start_time) DESC
541
+ LIMIT 1
542
+ ) AS last_visibility,
543
+ (
544
+ SELECT s2.user_query
545
+ FROM trace_sessions s2
546
+ WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
547
+ AND s2.unit_type = 'main_loop'
548
+ ORDER BY s2.start_time ASC
549
+ LIMIT 1
550
+ ) AS topic_query,
551
+ (
552
+ SELECT 1
553
+ FROM trace_sessions s2
554
+ WHERE COALESCE(s2.conversation_session_id, s2.session_id) = g.conversation_id
555
+ AND s2.unit_type = 'main_loop'
556
+ LIMIT 1
557
+ ) AS has_main_loop
558
+ FROM grouped g
559
+ ORDER BY g.latest_end_time DESC
560
+ LIMIT ?
561
+ """
562
+ query_params = list(params)
563
+ if status:
564
+ query_params.append(status)
565
+ query_params.append(status)
566
+ query_params.append(status)
567
+ query_params.append(limit)
568
+
569
+ with self._get_connection() as conn:
570
+ rows = conn.execute(query, query_params).fetchall()
571
+ items = [
572
+ {
573
+ "session_id": row["session_id"],
574
+ "graph_name": row["graph_name"],
575
+ "run_count": row["run_count"],
576
+ "latest_end_time": row["latest_end_time"],
577
+ "last_status": row["last_status"],
578
+ "last_visibility": row["last_visibility"] or "default",
579
+ "avg_latency_ms": row["avg_latency_ms"] or 0.0,
580
+ "total_input_tokens": int(row["total_input_tokens"] or 0),
581
+ "total_output_tokens": int(row["total_output_tokens"] or 0),
582
+ "topic_query": row["topic_query"] or "",
583
+ "has_main_loop": bool(row["has_main_loop"]),
584
+ }
585
+ for row in rows
586
+ ]
587
+ # 过滤顺序约定:
588
+ # 1) main_loop_only 先收敛“会话语义”(只保留含主 run 的 conversation)
589
+ # 2) include_hidden 再决定是否展示 hidden 可见性项
590
+ if main_loop_only:
591
+ items = [item for item in items if item["has_main_loop"]]
592
+ if include_hidden:
593
+ return items
594
+ return [item for item in items if item["last_visibility"] != "hidden"]
595
+
596
+ def get_runs_by_conversation(
597
+ self,
598
+ conversation_session_id: str,
599
+ limit: int = 50,
600
+ ) -> List[TraceSession]:
601
+ """获取某个会话下的 runs(每条 run_id 为 TraceSession.session_id)。"""
602
+ query = """
603
+ SELECT *
604
+ FROM trace_sessions
605
+ WHERE COALESCE(conversation_session_id, session_id) = ?
606
+ ORDER BY COALESCE(end_time, start_time) DESC
607
+ LIMIT ?
608
+ """
609
+ with self._get_connection() as conn:
610
+ rows = conn.execute(query, (conversation_session_id, limit)).fetchall()
611
+ return [self._row_to_session(row) for row in rows]
612
+
613
+ def get_span_tree(self, session_id: str) -> List[TraceSpan]:
614
+ """
615
+ 获取 session 的所有 span。
616
+
617
+ Args:
618
+ session_id: session 唯一标识
619
+
620
+ Returns:
621
+ TraceSpan 列表
622
+ """
623
+ with self._get_connection() as conn:
624
+ rows = conn.execute("""
625
+ SELECT * FROM trace_spans
626
+ WHERE session_id = ?
627
+ ORDER BY start_time
628
+ """, (session_id,)).fetchall()
629
+
630
+ from .models import SpanType, SpanStatus
631
+
632
+ return [
633
+ TraceSpan(
634
+ span_id=row["span_id"],
635
+ session_id=row["session_id"],
636
+ parent_span_id=row["parent_span_id"],
637
+ span_type=SpanType(row["span_type"]),
638
+ name=row["name"],
639
+ start_time=row["start_time"],
640
+ end_time=row["end_time"],
641
+ latency_ms=row["latency_ms"],
642
+ status=SpanStatus(row["status"]),
643
+ input_data=json.loads(row["input_json"]) if row["input_json"] else None,
644
+ output_data=json.loads(row["output_json"]) if row["output_json"] else None,
645
+ error=row["error"],
646
+ metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
647
+ )
648
+ for row in rows
649
+ ]
650
+
651
+ def get_span_trees(self, session_ids: List[str]) -> Dict[str, List[TraceSpan]]:
652
+ """
653
+ 批量获取多个 session 的 spans,减少 N 次往返查询。
654
+
655
+ Args:
656
+ session_ids: session_id 列表
657
+
658
+ Returns:
659
+ Dict[session_id, List[TraceSpan]]
660
+ """
661
+ result: Dict[str, List[TraceSpan]] = {sid: [] for sid in session_ids}
662
+ if not session_ids:
663
+ return result
664
+
665
+ placeholders = ",".join("?" for _ in session_ids)
666
+ query = (
667
+ "SELECT * FROM trace_spans "
668
+ f"WHERE session_id IN ({placeholders}) "
669
+ "ORDER BY session_id, start_time"
670
+ )
671
+ with self._get_connection() as conn:
672
+ rows = conn.execute(query, session_ids).fetchall()
673
+ from .models import SpanType, SpanStatus
674
+
675
+ for row in rows:
676
+ span = TraceSpan(
677
+ span_id=row["span_id"],
678
+ session_id=row["session_id"],
679
+ parent_span_id=row["parent_span_id"],
680
+ span_type=SpanType(row["span_type"]),
681
+ name=row["name"],
682
+ start_time=row["start_time"],
683
+ end_time=row["end_time"],
684
+ latency_ms=row["latency_ms"],
685
+ status=SpanStatus(row["status"]),
686
+ input_data=json.loads(row["input_json"]) if row["input_json"] else None,
687
+ output_data=json.loads(row["output_json"]) if row["output_json"] else None,
688
+ error=row["error"],
689
+ metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
690
+ )
691
+ if span.session_id not in result:
692
+ result[span.session_id] = []
693
+ result[span.session_id].append(span)
694
+ return result
695
+
696
+ def cleanup_old_sessions(self, days: int = 7) -> int:
697
+ """
698
+ 清理过期的 session。
699
+
700
+ Args:
701
+ days: 保留天数
702
+
703
+ Returns:
704
+ 删除的 session 数量
705
+ """
706
+ import time
707
+ cutoff_time = time.time() - (days * 24 * 3600)
708
+
709
+ with self._get_connection() as conn:
710
+ # 删除 spans
711
+ conn.execute("""
712
+ DELETE FROM trace_spans
713
+ WHERE session_id IN (
714
+ SELECT session_id FROM trace_sessions
715
+ WHERE start_time < ?
716
+ )
717
+ """, (cutoff_time,))
718
+
719
+ # 删除 sessions
720
+ cursor = conn.execute("""
721
+ DELETE FROM trace_sessions WHERE start_time < ?
722
+ """, (cutoff_time,))
723
+
724
+ deleted_count = cursor.rowcount
725
+ conn.commit()
726
+
727
+ return deleted_count
728
+
729
+ def cleanup_sessions_by_retention(
730
+ self,
731
+ *,
732
+ failed_days: int = 180,
733
+ normal_days: int = 30,
734
+ ) -> int:
735
+ """
736
+ 按状态分层清理过期 session。
737
+
738
+ 保留策略:
739
+ - failed/interrupted:failed_days
740
+ - 其他状态(completed/running 等):normal_days
741
+ """
742
+ import time
743
+
744
+ now = time.time()
745
+ failed_cutoff = now - (int(failed_days) * 24 * 3600)
746
+ normal_cutoff = now - (int(normal_days) * 24 * 3600)
747
+ with self._get_connection() as conn:
748
+ # 先删 span(级联在 sqlite 默认未开启,显式清理)
749
+ conn.execute(
750
+ """
751
+ DELETE FROM trace_spans
752
+ WHERE session_id IN (
753
+ SELECT session_id
754
+ FROM trace_sessions
755
+ WHERE (status IN ('failed', 'interrupted') AND start_time < ?)
756
+ OR (status NOT IN ('failed', 'interrupted') AND start_time < ?)
757
+ )
758
+ """,
759
+ (failed_cutoff, normal_cutoff),
760
+ )
761
+ cursor = conn.execute(
762
+ """
763
+ DELETE FROM trace_sessions
764
+ WHERE (status IN ('failed', 'interrupted') AND start_time < ?)
765
+ OR (status NOT IN ('failed', 'interrupted') AND start_time < ?)
766
+ """,
767
+ (failed_cutoff, normal_cutoff),
768
+ )
769
+ deleted_count = int(cursor.rowcount or 0)
770
+ conn.commit()
771
+ return deleted_count
772
+
773
+ def mark_sessions_interrupted(
774
+ self,
775
+ *,
776
+ status_filter: str = "running",
777
+ start_before: float,
778
+ new_status: str = "interrupted",
779
+ terminal_reason: str = "process_crash_or_timeout",
780
+ ) -> int:
781
+ """将超时 running 会话标记为 interrupted。"""
782
+ with self._get_connection() as conn:
783
+ cursor = conn.execute(
784
+ """
785
+ UPDATE trace_sessions
786
+ SET status = ?, terminal_reason = ?, end_time = ?
787
+ WHERE status = ? AND start_time < ?
788
+ """,
789
+ (new_status, terminal_reason, time.time(), status_filter, start_before),
790
+ )
791
+ conn.commit()
792
+ return cursor.rowcount
793
+
794
+ def recover_orphan_sessions(self, timeout_seconds: int = 3600) -> int:
795
+ """恢复孤儿会话(服务启动时调用)。"""
796
+ cutoff = time.time() - timeout_seconds
797
+ return self.mark_sessions_interrupted(
798
+ status_filter="running",
799
+ start_before=cutoff,
800
+ new_status="interrupted",
801
+ terminal_reason="process_crash_or_timeout",
802
+ )
803
+
804
+ def get_stats(self) -> Dict[str, Any]:
805
+ """返回全局聚合统计,供 API/CLI 使用。"""
806
+ with self._get_connection() as conn:
807
+ row = conn.execute(
808
+ """
809
+ SELECT
810
+ COUNT(*) AS total_runs,
811
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
812
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
813
+ SUM(CASE WHEN status = 'interrupted' THEN 1 ELSE 0 END) AS interrupted,
814
+ AVG(total_latency_ms) AS avg_latency_ms,
815
+ AVG(loop_count) AS avg_steps,
816
+ AVG(total_input_tokens) AS avg_tokens_in,
817
+ AVG(total_output_tokens) AS avg_tokens_out
818
+ FROM trace_sessions
819
+ """
820
+ ).fetchone()
821
+ tool_row = conn.execute(
822
+ "SELECT AVG(tool_count) AS avg_tool_calls FROM v_session_summary"
823
+ ).fetchone()
824
+ llm_row = conn.execute(
825
+ """
826
+ SELECT AVG(cnt) AS avg_llm_calls FROM (
827
+ SELECT session_id, COUNT(*) AS cnt
828
+ FROM trace_spans
829
+ WHERE span_type = 'llm_call'
830
+ GROUP BY session_id
831
+ )
832
+ """
833
+ ).fetchone()
834
+ deg_row = conn.execute(
835
+ """
836
+ SELECT
837
+ COUNT(*) AS total_llm,
838
+ SUM(CASE WHEN metadata_json LIKE '%"detected": true%' THEN 1 ELSE 0 END) AS degraded
839
+ FROM trace_spans
840
+ WHERE span_type = 'llm_call'
841
+ """
842
+ ).fetchone()
843
+ total_runs = int((row["total_runs"] if row else 0) or 0)
844
+ total_llm = int((deg_row["total_llm"] if deg_row else 0) or 0)
845
+ degraded = int((deg_row["degraded"] if deg_row else 0) or 0)
846
+ return {
847
+ "total_sessions": total_runs,
848
+ "total_runs": total_runs,
849
+ "completed": int((row["completed"] if row else 0) or 0),
850
+ "failed": int((row["failed"] if row else 0) or 0),
851
+ "interrupted": int((row["interrupted"] if row else 0) or 0),
852
+ "avg_latency_ms": round(float((row["avg_latency_ms"] if row else 0.0) or 0.0), 1),
853
+ "avg_steps": round(float((row["avg_steps"] if row else 0.0) or 0.0), 1),
854
+ "avg_tokens_in": round(float((row["avg_tokens_in"] if row else 0.0) or 0.0), 0),
855
+ "avg_tokens_out": round(float((row["avg_tokens_out"] if row else 0.0) or 0.0), 0),
856
+ "avg_tool_calls": round(float((tool_row["avg_tool_calls"] if tool_row else 0.0) or 0.0), 1),
857
+ "avg_llm_calls": round(
858
+ float(0.0 if llm_row is None or llm_row["avg_llm_calls"] is None else llm_row["avg_llm_calls"]),
859
+ 1,
860
+ ),
861
+ "degradation_rate_percent": round((degraded / total_llm) * 100, 1) if total_llm else 0.0,
862
+ }
863
+
864
+ @classmethod
865
+ def from_env(cls) -> "SqliteTraceStore":
866
+ """创建 SqliteTraceStore。
867
+
868
+ 若设置 ``AGENTX_TRACE_DB_PATH``,使用该路径;否则 ``db_path`` 为 ``None``,
869
+ 与无参 ``SqliteTraceStore()`` 相同,经 ``resolve_agent_workspace_config()`` 使用
870
+ ``traces_db_path``(见 ``docs/design-docs/trace/OB-D09-trace-workspace-bootstrap.md`` §3)。
871
+ """
872
+ db_path = os.getenv("AGENTX_TRACE_DB_PATH")
873
+ return cls(db_path=db_path)