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,227 @@
1
+ """
2
+ LangChain AgentX agent 的模型绑定与执行工具。
3
+
4
+ 本模块从 `factory.py` 中拆分出"模型调用路径"的通用逻辑:
5
+ - 根据 response_format / 模型能力 / tools 决定如何 bind 模型(含 ProviderStrategy / ToolStrategy)
6
+ - 统一处理模型输出(messages + optional structured_response)
7
+ - 提供同步 / 异步模型执行的辅助函数,供 model 节点使用
8
+
9
+ 注意:这里不包含图路由(edges)或 OpenCode 状态字段更新,只关注"模型 I/O 层"。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, Tuple, List
15
+
16
+ from langchain.agents.structured_output import (
17
+ AutoStrategy,
18
+ ProviderStrategy,
19
+ ProviderStrategyBinding,
20
+ ResponseFormat,
21
+ StructuredOutputError,
22
+ StructuredOutputValidationError,
23
+ ToolStrategy,
24
+ OutputToolBinding,
25
+ MultipleStructuredOutputsError,
26
+ )
27
+ from langchain_core.messages import AIMessage, ToolMessage
28
+ from langchain_core.tools import BaseTool
29
+
30
+ from .schema_and_format import _supports_provider_strategy, _handle_structured_output_error
31
+
32
+
33
+ def handle_model_output(
34
+ output: AIMessage,
35
+ effective_response_format: ResponseFormat[Any] | None,
36
+ structured_output_tools: Dict[str, OutputToolBinding[Any]],
37
+ ) -> Dict[str, Any]:
38
+ """处理模型输出(含结构化输出),返回 {'messages': [...], 'structured_response': ...}."""
39
+ if isinstance(effective_response_format, ProviderStrategy):
40
+ if not output.tool_calls:
41
+ provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(
42
+ effective_response_format.schema_spec
43
+ )
44
+ try:
45
+ structured_response = provider_strategy_binding.parse(output)
46
+ except Exception as exc:
47
+ schema_name = getattr(
48
+ effective_response_format.schema_spec.schema, "__name__", "response_format"
49
+ )
50
+ validation_error = StructuredOutputValidationError(schema_name, exc, output)
51
+ raise validation_error from exc
52
+ else:
53
+ return {"messages": [output], "structured_response": structured_response}
54
+ return {"messages": [output]}
55
+
56
+ if (
57
+ isinstance(effective_response_format, ToolStrategy)
58
+ and isinstance(output, AIMessage)
59
+ and output.tool_calls
60
+ ):
61
+ structured_tool_calls = [
62
+ tc for tc in output.tool_calls if tc["name"] in structured_output_tools
63
+ ]
64
+
65
+ if structured_tool_calls:
66
+ exception: StructuredOutputError | None = None
67
+ if len(structured_tool_calls) > 1:
68
+ tool_names = [tc["name"] for tc in structured_tool_calls]
69
+ exception = MultipleStructuredOutputsError(tool_names, output)
70
+ should_retry, error_message = _handle_structured_output_error(
71
+ exception, effective_response_format
72
+ )
73
+ if not should_retry:
74
+ raise exception
75
+
76
+ tool_messages = [
77
+ ToolMessage(
78
+ content=error_message,
79
+ tool_call_id=tc["id"],
80
+ name=tc["name"],
81
+ )
82
+ for tc in structured_tool_calls
83
+ ]
84
+ return {"messages": [output, *tool_messages]}
85
+
86
+ tool_call = structured_tool_calls[0]
87
+ try:
88
+ structured_tool_binding = structured_output_tools[tool_call["name"]]
89
+ structured_response = structured_tool_binding.parse(tool_call["args"])
90
+
91
+ tool_message_content = (
92
+ effective_response_format.tool_message_content
93
+ or f"Returning structured response: {structured_response}"
94
+ )
95
+
96
+ return {
97
+ "messages": [
98
+ output,
99
+ ToolMessage(
100
+ content=tool_message_content,
101
+ tool_call_id=tool_call["id"],
102
+ name=tool_call["name"],
103
+ ),
104
+ ],
105
+ "structured_response": structured_response,
106
+ }
107
+ except Exception as exc:
108
+ exception = StructuredOutputValidationError(tool_call["name"], exc, output)
109
+ should_retry, error_message = _handle_structured_output_error(
110
+ exception, effective_response_format
111
+ )
112
+ if not should_retry:
113
+ raise exception from exc
114
+
115
+ return {
116
+ "messages": [
117
+ output,
118
+ ToolMessage(
119
+ content=error_message,
120
+ tool_call_id=tool_call["id"],
121
+ name=tool_call["name"],
122
+ ),
123
+ ],
124
+ }
125
+
126
+ return {"messages": [output]}
127
+
128
+
129
+ def get_bound_model(
130
+ *,
131
+ request,
132
+ structured_output_tools: Dict[str, OutputToolBinding[Any]],
133
+ initial_response_format: ResponseFormat[Any] | None,
134
+ tool_strategy_for_setup,
135
+ tool_node,
136
+ wrap_tool_call_wrapper,
137
+ awrap_tool_call_wrapper,
138
+ dynamic_tool_error_template: str,
139
+ ) -> Tuple[Any, ResponseFormat[Any] | None]:
140
+ """根据 request / response_format / tools 返回 (bound_model, effective_response_format)."""
141
+ has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper
142
+
143
+ available_tools_by_name: Dict[str, Any] = {}
144
+ if tool_node:
145
+ available_tools_by_name = tool_node.tools_by_name.copy()
146
+
147
+ if not has_wrap_tool_call:
148
+ unknown_tool_names: List[str] = []
149
+ for t in request.tools:
150
+ if isinstance(t, dict):
151
+ continue
152
+ if isinstance(t, BaseTool) and t.name not in available_tools_by_name:
153
+ unknown_tool_names.append(t.name)
154
+
155
+ if unknown_tool_names:
156
+ available_tool_names = sorted(available_tools_by_name.keys())
157
+ msg = dynamic_tool_error_template.format(
158
+ unknown_tool_names=unknown_tool_names,
159
+ available_tool_names=available_tool_names,
160
+ )
161
+ raise ValueError(msg)
162
+
163
+ response_format: ResponseFormat[Any] | Any | None = request.response_format
164
+ if response_format is not None and not isinstance(
165
+ response_format, (AutoStrategy, ToolStrategy, ProviderStrategy)
166
+ ):
167
+ response_format = AutoStrategy(schema=response_format)
168
+
169
+ if isinstance(response_format, AutoStrategy):
170
+ if _supports_provider_strategy(request.model, tools=request.tools):
171
+ effective_response_format: ResponseFormat[Any] | None = ProviderStrategy(
172
+ schema=response_format.schema
173
+ )
174
+ elif response_format is initial_response_format and tool_strategy_for_setup is not None:
175
+ effective_response_format = tool_strategy_for_setup
176
+ else:
177
+ effective_response_format = ToolStrategy(schema=response_format.schema)
178
+ else:
179
+ effective_response_format = response_format
180
+
181
+ final_tools: List[Any] = list(request.tools)
182
+ if isinstance(effective_response_format, ToolStrategy):
183
+ structured_tools = [info.tool for info in structured_output_tools.values()]
184
+ final_tools.extend(structured_tools)
185
+
186
+ if isinstance(effective_response_format, ProviderStrategy):
187
+ kwargs = effective_response_format.to_model_kwargs()
188
+ return (
189
+ request.model.bind_tools(
190
+ final_tools, strict=True, **kwargs, **request.model_settings
191
+ ),
192
+ effective_response_format,
193
+ )
194
+
195
+ if isinstance(effective_response_format, ToolStrategy):
196
+ for tc in effective_response_format.schema_specs:
197
+ if tc.name not in structured_output_tools:
198
+ msg = (
199
+ f"ToolStrategy specifies tool '{tc.name}' "
200
+ "which wasn't declared in the original "
201
+ "response format when creating the agent."
202
+ )
203
+ raise ValueError(msg)
204
+
205
+ tool_choice = "any" if structured_output_tools else request.tool_choice
206
+ return (
207
+ request.model.bind_tools(
208
+ final_tools, tool_choice=tool_choice, **request.model_settings
209
+ ),
210
+ effective_response_format,
211
+ )
212
+
213
+ if final_tools:
214
+ return (
215
+ request.model.bind_tools(
216
+ final_tools, tool_choice=request.tool_choice, **request.model_settings
217
+ ),
218
+ None,
219
+ )
220
+ return request.model.bind(**request.model_settings), None
221
+
222
+
223
+ __all__ = [
224
+ "handle_model_output",
225
+ "get_bound_model",
226
+ ]
227
+
@@ -0,0 +1,443 @@
1
+ """
2
+ 工具调用退化纠偏器(Tool Call Degradation Corrector)
3
+ ====================================================
4
+
5
+ 目的:
6
+ - 解决模型/网关在特定条件下,把结构化 tool_calls 退化为 `<tool_call>...</tool_call>` 文本的问题。
7
+ - 将退化文本解析为 LangChain 可执行的结构化 `tool_calls`,从而让 Agent 继续运行而不中断。
8
+
9
+ 设计约束:
10
+ - 仅作为内部能力在 LangChain-AgentX Agent 中使用,对外不导出公共接口。
11
+ - 必须做工具白名单校验:只允许调用已注册/已注入的工具。
12
+
13
+ 说明:
14
+ - 该纠偏器以"包装器"的方式工作:对外暴露 invoke/ainvoke,并把其他属性/方法透传给底层 llm。
15
+ - stream/astream 不经过纠偏(流式分块无法整体解析),如需纠偏请使用 invoke/ainvoke。
16
+ - DeepAgent/中间件一般以 Runnable 方式使用 model,因此 duck typing 足够。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import re
24
+ import uuid
25
+ from typing import Any, Dict, List, Optional, Sequence, Set
26
+
27
+ from langchain.agents.structured_output import OutputToolBinding
28
+ from langgraph.prebuilt.tool_node import ToolNode
29
+ from langchain_core.messages import AIMessage
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ # <tool_call>...</tool_call> 标签格式
35
+ _TOOL_CALL_TAG_RE = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
36
+
37
+ # 行内 CALL_TOOL / TOOL_CALL 前缀格式(仅用于定位起始位置,JSON 提取由 raw_decode 完成)
38
+ _CALL_TOOL_PREFIX_RE = re.compile(
39
+ r"(?:CALL_TOOL|TOOL_CALL)\s*:\s*",
40
+ re.DOTALL,
41
+ )
42
+
43
+ # 用于清理 content 中残留的 CALL_TOOL: {...} 文本
44
+ # 注意:清理时使用贪婪匹配到行尾,实际 JSON 边界已由 raw_decode 确定
45
+ _CALL_TOOL_CLEANUP_RE = re.compile(
46
+ r"(?:CALL_TOOL|TOOL_CALL)\s*:\s*\{[^}]*(?:\{[^}]*\}[^}]*)?\}",
47
+ re.DOTALL,
48
+ )
49
+
50
+ # XML 自闭合格式:<tool_name param1="val1" param2="val2" />
51
+ # 捕获组 1 = 工具名,捕获组 2 = 属性字符串
52
+ _XML_SELF_CLOSING_RE = re.compile(
53
+ r"<([A-Za-z_][A-Za-z0-9_]*)(\s[^>]*)?\s*/>",
54
+ re.DOTALL,
55
+ )
56
+
57
+ # XML 子元素格式:<tool_name><param>val</param>...</tool_name>
58
+ # 捕获组 1 = 工具名,捕获组 2 = 内部内容
59
+ _XML_ELEMENT_RE = re.compile(
60
+ r"<([A-Za-z_][A-Za-z0-9_]*)>(.*?)</\1>",
61
+ re.DOTALL,
62
+ )
63
+
64
+ # XML 属性解析:key="value" 或 key='value'
65
+ _XML_ATTR_RE = re.compile(r"""([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')""")
66
+
67
+ # XML 子元素解析:<key>value</key>
68
+ _XML_CHILD_RE = re.compile(r"<([A-Za-z_][A-Za-z0-9_]*)>(.*?)</\1>", re.DOTALL)
69
+
70
+ # <tool_code>...</tool_code> Python 代码块格式
71
+ _TOOL_CODE_RE = re.compile(r"<tool_code>(.*?)</tool_code>", re.DOTALL)
72
+
73
+ # Python 函数名(后跟 '('),用于定位调用起始位置;参数由平衡括号扫描提取
74
+ _PY_FUNC_NAME_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(")
75
+
76
+
77
+ def _extract_json_objects_from_text(text: str) -> list[str]:
78
+ """
79
+ 从任意文本中提取所有顶层 JSON 对象字符串。
80
+
81
+ 使用 json.JSONDecoder.raw_decode 逐字符扫描,正确处理嵌套大括号,
82
+ 避免正则截断嵌套 JSON 的问题(Bug 1 修复)。
83
+ """
84
+ decoder = json.JSONDecoder()
85
+ results: list[str] = []
86
+ i = 0
87
+ while i < len(text):
88
+ if text[i] != "{":
89
+ i += 1
90
+ continue
91
+ try:
92
+ obj, end = decoder.raw_decode(text, i)
93
+ if isinstance(obj, dict):
94
+ results.append(text[i:end])
95
+ i = end
96
+ except json.JSONDecodeError:
97
+ i += 1
98
+ return results
99
+
100
+
101
+ class ToolCallDegradationCorrector:
102
+ """
103
+ 包装一个 ChatModel,当检测到 `<tool_call>...</tool_call>` 退化输出时,进行纠偏。
104
+
105
+ 纠偏触发条件(严格):
106
+ - response.content 包含 `<tool_call>` 标签 或 `CALL_TOOL:` / `TOOL_CALL:` 前缀
107
+ - 且 response.tool_calls 为空 / 不存在
108
+
109
+ 不支持流式纠偏(stream/astream 直接透传)。
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ llm: Any,
115
+ allowed_tool_names: Set[str],
116
+ ) -> None:
117
+ self._llm = llm
118
+ self._allowed_tool_names = allowed_tool_names
119
+
120
+ # ----------------------------
121
+ # 属性透传(例如 llm.streaming)
122
+ # ----------------------------
123
+ @property
124
+ def streaming(self) -> Any:
125
+ return getattr(self._llm, "streaming", None)
126
+
127
+ @streaming.setter
128
+ def streaming(self, value: Any) -> None:
129
+ setattr(self._llm, "streaming", value)
130
+
131
+ def __getattr__(self, name: str) -> Any:
132
+ # 兜底透传:DeepAgent/中间件可能访问底层 llm 的其他方法/属性(如 bind_tools 等)
133
+ return getattr(self._llm, name)
134
+
135
+ def bind_tools(
136
+ self,
137
+ tools: Sequence[Any],
138
+ *,
139
+ tool_choice: str | None = None,
140
+ **kwargs: Any,
141
+ ) -> "ToolCallDegradationCorrector":
142
+ """
143
+ DeepAgent/LangChain 会在运行时调用 model.bind_tools(...) 生成"绑定工具后的模型"。
144
+
145
+ 如果这里直接透传到底层 llm,会导致 wrapper 丢失(返回的是底层 llm),从而纠偏逻辑失效。
146
+ 因此必须:
147
+ - 先对底层 llm 执行 bind_tools
148
+ - 再把返回的 bound model 重新包装为 ToolCallDegradationCorrector
149
+ - 同步更新 allowed_tool_names,避免新绑定工具被白名单拦截(Bug 5 修复)
150
+ """
151
+ bound = self._llm.bind_tools(tools, tool_choice=tool_choice, **kwargs)
152
+
153
+ # 从新绑定的 tools 中提取工具名,合并到现有白名单(Bug 5 修复)
154
+ new_names: Set[str] = set()
155
+ for t in tools:
156
+ if hasattr(t, "name") and isinstance(t.name, str):
157
+ new_names.add(t.name)
158
+ elif isinstance(t, dict) and isinstance(t.get("name"), str):
159
+ new_names.add(t["name"])
160
+
161
+ return ToolCallDegradationCorrector(
162
+ llm=bound,
163
+ allowed_tool_names=self._allowed_tool_names | new_names,
164
+ )
165
+
166
+ # ----------------------------
167
+ # Runnable 接口:invoke / ainvoke
168
+ # ----------------------------
169
+ def invoke(self, input: Any, **kwargs: Any) -> Any:
170
+ resp = self._llm.invoke(input, **kwargs)
171
+ return self._correct_if_needed(resp)
172
+
173
+ async def ainvoke(self, input: Any, **kwargs: Any) -> Any:
174
+ resp = await self._llm.ainvoke(input, **kwargs)
175
+ return self._correct_if_needed(resp)
176
+
177
+ # stream / astream 不经过纠偏(流式分块无法整体解析),直接透传给底层 llm。
178
+ # 如需纠偏,请改用 invoke / ainvoke。
179
+
180
+ # ----------------------------
181
+ # 核心纠偏逻辑
182
+ # ----------------------------
183
+ def _correct_if_needed(self, resp: Any) -> Any:
184
+ if not isinstance(resp, AIMessage):
185
+ return resp
186
+
187
+ tool_calls: Optional[Any] = getattr(resp, "tool_calls", None)
188
+ if tool_calls:
189
+ # 正常结构化 tool_calls,不需要纠偏
190
+ return resp
191
+
192
+ content = resp.content
193
+ if not isinstance(content, str):
194
+ return resp
195
+
196
+ # 支持多种退化格式,从文本中尽力解析 tool_calls
197
+ parsed_tool_calls = self._parse_text_tool_calls(content)
198
+ if not parsed_tool_calls:
199
+ return resp
200
+
201
+ # 清理所有退化格式残留文本
202
+ cleaned_content = _TOOL_CALL_TAG_RE.sub("", content)
203
+ cleaned_content = _CALL_TOOL_CLEANUP_RE.sub("", cleaned_content)
204
+ cleaned_content = _TOOL_CODE_RE.sub("", cleaned_content)
205
+ # XML 自闭合:只清理命中白名单的标签
206
+ def _remove_xml_self_closing(m: re.Match) -> str:
207
+ return "" if m.group(1) in self._allowed_tool_names else m.group(0)
208
+ cleaned_content = _XML_SELF_CLOSING_RE.sub(_remove_xml_self_closing, cleaned_content)
209
+ # XML 子元素:只清理命中白名单的标签
210
+ def _remove_xml_element(m: re.Match) -> str:
211
+ return "" if m.group(1) in self._allowed_tool_names else m.group(0)
212
+ cleaned_content = _XML_ELEMENT_RE.sub(_remove_xml_element, cleaned_content)
213
+ cleaned_content = cleaned_content.strip()
214
+
215
+ logger.warning(
216
+ "检测到工具调用退化,已纠偏为结构化 tool_calls | tools=%s",
217
+ [tc.get("name") for tc in parsed_tool_calls],
218
+ )
219
+
220
+ return self._clone_ai_message_with_tool_calls(
221
+ original=resp,
222
+ content=cleaned_content,
223
+ tool_calls=parsed_tool_calls,
224
+ )
225
+
226
+ def _parse_text_tool_calls(self, content: str) -> List[Dict[str, Any]]:
227
+ """
228
+ 从 content 中解析一个或多个退化格式的工具调用。
229
+
230
+ 支持格式:
231
+ - <tool_call>{"name": "...", "args": {...}}</tool_call>
232
+ - CALL_TOOL: {"name": "...", "args": {...}} / TOOL_CALL: {...}
233
+ - <tool_name param="val" /> — XML 自闭合
234
+ - <tool_name><param>val</param></tool_name> — XML 子元素
235
+ - <tool_code>print(tool_name(...))</tool_code> — Python 代码块
236
+
237
+ 支持三类 JSON 结构:
238
+ - {"name": "...", "args": {...}} — LangChain 标准结构
239
+ - {"name": "...", "arguments": {...}} — OpenAI 原生格式(arguments 为 dict 或 JSON 字符串)
240
+ - {"name": "...", "param1": "..."} — 扁平化参数
241
+ """
242
+ # 收集 (tool_name, tool_args) 对,最后统一去重
243
+ candidates: List[tuple[str, Dict[str, Any]]] = []
244
+
245
+ # 1) <tool_call>...</tool_call> 包裹的 JSON
246
+ for tag_content in _TOOL_CALL_TAG_RE.findall(content):
247
+ for raw in _extract_json_objects_from_text(tag_content.strip()):
248
+ parsed = self._parse_json_payload(raw)
249
+ if parsed:
250
+ candidates.append(parsed)
251
+
252
+ # 2) CALL_TOOL: / TOOL_CALL: 前缀后的 JSON(Bug 1 修复:用 raw_decode 提取)
253
+ for m in _CALL_TOOL_PREFIX_RE.finditer(content):
254
+ after_prefix = content[m.end():]
255
+ objs = _extract_json_objects_from_text(after_prefix)
256
+ if objs:
257
+ parsed = self._parse_json_payload(objs[0])
258
+ if parsed:
259
+ candidates.append(parsed)
260
+
261
+ # 3) XML 自闭合格式:<tool_name attr="val" />
262
+ for m in _XML_SELF_CLOSING_RE.finditer(content):
263
+ tool_name = m.group(1)
264
+ if tool_name not in self._allowed_tool_names:
265
+ continue
266
+ attrs_str = m.group(2) or ""
267
+ tool_args = {
268
+ k: v1 if v1 is not None else v2
269
+ for k, v1, v2 in _XML_ATTR_RE.findall(attrs_str)
270
+ }
271
+ candidates.append((tool_name, tool_args))
272
+
273
+ # 4) XML 子元素格式:<tool_name><param>val</param></tool_name>
274
+ # 跳过已被 <tool_call> 处理的标签,以及 tool_call / tool_code 本身
275
+ _skip_tags = {"tool_call", "tool_code"}
276
+ for m in _XML_ELEMENT_RE.finditer(content):
277
+ tool_name = m.group(1)
278
+ if tool_name in _skip_tags:
279
+ continue
280
+ if tool_name not in self._allowed_tool_names:
281
+ continue
282
+ inner = m.group(2)
283
+ tool_args = {k: v.strip() for k, v in _XML_CHILD_RE.findall(inner)}
284
+ candidates.append((tool_name, tool_args))
285
+
286
+ # 5) <tool_code>...</tool_code> Python 代码块
287
+ for code_block in _TOOL_CODE_RE.findall(content):
288
+ for m in _PY_FUNC_NAME_RE.finditer(code_block):
289
+ tool_name = m.group(1)
290
+ if tool_name not in self._allowed_tool_names:
291
+ continue
292
+ # 平衡括号扫描,提取完整参数字符串(支持嵌套调用)
293
+ start = m.end() # 指向 '(' 之后
294
+ depth = 1
295
+ i = start
296
+ while i < len(code_block) and depth > 0:
297
+ if code_block[i] == "(":
298
+ depth += 1
299
+ elif code_block[i] == ")":
300
+ depth -= 1
301
+ i += 1
302
+ args_str = code_block[start:i - 1].strip()
303
+ tool_args = self._parse_python_kwargs(args_str)
304
+ candidates.append((tool_name, tool_args))
305
+
306
+ if not candidates:
307
+ return []
308
+
309
+ # Bug 6 修复:去重(以 tool_name + sorted args 为 key)
310
+ seen: Set[str] = set()
311
+ tool_calls: List[Dict[str, Any]] = []
312
+ for tool_name, tool_args in candidates:
313
+ if tool_name not in self._allowed_tool_names:
314
+ logger.warning(
315
+ "退化工具调用包含未注册工具,已忽略 | tool=%s | allowed=%s",
316
+ tool_name,
317
+ sorted(self._allowed_tool_names),
318
+ )
319
+ continue
320
+ dedup_key = f"{tool_name}:{json.dumps(tool_args, sort_keys=True)}"
321
+ if dedup_key in seen:
322
+ continue
323
+ seen.add(dedup_key)
324
+ tool_calls.append(
325
+ {
326
+ "name": tool_name,
327
+ "args": tool_args,
328
+ "id": f"degraded-corrected-{uuid.uuid4().hex[:12]}",
329
+ "type": "tool_call",
330
+ }
331
+ )
332
+
333
+ return tool_calls
334
+
335
+ def _parse_json_payload(self, raw: str) -> tuple[str, Dict[str, Any]] | None:
336
+ """解析单个 JSON 字符串为 (tool_name, tool_args),失败返回 None。"""
337
+ raw = raw.strip()
338
+ if not raw:
339
+ return None
340
+ try:
341
+ payload = json.loads(raw)
342
+ except Exception as e: # noqa: BLE001
343
+ logger.warning("解析退化工具调用 JSON 失败:%s | raw=%r", e, raw[:300])
344
+ return None
345
+ if not isinstance(payload, dict):
346
+ return None
347
+ tool_name = payload.get("name")
348
+ if not tool_name or not isinstance(tool_name, str):
349
+ return None
350
+ if "args" in payload and isinstance(payload.get("args"), dict):
351
+ tool_args = payload["args"]
352
+ elif "arguments" in payload:
353
+ raw_args = payload["arguments"]
354
+ if isinstance(raw_args, str):
355
+ try:
356
+ tool_args = json.loads(raw_args)
357
+ except json.JSONDecodeError:
358
+ logger.warning("arguments 字段不是合法 JSON:%r", raw_args[:200])
359
+ return None
360
+ elif isinstance(raw_args, dict):
361
+ tool_args = raw_args
362
+ else:
363
+ tool_args = {}
364
+ else:
365
+ tool_args = {k: v for k, v in payload.items() if k != "name"}
366
+ return tool_name, tool_args
367
+
368
+ @staticmethod
369
+ def _parse_python_kwargs(args_str: str) -> Dict[str, Any]:
370
+ """从 Python 函数调用参数字符串中提取 keyword arguments。"""
371
+ result: Dict[str, Any] = {}
372
+ # 匹配 key="val" 或 key='val' 或 key=bare_word
373
+ for m in re.finditer(
374
+ r"""([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))""",
375
+ args_str,
376
+ ):
377
+ key = m.group(1)
378
+ val = m.group(2) if m.group(2) is not None else (
379
+ m.group(3) if m.group(3) is not None else m.group(4)
380
+ )
381
+ result[key] = val
382
+ return result
383
+
384
+ def _clone_ai_message_with_tool_calls(
385
+ self,
386
+ *,
387
+ original: AIMessage,
388
+ content: str,
389
+ tool_calls: List[Dict[str, Any]],
390
+ ) -> AIMessage:
391
+ """
392
+ 生成一个新的 AIMessage,保留原消息的元信息,替换 content/tool_calls。
393
+ """
394
+ # AIMessage 字段随 langchain_core 版本可能变化,这里尽量"保守拷贝"
395
+ # 只拷贝常见字段,避免传入未知字段导致构造失败。
396
+ kwargs: Dict[str, Any] = {}
397
+
398
+ for key in ("additional_kwargs", "response_metadata", "usage_metadata", "id", "name"):
399
+ if hasattr(original, key):
400
+ kwargs[key] = getattr(original, key)
401
+
402
+ # 如果发生退化并且我们已经成功恢复了结构化 tool_calls,
403
+ # 需要同步修正 finish_reason,否则上层 LangChain-AgentX Agent 会因为
404
+ # finish_reason="stop" 误判为结束而不去执行工具。
405
+ #
406
+ # Bug 3 修复:浅拷贝 response_metadata,避免原地修改污染 original 的元数据。
407
+ rm = dict(kwargs.get("response_metadata") or {})
408
+ rm["finish_reason"] = "tool-calls"
409
+ kwargs["response_metadata"] = rm
410
+
411
+ # invalid_tool_calls 也一起保留(如果存在)
412
+ if hasattr(original, "invalid_tool_calls"):
413
+ kwargs["invalid_tool_calls"] = getattr(original, "invalid_tool_calls")
414
+
415
+ return AIMessage(
416
+ content=content,
417
+ tool_calls=tool_calls,
418
+ **kwargs,
419
+ )
420
+
421
+
422
+ def _build_allowed_tool_names_for_degradation_correction(
423
+ tool_node: ToolNode | None,
424
+ built_in_tools: list[dict[str, Any] | Any],
425
+ structured_output_tools: dict[str, OutputToolBinding[Any]],
426
+ ) -> Set[str]:
427
+ """Build tool name whitelist for ToolCallDegradationCorrector."""
428
+ allowed_tool_names: Set[str] = set()
429
+
430
+ # 1) Tools executed via ToolNode (client-side BaseTool / callables)
431
+ if tool_node:
432
+ allowed_tool_names.update(tool_node.tools_by_name.keys())
433
+
434
+ # 2) Provider-side tools (dict format) passed through to the LLM
435
+ for t in built_in_tools:
436
+ name_attr = t.get("name") if isinstance(t, dict) else None
437
+ if isinstance(name_attr, str):
438
+ allowed_tool_names.add(name_attr)
439
+
440
+ # 3) Structured output tools derived from response_format / AutoStrategy
441
+ allowed_tool_names.update(structured_output_tools.keys())
442
+
443
+ return allowed_tool_names