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,447 @@
1
+ """
2
+ runtime/policy.py — 工具权限策略引擎
3
+
4
+ 职责:
5
+ 提供可复用的最小策略层,将工具本地校验(validate_input)与
6
+ 全局权限治理(check_permissions)分开。PolicyEngine 作为可注入
7
+ 的策略对象,由 RuntimeTool.__init__ 接收,在 check_permissions
8
+ 默认实现中委托调用。
9
+
10
+ DefaultPolicyEngine 实现 v1.5 范围:
11
+ - read_roots / write_roots 读写分离路径限制
12
+ - deny_globs 用户自定义禁止模式匹配
13
+ - ask_globs 需要人工确认的敏感路径模式(headless 时降级为 deny)
14
+ - enable_builtin_deny 内置危险路径黑名单(.env/.ssh/*.key 等)
15
+ - read_only_mode 全局只读
16
+ - allow / deny / ask 三种决策
17
+
18
+ 决策优先级(严格顺序):
19
+ [1] read_only_mode + is_destructive → deny
20
+ [2] enable_builtin_deny → 内置黑名单 → deny
21
+ [3] deny_globs 命中 → deny
22
+ [4] ask_globs 命中 → ask(headless 模式下由 pipeline 降级为 deny)
23
+ [5] 读写分权:is_read_only → read_roots;否则 → write_roots
24
+ [6] 通过 → allow
25
+
26
+ 与 CC 对比:
27
+ CC 的权限系统分三层:
28
+ Layer 1: 每个工具的 checkPermissions()
29
+ Layer 2: hasPermissionsToUseToolInner() 全局引擎
30
+ alwaysDenyRules → alwaysAskRules → alwaysAllowRules → PermissionMode
31
+ Layer 3: HITL race(User UI + Hooks + Classifier + Bridge)
32
+ 本框架 v1.5 实现 Layer 1 + Layer 2 完整版:
33
+ - deny_globs + BUILTIN_DENY_GLOBS 对应 CC alwaysDenyRules
34
+ - ask_globs 对应 CC alwaysAskRules(需人工确认路径)
35
+ - read_roots / write_roots 对应 CC allWorkingDirectories
36
+ - headless 模式 ask → deny 对应 CC shouldAvoidPermissionPrompts
37
+ - HITL 交互(Layer 3 完整 race)延期到 v2
38
+
39
+ 详细设计见 docs/design-docs/tool-runtime/tool-permission-system.md
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import fnmatch
45
+ import os
46
+ from abc import ABC, abstractmethod
47
+ from dataclasses import dataclass, field
48
+ from typing import Any
49
+
50
+ from .models import AuthorizationDecision, ToolExecutionContext
51
+ from .permission_context import get_active_auto_approved_tools
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # ToolPolicyConfig
56
+ # ---------------------------------------------------------------------------
57
+
58
+ @dataclass
59
+ class ToolPolicyConfig:
60
+ """
61
+ 工具策略配置。
62
+
63
+ read_roots: 允许读操作的路径根列表(read/glob/grep 等只读工具)。
64
+ 空列表 = 不限制(开发模式)。
65
+ write_roots: 允许写操作的路径根列表(write/edit/bash 等写工具)。
66
+ 通常是 read_roots 的子集。空列表 = 不限制(开发模式)。
67
+ deny_globs: 硬拒绝的 glob 模式列表,优先级高于 roots 检查。
68
+ 对应 CC alwaysDenyRules。
69
+ 示例:["**/secrets/**", "**/*.prod.env"]
70
+ ask_globs: 需要人工确认的敏感路径 glob 模式列表。
71
+ 对应 CC alwaysAskRules。
72
+ headless 模式下自动降级为 deny;交互模式下触发 HITL。
73
+ 示例:["**/config/**", "**/.claude/**"]
74
+ read_only_mode: 全局只读模式,is_destructive=True 的工具直接拒绝。
75
+ enable_builtin_deny: 启用内置危险路径保护(.env/.ssh/*.key/credentials 等)。
76
+ 生产环境建议保持 True。
77
+ """
78
+
79
+ read_roots: list[str] = field(default_factory=list)
80
+ write_roots: list[str] = field(default_factory=list)
81
+ deny_globs: list[str] = field(default_factory=list)
82
+ ask_globs: list[str] = field(default_factory=list)
83
+ read_only_mode: bool = False
84
+ enable_builtin_deny: bool = True
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # PolicyEngine 抽象基类
89
+ # ---------------------------------------------------------------------------
90
+
91
+ class PolicyEngine(ABC):
92
+ """
93
+ 权限策略引擎抽象基类。
94
+
95
+ 由 RuntimeTool.check_permissions() 默认实现委托调用。
96
+ 子类实现 authorize() / aauthorize() 提供具体策略决策。
97
+ """
98
+
99
+ @abstractmethod
100
+ def authorize(
101
+ self,
102
+ *,
103
+ tool_name: str,
104
+ input_data: dict[str, Any],
105
+ ctx: ToolExecutionContext,
106
+ ) -> AuthorizationDecision:
107
+ """同步授权决策。"""
108
+ ...
109
+
110
+ async def aauthorize(
111
+ self,
112
+ *,
113
+ tool_name: str,
114
+ input_data: dict[str, Any],
115
+ ctx: ToolExecutionContext,
116
+ ) -> AuthorizationDecision:
117
+ """
118
+ 异步授权决策,默认委托同步 authorize()。
119
+ 有真正异步需求(如远程策略服务)的子类可覆盖。
120
+ """
121
+ return self.authorize(tool_name=tool_name, input_data=input_data, ctx=ctx)
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # DefaultPolicyEngine — v1 实现
126
+ # ---------------------------------------------------------------------------
127
+
128
+ class DefaultPolicyEngine(PolicyEngine):
129
+ """
130
+ v1.5 默认策略引擎。
131
+
132
+ 决策优先级(严格顺序):
133
+ [1] read_only_mode + is_destructive → deny 最高优先级
134
+ [2] enable_builtin_deny → BUILTIN_DENY_GLOBS → deny
135
+ [3] deny_globs 命中 → deny
136
+ [4] ask_globs 命中 → ask(headless 时 pipeline 降级为 deny)
137
+ [5] 读写分权:
138
+ is_read_only=True → 检查 read_roots
139
+ is_read_only=False → 检查 write_roots
140
+ roots 为空列表 → 跳过,视为不限制
141
+ [6] 通过 → allow
142
+
143
+ 对应 CC:
144
+ [1] ~ read_only_mode(全局禁止写操作)
145
+ [2][3] ~ alwaysDenyRules(固定拒绝)
146
+ [4] ~ alwaysAskRules(需确认)
147
+ [5] ~ allWorkingDirectories(访问空间限制)
148
+
149
+ 工具读写标志来源:
150
+ 从 ctx.tool_flags["is_read_only"] / ctx.tool_flags["is_destructive"] 读取。
151
+ 由 LangChainAdapter.build_tool_execution_context() 在构造 ctx 时注入。
152
+
153
+ 路径提取规则:
154
+ 从 input_data 中按优先级查找路径字段:file_path > path > pattern
155
+ 找不到路径字段时跳过路径相关检查(直接 allow)。
156
+
157
+ 路径校验:
158
+ 使用 realpath 双检(对 path 和 root 均做 realpath),
159
+ 防路径遍历(../)、符号链接绕过、前缀误匹配。
160
+ """
161
+
162
+ # 按优先级尝试的路径字段名
163
+ _PATH_FIELDS = ("file_path", "path", "pattern")
164
+
165
+ # 内置危险路径黑名单,对应 CC 的危险目录/文件检查
166
+ BUILTIN_DENY_GLOBS: list[str] = [
167
+ # 环境变量 / 配置
168
+ "**/.env", "**/.env.*", "**/*.env",
169
+ # SSH 密钥
170
+ "**/id_rsa", "**/id_rsa.pub", "**/id_ed25519", "**/id_ed25519.pub",
171
+ "**/.ssh/**",
172
+ # 证书与密钥文件
173
+ "**/*.pem", "**/*.key", "**/*.p12", "**/*.pfx",
174
+ # 云服务凭证
175
+ "**/.aws/credentials", "**/.aws/config",
176
+ "**/.gcp/**", "**/service-account*.json",
177
+ # 密码/密钥目录
178
+ "**/secrets/**", "**/.secrets/**", "**/credentials/**",
179
+ ]
180
+
181
+ def __init__(self, config: ToolPolicyConfig) -> None:
182
+ self._config = config
183
+ self._read_roots = [os.path.realpath(r) for r in config.read_roots]
184
+ self._write_roots = [os.path.realpath(r) for r in config.write_roots]
185
+
186
+ def authorize(
187
+ self,
188
+ *,
189
+ tool_name: str,
190
+ input_data: dict[str, Any],
191
+ ctx: ToolExecutionContext,
192
+ ) -> AuthorizationDecision:
193
+ tool_flags = ctx.tool_flags if hasattr(ctx, "tool_flags") else {}
194
+ is_destructive = tool_flags.get("is_destructive", False)
195
+ is_read_only_tool = tool_flags.get("is_read_only", True)
196
+ active_auto_approved_tools = get_active_auto_approved_tools(
197
+ getattr(ctx, "session_store", None)
198
+ )
199
+ active_auto_approved_tools_lower = {
200
+ name.lower() for name in active_auto_approved_tools
201
+ }
202
+
203
+ # [1] 全局只读模式:拒绝破坏性工具
204
+ if self._config.read_only_mode and is_destructive:
205
+ return self._with_decision_source(
206
+ AuthorizationDecision(
207
+ behavior="deny",
208
+ message="Operation not permitted: agent is in read-only mode.",
209
+ policy_id="read_only_mode",
210
+ ),
211
+ matched_rule="read_only_mode",
212
+ )
213
+
214
+ # Bash 工具按“命令字符串”做规则匹配,不参与路径 roots 校验。
215
+ # 对齐 docs/design-docs/tool-design/bash-tool.md 的 v2 方案。
216
+ if tool_name == "bash":
217
+ command = input_data.get("command")
218
+ if not isinstance(command, str) or not command.strip():
219
+ return AuthorizationDecision(behavior="allow")
220
+
221
+ deny_result = self._check_deny_globs(command)
222
+ if deny_result is not None:
223
+ return self._with_decision_source(
224
+ deny_result,
225
+ matched_rule=deny_result.policy_id,
226
+ )
227
+
228
+ ask_result = self._check_ask_globs(command)
229
+ if ask_result is not None:
230
+ return self._with_decision_source(
231
+ ask_result,
232
+ matched_rule=ask_result.policy_id,
233
+ )
234
+
235
+ return self._with_decision_source(
236
+ AuthorizationDecision(behavior="allow"),
237
+ matched_rule="default_allow",
238
+ )
239
+
240
+ # 提取并规范化路径字段
241
+ path = self._extract_path(input_data)
242
+ if path is None:
243
+ return self._with_decision_source(
244
+ AuthorizationDecision(behavior="allow"),
245
+ matched_rule="no_path_field",
246
+ )
247
+
248
+ real_path = os.path.realpath(os.path.expanduser(path))
249
+
250
+ # [2] 用户自定义 deny_globs
251
+ deny_result = self._check_deny_globs(real_path)
252
+ if deny_result is not None:
253
+ return self._with_decision_source(
254
+ deny_result,
255
+ matched_rule=deny_result.policy_id,
256
+ )
257
+
258
+ # [3] 内置危险路径黑名单
259
+ if self._config.enable_builtin_deny:
260
+ for pattern in self.BUILTIN_DENY_GLOBS:
261
+ if fnmatch.fnmatch(real_path, pattern):
262
+ return self._with_decision_source(
263
+ AuthorizationDecision(
264
+ behavior="deny",
265
+ message="Permission denied: access to this path is not allowed.",
266
+ policy_id="builtin_deny",
267
+ ),
268
+ matched_rule="builtin_deny",
269
+ )
270
+
271
+ # [4] ask_globs:敏感路径需要人工确认
272
+ # headless 模式下 pipeline 会将 ask 自动降级为 deny;交互模式触发 HITL
273
+ ask_result = self._check_ask_globs(real_path)
274
+ if ask_result is not None:
275
+ if tool_name.lower() in active_auto_approved_tools_lower:
276
+ decision = AuthorizationDecision(
277
+ behavior="allow",
278
+ policy_id="skill_auto_approved_bypass_ask",
279
+ metadata={
280
+ "tool_name": tool_name,
281
+ "source": "skill_allowed_tools",
282
+ "decision_trace": [
283
+ {
284
+ "stage": "ask_globs",
285
+ "matched": True,
286
+ "decision": "allow",
287
+ "reason": "skill_auto_approved_bypass_ask",
288
+ "tool_name": tool_name,
289
+ "active_auto_approved_tools": sorted(active_auto_approved_tools),
290
+ }
291
+ ],
292
+ },
293
+ )
294
+ return self._with_decision_source(
295
+ decision,
296
+ matched_rule="skill_auto_approved_bypass_ask",
297
+ )
298
+ return self._with_decision_source(
299
+ ask_result,
300
+ matched_rule=ask_result.policy_id,
301
+ )
302
+
303
+ # [5] 读写分权根目录检查
304
+ roots = self._read_roots if is_read_only_tool else self._write_roots
305
+ if roots:
306
+ if not any(self._within_root(real_path, r) for r in roots):
307
+ return self._with_decision_source(
308
+ AuthorizationDecision(
309
+ behavior="deny",
310
+ message="Permission denied: path is outside the allowed workspace.",
311
+ policy_id="read_roots" if is_read_only_tool else "write_roots",
312
+ ),
313
+ matched_rule="read_roots" if is_read_only_tool else "write_roots",
314
+ )
315
+
316
+ return self._with_decision_source(
317
+ AuthorizationDecision(behavior="allow"),
318
+ matched_rule="default_allow",
319
+ )
320
+
321
+ def authorize_path(
322
+ self,
323
+ path: str,
324
+ *,
325
+ is_write: bool,
326
+ ) -> AuthorizationDecision:
327
+ """
328
+ 对单个文件系统路径做授权。
329
+
330
+ 供 BashRuntimeTool v2 的路径安全层调用,避免 bash 因 command-string
331
+ 特殊分支而跳过 read_roots / write_roots / builtin deny 检查。
332
+ """
333
+ real_path = os.path.realpath(os.path.expanduser(path))
334
+
335
+ if self._config.read_only_mode and is_write:
336
+ return self._with_decision_source(
337
+ AuthorizationDecision(
338
+ behavior="deny",
339
+ message="Operation not permitted: agent is in read-only mode.",
340
+ policy_id="read_only_mode",
341
+ ),
342
+ matched_rule="read_only_mode",
343
+ )
344
+
345
+ deny_result = self._check_deny_globs(real_path)
346
+ if deny_result is not None:
347
+ return self._with_decision_source(
348
+ deny_result,
349
+ matched_rule=deny_result.policy_id,
350
+ )
351
+
352
+ if self._config.enable_builtin_deny:
353
+ for pattern in self.BUILTIN_DENY_GLOBS:
354
+ if fnmatch.fnmatch(real_path, pattern):
355
+ return self._with_decision_source(
356
+ AuthorizationDecision(
357
+ behavior="deny",
358
+ message="Permission denied: access to this path is not allowed.",
359
+ policy_id="builtin_deny",
360
+ ),
361
+ matched_rule="builtin_deny",
362
+ )
363
+
364
+ ask_result = self._check_ask_globs(real_path)
365
+ if ask_result is not None:
366
+ return self._with_decision_source(
367
+ ask_result,
368
+ matched_rule=ask_result.policy_id,
369
+ )
370
+
371
+ roots = self._write_roots if is_write else self._read_roots
372
+ if roots:
373
+ if not any(self._within_root(real_path, root) for root in roots):
374
+ return self._with_decision_source(
375
+ AuthorizationDecision(
376
+ behavior="deny",
377
+ message="Permission denied: path is outside the allowed workspace.",
378
+ policy_id="write_roots" if is_write else "read_roots",
379
+ ),
380
+ matched_rule="write_roots" if is_write else "read_roots",
381
+ )
382
+
383
+ return self._with_decision_source(
384
+ AuthorizationDecision(behavior="allow"),
385
+ matched_rule="default_allow",
386
+ )
387
+
388
+ # ---- 内部辅助 ----
389
+
390
+ def _extract_path(self, input_data: dict[str, Any]) -> str | None:
391
+ for field_name in self._PATH_FIELDS:
392
+ val = input_data.get(field_name)
393
+ if isinstance(val, str) and val:
394
+ return val
395
+ return None
396
+
397
+ def _check_deny_globs(self, real_path: str) -> AuthorizationDecision | None:
398
+ for glob_pattern in self._config.deny_globs:
399
+ if fnmatch.fnmatch(real_path, glob_pattern):
400
+ return AuthorizationDecision(
401
+ behavior="deny",
402
+ message="Permission denied: path matches a restricted pattern.",
403
+ policy_id=f"deny_glob:{glob_pattern}",
404
+ )
405
+ return None
406
+
407
+ @staticmethod
408
+ def _with_decision_source(
409
+ decision: AuthorizationDecision,
410
+ *,
411
+ matched_rule: str | None,
412
+ ) -> AuthorizationDecision:
413
+ meta = dict(decision.metadata or {})
414
+ meta.setdefault("rule_source", "default_policy_engine")
415
+ if matched_rule is not None:
416
+ meta.setdefault("matched_rule", matched_rule)
417
+ decision.metadata = meta
418
+ return decision
419
+
420
+ def _check_ask_globs(self, real_path: str) -> AuthorizationDecision | None:
421
+ """
422
+ 检查路径是否命中 ask_globs(需要人工确认的敏感路径)。
423
+ 命中时返回 behavior="ask",由 pipeline 根据 headless 标志决定后续处理:
424
+ headless=True → pipeline 自动降级为 deny
425
+ headless=False → pipeline 抛出 PermissionAskInterrupt
426
+ """
427
+ for glob_pattern in self._config.ask_globs:
428
+ if fnmatch.fnmatch(real_path, glob_pattern):
429
+ return AuthorizationDecision(
430
+ behavior="ask",
431
+ ask_prompt=(
432
+ f"Tool wants to access a sensitive path: {real_path}\n"
433
+ f"(matched pattern: {glob_pattern})\n"
434
+ f"Allow this operation?"
435
+ ),
436
+ policy_id=f"ask_glob:{glob_pattern}",
437
+ )
438
+ return None
439
+
440
+ @staticmethod
441
+ def _within_root(path: str, root: str) -> bool:
442
+ """
443
+ 判断 path 是否在 root 目录内。
444
+ root 已在 __init__ 中 realpath,path 在调用前已 realpath,
445
+ 使用 os.sep 边界确保不会前缀误匹配(如 /proj 不会匹配 /proj_evil)。
446
+ """
447
+ return path == root or path.startswith(root.rstrip(os.sep) + os.sep)
@@ -0,0 +1,81 @@
1
+ """
2
+ runtime/registry.py — 工具注册表
3
+
4
+ 职责:
5
+ 管理所有 RuntimeTool 的注册与查询。
6
+ register() 只存储 RuntimeTool,不构造 StructuredTool。
7
+ to_langchain_tools() 在需要时按需构造 StructuredTool;控制面由 RunnableConfig 注入。
8
+
9
+ 与 CC 对比:
10
+ CC 中工具通过静态 TOOLS 数组集中声明,没有显式注册表。
11
+ 本框架引入 RuntimeToolRegistry 作为运行时注册中心,支持动态注册。
12
+ StructuredTool 构造时序由 factory 控制(需 state 生命周期对齐),不在注册时提前构造。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ from langchain_core.tools import BaseTool
20
+
21
+ from .adapter import LangChainAdapter
22
+ from .base import RuntimeTool
23
+ from .errors import ToolNotFoundError, ToolRegistrationError
24
+ from .pipeline import ToolExecutorPipeline
25
+
26
+
27
+ class RuntimeToolRegistry:
28
+ """
29
+ 工具注册表。
30
+
31
+ register(tool) — 注册 RuntimeTool,工具名重复时抛 ToolRegistrationError
32
+ get(name) — 按名查询 RuntimeTool,不存在时抛 ToolNotFoundError
33
+ list() — 返回所有已注册 RuntimeTool 列表
34
+ to_langchain_tools() — 按需构造 StructuredTool 列表
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ pipeline: ToolExecutorPipeline,
41
+ adapter: LangChainAdapter,
42
+ ) -> None:
43
+ self._pipeline = pipeline
44
+ self._adapter = adapter
45
+ self._tools: dict[str, RuntimeTool] = {}
46
+
47
+ def register(self, tool: RuntimeTool) -> None:
48
+ """注册一个 RuntimeTool。工具名重复时抛 ToolRegistrationError。"""
49
+ if tool.name in self._tools:
50
+ raise ToolRegistrationError(tool.name)
51
+ self._tools[tool.name] = tool
52
+
53
+ def get(self, name: str) -> RuntimeTool:
54
+ """按名查询 RuntimeTool。不存在时抛 ToolNotFoundError。"""
55
+ if name not in self._tools:
56
+ raise ToolNotFoundError(name)
57
+ return self._tools[name]
58
+
59
+ def list(self) -> list[RuntimeTool]:
60
+ """返回所有已注册 RuntimeTool 列表(按注册顺序)。"""
61
+ return list(self._tools.values())
62
+
63
+ def to_langchain_tools(self) -> list[BaseTool]:
64
+ """按需构造并返回所有 StructuredTool。"""
65
+ return [
66
+ self._adapter.build_structured_tool(
67
+ tool=t,
68
+ pipeline=self._pipeline,
69
+ )
70
+ for t in self._tools.values()
71
+ ]
72
+
73
+ def __len__(self) -> int:
74
+ return len(self._tools)
75
+
76
+ def __contains__(self, name: str) -> bool:
77
+ return name in self._tools
78
+
79
+ def __repr__(self) -> str:
80
+ names = list(self._tools.keys())
81
+ return f"<RuntimeToolRegistry tools={names}>"
@@ -0,0 +1,27 @@
1
+ """
2
+ tool_runtime/resolvers/ — ask 分支权限 resolver 集合
3
+
4
+ 职责:
5
+ 汇总导出 PermissionResolver 协议与四类场景实现。
6
+
7
+ 链路位置:
8
+ 由会话/工作流装配层注入 ToolExecutorPipeline。
9
+
10
+ 当前裁剪范围:
11
+ 仅提供 resolver 定义与基础实现,不负责 UI 事件循环集成。
12
+ """
13
+
14
+ from .agent_session import AgentSessionPermissionResolver, PermissionRequest
15
+ from .background import BackgroundPermissionResolver
16
+ from .base import PermissionResolver
17
+ from .conversation import ConversationPermissionResolver
18
+ from .workflow import WorkflowPermissionResolver
19
+
20
+ __all__ = [
21
+ "PermissionResolver",
22
+ "PermissionRequest",
23
+ "AgentSessionPermissionResolver",
24
+ "ConversationPermissionResolver",
25
+ "BackgroundPermissionResolver",
26
+ "WorkflowPermissionResolver",
27
+ ]
@@ -0,0 +1,125 @@
1
+ """
2
+ tool_runtime/resolvers/agent_session.py — CLI 会话权限确认 resolver
3
+
4
+ 职责:
5
+ 在同进程交互场景中通过 asyncio.Queue + Future 等待用户批准/拒绝。
6
+
7
+ 链路位置:
8
+ AgentSession 注入 resolver -> ToolExecutorPipeline ask 分支 await。
9
+
10
+ 当前裁剪范围:
11
+ 仅实现单路 UI 队列确认,不实现 hook/classifier 多路 race。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from typing import Awaitable, Callable
20
+
21
+
22
+ @dataclass
23
+ class PermissionRequest:
24
+ tool_name: str
25
+ ask_prompt: str | None
26
+ future: asyncio.Future[bool]
27
+ created_at: float = field(default_factory=time.monotonic)
28
+
29
+
30
+ class _OnceGuard:
31
+ """多路 race 原子 claim,防止重复 resolve。"""
32
+
33
+ def __init__(self) -> None:
34
+ self._claimed = False
35
+ self._lock = asyncio.Lock()
36
+
37
+ async def claim(self) -> bool:
38
+ async with self._lock:
39
+ if self._claimed:
40
+ return False
41
+ self._claimed = True
42
+ return True
43
+
44
+
45
+ class AgentSessionPermissionResolver:
46
+ """CLI 交互 resolver:支持 UI future 与 hook 决策多路 race。"""
47
+
48
+ def __init__(
49
+ self,
50
+ request_queue: asyncio.Queue[PermissionRequest],
51
+ *,
52
+ timeout: float = 300.0,
53
+ hook_runner: Callable[[str, str | None], Awaitable[bool | None]] | None = None,
54
+ ) -> None:
55
+ self._queue = request_queue
56
+ self._timeout = timeout
57
+ self._hook_runner = hook_runner
58
+
59
+ async def __call__(self, tool_name: str, ask_prompt: str | None) -> bool:
60
+ loop = asyncio.get_running_loop()
61
+ future: asyncio.Future[bool] = loop.create_future()
62
+ await self._queue.put(
63
+ PermissionRequest(
64
+ tool_name=tool_name,
65
+ ask_prompt=ask_prompt,
66
+ future=future,
67
+ )
68
+ )
69
+ guard = _OnceGuard()
70
+ ui_task = asyncio.create_task(self._await_ui_future(future, guard))
71
+ tasks: list[asyncio.Task[bool | None]] = [ui_task]
72
+ if self._hook_runner is not None:
73
+ tasks.append(asyncio.create_task(self._await_hook_decision(tool_name, ask_prompt, guard, future)))
74
+ try:
75
+ done, pending = await asyncio.wait(
76
+ tasks,
77
+ timeout=self._timeout,
78
+ return_when=asyncio.FIRST_COMPLETED,
79
+ )
80
+ if not done:
81
+ return False
82
+ result = done.pop().result()
83
+ return bool(result)
84
+ except asyncio.TimeoutError:
85
+ return False
86
+ finally:
87
+ for task in tasks:
88
+ if not task.done():
89
+ task.cancel()
90
+ if not future.done():
91
+ future.cancel()
92
+
93
+ async def _await_ui_future(self, future: asyncio.Future[bool], guard: _OnceGuard) -> bool | None:
94
+ try:
95
+ result = await asyncio.wait_for(future, timeout=self._timeout)
96
+ except asyncio.TimeoutError:
97
+ if await guard.claim():
98
+ return False
99
+ return None
100
+ except asyncio.CancelledError:
101
+ return None
102
+ if await guard.claim():
103
+ return result
104
+ return None
105
+
106
+ async def _await_hook_decision(
107
+ self,
108
+ tool_name: str,
109
+ ask_prompt: str | None,
110
+ guard: _OnceGuard,
111
+ ui_future: asyncio.Future[bool],
112
+ ) -> bool | None:
113
+ if self._hook_runner is None:
114
+ return None
115
+ hook_result = await self._hook_runner(tool_name, ask_prompt)
116
+ if hook_result is None:
117
+ return None
118
+ if await guard.claim():
119
+ if not ui_future.done():
120
+ ui_future.cancel()
121
+ return bool(hook_result)
122
+ return None
123
+
124
+
125
+ __all__ = ["PermissionRequest", "AgentSessionPermissionResolver"]