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,254 @@
1
+ """
2
+ task_runtime/integrations/tool_use_summary_provider.py — Tool Use Summary 聚合 provider
3
+
4
+ 职责:
5
+ 在 task 侧聚合工具调用结果,并异步生成 queued-command summary:
6
+ 1. record_tool_result:采集单次工具结果
7
+ 2. flush_scope_summary:按 scope 生成一次摘要命令
8
+ 3. build_batch / ack / nack:复用通用 queued-command provider
9
+
10
+ 边界:
11
+ - 不依赖 loop 结构;只产出 queued-command
12
+ - “异步”语义由 record 与 flush 解耦实现(可在不同阶段触发)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from collections import Counter
17
+ from dataclasses import dataclass, field
18
+ import time
19
+ from uuid import uuid4
20
+
21
+ from langchain_core.messages import ToolMessage
22
+
23
+ from ..core.types import QueuePriority, TaskScope
24
+ from .loop_adapter import LoopInjectionBatch
25
+ from .queued_command_provider import InMemoryQueuedCommandProvider, QueuedCommandEnvelope
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ToolUseRecord:
30
+ tool_name: str
31
+ status: str # success | error
32
+ latency_ms: int | None = None
33
+ tool_use_id: str | None = None
34
+ output_preview: str | None = None
35
+ created_at: float = field(default_factory=time.time)
36
+
37
+
38
+ class ToolUseSummaryProvider:
39
+ """聚合工具调用记录,输出 summary queued-command。"""
40
+
41
+ def __init__(
42
+ self,
43
+ backend: InMemoryQueuedCommandProvider | None = None,
44
+ *,
45
+ max_queue_size: int | None = None,
46
+ ttl_sec: float | None = None,
47
+ error_ratio_now_threshold: float = 0.5,
48
+ avg_latency_now_threshold_ms: int = 2000,
49
+ ) -> None:
50
+ self._backend = backend or InMemoryQueuedCommandProvider(
51
+ max_queue_size=max_queue_size,
52
+ ttl_sec=ttl_sec,
53
+ )
54
+ self._buffers: dict[str | None, list[ToolUseRecord]] = {}
55
+ self._seen_tool_call_ids: dict[str | None, set[str]] = {}
56
+ self._error_ratio_now_threshold = error_ratio_now_threshold
57
+ self._avg_latency_now_threshold_ms = avg_latency_now_threshold_ms
58
+
59
+ def record_tool_result(
60
+ self,
61
+ *,
62
+ scope: TaskScope,
63
+ tool_name: str,
64
+ status: str,
65
+ latency_ms: int | None = None,
66
+ tool_use_id: str | None = None,
67
+ output_preview: str | None = None,
68
+ ) -> None:
69
+ normalized_status = "success" if status not in {"error", "failed"} else "error"
70
+ self._buffers.setdefault(scope.agent_id, []).append(
71
+ ToolUseRecord(
72
+ tool_name=tool_name,
73
+ status=normalized_status,
74
+ latency_ms=latency_ms,
75
+ tool_use_id=tool_use_id,
76
+ output_preview=output_preview,
77
+ )
78
+ )
79
+
80
+ def flush_scope_summary(
81
+ self,
82
+ *,
83
+ scope: TaskScope,
84
+ priority: QueuePriority | None = None,
85
+ ) -> int:
86
+ records = self._buffers.get(scope.agent_id, [])
87
+ if not records:
88
+ return 0
89
+ summary_text, payload = _build_summary(records)
90
+ chosen_priority = priority or self._select_priority(payload)
91
+ dedup_key = (
92
+ f"tool_summary:{scope.agent_id}:{payload['total_calls']}:"
93
+ f"{payload['error_calls']}:{chosen_priority.value}"
94
+ )
95
+ command_id = f"tool_summary_{uuid4().hex[:10]}"
96
+ self._backend.enqueue(
97
+ QueuedCommandEnvelope(
98
+ command_id=command_id,
99
+ source="tool_use_summary",
100
+ summary=summary_text,
101
+ scope=scope,
102
+ priority=chosen_priority,
103
+ payload=payload,
104
+ dedup_key=dedup_key,
105
+ )
106
+ )
107
+ self._buffers[scope.agent_id] = []
108
+ return 1
109
+
110
+ def ingest_from_opencode_events(
111
+ self,
112
+ *,
113
+ scope: TaskScope,
114
+ events: list[dict],
115
+ ) -> int:
116
+ """从 LangChain AgentX 风格事件列表采集工具调用结果。"""
117
+ count = 0
118
+ for event in events:
119
+ event_type = str(event.get("event_type") or event.get("type") or "")
120
+ data = event.get("data") or {}
121
+ tool_name = str(data.get("tool_name") or "")
122
+ if not tool_name:
123
+ continue
124
+ if event_type in {"tool-result", "TOOL_RESULT"}:
125
+ self.record_tool_result(
126
+ scope=scope,
127
+ tool_name=tool_name,
128
+ status="success",
129
+ output_preview=str(data.get("output") or "")[:120],
130
+ )
131
+ count += 1
132
+ elif event_type in {"tool-error", "TOOL_ERROR"}:
133
+ self.record_tool_result(
134
+ scope=scope,
135
+ tool_name=tool_name,
136
+ status="error",
137
+ output_preview=str(data.get("error") or "")[:120],
138
+ )
139
+ count += 1
140
+ return count
141
+
142
+ def ingest_from_messages(
143
+ self,
144
+ *,
145
+ scope: TaskScope,
146
+ messages: list,
147
+ ) -> int:
148
+ """从 loop state.messages 自动提取 ToolMessage 结果。"""
149
+ count = 0
150
+ seen = self._seen_tool_call_ids.setdefault(scope.agent_id, set())
151
+ for msg in messages:
152
+ if not isinstance(msg, ToolMessage):
153
+ continue
154
+ tool_call_id = getattr(msg, "tool_call_id", None)
155
+ if not tool_call_id:
156
+ continue
157
+ tool_call_id = str(tool_call_id)
158
+ if tool_call_id in seen:
159
+ continue
160
+ seen.add(tool_call_id)
161
+ tool_name = _extract_tool_name(msg)
162
+ status = _extract_tool_status(msg)
163
+ content = msg.content
164
+ preview = content if isinstance(content, str) else str(content)
165
+ self.record_tool_result(
166
+ scope=scope,
167
+ tool_name=tool_name,
168
+ status=status,
169
+ output_preview=preview[:120],
170
+ tool_use_id=tool_call_id,
171
+ )
172
+ count += 1
173
+ return count
174
+
175
+ def build_batch(
176
+ self,
177
+ scope: TaskScope,
178
+ max_priority: QueuePriority = QueuePriority.NEXT,
179
+ *,
180
+ limit: int = 8,
181
+ ) -> LoopInjectionBatch:
182
+ return self._backend.build_batch(scope, max_priority=max_priority, limit=limit)
183
+
184
+ def ack_batch(self, batch: LoopInjectionBatch) -> None:
185
+ self._backend.ack_batch(batch)
186
+
187
+ def nack_batch(self, batch: LoopInjectionBatch, *, requeue: bool = True) -> None:
188
+ self._backend.nack_batch(batch, requeue=requeue)
189
+
190
+ def has_pending(
191
+ self, scope: TaskScope, max_priority: QueuePriority = QueuePriority.NEXT
192
+ ) -> bool:
193
+ return self._backend.has_pending(scope, max_priority=max_priority)
194
+
195
+ def _select_priority(self, payload: dict) -> QueuePriority:
196
+ total = int(payload.get("total_calls") or 0)
197
+ errors = int(payload.get("error_calls") or 0)
198
+ avg_latency_ms = payload.get("avg_latency_ms")
199
+ if total > 0 and (errors / total) >= self._error_ratio_now_threshold:
200
+ return QueuePriority.NOW
201
+ if isinstance(avg_latency_ms, int) and avg_latency_ms >= self._avg_latency_now_threshold_ms:
202
+ return QueuePriority.NOW
203
+ if errors > 0:
204
+ return QueuePriority.NEXT
205
+ return QueuePriority.LATER
206
+
207
+
208
+ def _build_summary(records: list[ToolUseRecord]) -> tuple[str, dict]:
209
+ total_calls = len(records)
210
+ error_calls = sum(1 for item in records if item.status == "error")
211
+ success_calls = total_calls - error_calls
212
+ tool_counter = Counter(item.tool_name for item in records)
213
+ tool_stat = [{"tool_name": k, "count": v} for k, v in tool_counter.items()]
214
+ latency_values = [item.latency_ms for item in records if item.latency_ms is not None]
215
+ avg_latency_ms = int(sum(latency_values) / len(latency_values)) if latency_values else None
216
+ summary = (
217
+ f"tool summary: total={total_calls}, success={success_calls}, "
218
+ f"error={error_calls}, tools={dict(tool_counter)}"
219
+ )
220
+ return summary, {
221
+ "total_calls": total_calls,
222
+ "success_calls": success_calls,
223
+ "error_calls": error_calls,
224
+ "tools": tool_stat,
225
+ "avg_latency_ms": avg_latency_ms,
226
+ }
227
+
228
+
229
+ def _extract_tool_name(msg: ToolMessage) -> str:
230
+ extra = getattr(msg, "additional_kwargs", {}) or {}
231
+ metadata = getattr(msg, "metadata", {}) or {}
232
+ name = (
233
+ getattr(msg, "tool_name", None)
234
+ or getattr(msg, "name", None)
235
+ or extra.get("tool_name")
236
+ or extra.get("name")
237
+ or metadata.get("tool_name")
238
+ or metadata.get("name")
239
+ )
240
+ return str(name) if name else "unknown_tool"
241
+
242
+
243
+ def _extract_tool_status(msg: ToolMessage) -> str:
244
+ extra = getattr(msg, "additional_kwargs", {}) or {}
245
+ metadata = getattr(msg, "metadata", {}) or {}
246
+ if extra.get("error") or metadata.get("error"):
247
+ return "error"
248
+ if str(metadata.get("status") or "").lower() == "error":
249
+ return "error"
250
+ content = msg.content
251
+ if isinstance(content, str) and content.lower().startswith("error"):
252
+ return "error"
253
+ return "success"
254
+
@@ -0,0 +1,5 @@
1
+ """Task lifecycle orchestration."""
2
+
3
+ from .runtime import DefaultTaskRuntime
4
+
5
+ __all__ = ["DefaultTaskRuntime"]
@@ -0,0 +1,386 @@
1
+ """
2
+ task_runtime/orchestrator/runtime.py — Task Runtime 默认编排实现
3
+
4
+ 职责:
5
+ 聚合 TaskStore 与 TaskCommandQueue,提供统一任务生命周期编排:
6
+ 1. spawn:创建并注册运行中任务
7
+ 2. cancel:将任务转换为终止态(§10.2 抢占 → STOPPED)
8
+ 3. mark_terminal:落终态并发布 task-notification
9
+ 4. drain/ack:按 scope 与 priority 读取和确认通知
10
+ 5. 可选:wait_for_terminal / TaskOutputSink / 租约 heartbeat(§10.2–§10.4)
11
+
12
+ 与 CC 对齐点:
13
+ - 终态通知按 (task_id, status) 做幂等去重
14
+ - 通知优先级采用 now > next > later
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import replace
20
+ from threading import RLock
21
+ from typing import Optional, TYPE_CHECKING
22
+ import time
23
+ import logging
24
+
25
+ from ..core.ids import (
26
+ new_notification_command_id,
27
+ new_reserved_batch_id,
28
+ new_run_id,
29
+ new_task_id,
30
+ )
31
+ from ..core.interfaces import TaskCommandQueue, TaskExecutor, TaskRuntime, TaskStore
32
+ from ..core.notification_priority import notification_priority_for_terminal_status
33
+ from ...observability.logging import ObservabilityLoggerAdapter, build_log_context
34
+
35
+ if TYPE_CHECKING:
36
+ from ..output.sink import TaskOutputSink
37
+ from ..core.types import (
38
+ QueuePriority,
39
+ ReservedNotificationBatch,
40
+ TaskExecutionResult,
41
+ TaskNotificationEnvelope,
42
+ TaskRecord,
43
+ TaskRuntimeEvent,
44
+ TaskRuntimeEventType,
45
+ TaskScope,
46
+ TaskSpec,
47
+ TaskStatus,
48
+ TaskType,
49
+ )
50
+
51
+ _TERMINAL_STATUSES = {TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.STOPPED}
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ class DefaultTaskRuntime(TaskRuntime):
56
+ def __init__(
57
+ self,
58
+ store: TaskStore,
59
+ queue: TaskCommandQueue,
60
+ *,
61
+ output_sink: Optional["TaskOutputSink"] = None,
62
+ lease_ttl_sec: Optional[float] = 300.0,
63
+ ) -> None:
64
+ self._store = store
65
+ self._queue = queue
66
+ self._output_sink = output_sink
67
+ self._lease_ttl_sec = lease_ttl_sec
68
+ self._lock = RLock()
69
+ self._reserved_batches: dict[str, ReservedNotificationBatch] = {}
70
+ self._executors: dict[TaskType, TaskExecutor] = {}
71
+ self._events: list[TaskRuntimeEvent] = []
72
+
73
+ def _emit_event(
74
+ self,
75
+ event_type: TaskRuntimeEventType,
76
+ record: TaskRecord,
77
+ summary: str,
78
+ *,
79
+ status: TaskStatus | None = None,
80
+ terminal_reason: str | None = None,
81
+ ) -> None:
82
+ with self._lock:
83
+ self._events.append(
84
+ TaskRuntimeEvent(
85
+ event_type=event_type,
86
+ task_id=record.task_id,
87
+ task_type=record.task_type,
88
+ status=status or record.status,
89
+ summary=summary,
90
+ scope=record.scope,
91
+ terminal_reason=terminal_reason,
92
+ )
93
+ )
94
+
95
+ def spawn(self, spec: TaskSpec) -> str:
96
+ task_id = new_task_id()
97
+ meta = spec.metadata or {}
98
+ ext = meta.get("external_ref")
99
+ external_ref = ext if isinstance(ext, str) else None
100
+ record = TaskRecord(
101
+ task_id=task_id,
102
+ task_type=spec.task_type,
103
+ status=TaskStatus.PENDING,
104
+ description=spec.description,
105
+ scope=spec.scope,
106
+ input=spec.input,
107
+ metadata=spec.metadata,
108
+ run_id=new_run_id(),
109
+ external_ref=external_ref,
110
+ )
111
+ self._store.create(record)
112
+ ObservabilityLoggerAdapter(
113
+ logger,
114
+ build_log_context(
115
+ run_id=record.run_id,
116
+ session_id=record.scope.agent_id,
117
+ task_id=task_id,
118
+ ).as_extra(),
119
+ ).info("task spawned type=%s", record.task_type.value)
120
+ return task_id
121
+
122
+ def register_executor(
123
+ self, task_type: TaskType, executor: TaskExecutor, *, overwrite: bool = False
124
+ ) -> None:
125
+ if (task_type in self._executors) and not overwrite:
126
+ raise ValueError(f"Executor already registered for task type: {task_type.value}")
127
+ self._executors[task_type] = executor
128
+
129
+ def run_task(self, task_id: str) -> bool:
130
+ record = self._store.get(task_id)
131
+ if record is None:
132
+ return False
133
+ task_log = ObservabilityLoggerAdapter(
134
+ logger,
135
+ build_log_context(
136
+ run_id=record.run_id,
137
+ session_id=record.scope.agent_id,
138
+ task_id=record.task_id,
139
+ ).as_extra(),
140
+ )
141
+ executor = self._executors.get(record.task_type)
142
+ if executor is None:
143
+ raise ValueError(f"No executor registered for task type: {record.task_type.value}")
144
+
145
+ now = time.time()
146
+ run_patch: dict = {
147
+ "status": TaskStatus.RUNNING,
148
+ "updated_at": now,
149
+ }
150
+ if self._lease_ttl_sec is not None:
151
+ run_patch["lease_until_ts"] = now + float(self._lease_ttl_sec)
152
+ run_patch["last_heartbeat_ts"] = now
153
+ self._store.update(task_id, run_patch)
154
+ running_record = self._store.get(task_id)
155
+ if running_record is None:
156
+ return False
157
+ self._emit_event(TaskRuntimeEventType.STARTED, running_record, "Task execution started")
158
+ task_log.info("task execution started")
159
+ try:
160
+ result: TaskExecutionResult = executor.execute(running_record)
161
+ except Exception as exc:
162
+ task_log.error("task execution failed: %s", exc)
163
+ return self.mark_terminal(
164
+ task_id,
165
+ TaskStatus.FAILED,
166
+ f"Executor failed: {exc}",
167
+ )
168
+
169
+ if result.status not in _TERMINAL_STATUSES:
170
+ raise ValueError(f"Executor must return terminal status, got {result.status}")
171
+ return self.mark_terminal(
172
+ task_id,
173
+ result.status,
174
+ result.summary,
175
+ output_file=result.output_file,
176
+ tool_use_id=result.tool_use_id,
177
+ usage=result.usage,
178
+ )
179
+
180
+ def cancel(self, task_id: str, reason: Optional[str] = None) -> None:
181
+ summary = reason or "Task cancelled"
182
+ self.mark_terminal(
183
+ task_id,
184
+ TaskStatus.STOPPED,
185
+ summary,
186
+ terminal_reason="cancelled",
187
+ )
188
+
189
+ def publish_progress(self, task_id: str, summary: str) -> bool:
190
+ record = self._store.get(task_id)
191
+ if record is None:
192
+ return False
193
+ self._emit_event(TaskRuntimeEventType.PROGRESS, record, summary)
194
+ return True
195
+
196
+ def drain_events(self) -> list[TaskRuntimeEvent]:
197
+ with self._lock:
198
+ events = list(self._events)
199
+ self._events.clear()
200
+ return events
201
+
202
+ def wait_for_terminal(
203
+ self,
204
+ task_id: str,
205
+ *,
206
+ timeout_sec: Optional[float] = None,
207
+ poll_interval_sec: float = 0.02,
208
+ ) -> Optional[TaskStatus]:
209
+ deadline = None if timeout_sec is None else time.monotonic() + float(timeout_sec)
210
+ poll_interval_sec = max(poll_interval_sec, 0.001)
211
+ while True:
212
+ record = self._store.get(task_id)
213
+ if record is None:
214
+ return None
215
+ if record.status in _TERMINAL_STATUSES:
216
+ return record.status
217
+ if deadline is not None and time.monotonic() >= deadline:
218
+ return None
219
+ time.sleep(poll_interval_sec)
220
+
221
+ def append_task_output(self, task_id: str, chunk: str) -> bool:
222
+ if self._output_sink is None:
223
+ return False
224
+ self._output_sink.append(task_id, chunk)
225
+ return True
226
+
227
+ def read_task_output(self, task_id: str, offset: int = 0) -> tuple[str, int]:
228
+ if self._output_sink is None:
229
+ return "", 0
230
+ return self._output_sink.read_from(task_id, offset)
231
+
232
+ def heartbeat(
233
+ self,
234
+ task_id: str,
235
+ *,
236
+ worker_id: Optional[str] = None,
237
+ extend_sec: Optional[float] = None,
238
+ ) -> bool:
239
+ record = self._store.get(task_id)
240
+ if record is None or record.status != TaskStatus.RUNNING:
241
+ return False
242
+ now = time.time()
243
+ base = self._lease_ttl_sec if self._lease_ttl_sec is not None else 300.0
244
+ ext = float(extend_sec) if extend_sec is not None else float(base)
245
+ patch: dict = {
246
+ "last_heartbeat_ts": now,
247
+ "lease_until_ts": now + ext,
248
+ "updated_at": now,
249
+ }
250
+ if worker_id is not None:
251
+ patch["worker_id"] = worker_id
252
+ self._store.update(task_id, patch)
253
+ return True
254
+
255
+ def reclaim_stale_running_tasks(self, *, now_ts: Optional[float] = None) -> list[str]:
256
+ now = time.time() if now_ts is None else float(now_ts)
257
+ reclaimed: list[str] = []
258
+ for record in self._store.list():
259
+ if record.status != TaskStatus.RUNNING:
260
+ continue
261
+ if record.lease_until_ts is None:
262
+ continue
263
+ if record.lease_until_ts >= now:
264
+ continue
265
+ tid = record.task_id
266
+ if self.mark_terminal(
267
+ tid,
268
+ TaskStatus.FAILED,
269
+ "Lease expired (worker heartbeat lost or process crash)",
270
+ terminal_reason="lease_expired",
271
+ ):
272
+ reclaimed.append(tid)
273
+ return reclaimed
274
+
275
+ def mark_terminal(
276
+ self,
277
+ task_id: str,
278
+ status: TaskStatus,
279
+ summary: str,
280
+ *,
281
+ output_file: Optional[str] = None,
282
+ tool_use_id: Optional[str] = None,
283
+ usage: Optional[dict[str, int]] = None,
284
+ requires_action: bool = False,
285
+ terminal_reason: Optional[str] = None,
286
+ ) -> bool:
287
+ if status not in _TERMINAL_STATUSES:
288
+ raise ValueError(f"status must be terminal, got {status}")
289
+ record = self._store.get(task_id)
290
+ if record is None:
291
+ return False
292
+ if status in record.terminal_emitted_statuses:
293
+ return False
294
+
295
+ now = time.time()
296
+ next_emitted = set(record.terminal_emitted_statuses)
297
+ next_emitted.add(status)
298
+ updated = replace(
299
+ record,
300
+ status=status,
301
+ output_file=output_file or record.output_file,
302
+ terminal_emitted_statuses=next_emitted,
303
+ updated_at=now,
304
+ )
305
+ self._store.update(task_id, updated.__dict__)
306
+
307
+ self._queue.enqueue(
308
+ TaskNotificationEnvelope(
309
+ command_id=new_notification_command_id(),
310
+ task_id=task_id,
311
+ task_type=updated.task_type,
312
+ status=status,
313
+ summary=summary,
314
+ scope=updated.scope,
315
+ priority=notification_priority_for_terminal_status(status),
316
+ output_file=updated.output_file,
317
+ tool_use_id=tool_use_id,
318
+ usage=usage,
319
+ requires_action=requires_action,
320
+ terminal_reason=terminal_reason,
321
+ )
322
+ )
323
+ self._emit_event(
324
+ TaskRuntimeEventType.TERMINAL,
325
+ updated,
326
+ summary,
327
+ status=status,
328
+ terminal_reason=terminal_reason,
329
+ )
330
+ ObservabilityLoggerAdapter(
331
+ logger,
332
+ build_log_context(
333
+ run_id=updated.run_id,
334
+ session_id=updated.scope.agent_id,
335
+ task_id=updated.task_id,
336
+ ).as_extra(),
337
+ ).info("task terminal status=%s summary=%s", status.value, summary)
338
+ return True
339
+
340
+ def drain_notifications(
341
+ self, scope: TaskScope, max_priority: QueuePriority
342
+ ) -> list[TaskNotificationEnvelope]:
343
+ return self._queue.peek_for_scope(scope, max_priority)
344
+
345
+ def ack_notifications(self, command_ids: list[str]) -> None:
346
+ self._queue.remove(command_ids)
347
+
348
+ def reserve_notifications(
349
+ self,
350
+ scope: TaskScope,
351
+ max_priority: QueuePriority,
352
+ *,
353
+ limit: int = 8,
354
+ ) -> Optional[ReservedNotificationBatch]:
355
+ if limit <= 0:
356
+ return None
357
+ items = self._queue.peek_for_scope(scope, max_priority)[:limit]
358
+ if not items:
359
+ return None
360
+ self._queue.remove([item.command_id for item in items])
361
+ batch = ReservedNotificationBatch(
362
+ batch_id=new_reserved_batch_id(),
363
+ scope=scope,
364
+ items=items,
365
+ )
366
+ with self._lock:
367
+ self._reserved_batches[batch.batch_id] = batch
368
+ return batch
369
+
370
+ def ack_reserved(self, batch_id: str) -> None:
371
+ with self._lock:
372
+ self._reserved_batches.pop(batch_id, None)
373
+
374
+ def nack_reserved(self, batch_id: str, *, requeue: bool = True) -> None:
375
+ with self._lock:
376
+ batch = self._reserved_batches.pop(batch_id, None)
377
+ if batch is None:
378
+ return
379
+ if requeue:
380
+ for item in batch.items:
381
+ self._queue.enqueue(item)
382
+
383
+ def has_pending_notifications(
384
+ self, scope: TaskScope, max_priority: QueuePriority
385
+ ) -> bool:
386
+ return bool(self._queue.peek_for_scope(scope, max_priority))
@@ -0,0 +1,5 @@
1
+ """Task output buffering (§10.4)."""
2
+
3
+ from .sink import InMemoryTaskOutputSink, TaskOutputSink
4
+
5
+ __all__ = ["InMemoryTaskOutputSink", "TaskOutputSink"]