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,1409 @@
1
+ """
2
+ LangChain AgentX Agent Factory
3
+
4
+ 与 LangChain create_agent 接口完全兼容,但使用 LangChain AgentX 的退出逻辑。
5
+
6
+ 核心差异:
7
+ 1. 退出条件:finish_reason not in ["tool-calls", "unknown"](主动)
8
+ 2. 退出位置:只在 model 节点退出
9
+ 3. 不支持 return_direct(会被忽略)
10
+
11
+ 参考设计文档:.claude/design/create-opencode-style-agent-design.md
12
+
13
+ LangChain AgentX Agent 状态图(文字版,含 loop_controller):
14
+
15
+ START
16
+
17
+ hook_loop_start
18
+
19
+ model 节点(同步/异步,带 OpenCode 退出判断)
20
+
21
+ hook_stop
22
+
23
+ loop_controller(集中处理 should_end / terminal_reason / 工具调用)
24
+ ├─ should_end=True → END
25
+ ├─ 有工具调用 → tools → (task_event_*/prune_and_compress) → model
26
+ └─ 无工具调用 → END
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from pathlib import Path
32
+ from typing import (
33
+ TYPE_CHECKING,
34
+ Any,
35
+ )
36
+
37
+ from langchain_core.language_models.chat_models import BaseChatModel
38
+ from langchain_core.messages import SystemMessage
39
+ from langchain_core.runnables import RunnableConfig
40
+ from langgraph._internal._runnable import RunnableCallable
41
+ from langgraph.constants import END, START
42
+ from langgraph.graph.state import StateGraph
43
+ from langgraph.prebuilt.tool_node import ToolNode
44
+ import asyncio
45
+ import logging
46
+ import threading
47
+ import uuid
48
+ from types import MethodType
49
+
50
+ from ..exit.exit_logic import (
51
+ CONTINUE_FINISH_REASONS,
52
+ LoopAgentStateFields,
53
+ )
54
+ from .graph_edges import _make_model_to_model_edge, _make_tools_to_model_edge, _make_loop_controller_edge
55
+ from ..model.schema_and_format import (
56
+ STRUCTURED_OUTPUT_ERROR_TEMPLATE,
57
+ FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT,
58
+ _resolve_schema,
59
+ )
60
+ from ..model.model_node import ModelNode
61
+ from ..model.model_nodes import make_model_nodes
62
+ from ..context import default_loop_context_compaction
63
+ from ..context.settings import CompactionSettings
64
+ from ..injection.dedup import filter_attachment_dicts_by_task_command_id
65
+ from ..model.tool_call_degradation_corrector import (
66
+ ToolCallDegradationCorrector,
67
+ _build_allowed_tool_names_for_degradation_correction,
68
+ )
69
+ from ..hook import HookEngine, HooksConfigSnapshot, WorkspaceTrustChecker
70
+ from ..hook.graph_wiring import HookGraphWiring
71
+ from ..hook.registry import HookRegistry
72
+ from ...task_runtime.integrations.loop_adapter import LoopInjectionBatch, LoopTaskInjectionAdapter
73
+ from ...task_runtime.integrations.loop_integration import (
74
+ QueuedCommandProvider,
75
+ decode_provider_batch_id,
76
+ encode_provider_batch_id,
77
+ )
78
+ from ...task_runtime.core.types import QueuePriority, TaskScope
79
+ from ...task_runtime.skill_prefetch import SkillPrefetchProvider
80
+ from ...observability.trace import TraceCollector, SqliteTraceStore
81
+ from ...observability.trace.trace_callback import TraceCallbackHandler
82
+ from ...observability.trace.trace_lifecycle_collector import TraceLifecycleCollector
83
+ from ...observability.trace.event_emitter import emit_task_event
84
+ from ...observability.logging import ObservabilityLoggerAdapter, build_log_context
85
+ from ...memory.instruction.runtime import (
86
+ CallbackActiveFilesProvider,
87
+ InstructionMemoryBootstrap,
88
+ )
89
+ from ...memory.memdir.agent_memory import (
90
+ AgentMemoryPromptBootstrap,
91
+ get_default_agent_memory_extraction_coordinator,
92
+ )
93
+ from ...memory.memdir.runtime import MemoryPromptBootstrap
94
+ from ...memory.memdir.extractor import MemoryExtractionCoordinator
95
+ from ...memory.session import (
96
+ SessionMemoryCompactBridge,
97
+ SessionMemoryManager,
98
+ make_session_memory_section,
99
+ )
100
+ from ..prompt import SystemPromptSection, build_effective_system_prompt
101
+ from ...tools.agent.prompt import build_explore_plan_guidance
102
+ from ..config import AgentLoopConfig, ModelContextResolver
103
+ from ..config.runtime_settings import compute_recursion_limit
104
+ from ...provider.model_profile import ModelProfileRegistry
105
+ from ...workspace import resolve_agent_workspace_config
106
+
107
+ logger = logging.getLogger(__name__)
108
+ SKILL_TOOL_NAME = "skill"
109
+ DYNAMIC_TOOL_ERROR_TEMPLATE = (
110
+ "Model returned tool calls that are not registered in this agent runtime. "
111
+ "Ensure tools are registered via ToolRuntimeLoader and passed via loader=..."
112
+ )
113
+
114
+
115
+ # ═══════════════════════════════════════════════════════════════════════════════
116
+ # LangChain 兼容性导入(支持新旧版本)
117
+ # ═══════════════════════════════════════════════════════════════════════════════
118
+ from langchain.agents.middleware.types import AgentState, ContextT, OmitFromSchema, ResponseT, _InputAgentState, _OutputAgentState
119
+
120
+ from langchain.agents.structured_output import (
121
+ AutoStrategy,
122
+ MultipleStructuredOutputsError,
123
+ OutputToolBinding,
124
+ ProviderStrategy,
125
+ ProviderStrategyBinding,
126
+ ResponseFormat,
127
+ StructuredOutputError,
128
+ StructuredOutputValidationError,
129
+ ToolStrategy,
130
+ )
131
+ from langchain.chat_models import init_chat_model
132
+ from ..model.retrier import ModelCallRetrier, ModelRetryConfig
133
+
134
+
135
+ if TYPE_CHECKING:
136
+ from collections.abc import Awaitable, Callable, Sequence
137
+
138
+ from langchain_core.runnables import Runnable, RunnableConfig
139
+ from langgraph.graph.state import CompiledStateGraph
140
+ from langgraph.runtime import Runtime
141
+ from ...task_runtime.core.interfaces import TaskRuntime
142
+ from ...tool_runtime import AgentSessionStore, ToolRuntimeLoader
143
+
144
+ class LoopGraphBuilder:
145
+ """create_loop_agent 内部图构建器,不对外暴露。
146
+
147
+ 职责:将 create_loop_agent 的图构建逻辑按职责分层,
148
+ compile() 返回 CompiledStateGraph。
149
+ CC 对照:无直接对应,属于本工程 LangGraph 适配层的内部拆分。
150
+ """
151
+
152
+ def __init__(
153
+ self,
154
+ model: str | BaseChatModel,
155
+ *,
156
+ system_prompt: str | SystemMessage | None = None,
157
+ override_system_prompt: str | None = None,
158
+ append_system_prompt: str | None = None,
159
+ dynamic_sections: Sequence[SystemPromptSection] = (),
160
+ response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,
161
+ fallback_model: BaseChatModel | None = None,
162
+ enable_tool_call_degradation_fix: bool = True,
163
+ session_id: str | None = None,
164
+ session_store: "AgentSessionStore | None" = None,
165
+ loader: "ToolRuntimeLoader | None" = None,
166
+ permission_resolver: Any | None = None,
167
+ state_schema: type[AgentState[ResponseT]] | None = None,
168
+ context_schema: type[ContextT] | None = None,
169
+ debug: bool = False,
170
+ name: str | None = None,
171
+ max_steps: int | None = None,
172
+ max_turns: int | None = None,
173
+ is_subagent: bool = False,
174
+ subagent_type: str | None = None,
175
+ container_type: str | None = None,
176
+ enable_trace: bool = True,
177
+ trace_collector: TraceCollector | None = None,
178
+ parent_run_id: str | None = None,
179
+ parent_tool_call_id: str | None = None,
180
+ parent_conversation_session_id: str | None = None,
181
+ trace_visibility: str | None = None,
182
+ task_runtime: TaskRuntime | None = None,
183
+ task_notification_max_priority: QueuePriority = QueuePriority.NEXT,
184
+ queued_command_providers: Sequence[QueuedCommandProvider] = (),
185
+ managed_rules: str | None = None,
186
+ active_files_getter: "Callable[[], list[str]] | None" = None,
187
+ workspace_root: str | Path | None = None,
188
+ agent_home: str = ".langchain_agentx",
189
+ workspace_trust_accepted: bool | None = None,
190
+ raw_hooks: "HookRegistry | None" = None,
191
+ enable_session_memory: bool = False,
192
+ ) -> None:
193
+ self._raw_model = model
194
+ self._raw_system_prompt = system_prompt
195
+ self._raw_override_system_prompt = override_system_prompt
196
+ self._raw_append_system_prompt = append_system_prompt
197
+ self._raw_dynamic_sections = list(dynamic_sections)
198
+ self._raw_response_format = response_format
199
+ self._raw_fallback_model = fallback_model
200
+ self._raw_enable_tool_call_degradation_fix = enable_tool_call_degradation_fix
201
+ self._raw_session_id = session_id
202
+ self._raw_session_store = session_store
203
+ self._raw_loader = loader
204
+ self._raw_permission_resolver = permission_resolver
205
+ self._raw_state_schema = state_schema
206
+ self._raw_context_schema = context_schema
207
+ self._raw_debug = debug
208
+ self._raw_name = name
209
+ self._raw_max_steps = max_steps
210
+ self._raw_max_turns = max_turns
211
+ self._raw_is_subagent = is_subagent
212
+ self._raw_subagent_type = subagent_type
213
+ self._raw_container_type = container_type
214
+ self._raw_enable_trace = enable_trace
215
+ self._raw_trace_collector = trace_collector
216
+ self._raw_parent_run_id = parent_run_id
217
+ self._raw_parent_tool_call_id = parent_tool_call_id
218
+ self._raw_parent_conversation_session_id = parent_conversation_session_id
219
+ self._raw_trace_visibility = trace_visibility
220
+ self._raw_task_runtime = task_runtime
221
+ self._raw_task_notification_max_priority = task_notification_max_priority
222
+ self._raw_queued_command_providers = list(queued_command_providers)
223
+ self._raw_managed_rules = managed_rules
224
+ self._raw_active_files_getter = active_files_getter
225
+ self._raw_workspace_root = workspace_root
226
+ self._raw_agent_home = agent_home
227
+ self._raw_workspace_trust_accepted = workspace_trust_accepted
228
+ self._raw_hooks = raw_hooks
229
+ self._raw_enable_session_memory = enable_session_memory
230
+
231
+ self._prepared = False
232
+
233
+ def _prepare_compile_context(self) -> None:
234
+ if self._prepared:
235
+ return
236
+ model, session_id, log = self._resolve_model_session_and_logger()
237
+ log.info(
238
+ "create_loop_agent start model=%s loader=%s max_steps=%s",
239
+ getattr(model, "model_name", type(model).__name__),
240
+ repr(self._raw_loader),
241
+ self._raw_max_steps,
242
+ )
243
+ session_store = self._resolve_session_store(session_id)
244
+ base_prompt = self._resolve_base_prompt()
245
+ self._dynamic_sections: list[SystemPromptSection] = list(self._raw_dynamic_sections)
246
+ append_system_prompt = self._raw_append_system_prompt
247
+ self._setup_loader_tools()
248
+ self._resolve_workspace_config()
249
+ self._model_profile_registry = ModelProfileRegistry.from_files(
250
+ agent_home_dir=self._workspace_cfg.agent_home_dir
251
+ )
252
+ self._model_context_resolver = ModelContextResolver()
253
+ self._session_memory_manager: SessionMemoryManager | None = None
254
+ self._session_memory_compact_bridge: SessionMemoryCompactBridge | None = None
255
+ if self._raw_enable_session_memory:
256
+ session_memory_path = self._workspace_cfg.session_memory_path(session_id)
257
+ self._session_memory_manager = SessionMemoryManager(
258
+ session_memory_path=session_memory_path,
259
+ llm=model,
260
+ is_subagent=self._raw_is_subagent,
261
+ )
262
+ self._session_memory_compact_bridge = SessionMemoryCompactBridge(
263
+ session_memory_path=session_memory_path,
264
+ extraction_state=self._session_memory_manager.extraction_state,
265
+ )
266
+ self._dynamic_sections.append(make_session_memory_section(session_memory_path))
267
+ self._dynamic_sections.extend(
268
+ InstructionMemoryBootstrap(
269
+ workspace_root=Path(self._workspace_root),
270
+ agent_home=self._agent_home,
271
+ managed_rules=self._raw_managed_rules,
272
+ active_files_provider=(
273
+ CallbackActiveFilesProvider(self._raw_active_files_getter)
274
+ if self._raw_active_files_getter is not None
275
+ else None
276
+ ),
277
+ ).build_sections()
278
+ )
279
+ self._dynamic_sections.extend(
280
+ MemoryPromptBootstrap(
281
+ workspace_root=Path(self._workspace_root),
282
+ agent_home=self._agent_home,
283
+ ).build_sections()
284
+ )
285
+ self._dynamic_sections.extend(
286
+ AgentMemoryPromptBootstrap(
287
+ workspace_cfg=self._workspace_cfg,
288
+ is_subagent=self._raw_is_subagent,
289
+ subagent_type=self._raw_subagent_type,
290
+ ).build_sections()
291
+ )
292
+ system_message = self._build_system_message(
293
+ base_prompt=base_prompt,
294
+ append_system_prompt=append_system_prompt,
295
+ )
296
+ collector = self._resolve_trace_collector()
297
+ (
298
+ initial_response_format,
299
+ tool_strategy_for_setup,
300
+ structured_output_tools,
301
+ ) = self._resolve_response_format_setup()
302
+ self._built_in_tools = []
303
+ self._available_tools = list(self._loader_tools)
304
+
305
+ if not self._raw_is_subagent:
306
+ append_system_prompt, system_message = self._apply_explore_guidance(
307
+ append_system_prompt=append_system_prompt,
308
+ base_prompt=base_prompt,
309
+ )
310
+
311
+ self._loop_config = AgentLoopConfig(
312
+ model=self._loop_config_model,
313
+ fallback_model=self._raw_fallback_model,
314
+ max_steps=self._raw_max_steps,
315
+ max_turns=self._raw_max_turns,
316
+ available_tools=self._available_tools,
317
+ managed_rules=self._raw_managed_rules,
318
+ permission_resolver=self._raw_permission_resolver,
319
+ session_id=session_id or "",
320
+ conversation_session_id=(
321
+ self._raw_parent_conversation_session_id
322
+ if self._raw_is_subagent and self._raw_parent_conversation_session_id
323
+ else (session_id or "")
324
+ ),
325
+ workspace_root=self._workspace_root,
326
+ agent_home=self._agent_home,
327
+ cwd=self._workspace_root,
328
+ subagent_type=self._raw_subagent_type,
329
+ trace_enabled=self._raw_enable_trace,
330
+ model_profile_registry=self._model_profile_registry,
331
+ )
332
+ self._compaction_settings = self._build_compaction_settings()
333
+ self._loop_context_compaction = default_loop_context_compaction().with_default_pipeline(
334
+ settings=self._compaction_settings
335
+ )
336
+ self._model = model
337
+ self._session_id = session_id
338
+ self._log = log
339
+ self._session_store = session_store
340
+ self._base_prompt = base_prompt
341
+ self._system_message = system_message
342
+ self._trace_collector = collector
343
+ self._initial_response_format = initial_response_format
344
+ self._tool_strategy_for_setup = tool_strategy_for_setup
345
+ self._structured_output_tools = structured_output_tools
346
+ self._fallback_model = self._raw_fallback_model
347
+ self._max_turns = self._raw_max_turns
348
+ self._name = self._raw_name
349
+ self._state_schema = self._raw_state_schema
350
+ self._context_schema = self._raw_context_schema
351
+ self._is_subagent = self._raw_is_subagent
352
+ self._task_runtime = self._raw_task_runtime
353
+ self._task_notification_max_priority = self._raw_task_notification_max_priority
354
+ self._queued_command_providers = list(self._raw_queued_command_providers)
355
+ self._enable_tool_call_degradation_fix = self._raw_enable_tool_call_degradation_fix
356
+ self._override_system_prompt = self._raw_override_system_prompt
357
+ self._append_system_prompt = append_system_prompt
358
+ self._max_steps_param = self._raw_max_steps
359
+ self._debug = self._raw_debug
360
+ if self._raw_loader is not None:
361
+ self._raw_loader.set_permission_resolver(self._raw_permission_resolver)
362
+ self._hook_snapshot = (
363
+ self._raw_hooks.build_snapshot()
364
+ if self._raw_hooks is not None
365
+ else HooksConfigSnapshot()
366
+ )
367
+ self._trace_callback_handler: TraceCallbackHandler | None = None
368
+ self._trace_tool_callback_handler: TraceCallbackHandler | None = None
369
+ hook_event_emitter = None
370
+ trace_lifecycle_collector = (
371
+ TraceLifecycleCollector(
372
+ collector,
373
+ graph_name="subagent" if self._raw_is_subagent else "agent",
374
+ default_run_id=session_id or None,
375
+ unit_type="subagent" if self._raw_is_subagent else "main_loop",
376
+ container_type=self._raw_container_type or "agent_session",
377
+ origin="tool" if self._raw_is_subagent else "user",
378
+ visibility=self._raw_trace_visibility or "default",
379
+ parent_run_id=self._raw_parent_run_id if self._raw_is_subagent else None,
380
+ parent_tool_call_id=self._raw_parent_tool_call_id if self._raw_is_subagent else None,
381
+ conversation_session_id=(
382
+ self._raw_parent_conversation_session_id
383
+ if self._raw_is_subagent
384
+ else None
385
+ ),
386
+ )
387
+ if self._raw_enable_trace
388
+ else None
389
+ )
390
+ if collector is not None:
391
+ from ...observability.trace.hook_event_emitter import HookEventEmitter
392
+
393
+ hook_event_emitter = HookEventEmitter(collector=collector)
394
+ is_headless_trust_context = bool(
395
+ self._raw_container_type in {"background", "batch", "worker", "sdk"}
396
+ )
397
+ trust_accepted = (
398
+ self._raw_workspace_trust_accepted
399
+ if self._raw_workspace_trust_accepted is not None
400
+ else self._workspace_cfg.workspace_trust_accepted
401
+ )
402
+ trust_checker = WorkspaceTrustChecker(
403
+ headless=is_headless_trust_context,
404
+ workspace_trust_accepted=trust_accepted,
405
+ )
406
+ hook_engine = HookEngine(
407
+ self._hook_snapshot,
408
+ event_emitter=hook_event_emitter,
409
+ trust_checker=trust_checker,
410
+ )
411
+ container_type = self._raw_container_type or "agent_session"
412
+ self._hook_graph_wiring = HookGraphWiring(
413
+ hook_engine,
414
+ trace_lifecycle_collector=trace_lifecycle_collector,
415
+ session_memory=self._session_memory_manager,
416
+ memory_extractor=MemoryExtractionCoordinator(),
417
+ agent_memory_extractor=get_default_agent_memory_extraction_coordinator(),
418
+ workspace_cfg=self._workspace_cfg,
419
+ session_id=session_id or "",
420
+ container_type=container_type,
421
+ subagent_type=self._raw_subagent_type,
422
+ )
423
+ if self._raw_enable_trace:
424
+ self._trace_callback_handler = TraceCallbackHandler(
425
+ collector=collector,
426
+ session_id=session_id or "",
427
+ capture_llm=True,
428
+ capture_tool=False,
429
+ )
430
+ self._trace_tool_callback_handler = TraceCallbackHandler(
431
+ collector=collector,
432
+ session_id=session_id or "",
433
+ capture_llm=False,
434
+ capture_tool=True,
435
+ )
436
+ self._prepared = True
437
+
438
+ def _resolve_model_session_and_logger(
439
+ self,
440
+ ) -> tuple[BaseChatModel, str, ObservabilityLoggerAdapter]:
441
+ model: str | BaseChatModel = self._raw_model
442
+ self._loop_config_model = model
443
+ if isinstance(model, str):
444
+ model = init_chat_model(model)
445
+ session_id = self._raw_session_id
446
+ if not session_id:
447
+ session_id = f"session-{uuid.uuid4().hex}"
448
+ logger.warning(
449
+ "session_id missing, generated ephemeral session_id=%s; "
450
+ "pass explicit session_id for multi-turn continuity",
451
+ session_id,
452
+ )
453
+ log = ObservabilityLoggerAdapter(
454
+ logger,
455
+ build_log_context(session_id=session_id).as_extra(),
456
+ )
457
+ return model, session_id, log
458
+
459
+ def _resolve_session_store(self, session_id: str) -> Any:
460
+ session_store = self._raw_session_store
461
+ if session_store is None and self._raw_loader is not None:
462
+ return self._raw_loader.create_session(session_id=session_id)
463
+ return session_store
464
+
465
+ def _resolve_base_prompt(self) -> str | None:
466
+ if self._raw_system_prompt is None:
467
+ return None
468
+ if isinstance(self._raw_system_prompt, SystemMessage):
469
+ return (
470
+ self._raw_system_prompt.content
471
+ if isinstance(self._raw_system_prompt.content, str)
472
+ else None
473
+ )
474
+ return self._raw_system_prompt
475
+
476
+ def _setup_loader_tools(self) -> None:
477
+ self._loader_tools = []
478
+ self._skill_runtime_tool = None
479
+ if self._raw_loader is not None:
480
+ self._loader_tools = self._raw_loader._registry.to_langchain_tools()
481
+ for rt in self._raw_loader._registry.list():
482
+ if self._skill_runtime_tool is None and rt.__class__.__name__ == "SkillRuntimeTool":
483
+ self._skill_runtime_tool = rt
484
+
485
+ def _resolve_workspace_config(self) -> None:
486
+ raw_workspace_root = self._raw_workspace_root
487
+ if raw_workspace_root is None:
488
+ workspace_root = Path.cwd()
489
+ else:
490
+ workspace_root = Path(raw_workspace_root)
491
+ workspace_cfg = resolve_agent_workspace_config(
492
+ workspace_root=workspace_root,
493
+ agent_home=self._raw_agent_home,
494
+ )
495
+ self._workspace_cfg = workspace_cfg
496
+ self._workspace_root = str(workspace_cfg.workspace_root)
497
+ self._agent_home = workspace_cfg.agent_home
498
+
499
+ @staticmethod
500
+ def _run_coro_sync(coro: Any) -> Any:
501
+ try:
502
+ asyncio.get_running_loop()
503
+ except RuntimeError:
504
+ return asyncio.run(coro)
505
+
506
+ result: dict[str, Any] = {}
507
+ error: dict[str, Exception] = {}
508
+
509
+ def _runner() -> None:
510
+ try:
511
+ result["value"] = asyncio.run(coro)
512
+ except Exception as exc: # pragma: no cover
513
+ error["value"] = exc
514
+
515
+ thread = threading.Thread(target=_runner, daemon=True)
516
+ thread.start()
517
+ thread.join()
518
+ if "value" in error:
519
+ raise error["value"]
520
+ return result.get("value")
521
+
522
+ def _build_system_message(
523
+ self,
524
+ *,
525
+ base_prompt: str | None,
526
+ append_system_prompt: str | None,
527
+ ) -> SystemMessage | None:
528
+ has_dynamic = bool(
529
+ self._dynamic_sections or self._raw_override_system_prompt or append_system_prompt
530
+ )
531
+ self._has_dynamic_prompt = has_dynamic
532
+ if not has_dynamic:
533
+ return SystemMessage(content=base_prompt) if base_prompt is not None else None
534
+ section_cache: dict[str, str | None] = {}
535
+ effective = build_effective_system_prompt(
536
+ system_prompt=base_prompt,
537
+ override_system_prompt=self._raw_override_system_prompt,
538
+ append_system_prompt=append_system_prompt,
539
+ dynamic_sections=self._dynamic_sections,
540
+ section_cache=section_cache,
541
+ )
542
+ return SystemMessage(content=effective) if effective is not None else None
543
+
544
+ def _resolve_trace_collector(self) -> TraceCollector | None:
545
+ if not self._raw_enable_trace:
546
+ return None
547
+ return self._raw_trace_collector or TraceCollector(SqliteTraceStore.from_env())
548
+
549
+ def _resolve_response_format_setup(
550
+ self,
551
+ ) -> tuple[
552
+ ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None,
553
+ ToolStrategy[Any] | None,
554
+ dict[str, OutputToolBinding[Any]],
555
+ ]:
556
+ initial_response_format: ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None
557
+ if self._raw_response_format is None:
558
+ initial_response_format = None
559
+ elif isinstance(self._raw_response_format, (ToolStrategy, ProviderStrategy, AutoStrategy)):
560
+ initial_response_format = self._raw_response_format
561
+ else:
562
+ initial_response_format = AutoStrategy(schema=self._raw_response_format)
563
+
564
+ tool_strategy_for_setup: ToolStrategy[Any] | None = None
565
+ if isinstance(initial_response_format, AutoStrategy):
566
+ tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)
567
+ elif isinstance(initial_response_format, ToolStrategy):
568
+ tool_strategy_for_setup = initial_response_format
569
+
570
+ structured_output_tools: dict[str, OutputToolBinding[Any]] = {}
571
+ if tool_strategy_for_setup:
572
+ for response_schema in tool_strategy_for_setup.schema_specs:
573
+ structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)
574
+ structured_output_tools[structured_tool_info.tool.name] = structured_tool_info
575
+ return initial_response_format, tool_strategy_for_setup, structured_output_tools
576
+
577
+ def _apply_explore_guidance(
578
+ self,
579
+ *,
580
+ append_system_prompt: str | None,
581
+ base_prompt: str | None,
582
+ ) -> tuple[str | None, SystemMessage | None]:
583
+ explore_guidance = build_explore_plan_guidance(self._available_tools)
584
+ if not explore_guidance:
585
+ return append_system_prompt, self._build_system_message(
586
+ base_prompt=base_prompt,
587
+ append_system_prompt=append_system_prompt,
588
+ )
589
+ updated_append_prompt = (
590
+ f"{append_system_prompt}\n\n{explore_guidance}"
591
+ if append_system_prompt
592
+ else explore_guidance
593
+ )
594
+ return updated_append_prompt, self._build_system_message(
595
+ base_prompt=base_prompt,
596
+ append_system_prompt=updated_append_prompt,
597
+ )
598
+
599
+ def _build_tool_node(self) -> Any:
600
+ self._prepare_compile_context()
601
+ need_tool_node = bool(self._available_tools)
602
+ tool_node: ToolNode | None = None
603
+ if need_tool_node:
604
+ tool_node = ToolNode(tools=self._available_tools)
605
+ if self._trace_tool_callback_handler is not None:
606
+ tool_node = tool_node.with_config({"callbacks": [self._trace_tool_callback_handler]})
607
+ return tool_node
608
+
609
+ def _prepare_for_model_nodes(self, tool_node: Any) -> None:
610
+ """在 make_model_nodes 之前设置 default_tools、模型包装与图节点所需字段。"""
611
+ if tool_node:
612
+ default_tools = list(tool_node.tools_by_name.values()) + self._built_in_tools
613
+ else:
614
+ default_tools = list(self._built_in_tools)
615
+ model = self._model
616
+ if self._enable_tool_call_degradation_fix and not isinstance(model, ToolCallDegradationCorrector):
617
+ allowed_tool_names = _build_allowed_tool_names_for_degradation_correction(
618
+ tool_node=tool_node,
619
+ built_in_tools=self._built_in_tools,
620
+ structured_output_tools=self._structured_output_tools,
621
+ )
622
+ model = ToolCallDegradationCorrector(
623
+ llm=model,
624
+ allowed_tool_names=allowed_tool_names,
625
+ )
626
+ self._default_tools = default_tools
627
+ self._model = model
628
+ _retrier = ModelCallRetrier(ModelRetryConfig())
629
+
630
+ self._model_retrier = _retrier
631
+
632
+ def _build_model_nodes(self, tool_node: Any) -> ModelNode:
633
+ """构建 model 节点,返回 ModelNode(Runnable) 实例,供 LangGraph 直接注册。"""
634
+ self._prepare_compile_context()
635
+ self._prepare_for_model_nodes(tool_node)
636
+ return ModelNode(
637
+ model=self._model,
638
+ default_tools=self._default_tools,
639
+ compaction_settings=self._compaction_settings,
640
+ max_output_tokens_cap=self._model_context_resolver.resolve_max_output_tokens(
641
+ self._loop_config
642
+ ),
643
+ system_message=self._system_message,
644
+ initial_response_format=self._initial_response_format,
645
+ max_steps=self._raw_max_steps,
646
+ max_turns=self._max_turns,
647
+ max_withhold_retries=self._loop_config.max_withhold_retries,
648
+ name=self._name,
649
+ structured_output_tools=self._structured_output_tools,
650
+ tool_strategy_for_setup=self._tool_strategy_for_setup,
651
+ retrier=self._model_retrier,
652
+ tool_node=tool_node,
653
+ dynamic_tool_error_template=DYNAMIC_TOOL_ERROR_TEMPLATE,
654
+ fallback_model=self._fallback_model,
655
+ llm_callback_handler=self._trace_callback_handler,
656
+ dynamic_sections=self._dynamic_sections if self._has_dynamic_prompt else None,
657
+ base_prompt=self._base_prompt,
658
+ override_prompt=self._override_system_prompt,
659
+ append_prompt=self._append_system_prompt,
660
+ )
661
+
662
+ def _build_compaction_settings(self) -> CompactionSettings:
663
+ base = CompactionSettings()
664
+ return CompactionSettings(
665
+ num_chars_per_token=base.num_chars_per_token,
666
+ prune_protect_tokens=base.prune_protect_tokens,
667
+ prune_minimum_tokens=base.prune_minimum_tokens,
668
+ prune_protected_tools=base.prune_protected_tools,
669
+ default_max_context_tokens=self._model_context_resolver.resolve_context_window(
670
+ self._loop_config
671
+ ),
672
+ default_compress_trigger_fraction=base.default_compress_trigger_fraction,
673
+ default_compress_keep_recent_messages=base.default_compress_keep_recent_messages,
674
+ autocompact_llm_prompt_kind=base.autocompact_llm_prompt_kind,
675
+ compact_custom_instructions=base.compact_custom_instructions,
676
+ autocompact_llm_instructions_override=base.autocompact_llm_instructions_override,
677
+ compaction_system_prompt=base.compaction_system_prompt,
678
+ tool_result_max_chars_per_message=base.tool_result_max_chars_per_message,
679
+ tool_result_budget_suffix=base.tool_result_budget_suffix,
680
+ snip_preserve_tail_messages=base.snip_preserve_tail_messages,
681
+ snip_max_tool_content_chars=base.snip_max_tool_content_chars,
682
+ snip_suffix=base.snip_suffix,
683
+ microcompact_skip_query_sources=base.microcompact_skip_query_sources,
684
+ collapse_dedupe_consecutive_same_tool_call_id=base.collapse_dedupe_consecutive_same_tool_call_id,
685
+ autocompact_skip_query_sources=base.autocompact_skip_query_sources,
686
+ autocompact_use_llm_when_available=base.autocompact_use_llm_when_available,
687
+ autocompact_llm_excerpt_chars_per_message=base.autocompact_llm_excerpt_chars_per_message,
688
+ autocompact_snip_carry_threshold_slack=base.autocompact_snip_carry_threshold_slack,
689
+ compaction_token_warn_fraction=base.compaction_token_warn_fraction,
690
+ compaction_block_compact_llm_fraction=base.compaction_block_compact_llm_fraction,
691
+ compaction_block_main_model_fraction=base.compaction_block_main_model_fraction,
692
+ enable_main_model_context_token_block=base.enable_main_model_context_token_block,
693
+ enable_tool_transcript_repair_before_llm=base.enable_tool_transcript_repair_before_llm,
694
+ emit_compaction_pipeline_meta=base.emit_compaction_pipeline_meta,
695
+ )
696
+
697
+ def _build_graph(
698
+ self,
699
+ model_node: ModelNode,
700
+ tool_node: Any,
701
+ resolved_state_schema: Any,
702
+ ) -> Any:
703
+ """构建 StateGraph,注册节点与边,返回未编译的 graph。"""
704
+ self._prepare_compile_context()
705
+ graph_inner = self._create_graph_skeleton(
706
+ model_node=model_node,
707
+ tool_node=tool_node,
708
+ resolved_state_schema=resolved_state_schema,
709
+ )
710
+ routing = self._wire_task_and_hook_nodes(graph_inner=graph_inner, tool_node=tool_node)
711
+ self._wire_main_edges(graph_inner=graph_inner, tool_node=tool_node, routing=routing)
712
+ return graph_inner
713
+
714
+ def _create_graph_skeleton(
715
+ self,
716
+ *,
717
+ model_node: ModelNode,
718
+ tool_node: Any,
719
+ resolved_state_schema: Any,
720
+ ) -> Any:
721
+ state_schemas: set[type] = set()
722
+ base_state = self._state_schema if self._state_schema is not None else AgentState
723
+ state_schemas.add(base_state)
724
+ state_schemas.add(LoopAgentStateFields)
725
+ input_schema = _resolve_schema(state_schemas, "InputSchema", "input")
726
+ output_schema = _resolve_schema(state_schemas, "OutputSchema", "output")
727
+ graph_inner: StateGraph[
728
+ AgentState[ResponseT], ContextT, _InputAgentState, _OutputAgentState[ResponseT]
729
+ ] = StateGraph(
730
+ state_schema=resolved_state_schema,
731
+ input_schema=input_schema,
732
+ output_schema=output_schema,
733
+ context_schema=self._context_schema,
734
+ )
735
+ graph_inner.add_node("model", model_node)
736
+ if tool_node is not None:
737
+ graph_inner.add_node("tools", tool_node)
738
+
739
+ def _prune_and_compress_node(state, runtime):
740
+ return self._run_prune_and_compress_with_trace(state)
741
+
742
+ graph_inner.add_node(
743
+ "prune_and_compress",
744
+ RunnableCallable(_prune_and_compress_node, None, trace=False),
745
+ )
746
+ return graph_inner
747
+
748
+ def _run_prune_and_compress_with_trace(self, state: dict[str, Any]) -> dict[str, Any]:
749
+ """执行 compaction,并在当前 session 内记录 compaction span。"""
750
+ collector = state.get("_trace_collector")
751
+ run_id = state.get("_run_id")
752
+ span_id: str | None = None
753
+ if (
754
+ collector is not None
755
+ and isinstance(run_id, str)
756
+ and run_id
757
+ and hasattr(collector, "has_session")
758
+ and collector.has_session(run_id)
759
+ ):
760
+ span_id = collector.span_start(
761
+ span_type="compaction",
762
+ name="prune_and_compress",
763
+ session_id=run_id,
764
+ metadata={"visibility": "summary_only"},
765
+ )
766
+ try:
767
+ if self._session_memory_compact_bridge is not None:
768
+ self._run_coro_sync(self._session_memory_compact_bridge.wait_for_extraction())
769
+ compacted_messages = self._session_memory_compact_bridge.try_compact(
770
+ list(state.get("messages") or [])
771
+ )
772
+ if compacted_messages is not None:
773
+ return {
774
+ "messages": compacted_messages,
775
+ "collapse_retry_available": True,
776
+ "compaction_pipeline_meta": {
777
+ "autocompact_mode": "session_memory",
778
+ "session_memory_compaction": "applied",
779
+ },
780
+ }
781
+ patch = self._loop_context_compaction.run(state, model=self._model)
782
+ compaction_meta = patch.get("compaction_pipeline_meta")
783
+ if isinstance(compaction_meta, dict):
784
+ patch["collapse_retry_available"] = bool(compaction_meta.get("tokens_freed_sum", 0))
785
+ except Exception as exc:
786
+ if span_id is not None:
787
+ collector.span_end(
788
+ span_id=span_id,
789
+ session_id=run_id,
790
+ error=str(exc),
791
+ metadata={"visibility": "summary_only"},
792
+ )
793
+ raise
794
+ if span_id is not None:
795
+ metadata: dict[str, Any] = {"visibility": "summary_only"}
796
+ compaction_meta = patch.get("compaction_pipeline_meta")
797
+ if isinstance(compaction_meta, dict):
798
+ metadata["compaction_pipeline_meta"] = compaction_meta
799
+ collector.span_end(
800
+ span_id=span_id,
801
+ session_id=run_id,
802
+ metadata=metadata,
803
+ )
804
+ return patch
805
+
806
+ def _wire_task_and_hook_nodes(self, *, graph_inner: Any, tool_node: Any) -> dict[str, Any]:
807
+ entry_node = "hook_before_model"
808
+ loop_entry_node = "hook_before_model"
809
+ raw_loop_entry_node = "hook_before_model"
810
+ provider_chain, has_skill_tool = self._build_provider_chain(tool_node)
811
+ if provider_chain:
812
+ loop_entry_node = self._wire_task_event_nodes(
813
+ graph_inner=graph_inner,
814
+ provider_chain=provider_chain,
815
+ tool_node=tool_node,
816
+ raw_loop_entry_node=raw_loop_entry_node,
817
+ )
818
+ skill_prefetch_provider = self._find_skill_prefetch_provider(
819
+ provider_chain=provider_chain,
820
+ has_skill_tool=has_skill_tool,
821
+ )
822
+ self._wire_hook_nodes(graph_inner)
823
+ self._wire_skill_entry(
824
+ graph_inner=graph_inner,
825
+ skill_prefetch_provider=skill_prefetch_provider,
826
+ entry_node=entry_node,
827
+ )
828
+ graph_inner.add_edge("hook_before_model", "model")
829
+ graph_inner.add_node("loop_controller", lambda state: state)
830
+ return {
831
+ "entry_node": entry_node,
832
+ "loop_entry_node": loop_entry_node,
833
+ "raw_loop_entry_node": raw_loop_entry_node,
834
+ "loop_exit_node": "model",
835
+ "exit_node": "hook_loop_end",
836
+ "provider_chain": provider_chain,
837
+ }
838
+
839
+ def _build_provider_chain(self, tool_node: Any) -> tuple[list[QueuedCommandProvider], bool]:
840
+ provider_chain: list[QueuedCommandProvider] = list(self._queued_command_providers)
841
+ has_skill_tool = bool(tool_node and SKILL_TOOL_NAME in tool_node.tools_by_name)
842
+ has_skill_prefetch_provider = any(
843
+ isinstance(item, SkillPrefetchProvider) for item in provider_chain
844
+ )
845
+ if has_skill_tool and not has_skill_prefetch_provider:
846
+ rt_skill = self._skill_runtime_tool
847
+ if rt_skill is not None:
848
+ wr_skill = getattr(rt_skill, "_workspace_root", None) or self._workspace_root
849
+ ah_skill = getattr(rt_skill, "_agent_home", None) or self._workspace_cfg.agent_home
850
+ else:
851
+ wr_skill = self._workspace_root
852
+ ah_skill = self._workspace_cfg.agent_home
853
+ if wr_skill is None:
854
+ wr_skill = Path.cwd()
855
+ provider_chain.insert(
856
+ 0,
857
+ SkillPrefetchProvider(workspace_root=wr_skill, agent_home=str(ah_skill)),
858
+ )
859
+ if self._task_runtime is not None:
860
+ provider_chain.append(LoopTaskInjectionAdapter(self._task_runtime))
861
+ return provider_chain, has_skill_tool
862
+
863
+ def _wire_task_event_nodes(
864
+ self,
865
+ *,
866
+ graph_inner: Any,
867
+ provider_chain: list[QueuedCommandProvider],
868
+ tool_node: Any,
869
+ raw_loop_entry_node: str,
870
+ ) -> str:
871
+ def _task_event_commit_ack_node(state, runtime):
872
+ pending_batch_id = state.get("pending_task_batch_id")
873
+ pending_command_ids = state.get("pending_task_command_ids")
874
+ task_events: list[dict[str, Any]] = []
875
+ if isinstance(pending_batch_id, str) and pending_batch_id:
876
+ emit_task_event(
877
+ state=state,
878
+ event_type="task.ack",
879
+ batch_id=pending_batch_id,
880
+ command_ids=list(pending_command_ids or []),
881
+ )
882
+ decoded = decode_provider_batch_id(pending_batch_id)
883
+ if decoded is not None:
884
+ provider_index, raw_batch_id = decoded
885
+ if 0 <= provider_index < len(provider_chain):
886
+ provider_chain[provider_index].ack_batch(
887
+ LoopInjectionBatch(
888
+ batch_id=raw_batch_id,
889
+ command_ids=list(pending_command_ids or []),
890
+ attachment_messages=[],
891
+ )
892
+ )
893
+ task_events.append(
894
+ {
895
+ "event_type": "task-notification-consumed",
896
+ "batch_id": pending_batch_id,
897
+ "command_ids": list(pending_command_ids or []),
898
+ "agent_id": state.get("agent_id"),
899
+ }
900
+ )
901
+ return {
902
+ "pending_task_batch_id": None,
903
+ "pending_task_command_ids": [],
904
+ "task_events": task_events,
905
+ }
906
+
907
+ def _task_event_reserve_node(state, runtime):
908
+ agent_id = state.get("agent_id")
909
+ scope = TaskScope(agent_id=agent_id if isinstance(agent_id, str) else None)
910
+ task_events: list[dict[str, Any]] = []
911
+
912
+ provider_index = -1
913
+ batch = None
914
+ try:
915
+ # Let providers optionally ingest current loop messages before reserve.
916
+ # This enables auto-bridge scenarios like tool_use_summary without external pump.
917
+ state_messages = list(state.get("messages") or [])
918
+ for provider in provider_chain:
919
+ ingest = getattr(provider, "ingest_from_messages", None)
920
+ if callable(ingest):
921
+ try:
922
+ ingest(scope=scope, messages=state_messages)
923
+ except Exception:
924
+ logger.debug(
925
+ "provider ingest_from_messages failed; skip provider=%s",
926
+ provider.__class__.__name__,
927
+ exc_info=True,
928
+ )
929
+ for idx, provider in enumerate(provider_chain):
930
+ candidate = provider.build_batch(
931
+ scope=scope,
932
+ max_priority=self._task_notification_max_priority,
933
+ limit=8,
934
+ )
935
+ if candidate.batch_id:
936
+ provider_index = idx
937
+ batch = candidate
938
+ break
939
+ if batch is None or not batch.batch_id:
940
+ if self._task_runtime is not None:
941
+ runtime_events = self._task_runtime.drain_events()
942
+ for event in runtime_events:
943
+ if str(event.event_type.value) == "terminal":
944
+ task_events.append(
945
+ {
946
+ "event_type": "task-terminated",
947
+ "task_id": event.task_id,
948
+ "task_type": event.task_type.value,
949
+ "status": event.status.value,
950
+ "summary": event.summary,
951
+ "agent_id": event.scope.agent_id,
952
+ "terminal_reason": event.terminal_reason,
953
+ "loop_terminal_reason": state.get("terminal_reason"),
954
+ }
955
+ )
956
+ return {
957
+ "pending_task_batch_id": None,
958
+ "pending_task_command_ids": [],
959
+ "task_events": task_events,
960
+ }
961
+ messages = list(state.get("messages") or [])
962
+ # 冻结契约:task 通知附件仅经本节点注入;按 command_id 与已有 messages 去重
963
+ # (见 docs/design-docs/loop/agent-loop/post-tools-injection-contract.md)
964
+ to_attach = filter_attachment_dicts_by_task_command_id(
965
+ messages, list(batch.attachment_messages)
966
+ )
967
+ messages.extend(to_attach)
968
+ emit_task_event(
969
+ state=state,
970
+ event_type="task.reserve",
971
+ batch_id=encode_provider_batch_id(provider_index, batch.batch_id),
972
+ command_ids=list(batch.command_ids),
973
+ attachment_count=len(to_attach),
974
+ )
975
+ task_events.append(
976
+ {
977
+ "event_type": "task-notification-enqueued",
978
+ "batch_id": encode_provider_batch_id(provider_index, batch.batch_id),
979
+ "command_ids": list(batch.command_ids),
980
+ "agent_id": scope.agent_id,
981
+ }
982
+ )
983
+ if self._task_runtime is not None:
984
+ runtime_events = self._task_runtime.drain_events()
985
+ for event in runtime_events:
986
+ if str(event.event_type.value) == "terminal":
987
+ task_events.append(
988
+ {
989
+ "event_type": "task-terminated",
990
+ "task_id": event.task_id,
991
+ "task_type": event.task_type.value,
992
+ "status": event.status.value,
993
+ "summary": event.summary,
994
+ "agent_id": event.scope.agent_id,
995
+ "terminal_reason": event.terminal_reason,
996
+ "loop_terminal_reason": state.get("terminal_reason"),
997
+ }
998
+ )
999
+ return {
1000
+ "messages": messages,
1001
+ "pending_task_batch_id": encode_provider_batch_id(
1002
+ provider_index, batch.batch_id
1003
+ ),
1004
+ "pending_task_command_ids": list(batch.command_ids),
1005
+ "task_events": task_events,
1006
+ }
1007
+ except Exception:
1008
+ if batch is not None and batch.batch_id:
1009
+ if 0 <= provider_index < len(provider_chain):
1010
+ provider_chain[provider_index].nack_batch(batch, requeue=True)
1011
+ raise
1012
+
1013
+ graph_inner.add_node(
1014
+ "task_event_commit_ack",
1015
+ RunnableCallable(_task_event_commit_ack_node, None, trace=False),
1016
+ )
1017
+ graph_inner.add_node(
1018
+ "task_event_reserve",
1019
+ RunnableCallable(_task_event_reserve_node, None, trace=False),
1020
+ )
1021
+ graph_inner.add_edge("task_event_commit_ack", "task_event_reserve")
1022
+ # Phase 5A:压缩收束到「每轮进入 model 之前」唯一入口;有 task 链时在注入附件之后再压缩。
1023
+ if tool_node is not None:
1024
+ graph_inner.add_edge("task_event_reserve", "prune_and_compress")
1025
+ graph_inner.add_edge("prune_and_compress", raw_loop_entry_node)
1026
+ else:
1027
+ graph_inner.add_edge("task_event_reserve", raw_loop_entry_node)
1028
+ return "task_event_commit_ack"
1029
+
1030
+ def _find_skill_prefetch_provider(
1031
+ self,
1032
+ *,
1033
+ provider_chain: list[QueuedCommandProvider],
1034
+ has_skill_tool: bool,
1035
+ ) -> SkillPrefetchProvider | None:
1036
+ if not has_skill_tool:
1037
+ return None
1038
+ return next(
1039
+ (p for p in provider_chain if isinstance(p, SkillPrefetchProvider)),
1040
+ None,
1041
+ )
1042
+
1043
+ def _wire_hook_nodes(self, graph_inner: Any) -> None:
1044
+ graph_inner.add_node(
1045
+ "hook_loop_start",
1046
+ RunnableCallable(self._hook_graph_wiring.loop_start_node, None, trace=False),
1047
+ )
1048
+ graph_inner.add_node(
1049
+ "hook_before_model",
1050
+ RunnableCallable(self._hook_graph_wiring.before_model_node, None, trace=False),
1051
+ )
1052
+ graph_inner.add_node(
1053
+ "hook_after_model",
1054
+ RunnableCallable(self._hook_graph_wiring.after_model_node, None, trace=False),
1055
+ )
1056
+ graph_inner.add_node(
1057
+ "hook_stop",
1058
+ RunnableCallable(self._hook_graph_wiring.stop_hooks_node, None, trace=False),
1059
+ )
1060
+ graph_inner.add_node(
1061
+ "hook_loop_end",
1062
+ RunnableCallable(self._hook_graph_wiring.loop_end_node, None, trace=False),
1063
+ )
1064
+ graph_inner.add_edge("hook_loop_end", END)
1065
+
1066
+ def _wire_skill_entry(
1067
+ self,
1068
+ *,
1069
+ graph_inner: Any,
1070
+ skill_prefetch_provider: SkillPrefetchProvider | None,
1071
+ entry_node: str,
1072
+ ) -> None:
1073
+ if skill_prefetch_provider is not None:
1074
+ skill_init_scope = TaskScope(agent_id=None)
1075
+
1076
+ def _skill_init_node(state, runtime):
1077
+ messages = list(state.get("messages") or [])
1078
+ # 已有 skill_listing 则幂等跳过(任何类型消息含 [skill_listing] 前缀)
1079
+ for msg in messages:
1080
+ content = getattr(msg, "content", "")
1081
+ if isinstance(content, str) and "[skill_listing]" in content:
1082
+ return {}
1083
+ hint = skill_prefetch_provider._build_listing_hint(scope=skill_init_scope)
1084
+ if hint is None:
1085
+ return {}
1086
+ return {"messages": [SystemMessage(content=f"[skill_listing] {hint.summary}")]}
1087
+
1088
+ graph_inner.add_node(
1089
+ "skill_init",
1090
+ RunnableCallable(_skill_init_node, None, trace=False),
1091
+ )
1092
+ graph_inner.add_edge(START, "skill_init")
1093
+ graph_inner.add_edge("skill_init", "hook_loop_start")
1094
+ graph_inner.add_edge("hook_loop_start", entry_node)
1095
+ return
1096
+ graph_inner.add_edge(START, "hook_loop_start")
1097
+ graph_inner.add_edge("hook_loop_start", entry_node)
1098
+
1099
+ def _wire_main_edges(self, *, graph_inner: Any, tool_node: Any, routing: dict[str, Any]) -> None:
1100
+ loop_entry_node = routing["loop_entry_node"]
1101
+ loop_exit_node = routing["loop_exit_node"]
1102
+ exit_node = routing["exit_node"]
1103
+ provider_chain = routing["provider_chain"]
1104
+ if tool_node is not None:
1105
+ self._wire_main_edges_with_tools(
1106
+ graph_inner=graph_inner,
1107
+ tool_node=tool_node,
1108
+ loop_entry_node=loop_entry_node,
1109
+ loop_exit_node=loop_exit_node,
1110
+ exit_node=exit_node,
1111
+ provider_chain=provider_chain,
1112
+ )
1113
+ return
1114
+ if len(self._structured_output_tools) > 0:
1115
+ self._wire_main_edges_with_structured_output(
1116
+ graph_inner=graph_inner,
1117
+ loop_entry_node=loop_entry_node,
1118
+ loop_exit_node=loop_exit_node,
1119
+ )
1120
+ return
1121
+ self._wire_main_edges_without_tools(
1122
+ graph_inner=graph_inner,
1123
+ loop_exit_node=loop_exit_node,
1124
+ )
1125
+
1126
+ def _wire_main_edges_with_tools(
1127
+ self,
1128
+ *,
1129
+ graph_inner: Any,
1130
+ tool_node: Any,
1131
+ loop_entry_node: str,
1132
+ loop_exit_node: str,
1133
+ exit_node: str,
1134
+ provider_chain: list[QueuedCommandProvider],
1135
+ ) -> None:
1136
+ if provider_chain:
1137
+ tools_model_destination = loop_entry_node
1138
+ tools_to_model_destinations = [loop_entry_node, exit_node]
1139
+ else:
1140
+ tools_model_destination = "prune_and_compress"
1141
+ tools_to_model_destinations = ["prune_and_compress", exit_node]
1142
+
1143
+ graph_inner.add_conditional_edges(
1144
+ "tools",
1145
+ RunnableCallable(
1146
+ _make_tools_to_model_edge(
1147
+ tool_node=tool_node,
1148
+ model_destination=tools_model_destination,
1149
+ structured_output_tools=self._structured_output_tools,
1150
+ end_destination=exit_node,
1151
+ ),
1152
+ trace=False,
1153
+ ),
1154
+ tools_to_model_destinations,
1155
+ )
1156
+ if not provider_chain:
1157
+ graph_inner.add_edge("prune_and_compress", loop_entry_node)
1158
+ graph_inner.add_edge(loop_exit_node, "hook_after_model")
1159
+ graph_inner.add_edge("hook_after_model", "hook_stop")
1160
+ graph_inner.add_edge("hook_stop", "loop_controller")
1161
+
1162
+ loop_controller_destinations = ["tools", loop_entry_node, exit_node]
1163
+ if provider_chain and "task_event_commit_ack" not in loop_controller_destinations:
1164
+ loop_controller_destinations.append("task_event_commit_ack")
1165
+ graph_inner.add_conditional_edges(
1166
+ "loop_controller",
1167
+ RunnableCallable(
1168
+ _make_loop_controller_edge(
1169
+ model_destination=loop_entry_node,
1170
+ tools_destination="tools",
1171
+ structured_output_tools=self._structured_output_tools,
1172
+ end_destination=exit_node,
1173
+ max_output_tokens_cap=self._model_context_resolver.resolve_max_output_tokens(
1174
+ self._loop_config
1175
+ ),
1176
+ token_budget_max_continuations=self._loop_config.token_budget_max_continuations,
1177
+ task_reconcile_destination=(
1178
+ "task_event_commit_ack" if provider_chain else None
1179
+ ),
1180
+ has_pending_task_notifications=self._build_pending_task_notifications_checker(
1181
+ provider_chain
1182
+ ),
1183
+ ),
1184
+ trace=False,
1185
+ ),
1186
+ loop_controller_destinations,
1187
+ )
1188
+
1189
+ def _wire_main_edges_with_structured_output(
1190
+ self,
1191
+ *,
1192
+ graph_inner: Any,
1193
+ loop_entry_node: str,
1194
+ loop_exit_node: str,
1195
+ ) -> None:
1196
+ graph_inner.add_edge(loop_exit_node, "hook_after_model")
1197
+ graph_inner.add_conditional_edges(
1198
+ "hook_after_model",
1199
+ RunnableCallable(
1200
+ _make_model_to_model_edge(
1201
+ model_destination=loop_entry_node,
1202
+ end_destination="hook_stop",
1203
+ ),
1204
+ trace=False,
1205
+ ),
1206
+ [loop_entry_node, "hook_stop"],
1207
+ )
1208
+ graph_inner.add_edge("hook_stop", "hook_loop_end")
1209
+
1210
+ def _wire_main_edges_without_tools(
1211
+ self,
1212
+ *,
1213
+ graph_inner: Any,
1214
+ loop_exit_node: str,
1215
+ ) -> None:
1216
+ graph_inner.add_edge(loop_exit_node, "hook_after_model")
1217
+ graph_inner.add_edge("hook_after_model", "hook_stop")
1218
+ graph_inner.add_edge("hook_stop", "hook_loop_end")
1219
+
1220
+ def _build_pending_task_notifications_checker(
1221
+ self,
1222
+ provider_chain: list[QueuedCommandProvider],
1223
+ ) -> Callable[[dict[str, Any]], bool] | None:
1224
+ if not provider_chain:
1225
+ return None
1226
+
1227
+ def has_pending(state: dict[str, Any]) -> bool:
1228
+ return any(
1229
+ provider.has_pending(
1230
+ TaskScope(
1231
+ agent_id=state.get("agent_id")
1232
+ if isinstance(state.get("agent_id"), str)
1233
+ else None
1234
+ ),
1235
+ max_priority=self._task_notification_max_priority,
1236
+ )
1237
+ for provider in provider_chain
1238
+ )
1239
+
1240
+ return has_pending
1241
+
1242
+ def build_compiled(self) -> Any:
1243
+ """完整构建并编译 graph,等价于 create_loop_agent 内部流程。"""
1244
+ self._prepare_compile_context()
1245
+ tool_node = self._build_tool_node()
1246
+ model_node = self._build_model_nodes(tool_node)
1247
+ state_schemas: set[type] = set()
1248
+ base_state = self._raw_state_schema if self._raw_state_schema is not None else AgentState
1249
+ state_schemas.add(base_state)
1250
+ state_schemas.add(LoopAgentStateFields)
1251
+ resolved_state_schema = _resolve_schema(state_schemas, "StateSchema", None)
1252
+ graph = self._build_graph(model_node, tool_node, resolved_state_schema)
1253
+ return self.compile(graph)
1254
+
1255
+ def compile(self, graph: Any) -> Any:
1256
+ """编译 graph,注入 configurable,返回 CompiledStateGraph。"""
1257
+ self._prepare_compile_context()
1258
+ config: RunnableConfig = {"recursion_limit": compute_recursion_limit(self._max_steps_param)}
1259
+ if self._name:
1260
+ config["metadata"] = {"lc_agent_name": self._name}
1261
+ if self._session_store is not None:
1262
+ if "configurable" not in config:
1263
+ config["configurable"] = {}
1264
+ config["configurable"]["session_store"] = self._session_store
1265
+ if self._raw_loader is not None:
1266
+ if "configurable" not in config:
1267
+ config["configurable"] = {}
1268
+ config["configurable"]["tool_loader"] = self._raw_loader
1269
+ config["configurable"]["tool_registry"] = self._raw_loader.registry
1270
+ compiled = graph.compile(
1271
+ debug=self._debug,
1272
+ name=self._name,
1273
+ )
1274
+ compiled = compiled.with_config(config)
1275
+ compiled = compiled.with_config({"configurable": {"loop_config": self._loop_config}})
1276
+ if self._trace_tool_callback_handler is not None:
1277
+ compiled = compiled.with_config({"callbacks": [self._trace_tool_callback_handler]})
1278
+ self._wrap_compiled_ainvoke_with_trace_failure_fallback(compiled)
1279
+ self._log.info("create_loop_agent completed")
1280
+ return compiled
1281
+
1282
+ def _wrap_compiled_ainvoke_with_trace_failure_fallback(self, compiled: Any) -> None:
1283
+ """在 graph 异常路径上兜底 end_session,避免 loop_end_node 未执行导致会话悬挂。"""
1284
+ collector = self._trace_collector
1285
+ session_id = self._session_id
1286
+ if collector is None or not session_id or not hasattr(compiled, "ainvoke"):
1287
+ return
1288
+
1289
+ original_ainvoke = compiled.ainvoke
1290
+
1291
+ async def _ainvoke_with_trace_fallback(this: Any, *args: Any, **kwargs: Any) -> Any:
1292
+ try:
1293
+ return await original_ainvoke(*args, **kwargs)
1294
+ except Exception as exc:
1295
+ if collector.has_session(session_id):
1296
+ collector.end_session(
1297
+ session_id=session_id,
1298
+ status="failed",
1299
+ terminal_reason=str(exc),
1300
+ )
1301
+ raise
1302
+
1303
+ compiled.ainvoke = MethodType(_ainvoke_with_trace_fallback, compiled)
1304
+
1305
+
1306
+ def create_loop_agent(
1307
+ model: str | BaseChatModel,
1308
+ *,
1309
+ # Prompt 配置
1310
+ system_prompt: str | SystemMessage | None = None,
1311
+ override_system_prompt: str | None = None,
1312
+ append_system_prompt: str | None = None,
1313
+ dynamic_sections: Sequence[SystemPromptSection] = (),
1314
+ # Model 配置
1315
+ response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,
1316
+ fallback_model: BaseChatModel | None = None,
1317
+ enable_tool_call_degradation_fix: bool = True,
1318
+ # Session / Runtime 配置
1319
+ session_id: str | None = None,
1320
+ session_store: "AgentSessionStore | None" = None,
1321
+ loader: "ToolRuntimeLoader | None" = None,
1322
+ permission_resolver: Any | None = None,
1323
+ # Graph / Runtime 基础配置
1324
+ state_schema: type[AgentState[ResponseT]] | None = None,
1325
+ context_schema: type[ContextT] | None = None,
1326
+ debug: bool = False,
1327
+ name: str | None = None,
1328
+ # LangChain AgentX 特有参数
1329
+ max_steps: int | None = None,
1330
+ max_turns: int | None = None,
1331
+ is_subagent: bool = False,
1332
+ subagent_type: str | None = None,
1333
+ container_type: str | None = None,
1334
+ # Observability
1335
+ enable_trace: bool = True,
1336
+ trace_collector: TraceCollector | None = None,
1337
+ parent_run_id: str | None = None,
1338
+ parent_tool_call_id: str | None = None,
1339
+ parent_conversation_session_id: str | None = None,
1340
+ trace_visibility: str | None = None,
1341
+ # Task Runtime / Provider
1342
+ task_runtime: TaskRuntime | None = None,
1343
+ task_notification_max_priority: QueuePriority = QueuePriority.NEXT,
1344
+ queued_command_providers: Sequence[QueuedCommandProvider] = (),
1345
+ managed_rules: str | None = None,
1346
+ active_files_getter: "Callable[[], list[str]] | None" = None,
1347
+ workspace_root: str | Path | None = None,
1348
+ agent_home: str = ".langchain_agentx",
1349
+ workspace_trust_accepted: bool | None = None,
1350
+ hooks: "HookRegistry | None" = None,
1351
+ enable_session_memory: bool = False,
1352
+ ) -> CompiledStateGraph[
1353
+ AgentState[ResponseT], ContextT, _InputAgentState, _OutputAgentState[ResponseT]
1354
+ ]:
1355
+
1356
+
1357
+ builder = LoopGraphBuilder(
1358
+ model,
1359
+ system_prompt=system_prompt,
1360
+ override_system_prompt=override_system_prompt,
1361
+ append_system_prompt=append_system_prompt,
1362
+ dynamic_sections=dynamic_sections,
1363
+ response_format=response_format,
1364
+ fallback_model=fallback_model,
1365
+ enable_tool_call_degradation_fix=enable_tool_call_degradation_fix,
1366
+ session_id=session_id,
1367
+ session_store=session_store,
1368
+ loader=loader,
1369
+ permission_resolver=permission_resolver,
1370
+ state_schema=state_schema,
1371
+ context_schema=context_schema,
1372
+ debug=debug,
1373
+ name=name,
1374
+ max_steps=max_steps,
1375
+ max_turns=max_turns,
1376
+ is_subagent=is_subagent,
1377
+ subagent_type=subagent_type,
1378
+ container_type=container_type,
1379
+ enable_trace=enable_trace,
1380
+ trace_collector=trace_collector,
1381
+ parent_run_id=parent_run_id,
1382
+ parent_tool_call_id=parent_tool_call_id,
1383
+ parent_conversation_session_id=parent_conversation_session_id,
1384
+ trace_visibility=trace_visibility,
1385
+ task_runtime=task_runtime,
1386
+ task_notification_max_priority=task_notification_max_priority,
1387
+ queued_command_providers=queued_command_providers,
1388
+ managed_rules=managed_rules,
1389
+ active_files_getter=active_files_getter,
1390
+ workspace_root=workspace_root,
1391
+ agent_home=agent_home,
1392
+ workspace_trust_accepted=workspace_trust_accepted,
1393
+ raw_hooks=hooks,
1394
+ enable_session_memory=enable_session_memory,
1395
+ )
1396
+ tool_node = builder._build_tool_node()
1397
+ model_node = builder._build_model_nodes(tool_node)
1398
+ state_schemas: set[type] = set()
1399
+ base_state = state_schema if state_schema is not None else AgentState
1400
+ state_schemas.add(base_state)
1401
+ state_schemas.add(LoopAgentStateFields)
1402
+ resolved_state_schema = _resolve_schema(state_schemas, "StateSchema", None)
1403
+ graph = builder._build_graph(model_node, tool_node, resolved_state_schema)
1404
+ return builder.compile(graph)
1405
+
1406
+
1407
+ __all__ = [
1408
+ "create_loop_agent",
1409
+ ]