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,321 @@
1
+ """
2
+ engine.py — Hook 执行引擎(P1/P2)。
3
+
4
+ 职责:
5
+ 负责按顺序执行 Hook 候选并聚合结果,输出 AggregatedHookResult。
6
+
7
+ 链路位置:
8
+ graph wiring 节点调用 HookEngine.execute(event, ctx) 获取可消费结果。
9
+
10
+ 当前裁剪范围:
11
+ 已覆盖 function/callback + command/http/prompt/agent 执行器;
12
+ 当前执行策略为 callback/function 顺序、其余执行器并行。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import inspect
19
+ import logging
20
+ import time
21
+ from typing import Any, TYPE_CHECKING
22
+
23
+ from .config import (
24
+ HookCandidate,
25
+ HookCandidateDecision,
26
+ HooksConfigSnapshot,
27
+ resolve_executor_callable,
28
+ )
29
+ from .executors import CommandExecutor
30
+ from .executors import HttpExecutor
31
+ from .executors import PromptExecutor
32
+ from .executors import AgentExecutor
33
+ from .types import (
34
+ AggregatedHookResult,
35
+ HookContext,
36
+ HookExecutionRecord,
37
+ HookResult,
38
+ PermissionBehavior,
39
+ )
40
+ from .trust import WorkspaceTrustChecker
41
+
42
+ if TYPE_CHECKING:
43
+ from ...observability.trace.hook_event_emitter import HookEventEmitter
44
+
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class FunctionExecutor:
50
+ """执行 function 类型 hook。"""
51
+
52
+ async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
53
+ fn = resolve_executor_callable(candidate.spec)
54
+ out = fn(ctx)
55
+ if inspect.isawaitable(out):
56
+ out = await out
57
+ return normalize_hook_result(out)
58
+
59
+
60
+ class CallbackExecutor:
61
+ """执行 callback 类型 hook,支持 timeout。"""
62
+
63
+ async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
64
+ cb = resolve_executor_callable(candidate.spec)
65
+ coro = cb(ctx)
66
+ if not inspect.isawaitable(coro):
67
+ return normalize_hook_result(coro)
68
+ timeout = candidate.spec.timeout
69
+ try:
70
+ out = await asyncio.wait_for(coro, timeout=timeout)
71
+ except asyncio.TimeoutError:
72
+ return HookResult(
73
+ outcome="non_blocking_error",
74
+ blocking_errors=[f"hook timeout after {timeout:.1f}s"],
75
+ )
76
+ return normalize_hook_result(out)
77
+
78
+
79
+ class HookEngine:
80
+ """统一调度器(P1 顺序执行)。"""
81
+
82
+ def __init__(
83
+ self,
84
+ snapshot: HooksConfigSnapshot,
85
+ *,
86
+ event_emitter: HookEventEmitter | None = None,
87
+ trust_checker: WorkspaceTrustChecker | None = None,
88
+ ) -> None:
89
+ self._snapshot = snapshot
90
+ self._function_executor = FunctionExecutor()
91
+ self._callback_executor = CallbackExecutor()
92
+ self._command_executor = CommandExecutor()
93
+ self._http_executor = HttpExecutor()
94
+ self._prompt_executor = PromptExecutor()
95
+ self._agent_executor = AgentExecutor()
96
+ self._event_emitter = event_emitter
97
+ self._trust_checker = trust_checker
98
+
99
+ async def execute(self, ctx: HookContext) -> AggregatedHookResult:
100
+ candidates, skipped = self._snapshot.get_candidates_with_decisions(ctx)
101
+ self._emit_skipped_records(ctx, skipped)
102
+ if not candidates:
103
+ return AggregatedHookResult()
104
+ if self._trust_checker is not None:
105
+ candidates = self._filter_trusted_candidates(ctx, candidates)
106
+ if not candidates:
107
+ return AggregatedHookResult()
108
+
109
+ result = AggregatedHookResult()
110
+ sequential_candidates = [
111
+ candidate
112
+ for candidate in candidates
113
+ if candidate.spec.executor_type in {"callback", "function"}
114
+ ]
115
+ parallel_candidates = [
116
+ candidate
117
+ for candidate in candidates
118
+ if candidate.spec.executor_type in {"command", "http", "prompt", "agent"}
119
+ ]
120
+
121
+ for candidate in sequential_candidates:
122
+ one = await self._run_with_record(candidate, ctx)
123
+ self._merge(result, one)
124
+
125
+ if parallel_candidates:
126
+ parallel_results = await asyncio.gather(
127
+ *(self._run_with_record(candidate, ctx) for candidate in parallel_candidates)
128
+ )
129
+ for one in parallel_results:
130
+ self._merge(result, one)
131
+ return result
132
+
133
+ def _filter_trusted_candidates(
134
+ self,
135
+ ctx: HookContext,
136
+ candidates: list[HookCandidate],
137
+ ) -> list[HookCandidate]:
138
+ trusted: list[HookCandidate] = []
139
+ for candidate in candidates:
140
+ executor_type = candidate.spec.executor_type
141
+ if self._trust_checker is not None and self._trust_checker.should_skip(executor_type):
142
+ self._emit_trust_skipped_record(ctx, candidate)
143
+ continue
144
+ trusted.append(candidate)
145
+ return trusted
146
+
147
+ async def _run_with_record(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
148
+ started_at = time.perf_counter()
149
+ one = await self._run_one(candidate, ctx)
150
+ duration_ms = (time.perf_counter() - started_at) * 1000
151
+ outcome = self._emit_execution_record(ctx, candidate, one, duration_ms)
152
+ if outcome == "success" and candidate.spec.on_success is not None:
153
+ candidate.spec.on_success()
154
+ return one
155
+
156
+ async def _run_one(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
157
+ et = candidate.spec.executor_type
158
+ if et == "function":
159
+ return await self._function_executor.run(candidate, ctx)
160
+ if et == "callback":
161
+ return await self._callback_executor.run(candidate, ctx)
162
+ if et == "command":
163
+ return await self._command_executor.run(candidate, ctx)
164
+ if et == "http":
165
+ return await self._http_executor.run(candidate, ctx)
166
+ if et == "prompt":
167
+ return await self._prompt_executor.run(candidate, ctx)
168
+ if et == "agent":
169
+ return await self._agent_executor.run(candidate, ctx)
170
+ return HookResult(
171
+ outcome="non_blocking_error",
172
+ blocking_errors=[f"unsupported executor in P1: {et}"],
173
+ )
174
+
175
+ @staticmethod
176
+ def _merge(agg: AggregatedHookResult, one: HookResult) -> None:
177
+ if one.prevent_continuation:
178
+ agg.prevent_continuation = True
179
+ if one.blocking_errors:
180
+ agg.blocking_errors.extend(one.blocking_errors)
181
+ if one.additional_context:
182
+ agg.additional_contexts.append(one.additional_context)
183
+ if one.updated_input is not None:
184
+ agg.updated_input = dict(one.updated_input)
185
+ if one.state_patch:
186
+ agg.state_patch.update(one.state_patch)
187
+
188
+ agg.permission_behavior = merge_permission_behavior(
189
+ agg.permission_behavior,
190
+ one.permission_behavior,
191
+ )
192
+ if one.hook_permission_decision_reason:
193
+ agg.hook_permission_decision_reason = one.hook_permission_decision_reason
194
+
195
+ def _emit_skipped_records(
196
+ self,
197
+ ctx: HookContext,
198
+ skipped: list[HookCandidateDecision],
199
+ ) -> None:
200
+ for decision in skipped:
201
+ record = HookExecutionRecord(
202
+ hook_id=decision.spec.hook_id,
203
+ hook_event=ctx.event.value,
204
+ hook_source=decision.spec.source,
205
+ matcher=decision.spec.matcher,
206
+ matched=decision.matched,
207
+ skip_reason=decision.skip_reason,
208
+ snapshot_id=self._snapshot.snapshot_id,
209
+ policy_source=self._snapshot.policy_source,
210
+ outcome="cancelled",
211
+ prevent_continuation=False,
212
+ duration_ms=0.0,
213
+ )
214
+ self._try_emit_record(ctx.session_id, record)
215
+
216
+ def _emit_execution_record(
217
+ self,
218
+ ctx: HookContext,
219
+ candidate: HookCandidate,
220
+ result: HookResult,
221
+ duration_ms: float,
222
+ ) -> str:
223
+ outcome = result.outcome
224
+ if result.prevent_continuation or result.blocking_errors:
225
+ outcome = "blocking"
226
+ record = HookExecutionRecord(
227
+ hook_id=candidate.spec.hook_id,
228
+ hook_event=ctx.event.value,
229
+ hook_source=candidate.spec.source,
230
+ matcher=candidate.spec.matcher,
231
+ matched=True,
232
+ skip_reason=None,
233
+ snapshot_id=self._snapshot.snapshot_id,
234
+ policy_source=self._snapshot.policy_source,
235
+ outcome=outcome,
236
+ prevent_continuation=result.prevent_continuation,
237
+ duration_ms=duration_ms,
238
+ )
239
+ self._try_emit_record(ctx.session_id, record)
240
+ return outcome
241
+
242
+ def _emit_trust_skipped_record(
243
+ self,
244
+ ctx: HookContext,
245
+ candidate: HookCandidate,
246
+ ) -> None:
247
+ reason = (
248
+ self._trust_checker.skip_reason(candidate.spec.executor_type)
249
+ if self._trust_checker is not None
250
+ else "trust_check_skipped"
251
+ )
252
+ record = HookExecutionRecord(
253
+ hook_id=candidate.spec.hook_id,
254
+ hook_event=ctx.event.value,
255
+ hook_source=candidate.spec.source,
256
+ matcher=candidate.spec.matcher,
257
+ matched=True,
258
+ skip_reason=reason,
259
+ snapshot_id=self._snapshot.snapshot_id,
260
+ policy_source=self._snapshot.policy_source,
261
+ outcome="trust_skipped",
262
+ prevent_continuation=False,
263
+ duration_ms=0.0,
264
+ )
265
+ self._try_emit_record(ctx.session_id, record)
266
+
267
+ def _try_emit_record(self, session_id: str | None, record: HookExecutionRecord) -> None:
268
+ if self._event_emitter is None or not session_id:
269
+ return
270
+ if not self._event_emitter.collector.has_session(session_id):
271
+ return
272
+ try:
273
+ self._event_emitter.emit_record(session_id, record)
274
+ except Exception as exc:
275
+ # 发射失败不应阻断主链路
276
+ logger.debug("hook event emit failed (non-blocking): %s", exc)
277
+ return
278
+
279
+
280
+ def normalize_hook_result(value: Any) -> HookResult:
281
+ """兼容 dict/HookResult 返回值。"""
282
+ if value is None:
283
+ return HookResult()
284
+ if isinstance(value, HookResult):
285
+ return value
286
+ if isinstance(value, dict):
287
+ kwargs = dict(value)
288
+ # 文档约定别名
289
+ if "preventContinuation" in kwargs and "prevent_continuation" not in kwargs:
290
+ kwargs["prevent_continuation"] = kwargs.pop("preventContinuation")
291
+ if "updatedInput" in kwargs and "updated_input" not in kwargs:
292
+ kwargs["updated_input"] = kwargs.pop("updatedInput")
293
+ if "permissionBehavior" in kwargs and "permission_behavior" not in kwargs:
294
+ kwargs["permission_behavior"] = kwargs.pop("permissionBehavior")
295
+ return HookResult(**kwargs)
296
+ raise TypeError(f"Unsupported hook result type: {type(value).__name__}")
297
+
298
+
299
+ def merge_permission_behavior(
300
+ current: PermissionBehavior | None,
301
+ incoming: PermissionBehavior | None,
302
+ ) -> PermissionBehavior | None:
303
+ """deny > ask > allow > passthrough(None effect)."""
304
+ if incoming is None or incoming == "passthrough":
305
+ return current
306
+ if incoming == "deny":
307
+ return "deny"
308
+ if incoming == "ask":
309
+ return "ask" if current != "deny" else "deny"
310
+ if incoming == "allow":
311
+ return current or "allow"
312
+ return current
313
+
314
+
315
+ __all__ = [
316
+ "CallbackExecutor",
317
+ "FunctionExecutor",
318
+ "HookEngine",
319
+ "merge_permission_behavior",
320
+ "normalize_hook_result",
321
+ ]
@@ -0,0 +1,9 @@
1
+ """Hook 执行器子包(P2)。"""
2
+
3
+ from .agent import AgentExecutor
4
+ from .command import CommandExecutor
5
+ from .http import HttpExecutor
6
+ from .prompt import PromptExecutor
7
+
8
+ __all__ = ["AgentExecutor", "CommandExecutor", "HttpExecutor", "PromptExecutor"]
9
+
@@ -0,0 +1,107 @@
1
+ """
2
+ agent.py — AgentExecutor(P2)。
3
+
4
+ 职责:
5
+ 执行 agent 类型 hook,构建单轮验证器输入并解析 decision。
6
+
7
+ 链路位置:
8
+ HookEngine._run_one() 在 executor_type=="agent" 时调用本执行器。
9
+
10
+ 当前裁剪范围:
11
+ 通过配置注入可调用对象执行验证器;prompt 字段仅在运行时读取,不做预处理。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import inspect
17
+ import json
18
+ from dataclasses import dataclass
19
+ from typing import Any, Callable
20
+
21
+ from ..config import HookCandidate
22
+ from ..types import HookContext, HookResult
23
+
24
+
25
+ @dataclass
26
+ class AgentExecutor:
27
+ """agent 类型 hook 执行器。"""
28
+
29
+ async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
30
+ config = candidate.spec.config
31
+ # 对齐 gh-24920 / CC-79:prompt 在运行期按原值读取,不在构建阶段做任何 transform。
32
+ system_prompt = config.get("prompt")
33
+ if not isinstance(system_prompt, str) or not system_prompt.strip():
34
+ return HookResult(
35
+ outcome="non_blocking_error",
36
+ blocking_errors=["agent hook missing 'prompt'"],
37
+ )
38
+
39
+ runner = self._resolve_runner(config)
40
+ if runner is None:
41
+ return HookResult(
42
+ outcome="non_blocking_error",
43
+ blocking_errors=["agent hook missing callable runner"],
44
+ )
45
+
46
+ user_payload = json.dumps(
47
+ {
48
+ "event": ctx.event.value,
49
+ "tool_name": ctx.tool_name,
50
+ "tool_input": ctx.tool_input,
51
+ },
52
+ ensure_ascii=False,
53
+ )
54
+ output_text = await self._call_runner(
55
+ runner=runner,
56
+ system_prompt=system_prompt,
57
+ user_payload=user_payload,
58
+ model=config.get("model"),
59
+ ctx=ctx,
60
+ )
61
+ return self._parse_decision(output_text)
62
+
63
+ @staticmethod
64
+ def _resolve_runner(config: dict[str, Any]) -> Callable[..., Any] | None:
65
+ runner = config.get("runner") or config.get("agent_runner")
66
+ return runner if callable(runner) else None
67
+
68
+ @staticmethod
69
+ async def _call_runner(
70
+ *,
71
+ runner: Callable[..., Any],
72
+ system_prompt: str,
73
+ user_payload: str,
74
+ model: Any,
75
+ ctx: HookContext,
76
+ ) -> str:
77
+ output = runner(system_prompt, user_payload, model, ctx)
78
+ if inspect.isawaitable(output):
79
+ output = await output
80
+ return str(output or "")
81
+
82
+ @staticmethod
83
+ def _parse_decision(text: str) -> HookResult:
84
+ lowered = text.lower()
85
+ if "deny" in lowered:
86
+ return HookResult(
87
+ outcome="success",
88
+ permission_behavior="deny",
89
+ hook_permission_decision_reason=text.strip() or None,
90
+ )
91
+ if "ask" in lowered:
92
+ return HookResult(
93
+ outcome="success",
94
+ permission_behavior="ask",
95
+ hook_permission_decision_reason=text.strip() or None,
96
+ )
97
+ if "allow" in lowered:
98
+ return HookResult(
99
+ outcome="success",
100
+ permission_behavior="allow",
101
+ hook_permission_decision_reason=text.strip() or None,
102
+ )
103
+ return HookResult(
104
+ outcome="non_blocking_error",
105
+ blocking_errors=["agent hook cannot parse decision"],
106
+ )
107
+
@@ -0,0 +1,230 @@
1
+ """
2
+ command.py — CommandExecutor(P2)。
3
+
4
+ 职责:
5
+ 执行 command 类型 hook,负责子进程调用、退出码语义与 JSON 响应解析。
6
+
7
+ 链路位置:
8
+ HookEngine._run_one() 在 executor_type=="command" 时调用本执行器。
9
+
10
+ 当前裁剪范围:
11
+ 实现 command 主路径、async/async_rewake、SESSION_START 的 CLAUDE_ENV_FILE 解析;
12
+ 并行调度语义由 HookEngine 在 F6 阶段统一处理。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ import os
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ from langchain_core.messages import HumanMessage
24
+
25
+ from ..config import HookCandidate
26
+ from ..types import HookContext, HookEvent, HookResult
27
+
28
+
29
+ @dataclass
30
+ class CommandExecutor:
31
+ """command 类型 hook 执行器。"""
32
+
33
+ async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
34
+ config = candidate.spec.config
35
+ command = str(config.get("command") or "").strip()
36
+ if not command:
37
+ return HookResult(
38
+ outcome="non_blocking_error",
39
+ blocking_errors=["command hook missing 'command'"],
40
+ )
41
+
42
+ shell = str(config.get("shell") or "bash")
43
+ async_mode = bool(config.get("async", False))
44
+ async_rewake = bool(config.get("asyncRewake", False) or config.get("async_rewake", False))
45
+ timeout = candidate.spec.timeout
46
+ env = self._build_hook_env(candidate, ctx)
47
+
48
+ if async_mode:
49
+ asyncio.create_task(
50
+ self._run_and_maybe_rewake(
51
+ command=command,
52
+ shell=shell,
53
+ timeout=timeout,
54
+ env=env,
55
+ ctx=ctx,
56
+ async_rewake=async_rewake,
57
+ )
58
+ )
59
+ return HookResult(outcome="success")
60
+
61
+ proc = await self._spawn_process(command=command, shell=shell, env=env)
62
+ return await self._finalize_process(proc=proc, ctx=ctx, timeout=timeout, env=env)
63
+
64
+ async def _run_and_maybe_rewake(
65
+ self,
66
+ *,
67
+ command: str,
68
+ shell: str,
69
+ timeout: float,
70
+ env: dict[str, str],
71
+ ctx: HookContext,
72
+ async_rewake: bool,
73
+ ) -> None:
74
+ proc = await self._spawn_process(command=command, shell=shell, env=env)
75
+ result = await self._finalize_process(proc=proc, ctx=ctx, timeout=timeout, env=env)
76
+ if not async_rewake:
77
+ return
78
+ if result.outcome != "success":
79
+ return
80
+ rewake_text = (result.additional_context or "").strip()
81
+ if rewake_text:
82
+ messages = ctx.state.get("messages")
83
+ if isinstance(messages, list):
84
+ messages.append(HumanMessage(content=rewake_text))
85
+
86
+ async def _spawn_process(
87
+ self,
88
+ *,
89
+ command: str,
90
+ shell: str,
91
+ env: dict[str, str],
92
+ ) -> asyncio.subprocess.Process:
93
+ if shell == "bash":
94
+ return await asyncio.create_subprocess_exec(
95
+ "bash",
96
+ "-lc",
97
+ command,
98
+ stdout=asyncio.subprocess.PIPE,
99
+ stderr=asyncio.subprocess.PIPE,
100
+ env=env,
101
+ )
102
+ if shell == "sh":
103
+ return await asyncio.create_subprocess_exec(
104
+ "sh",
105
+ "-c",
106
+ command,
107
+ stdout=asyncio.subprocess.PIPE,
108
+ stderr=asyncio.subprocess.PIPE,
109
+ env=env,
110
+ )
111
+ return await asyncio.create_subprocess_shell(
112
+ command,
113
+ stdout=asyncio.subprocess.PIPE,
114
+ stderr=asyncio.subprocess.PIPE,
115
+ env=env,
116
+ )
117
+
118
+ async def _finalize_process(
119
+ self,
120
+ *,
121
+ proc: asyncio.subprocess.Process,
122
+ ctx: HookContext,
123
+ timeout: float,
124
+ env: dict[str, str],
125
+ ) -> HookResult:
126
+ try:
127
+ stdout_raw, stderr_raw = await asyncio.wait_for(proc.communicate(), timeout=timeout)
128
+ except asyncio.TimeoutError:
129
+ proc.kill()
130
+ return HookResult(
131
+ outcome="non_blocking_error",
132
+ blocking_errors=[f"command hook timeout after {timeout:.1f}s"],
133
+ )
134
+
135
+ stdout = stdout_raw.decode("utf-8", errors="replace")
136
+ stderr = stderr_raw.decode("utf-8", errors="replace")
137
+ returncode = int(proc.returncode or 0)
138
+
139
+ result = HookResult(outcome="success")
140
+ parsed_json = self._parse_json(stdout)
141
+ if isinstance(parsed_json, dict):
142
+ self._apply_json_result(result, parsed_json)
143
+
144
+ if ctx.event == HookEvent.SESSION_START:
145
+ session_env_patch = self._read_claude_env_file(env)
146
+ if session_env_patch:
147
+ result.state_patch["_session_env"] = session_env_patch
148
+
149
+ if returncode == 0:
150
+ if not result.additional_context and stdout.strip():
151
+ result.additional_context = stdout.strip()
152
+ return result
153
+ if returncode == 2:
154
+ result.outcome = "blocking"
155
+ result.permission_behavior = result.permission_behavior or "deny"
156
+ if not result.blocking_errors:
157
+ result.blocking_errors = [stderr.strip() or "command hook blocked request"]
158
+ return result
159
+ result.outcome = "non_blocking_error"
160
+ if not result.blocking_errors:
161
+ result.blocking_errors = [stderr.strip() or f"command hook exited with {returncode}"]
162
+ return result
163
+
164
+ @staticmethod
165
+ def _parse_json(stdout: str) -> dict[str, Any] | None:
166
+ text = stdout.strip()
167
+ if not text:
168
+ return None
169
+ try:
170
+ data = json.loads(text)
171
+ except json.JSONDecodeError:
172
+ return None
173
+ return data if isinstance(data, dict) else None
174
+
175
+ @staticmethod
176
+ def _apply_json_result(result: HookResult, payload: dict[str, Any]) -> None:
177
+ if payload.get("continue") is False:
178
+ result.prevent_continuation = True
179
+ stop_reason = payload.get("stopReason")
180
+ if isinstance(stop_reason, str) and stop_reason.strip():
181
+ result.blocking_errors.append(stop_reason.strip())
182
+ decision = payload.get("decision")
183
+ if decision in {"allow", "ask", "deny"}:
184
+ result.permission_behavior = decision
185
+ reason = payload.get("reason")
186
+ if isinstance(reason, str) and reason.strip():
187
+ result.hook_permission_decision_reason = reason.strip()
188
+ updated_input = payload.get("updatedInput")
189
+ if isinstance(updated_input, dict):
190
+ result.updated_input = dict(updated_input)
191
+ if payload.get("suppressOutput") is True:
192
+ result.additional_context = ""
193
+
194
+ @staticmethod
195
+ def _build_hook_env(candidate: HookCandidate, ctx: HookContext) -> dict[str, str]:
196
+ env = dict(os.environ)
197
+ session_env = ctx.state.get("_session_env") if isinstance(ctx.state, dict) else None
198
+ if isinstance(session_env, dict):
199
+ for key, value in session_env.items():
200
+ if isinstance(key, str) and isinstance(value, str):
201
+ env[key] = value
202
+ env["HOOK_EVENT_NAME"] = ctx.event.value
203
+ env["HOOK_TOOL_NAME"] = ctx.tool_name or ""
204
+ env["HOOK_TOOL_INPUT"] = json.dumps(ctx.tool_input or {}, ensure_ascii=False)
205
+ env["HOOK_SESSION_ID"] = ctx.session_id or ""
206
+ env["HOOK_SNAPSHOT_ID"] = candidate.stable_key
207
+ return env
208
+
209
+ @staticmethod
210
+ def _read_claude_env_file(env: dict[str, str]) -> dict[str, str]:
211
+ path = env.get("CLAUDE_ENV_FILE")
212
+ if not path:
213
+ return {}
214
+ try:
215
+ with open(path, "r", encoding="utf-8") as f:
216
+ lines = f.readlines()
217
+ except OSError:
218
+ return {}
219
+
220
+ parsed: dict[str, str] = {}
221
+ for raw in lines:
222
+ line = raw.strip()
223
+ if not line or line.startswith("#") or "=" not in line:
224
+ continue
225
+ key, value = line.split("=", 1)
226
+ key = key.strip()
227
+ if key:
228
+ parsed[key] = value.strip()
229
+ return parsed
230
+