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,820 @@
1
+ """
2
+ LangChain AgentX agent 的图路由与条件边构建。
3
+
4
+ 本模块从 `factory.py` 中拆分出与具体业务无关的 LangGraph 结构逻辑:
5
+ - 基于 OpenCode 状态字段(例如 `should_end`、`structured_response`)决定下一跳节点
6
+ - 构建 model ↔ tools ↔ hook 主链 之间的条件边
7
+ - 封装 middleware `jump_to` 语义到统一的 `_resolve_jump` 辅助函数
8
+
9
+ 与 LangChain 官方 agent 的差异在于:
10
+ - 这里的边逻辑假设退出由 LangChain AgentX 的状态/finish_reason 控制,
11
+ 而不是传统的"是否还有 tool_calls 或 recursion_limit"模式。
12
+
13
+ 可观测性:
14
+ - loop_controller 边函数中采集 loop.decision 事件(含 reason_code / evidence)
15
+ - 通过 event_emitter 统一发射事件,避免直接依赖 TraceCollector
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Callable, List, Tuple
21
+
22
+ from langchain.agents.middleware.types import (
23
+ AgentState,
24
+ ContextT,
25
+ JumpTo,
26
+ ResponseT,
27
+ _InputAgentState,
28
+ _OutputAgentState,
29
+ )
30
+ from langchain.agents.structured_output import OutputToolBinding
31
+ from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, ToolMessage
32
+ from langgraph._internal._runnable import RunnableCallable
33
+ from langgraph.graph.state import StateGraph
34
+ from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode
35
+ from langgraph.types import Send
36
+ from ..hook.async_hook_runner import AsyncHookRunner
37
+ from ..hook.types import HookContext, HookEvent
38
+
39
+ # 导入事件发射器(CLI Phase 1A 骨架)
40
+ from ...observability.trace.event_emitter import emit_loop_decision_event
41
+
42
+ from ..exit.exit_logic import (
43
+ ESCALATED_MAX_OUTPUT_TOKENS,
44
+ MAX_OUTPUT_TOKENS_RECOVERY_LIMIT,
45
+ MAX_STEPS_PROMPT,
46
+ TOKEN_BUDGET_NUDGE_USER_MESSAGE,
47
+ resolve_escalated_max_output_tokens,
48
+ )
49
+ from ..exit.reason_codes import (
50
+ DEFAULT_TOKEN_BUDGET_MAX_CONTINUATIONS,
51
+ TERMINAL_REASON_ABORTED_STREAMING,
52
+ TERMINAL_REASON_ABORTED_TOOLS,
53
+ TERMINAL_REASON_COMPLETED,
54
+ TERMINAL_REASON_HOOK_STOPPED,
55
+ TERMINAL_REASON_MAX_STEPS,
56
+ TERMINAL_REASON_MAX_TOKENS,
57
+ TERMINAL_REASON_STOP_HOOK_PREVENTED,
58
+ TRANSITION_API_ERROR_TERMINAL,
59
+ TRANSITION_COLLAPSE_DRAIN_RETRY,
60
+ TRANSITION_MAX_OUTPUT_TOKENS_ESCALATE,
61
+ TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY,
62
+ TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY_EXHAUSTED,
63
+ TRANSITION_REACTIVE_COMPACT_RETRY,
64
+ TRANSITION_STOP_HOOK_BLOCKING,
65
+ TRANSITION_STOP_HOOK_PREVENTED,
66
+ TRANSITION_TASK_NOTIFICATION_ARRIVED,
67
+ TRANSITION_TOKEN_BUDGET_CONTINUATION,
68
+ TRANSITION_WITHHOLD_RECOVERY_RETRY,
69
+ )
70
+
71
+ _ABORT_TERMINAL_REASONS = frozenset(
72
+ {
73
+ TERMINAL_REASON_ABORTED_STREAMING,
74
+ TERMINAL_REASON_ABORTED_TOOLS,
75
+ TERMINAL_REASON_HOOK_STOPPED,
76
+ }
77
+ )
78
+
79
+
80
+ def _execute_hook_common(
81
+ state: dict[str, Any],
82
+ *,
83
+ event: HookEvent,
84
+ tool_name: str | None = None,
85
+ tool_input: dict[str, Any] | None = None,
86
+ tool_result: Any = None,
87
+ ) -> dict[str, Any] | None:
88
+ hook_engine = state.get("_hook_engine")
89
+ if hook_engine is None:
90
+ return None
91
+ hook_runner = state.get("_hook_runner")
92
+ if not isinstance(hook_runner, AsyncHookRunner):
93
+ hook_runner = AsyncHookRunner()
94
+ ctx_kwargs: dict[str, Any] = {
95
+ "event": event,
96
+ "state": state,
97
+ "session_id": state.get("_run_id"),
98
+ }
99
+ if tool_name is not None:
100
+ ctx_kwargs["tool_name"] = tool_name
101
+ if tool_input is not None:
102
+ ctx_kwargs["tool_input"] = tool_input
103
+ if tool_result is not None:
104
+ ctx_kwargs["tool_result"] = tool_result
105
+ ctx = HookContext(**ctx_kwargs)
106
+ result = hook_runner.run(hook_engine, ctx)
107
+ return {
108
+ "prevent_continuation": bool(result.prevent_continuation),
109
+ "blocking_errors": list(result.blocking_errors) or None,
110
+ "permission_behavior": result.permission_behavior,
111
+ "hook_permission_decision_reason": result.hook_permission_decision_reason,
112
+ "updated_input": result.updated_input,
113
+ }
114
+
115
+
116
+ def _execute_hook_for_tool(
117
+ state: dict[str, Any],
118
+ *,
119
+ event: HookEvent,
120
+ tool_name: str,
121
+ tool_input: dict[str, Any] | None,
122
+ tool_result: Any = None,
123
+ ) -> dict[str, Any] | None:
124
+ return _execute_hook_common(
125
+ state,
126
+ event=event,
127
+ tool_name=tool_name,
128
+ tool_input=tool_input,
129
+ tool_result=tool_result,
130
+ )
131
+
132
+
133
+ def _execute_hook_inline(
134
+ state: dict[str, Any],
135
+ *,
136
+ event: HookEvent,
137
+ ) -> dict[str, Any] | None:
138
+ """执行非工具类 hook 事件(如 STOP_FAILURE),不改变既有路由语义。"""
139
+ return _execute_hook_common(
140
+ state,
141
+ event=event,
142
+ tool_name=None,
143
+ tool_input=None,
144
+ tool_result=None,
145
+ )
146
+
147
+
148
+ def _is_api_error_assistant_message(msg: AIMessage | None) -> bool:
149
+ """CC §5.7 优先级 4:API/鉴权类错误应跳过 stop hooks,直接终局。"""
150
+ if msg is None:
151
+ return False
152
+ meta = getattr(msg, "response_metadata", None) or {}
153
+ if not isinstance(meta, dict):
154
+ return False
155
+ if meta.get("langchain_agentx_api_error") is True:
156
+ return True
157
+ err = meta.get("error")
158
+ return isinstance(err, dict) and bool(err)
159
+
160
+
161
+ def _resolve_jump(
162
+ jump_to: JumpTo | None,
163
+ *,
164
+ model_destination: str,
165
+ end_destination: str,
166
+ ) -> str | None:
167
+ if jump_to == "model":
168
+ return model_destination
169
+ if jump_to == "end":
170
+ return end_destination
171
+ if jump_to == "tools":
172
+ return "tools"
173
+ return None
174
+
175
+
176
+ def _fetch_last_ai_and_tool_messages(
177
+ messages: List[AnyMessage],
178
+ ) -> Tuple[AIMessage | None, List[ToolMessage]]:
179
+ """Return the last AI message and any subsequent tool messages."""
180
+ for i in range(len(messages) - 1, -1, -1):
181
+ if isinstance(messages[i], AIMessage):
182
+ last_ai_message = messages[i]
183
+ tool_messages = [m for m in messages[i + 1 :] if isinstance(m, ToolMessage)]
184
+ return last_ai_message, tool_messages
185
+
186
+ return None, []
187
+
188
+
189
+ class LoopControllerEdge:
190
+ """Loop controller 边决策器。"""
191
+
192
+ def __init__(
193
+ self,
194
+ *,
195
+ model_destination: str,
196
+ tools_destination: str,
197
+ structured_output_tools: dict[str, OutputToolBinding[Any]],
198
+ end_destination: str,
199
+ max_output_tokens_cap: int | None = None,
200
+ task_reconcile_destination: str | None = None,
201
+ has_pending_task_notifications: Callable[[dict[str, Any]], bool] | None = None,
202
+ token_budget_max_continuations: int | None = None,
203
+ ) -> None:
204
+ self._model_destination = model_destination
205
+ self._tools_destination = tools_destination
206
+ self._structured_output_tools = structured_output_tools
207
+ self._end_destination = end_destination
208
+ self._max_output_tokens_cap = max_output_tokens_cap
209
+ self._task_reconcile_destination = task_reconcile_destination
210
+ self._has_pending_task_notifications = has_pending_task_notifications
211
+ self._tb_max_cap = int(
212
+ token_budget_max_continuations
213
+ if token_budget_max_continuations is not None
214
+ else DEFAULT_TOKEN_BUDGET_MAX_CONTINUATIONS
215
+ )
216
+
217
+ def __call__(self, state: dict[str, Any]) -> str | List[Send] | None:
218
+ jump_result = self._handle_jump(state)
219
+ if jump_result is not None:
220
+ return jump_result
221
+ abort_result = self._handle_abort(state)
222
+ if abort_result is not None:
223
+ return abort_result
224
+ withheld_result = self._handle_withheld_error(state)
225
+ if withheld_result is not None:
226
+ return withheld_result
227
+ if bool(state.get("should_end")):
228
+ return self._handle_terminal(state)
229
+ return self._handle_continue(state)
230
+
231
+ def _handle_jump(self, state: dict[str, Any]) -> str | None:
232
+ if jump_to := state.get("jump_to"):
233
+ resolved = _resolve_jump(
234
+ jump_to,
235
+ model_destination=self._model_destination,
236
+ end_destination=self._end_destination,
237
+ )
238
+ emit_loop_decision_event(
239
+ state=state,
240
+ decision="jump_to",
241
+ selected_edge=resolved,
242
+ reason_code="middleware_jump_to",
243
+ evidence={"jump_to": jump_to, "step": state.get("step", 0)},
244
+ )
245
+ return resolved
246
+ return None
247
+
248
+ def _task_reconcile_or_end(self, state: dict[str, Any]) -> str:
249
+ if (
250
+ self._task_reconcile_destination
251
+ and self._has_pending_task_notifications
252
+ and self._has_pending_task_notifications(state)
253
+ ):
254
+ state["transition"] = {"reason": TRANSITION_TASK_NOTIFICATION_ARRIVED}
255
+ emit_loop_decision_event(
256
+ state=state,
257
+ decision="task_reconcile",
258
+ selected_edge=self._task_reconcile_destination,
259
+ reason_code="pending_task_notifications",
260
+ evidence={
261
+ "should_end": state.get("should_end"),
262
+ "terminal_reason": state.get("terminal_reason"),
263
+ "step": state.get("step", 0),
264
+ },
265
+ )
266
+ return self._task_reconcile_destination
267
+ emit_loop_decision_event(
268
+ state=state,
269
+ decision="terminal",
270
+ selected_edge=self._end_destination,
271
+ reason_code="should_end_terminal",
272
+ evidence={
273
+ "should_end": state.get("should_end"),
274
+ "terminal_reason": state.get("terminal_reason"),
275
+ "step": state.get("step", 0),
276
+ },
277
+ )
278
+ return self._end_destination
279
+
280
+ def _handle_abort(self, state: dict[str, Any]) -> str | None:
281
+ abort_tr = state.get("abort_terminal_reason")
282
+ if abort_tr not in _ABORT_TERMINAL_REASONS:
283
+ return None
284
+ state["should_end"] = True
285
+ state["terminal_reason"] = abort_tr
286
+ state["abort_terminal_reason"] = None
287
+ state["transition"] = {"reason": abort_tr}
288
+ emit_loop_decision_event(
289
+ state=state,
290
+ decision="terminal",
291
+ selected_edge=(
292
+ self._end_destination
293
+ if not self._task_reconcile_destination
294
+ else "task_reconcile_or_end"
295
+ ),
296
+ reason_code=abort_tr,
297
+ evidence={
298
+ "abort_terminal_reason": abort_tr,
299
+ "step": state.get("step", 0),
300
+ },
301
+ )
302
+ return self._task_reconcile_or_end(state)
303
+
304
+ def _handle_withheld_error(self, state: dict[str, Any]) -> str | None:
305
+ withheld_error = state.get("withheld_error")
306
+ if not withheld_error:
307
+ return None
308
+ if withheld_error.get("reason") == TERMINAL_REASON_MAX_TOKENS:
309
+ return self._handle_max_tokens_recovery(state, withheld_error)
310
+ return self._handle_generic_withheld(state, withheld_error)
311
+
312
+ def _handle_max_tokens_recovery(
313
+ self,
314
+ state: dict[str, Any],
315
+ withheld_error: dict[str, Any],
316
+ ) -> str:
317
+ override = state.get("max_output_tokens_override")
318
+ recovery_count = int(state.get("max_output_tokens_recovery_count", 0) or 0)
319
+ if override is None:
320
+ escalated_override = resolve_escalated_max_output_tokens(self._max_output_tokens_cap)
321
+ state["max_output_tokens_override"] = escalated_override
322
+ state["transition"] = {
323
+ "reason": TRANSITION_MAX_OUTPUT_TOKENS_ESCALATE,
324
+ "error_reason": TERMINAL_REASON_MAX_TOKENS,
325
+ "override": escalated_override,
326
+ }
327
+ emit_loop_decision_event(
328
+ state=state,
329
+ decision="continue",
330
+ selected_edge=self._model_destination,
331
+ reason_code="max_output_tokens_escalate",
332
+ evidence={
333
+ "withheld_error": withheld_error,
334
+ "override": escalated_override,
335
+ "step": state.get("step", 0),
336
+ },
337
+ )
338
+ return self._model_destination
339
+ if recovery_count < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT:
340
+ state["max_output_tokens_recovery_count"] = recovery_count + 1
341
+ state["transition"] = {
342
+ "reason": TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY,
343
+ "error_reason": TERMINAL_REASON_MAX_TOKENS,
344
+ "attempt": recovery_count + 1,
345
+ }
346
+ emit_loop_decision_event(
347
+ state=state,
348
+ decision="continue",
349
+ selected_edge=self._model_destination,
350
+ reason_code="max_output_tokens_recovery",
351
+ evidence={
352
+ "withheld_error": withheld_error,
353
+ "recovery_count": recovery_count + 1,
354
+ "step": state.get("step", 0),
355
+ },
356
+ )
357
+ return self._model_destination
358
+ state["withheld_error"] = None
359
+ state["should_end"] = True
360
+ state["terminal_reason"] = TERMINAL_REASON_MAX_TOKENS
361
+ state["transition"] = {
362
+ "reason": TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY_EXHAUSTED,
363
+ "error_reason": TERMINAL_REASON_MAX_TOKENS,
364
+ "attempt": recovery_count,
365
+ }
366
+ emit_loop_decision_event(
367
+ state=state,
368
+ decision="terminal",
369
+ selected_edge=self._end_destination,
370
+ reason_code="max_output_tokens_recovery_exhausted",
371
+ evidence={
372
+ "withheld_error": withheld_error,
373
+ "recovery_count": recovery_count,
374
+ "step": state.get("step", 0),
375
+ },
376
+ )
377
+ return self._end_destination
378
+
379
+ def _handle_generic_withheld(
380
+ self,
381
+ state: dict[str, Any],
382
+ withheld_error: dict[str, Any],
383
+ ) -> str:
384
+ prev_transition = state.get("transition")
385
+ prev_transition_reason = (
386
+ prev_transition.get("reason") if isinstance(prev_transition, dict) else None
387
+ )
388
+ if (
389
+ state.get("collapse_retry_available")
390
+ and prev_transition_reason != TRANSITION_COLLAPSE_DRAIN_RETRY
391
+ ):
392
+ state["collapse_retry_available"] = False
393
+ state["transition"] = {
394
+ "reason": TRANSITION_COLLAPSE_DRAIN_RETRY,
395
+ "error_reason": withheld_error.get("reason"),
396
+ }
397
+ emit_loop_decision_event(
398
+ state=state,
399
+ decision="continue",
400
+ selected_edge=self._model_destination,
401
+ reason_code="collapse_drain_retry",
402
+ evidence={"withheld_error": withheld_error, "step": state.get("step", 0)},
403
+ )
404
+ return self._model_destination
405
+ if not state.get("has_attempted_reactive_compact", False):
406
+ state["has_attempted_reactive_compact"] = True
407
+ state["transition"] = {
408
+ "reason": TRANSITION_REACTIVE_COMPACT_RETRY,
409
+ "error_reason": withheld_error.get("reason"),
410
+ }
411
+ emit_loop_decision_event(
412
+ state=state,
413
+ decision="continue",
414
+ selected_edge=self._model_destination,
415
+ reason_code="reactive_compact_retry",
416
+ evidence={"withheld_error": withheld_error, "step": state.get("step", 0)},
417
+ )
418
+ return self._model_destination
419
+ state["transition"] = {
420
+ "reason": TRANSITION_WITHHOLD_RECOVERY_RETRY,
421
+ "error_reason": withheld_error.get("reason"),
422
+ "attempt": state.get("withhold_retry_count"),
423
+ }
424
+ emit_loop_decision_event(
425
+ state=state,
426
+ decision="continue",
427
+ selected_edge=self._model_destination,
428
+ reason_code="withhold_recovery_retry",
429
+ evidence={
430
+ "withheld_error": withheld_error,
431
+ "withhold_retry_count": state.get("withhold_retry_count"),
432
+ "step": state.get("step", 0),
433
+ },
434
+ )
435
+ return self._model_destination
436
+
437
+ def _handle_terminal(self, state: dict[str, Any]) -> str:
438
+ terminal_reason = state.get("terminal_reason")
439
+ last_ai_message, _ = _fetch_last_ai_and_tool_messages(state["messages"])
440
+ tb_count = int(state.get("token_budget_continuation_count", 0) or 0)
441
+ tb_max = self._tb_max_cap
442
+ max_steps_result = self._handle_max_steps_terminal(state, terminal_reason)
443
+ if max_steps_result is not None:
444
+ return max_steps_result
445
+ api_error_result = self._handle_api_error_terminal(state, last_ai_message)
446
+ if api_error_result is not None:
447
+ return api_error_result
448
+ sh_blk = state.get("__hook_result__")
449
+ stop_hook_result = self._handle_stop_hook_terminal(state, sh_blk)
450
+ if stop_hook_result is not None:
451
+ return stop_hook_result
452
+ token_budget_result = self._handle_token_budget_continuation(state, tb_count, tb_max)
453
+ if token_budget_result is not None:
454
+ return token_budget_result
455
+ return self._task_reconcile_or_end(state)
456
+
457
+ def _handle_max_steps_terminal(
458
+ self,
459
+ state: dict[str, Any],
460
+ terminal_reason: Any,
461
+ ) -> str | None:
462
+ if terminal_reason != TERMINAL_REASON_MAX_STEPS:
463
+ return None
464
+ messages = list(state.get("messages") or [])
465
+ messages.append(AIMessage(content=MAX_STEPS_PROMPT))
466
+ state["messages"] = messages
467
+ state["should_end"] = False
468
+ state["terminal_reason"] = None
469
+ state["force_text_exit"] = True
470
+ emit_loop_decision_event(
471
+ state=state,
472
+ decision="continue",
473
+ selected_edge=self._model_destination,
474
+ reason_code="max_steps_force_text_exit",
475
+ evidence={"terminal_reason": terminal_reason, "step": state.get("step", 0)},
476
+ )
477
+ return self._model_destination
478
+
479
+ def _handle_api_error_terminal(
480
+ self,
481
+ state: dict[str, Any],
482
+ last_ai_message: AIMessage | None,
483
+ ) -> str | None:
484
+ if not _is_api_error_assistant_message(last_ai_message):
485
+ return None
486
+ _execute_hook_inline(state, event=HookEvent.STOP_FAILURE)
487
+ state["terminal_reason"] = TERMINAL_REASON_COMPLETED
488
+ state["__hook_result__"] = None
489
+ state["transition"] = {"reason": TRANSITION_API_ERROR_TERMINAL}
490
+ emit_loop_decision_event(
491
+ state=state,
492
+ decision="terminal",
493
+ selected_edge=(
494
+ self._end_destination
495
+ if not self._task_reconcile_destination
496
+ else "task_reconcile_or_end"
497
+ ),
498
+ reason_code="api_error_terminal",
499
+ evidence={"step": state.get("step", 0)},
500
+ )
501
+ return self._task_reconcile_or_end(state)
502
+
503
+ def _handle_stop_hook_terminal(
504
+ self,
505
+ state: dict[str, Any],
506
+ sh_blk: Any,
507
+ ) -> str | None:
508
+ if not isinstance(sh_blk, dict):
509
+ return None
510
+ if sh_blk.get("prevent_continuation") is True or sh_blk.get("prevented") is True:
511
+ state["terminal_reason"] = TERMINAL_REASON_STOP_HOOK_PREVENTED
512
+ state["transition"] = {"reason": TRANSITION_STOP_HOOK_PREVENTED}
513
+ emit_loop_decision_event(
514
+ state=state,
515
+ decision="terminal",
516
+ selected_edge=(
517
+ self._end_destination
518
+ if not self._task_reconcile_destination
519
+ else "task_reconcile_or_end"
520
+ ),
521
+ reason_code="stop_hook_prevented",
522
+ evidence={"hook_result": sh_blk, "step": state.get("step", 0)},
523
+ )
524
+ return self._task_reconcile_or_end(state)
525
+ if sh_blk.get("blocking_errors"):
526
+ errs = list(sh_blk.get("blocking_errors") or [])
527
+ new_sh = dict(sh_blk)
528
+ new_sh["blocking_errors"] = None
529
+ state["__hook_result__"] = new_sh
530
+ state["max_output_tokens_recovery_count"] = 0
531
+ state["should_end"] = False
532
+ state["terminal_reason"] = None
533
+ state["transition"] = {"reason": TRANSITION_STOP_HOOK_BLOCKING, "errors": errs}
534
+ emit_loop_decision_event(
535
+ state=state,
536
+ decision="continue",
537
+ selected_edge=self._model_destination,
538
+ reason_code="stop_hook_blocking_retry",
539
+ evidence={"blocking_errors": errs, "step": state.get("step", 0)},
540
+ )
541
+ return self._model_destination
542
+ return None
543
+
544
+ def _handle_token_budget_continuation(
545
+ self,
546
+ state: dict[str, Any],
547
+ tb_count: int,
548
+ tb_max: int,
549
+ ) -> str | None:
550
+ if not state.get("token_budget_continue_requested") or tb_count >= tb_max:
551
+ return None
552
+ messages_tb = list(state.get("messages") or [])
553
+ messages_tb.append(HumanMessage(content=TOKEN_BUDGET_NUDGE_USER_MESSAGE))
554
+ state["messages"] = messages_tb
555
+ state["token_budget_continue_requested"] = False
556
+ state["should_end"] = False
557
+ state["terminal_reason"] = None
558
+ state["token_budget_continuation_count"] = tb_count + 1
559
+ state["transition"] = {
560
+ "reason": TRANSITION_TOKEN_BUDGET_CONTINUATION,
561
+ "attempt": tb_count + 1,
562
+ }
563
+ emit_loop_decision_event(
564
+ state=state,
565
+ decision="continue",
566
+ selected_edge=self._model_destination,
567
+ reason_code="token_budget_continuation",
568
+ evidence={"attempt": tb_count + 1, "max": tb_max, "step": state.get("step", 0)},
569
+ )
570
+ return self._model_destination
571
+
572
+ def _handle_continue(self, state: dict[str, Any]) -> str | List[Send]:
573
+ last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])
574
+ no_message_result = self._handle_missing_last_ai_message(state, last_ai_message)
575
+ if no_message_result is not None:
576
+ return no_message_result
577
+ tool_message_ids = [m.tool_call_id for m in tool_messages]
578
+ pending_tool_calls = [
579
+ c
580
+ for c in last_ai_message.tool_calls
581
+ if c["id"] not in tool_message_ids and c["name"] not in self._structured_output_tools
582
+ ]
583
+ pending_tools_result = self._handle_pending_tool_calls(state, pending_tool_calls)
584
+ if pending_tools_result is not None:
585
+ return pending_tools_result
586
+ structured_result = self._handle_structured_response_ready(state)
587
+ if structured_result is not None:
588
+ return structured_result
589
+ task_reconcile_result = self._handle_task_reconcile_continue(state)
590
+ if task_reconcile_result is not None:
591
+ return task_reconcile_result
592
+ emit_loop_decision_event(
593
+ state=state,
594
+ decision="terminal",
595
+ selected_edge=self._end_destination,
596
+ reason_code="no_tools_and_not_continue",
597
+ evidence={"step": state.get("step", 0)},
598
+ )
599
+ return self._end_destination
600
+
601
+ def _handle_missing_last_ai_message(
602
+ self,
603
+ state: dict[str, Any],
604
+ last_ai_message: AIMessage | None,
605
+ ) -> str | None:
606
+ if last_ai_message is not None:
607
+ return None
608
+ emit_loop_decision_event(
609
+ state=state,
610
+ decision="terminal",
611
+ selected_edge=self._end_destination,
612
+ reason_code="no_last_ai_message",
613
+ evidence={"step": state.get("step", 0)},
614
+ )
615
+ return self._end_destination
616
+
617
+ def _handle_pending_tool_calls(
618
+ self,
619
+ state: dict[str, Any],
620
+ pending_tool_calls: list[dict[str, Any]],
621
+ ) -> str | list[Send] | None:
622
+ if not pending_tool_calls:
623
+ return None
624
+ sends: list[Send] = []
625
+ blocked_tool_messages: list[ToolMessage] = []
626
+ for tool_call in pending_tool_calls:
627
+ hook_handled = self._prepare_single_tool_dispatch(state, tool_call)
628
+ if hook_handled["blocked_message"] is not None:
629
+ blocked_tool_messages.append(hook_handled["blocked_message"])
630
+ continue
631
+ sends.append(
632
+ Send(
633
+ self._tools_destination,
634
+ ToolCallWithContext(
635
+ __type="tool_call_with_context",
636
+ tool_call=hook_handled["tool_call"],
637
+ state=state,
638
+ ),
639
+ )
640
+ )
641
+ if blocked_tool_messages:
642
+ state["messages"] = list(state.get("messages") or []) + blocked_tool_messages
643
+ emit_loop_decision_event(
644
+ state=state,
645
+ decision="tool_dispatch",
646
+ selected_edge=self._tools_destination,
647
+ reason_code="pending_tool_calls",
648
+ evidence={
649
+ "pending_count": len(pending_tool_calls),
650
+ "tool_names": [c.get("name") for c in pending_tool_calls],
651
+ "step": state.get("step", 0),
652
+ },
653
+ )
654
+ return sends if sends else self._end_destination
655
+
656
+ def _prepare_single_tool_dispatch(
657
+ self,
658
+ state: dict[str, Any],
659
+ tool_call: dict[str, Any],
660
+ ) -> dict[str, Any]:
661
+ tool_name = str(tool_call.get("name") or "")
662
+ tool_args = tool_call.get("args")
663
+ hook_result = _execute_hook_for_tool(
664
+ state,
665
+ event=HookEvent.PRE_TOOL_USE,
666
+ tool_name=tool_name,
667
+ tool_input=tool_args if isinstance(tool_args, dict) else {},
668
+ )
669
+ next_tool_call = tool_call
670
+ blocked_message: ToolMessage | None = None
671
+ if isinstance(hook_result, dict):
672
+ state["__hook_result__"] = hook_result
673
+ if isinstance(hook_result.get("updated_input"), dict):
674
+ patched_tool_call = dict(tool_call)
675
+ patched_tool_call["args"] = dict(hook_result["updated_input"])
676
+ next_tool_call = patched_tool_call
677
+ if hook_result.get("permission_behavior") == "deny":
678
+ blocked_message = ToolMessage(
679
+ content=(
680
+ hook_result.get("hook_permission_decision_reason")
681
+ or "Tool call denied by PRE_TOOL_USE hook"
682
+ ),
683
+ tool_call_id=str(next_tool_call.get("id") or ""),
684
+ )
685
+ return {"tool_call": next_tool_call, "blocked_message": blocked_message}
686
+
687
+ def _handle_structured_response_ready(self, state: dict[str, Any]) -> str | None:
688
+ if "structured_response" not in state:
689
+ return None
690
+ emit_loop_decision_event(
691
+ state=state,
692
+ decision="terminal",
693
+ selected_edge=self._end_destination,
694
+ reason_code="structured_response_ready",
695
+ evidence={"step": state.get("step", 0)},
696
+ )
697
+ return self._end_destination
698
+
699
+ def _handle_task_reconcile_continue(self, state: dict[str, Any]) -> str | None:
700
+ if not (
701
+ self._task_reconcile_destination
702
+ and self._has_pending_task_notifications
703
+ and self._has_pending_task_notifications(state)
704
+ ):
705
+ return None
706
+ state["transition"] = {"reason": TRANSITION_TASK_NOTIFICATION_ARRIVED}
707
+ emit_loop_decision_event(
708
+ state=state,
709
+ decision="task_reconcile",
710
+ selected_edge=self._task_reconcile_destination,
711
+ reason_code="pending_task_notifications",
712
+ evidence={"step": state.get("step", 0)},
713
+ )
714
+ return self._task_reconcile_destination
715
+
716
+
717
+ def _make_loop_controller_edge(
718
+ *,
719
+ model_destination: str,
720
+ tools_destination: str,
721
+ structured_output_tools: dict[str, OutputToolBinding[Any]],
722
+ end_destination: str,
723
+ max_output_tokens_cap: int | None = None,
724
+ task_reconcile_destination: str | None = None,
725
+ has_pending_task_notifications: Callable[[dict[str, Any]], bool] | None = None,
726
+ token_budget_max_continuations: int | None = None,
727
+ ) -> Callable[[dict[str, Any]], str | List[Send] | None]:
728
+ """LangChain AgentX 的 loop_controller 条件边工厂。"""
729
+ return LoopControllerEdge(
730
+ model_destination=model_destination,
731
+ tools_destination=tools_destination,
732
+ structured_output_tools=structured_output_tools,
733
+ end_destination=end_destination,
734
+ max_output_tokens_cap=max_output_tokens_cap,
735
+ task_reconcile_destination=task_reconcile_destination,
736
+ has_pending_task_notifications=has_pending_task_notifications,
737
+ token_budget_max_continuations=token_budget_max_continuations,
738
+ )
739
+
740
+
741
+ def _make_model_to_model_edge(
742
+ *,
743
+ model_destination: str,
744
+ end_destination: str,
745
+ ) -> Callable[[dict[str, Any]], str | List[Send] | None]:
746
+ """model → model/END 条件边,用于仅依赖结构化输出的场景。"""
747
+
748
+ def model_to_model(
749
+ state: dict[str, Any],
750
+ ) -> str | List[Send] | None:
751
+ # 优先遵循中间件显式 jump_to 指令。
752
+ if jump_to := state.get("jump_to"):
753
+ return _resolve_jump(
754
+ jump_to,
755
+ model_destination=model_destination,
756
+ end_destination=end_destination,
757
+ )
758
+
759
+ # 产出结构化响应后结束。
760
+ if "structured_response" in state:
761
+ return end_destination
762
+
763
+ # 默认继续下一轮 model,处理结构化输出可能的重试场景。
764
+ return model_destination
765
+
766
+ return model_to_model
767
+
768
+
769
+ def _make_tools_to_model_edge(
770
+ *,
771
+ tool_node: ToolNode,
772
+ model_destination: str,
773
+ structured_output_tools: dict[str, OutputToolBinding[Any]],
774
+ end_destination: str,
775
+ ) -> Callable[[dict[str, Any]], str | None]:
776
+ """LangChain AgentX 的 tools → model 条件边。
777
+
778
+ 总是返回 model,不支持 return_direct 退出。
779
+ 让 LLM 处理工具结果后决定是否结束。
780
+ """
781
+
782
+ def tools_to_model(state: dict[str, Any]) -> str | None:
783
+ messages = list(state.get("messages") or [])
784
+ last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(messages)
785
+ if last_ai_message is not None and tool_messages:
786
+ tool_call_map = {
787
+ str(tc.get("id")): tc for tc in (last_ai_message.tool_calls or []) if isinstance(tc, dict)
788
+ }
789
+ for tm in tool_messages:
790
+ tc = tool_call_map.get(str(getattr(tm, "tool_call_id", "")))
791
+ if not tc:
792
+ continue
793
+ is_failure = getattr(tm, "status", "success") == "error"
794
+ event = HookEvent.POST_TOOL_USE_FAILURE if is_failure else HookEvent.POST_TOOL_USE
795
+ hook_result = _execute_hook_for_tool(
796
+ state,
797
+ event=event,
798
+ tool_name=str(tc.get("name") or ""),
799
+ tool_input=tc.get("args") if isinstance(tc.get("args"), dict) else {},
800
+ tool_result=getattr(tm, "content", None),
801
+ )
802
+ if isinstance(hook_result, dict):
803
+ state["__hook_result__"] = hook_result
804
+ # Phase 1:记录工具轮次(不参与任何分支判断)
805
+ # 语义:tools 节点完成一次执行回到 model 之前,算一次 turn。
806
+ state["turn_count"] = int(state.get("turn_count", 0) or 0) + 1
807
+
808
+ # 注意:return_direct 和结构化输出工具不会直接退出
809
+ # LLM 会在 model_node 中通过 finish_reason 决定是否结束
810
+ return model_destination
811
+
812
+ return tools_to_model
813
+
814
+
815
+ __all__ = [
816
+ "_make_model_to_model_edge",
817
+ "_make_tools_to_model_edge",
818
+ "_make_loop_controller_edge",
819
+ ]
820
+