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,380 @@
1
+ """
2
+ memory/memdir/agent_memory.py — Layer 4 agent 记忆路径协作者。
3
+
4
+ 职责:
5
+ 负责 agent_type 安全化与 Layer 4 路径边界判断,供加载/权限/抽取链路复用。
6
+
7
+ 链路位置:
8
+ subagent 启动加载与 Layer 4 写权限判定都会调用本模块。
9
+
10
+ 当前裁剪范围:
11
+ Phase 2 补充 prompt 注入能力(subagent 专用),抽取编排在后续 Phase。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from dataclasses import dataclass
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable
21
+
22
+ from langchain_agentx.workspace import AgentWorkspaceConfig
23
+ from langchain_agentx.workspace.config import _sanitize_agent_type
24
+
25
+ from .loader import MemoryPromptLoader
26
+ from .paths import is_memory_enabled
27
+
28
+ if TYPE_CHECKING:
29
+ from langchain_agentx.loop.prompt.sections import SystemPromptSection
30
+
31
+
32
+ _SCOPE_GUIDANCE: dict[str, str] = {
33
+ "user": "- Since this memory is user-scope, keep learnings general since they apply across all projects",
34
+ "project": "- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project",
35
+ "local": "- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine",
36
+ }
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class _AgentMemoryWorkspaceView:
43
+ memory_dir: Path
44
+ memory_entrypoint: Path
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class AgentMemoryExtractionContext:
49
+ session_id: str
50
+ container_type: str
51
+ workspace_cfg: AgentWorkspaceConfig
52
+ agent_type: str
53
+ messages: list[Any]
54
+ agent_id: str | None = None
55
+
56
+
57
+ class AgentMemoryPathPolicy:
58
+ """Layer 4 路径策略协作者。"""
59
+
60
+ def __init__(self, cfg: AgentWorkspaceConfig) -> None:
61
+ self._cfg = cfg
62
+ self._agents_root = (cfg.memory_dir / "agents").resolve()
63
+
64
+ def is_agent_memory_path(self, absolute_path: str | Path) -> bool:
65
+ resolved = Path(absolute_path).resolve()
66
+ try:
67
+ resolved.relative_to(self._agents_root)
68
+ return True
69
+ except ValueError:
70
+ return False
71
+
72
+ def is_agent_memory_path_for_type(
73
+ self,
74
+ *,
75
+ agent_type: str,
76
+ absolute_path: str | Path,
77
+ allowed_scopes: tuple[str, ...] = ("project", "user", "local"),
78
+ ) -> bool:
79
+ resolved = Path(absolute_path).resolve()
80
+ sanitized = _sanitize_agent_type(agent_type)
81
+ for scope in allowed_scopes:
82
+ mem_dir = self._cfg.agent_memory_dir(sanitized, scope).resolve()
83
+ try:
84
+ resolved.relative_to(mem_dir)
85
+ return True
86
+ except ValueError:
87
+ continue
88
+ return False
89
+
90
+
91
+ def sanitize_agent_type(agent_type: str) -> str:
92
+ return _sanitize_agent_type(agent_type)
93
+
94
+
95
+ class AgentMemoryPromptService:
96
+ """Layer 4 agent memory prompt 构建协作者。"""
97
+
98
+ def __init__(self, cfg: AgentWorkspaceConfig) -> None:
99
+ self._cfg = cfg
100
+
101
+ def load_agent_memory_prompt(self, *, agent_type: str, scope: str = "project") -> str | None:
102
+ sanitized = _sanitize_agent_type(agent_type)
103
+ memory_dir = self._cfg.agent_memory_dir(sanitized, scope)
104
+ entrypoint = self._cfg.agent_memory_entrypoint(sanitized, scope)
105
+ prompt = MemoryPromptLoader(
106
+ _AgentMemoryWorkspaceView(memory_dir=memory_dir, memory_entrypoint=entrypoint) # type: ignore[arg-type]
107
+ ).load_memory_prompt()
108
+ if prompt is None:
109
+ return None
110
+ guidance = _SCOPE_GUIDANCE.get(scope)
111
+ if guidance:
112
+ return prompt + "\n\n" + guidance
113
+ return prompt
114
+
115
+
116
+ class AgentMemoryPromptBootstrap:
117
+ """Loop 侧 Layer 4 section 注入协作者。"""
118
+
119
+ def __init__(
120
+ self,
121
+ *,
122
+ workspace_cfg: AgentWorkspaceConfig,
123
+ is_subagent: bool,
124
+ subagent_type: str | None,
125
+ scope: str = "project",
126
+ ) -> None:
127
+ self._workspace_cfg = workspace_cfg
128
+ self._is_subagent = is_subagent
129
+ self._subagent_type = subagent_type
130
+ self._scope = scope
131
+
132
+ def build_sections(self) -> list["SystemPromptSection"]:
133
+ if not self._is_subagent or not self._subagent_type:
134
+ return []
135
+ from langchain_agentx.loop.prompt.sections import section
136
+
137
+ service = AgentMemoryPromptService(self._workspace_cfg)
138
+ return [
139
+ section(
140
+ "agent_memory",
141
+ lambda: service.load_agent_memory_prompt(
142
+ agent_type=self._subagent_type or "",
143
+ scope=self._scope,
144
+ ),
145
+ )
146
+ ]
147
+
148
+
149
+ class AgentMemoryExtractionCoordinator:
150
+ """Layer 4 agent memory 后台抽取协作者(独立于 Layer 2)。"""
151
+
152
+ def __init__(
153
+ self,
154
+ *,
155
+ extraction_executor: Callable[[AgentMemoryExtractionContext, list[Any]], Awaitable[None]]
156
+ | None = None,
157
+ ) -> None:
158
+ self._executor = extraction_executor or _noop_agent_memory_extraction_executor
159
+ self._in_progress = False
160
+ self._pending_context: AgentMemoryExtractionContext | None = None
161
+ self._last_message_id: dict[str, str] = {}
162
+ self._tasks: set[asyncio.Task[None]] = set()
163
+
164
+ def trigger_from_state(
165
+ self,
166
+ *,
167
+ state: dict[str, Any],
168
+ workspace_cfg: AgentWorkspaceConfig,
169
+ session_id: str,
170
+ container_type: str,
171
+ agent_type: str | None,
172
+ ) -> None:
173
+ if not agent_type:
174
+ logger.warning(
175
+ "skip agent memory extraction trigger: empty agent_type (container=%s)",
176
+ container_type,
177
+ )
178
+ return
179
+ context = AgentMemoryExtractionContext(
180
+ session_id=session_id,
181
+ container_type=container_type,
182
+ workspace_cfg=workspace_cfg,
183
+ agent_type=agent_type,
184
+ messages=list(state.get("messages") or []),
185
+ agent_id=state.get("agent_id"),
186
+ )
187
+ self.trigger(context)
188
+
189
+ def trigger(self, context: AgentMemoryExtractionContext) -> None:
190
+ try:
191
+ loop = asyncio.get_running_loop()
192
+ except RuntimeError:
193
+ logger.warning("skip agent memory extraction trigger: no running event loop")
194
+ return
195
+ task = loop.create_task(self.execute_extract_memories(context))
196
+ self._tasks.add(task)
197
+ task.add_done_callback(self._tasks.discard)
198
+
199
+ async def execute_extract_memories(self, context: AgentMemoryExtractionContext) -> None:
200
+ if not self._can_extract(context):
201
+ return
202
+ if self._in_progress:
203
+ self._pending_context = context
204
+ return
205
+ self._in_progress = True
206
+ try:
207
+ current = context
208
+ while True:
209
+ await self._execute_once(current)
210
+ if self._pending_context is None:
211
+ break
212
+ current = self._pending_context
213
+ self._pending_context = None
214
+ finally:
215
+ self._in_progress = False
216
+
217
+ async def drain_pending_extraction(self, timeout_ms: int = 60_000) -> None:
218
+ if not self._tasks:
219
+ return
220
+ try:
221
+ await asyncio.wait_for(
222
+ asyncio.gather(*list(self._tasks), return_exceptions=True),
223
+ timeout=timeout_ms / 1000.0,
224
+ )
225
+ except asyncio.TimeoutError:
226
+ logger.warning("agent memory extraction drain timed out after %sms", timeout_ms)
227
+
228
+ async def _execute_once(self, context: AgentMemoryExtractionContext) -> None:
229
+ new_messages = self._messages_since_cursor(context)
230
+ if not new_messages:
231
+ return
232
+ if self._has_agent_memory_writes_since(context, new_messages):
233
+ self._advance_cursor(context, new_messages)
234
+ return
235
+ await self._executor(context, new_messages)
236
+ self._advance_cursor(context, new_messages)
237
+
238
+ @staticmethod
239
+ def _can_extract(context: AgentMemoryExtractionContext) -> bool:
240
+ return (
241
+ context.container_type == "subagent"
242
+ and bool(context.agent_type)
243
+ and is_memory_enabled(context.workspace_cfg)
244
+ )
245
+
246
+ def _messages_since_cursor(self, context: AgentMemoryExtractionContext) -> list[Any]:
247
+ if not context.messages:
248
+ return []
249
+ cursor = self._last_message_id.get(self._session_key(context))
250
+ if cursor is None:
251
+ return context.messages
252
+ for idx, message in enumerate(context.messages):
253
+ if self._message_id(message) == cursor:
254
+ return context.messages[idx + 1 :]
255
+ return context.messages
256
+
257
+ def _advance_cursor(self, context: AgentMemoryExtractionContext, messages: list[Any]) -> None:
258
+ last_id = self._message_id(messages[-1])
259
+ if last_id:
260
+ self._last_message_id[self._session_key(context)] = last_id
261
+
262
+ def _has_agent_memory_writes_since(
263
+ self, context: AgentMemoryExtractionContext, messages: list[Any]
264
+ ) -> bool:
265
+ for message in messages:
266
+ calls = []
267
+ if isinstance(message, dict):
268
+ calls = list(message.get("tool_calls") or [])
269
+ else:
270
+ calls = list(getattr(message, "tool_calls", None) or [])
271
+ for call in calls:
272
+ if _is_agent_memory_write_call(call, context.workspace_cfg, context.agent_type):
273
+ return True
274
+ return False
275
+
276
+ @staticmethod
277
+ def _session_key(context: AgentMemoryExtractionContext) -> str:
278
+ return f"{context.container_type}:{context.session_id}:{context.agent_type}"
279
+
280
+ @staticmethod
281
+ def _message_id(message: Any) -> str | None:
282
+ if isinstance(message, dict):
283
+ value = message.get("id")
284
+ return str(value) if value else None
285
+ value = getattr(message, "id", None)
286
+ return str(value) if value else None
287
+
288
+
289
+ async def _noop_agent_memory_extraction_executor(
290
+ context: AgentMemoryExtractionContext, messages: list[Any]
291
+ ) -> None:
292
+ logger.info(
293
+ "agent memory extraction placeholder executor triggered session=%s agent_type=%s message_count=%s",
294
+ context.session_id,
295
+ context.agent_type,
296
+ len(messages),
297
+ )
298
+ return None
299
+
300
+
301
+ def _is_agent_memory_write_call(
302
+ call: Any,
303
+ workspace_cfg: AgentWorkspaceConfig,
304
+ agent_type: str,
305
+ ) -> bool:
306
+ if isinstance(call, dict):
307
+ name = str(call.get("name") or call.get("tool_name") or "")
308
+ args = call.get("args") or call.get("tool_input") or {}
309
+ else:
310
+ name = str(getattr(call, "name", "") or getattr(call, "tool_name", ""))
311
+ args = getattr(call, "args", None) or getattr(call, "tool_input", None) or {}
312
+ if name not in {"Write", "Edit"}:
313
+ return False
314
+ if not isinstance(args, dict):
315
+ return False
316
+ candidate = args.get("path") or args.get("file_path")
317
+ if not candidate:
318
+ return False
319
+ return is_agent_memory_path_for_type(
320
+ workspace_cfg,
321
+ agent_type,
322
+ candidate,
323
+ allowed_scopes=("project",),
324
+ )
325
+
326
+
327
+ _DEFAULT_AGENT_MEMORY_COORDINATOR = AgentMemoryExtractionCoordinator()
328
+
329
+
330
+ def get_default_agent_memory_extraction_coordinator() -> AgentMemoryExtractionCoordinator:
331
+ return _DEFAULT_AGENT_MEMORY_COORDINATOR
332
+
333
+
334
+ async def drain_pending_agent_memory_extraction(timeout_ms: int = 60_000) -> None:
335
+ await _DEFAULT_AGENT_MEMORY_COORDINATOR.drain_pending_extraction(timeout_ms=timeout_ms)
336
+
337
+
338
+ def is_agent_memory_path(cfg: AgentWorkspaceConfig, absolute_path: str | Path) -> bool:
339
+ return AgentMemoryPathPolicy(cfg).is_agent_memory_path(absolute_path)
340
+
341
+
342
+ def is_agent_memory_path_for_type(
343
+ cfg: AgentWorkspaceConfig,
344
+ agent_type: str,
345
+ absolute_path: str | Path,
346
+ *,
347
+ allowed_scopes: tuple[str, ...] = ("project", "user", "local"),
348
+ ) -> bool:
349
+ return AgentMemoryPathPolicy(cfg).is_agent_memory_path_for_type(
350
+ agent_type=agent_type,
351
+ absolute_path=absolute_path,
352
+ allowed_scopes=allowed_scopes,
353
+ )
354
+
355
+
356
+ def load_agent_memory_prompt(
357
+ cfg: AgentWorkspaceConfig,
358
+ *,
359
+ agent_type: str,
360
+ scope: str = "project",
361
+ ) -> str | None:
362
+ return AgentMemoryPromptService(cfg).load_agent_memory_prompt(
363
+ agent_type=agent_type,
364
+ scope=scope,
365
+ )
366
+
367
+
368
+ __all__ = [
369
+ "AgentMemoryExtractionContext",
370
+ "AgentMemoryExtractionCoordinator",
371
+ "AgentMemoryPromptBootstrap",
372
+ "AgentMemoryPromptService",
373
+ "AgentMemoryPathPolicy",
374
+ "drain_pending_agent_memory_extraction",
375
+ "get_default_agent_memory_extraction_coordinator",
376
+ "is_agent_memory_path",
377
+ "is_agent_memory_path_for_type",
378
+ "load_agent_memory_prompt",
379
+ "sanitize_agent_type",
380
+ ]
@@ -0,0 +1,309 @@
1
+ """
2
+ memory/memdir/extractor.py — Layer 2 后台抽取协作者。
3
+
4
+ 职责:
5
+ 在 loop stop 时以 fire-and-forget 触发记忆抽取,提供并发保护与 session 退出 drain。
6
+
7
+ 链路位置:
8
+ HookGraphWiring.stop_hooks_node() 触发 -> MemoryExtractionCoordinator 执行。
9
+
10
+ 当前裁剪范围:
11
+ Phase 4 先实现门禁链、游标推进、并发与 drain;抽取写入细节使用可注入执行器。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from dataclasses import dataclass
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import Any, Awaitable, Callable
21
+
22
+ from langchain_agentx.workspace import AgentWorkspaceConfig
23
+
24
+ from .agent_memory import is_agent_memory_path
25
+ from .paths import is_memory_enabled
26
+ from .scan import format_memory_manifest, scan_memory_files
27
+ from .types import HOW_TO_SAVE_SECTION, TYPES_SECTION_INDIVIDUAL, WHAT_NOT_TO_SAVE_SECTION
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _WRITE_TOOLS = {"Write", "Edit"}
32
+ _READ_TOOLS = {"Read", "Grep", "Glob"}
33
+ _READ_ONLY_BASH_PREFIX = ("ls", "find", "grep", "cat", "stat", "wc", "head", "tail")
34
+ DEFAULT_EXTRACTION_WINDOW_MESSAGES = 30
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class MemoryExtractionContext:
39
+ session_id: str
40
+ container_type: str
41
+ workspace_cfg: AgentWorkspaceConfig
42
+ messages: list[Any]
43
+ agent_id: str | None = None
44
+
45
+
46
+ class MemoryExtractionCoordinator:
47
+ def __init__(
48
+ self,
49
+ *,
50
+ extraction_executor: Callable[[MemoryExtractionContext, list[Any]], Awaitable[None]] | None = None,
51
+ ) -> None:
52
+ self._executor = extraction_executor or _noop_extraction_executor
53
+ self._in_progress = False
54
+ self._pending_context: MemoryExtractionContext | None = None
55
+ self._last_message_id: dict[str, str] = {}
56
+ self._tasks: set[asyncio.Task[None]] = set()
57
+
58
+ def trigger_from_state(
59
+ self,
60
+ *,
61
+ state: dict[str, Any],
62
+ workspace_cfg: AgentWorkspaceConfig,
63
+ session_id: str,
64
+ container_type: str,
65
+ ) -> None:
66
+ context = MemoryExtractionContext(
67
+ session_id=session_id,
68
+ container_type=container_type,
69
+ workspace_cfg=workspace_cfg,
70
+ messages=list(state.get("messages") or []),
71
+ agent_id=state.get("agent_id"),
72
+ )
73
+ self.trigger(context)
74
+
75
+ def trigger(self, context: MemoryExtractionContext) -> None:
76
+ try:
77
+ loop = asyncio.get_running_loop()
78
+ except RuntimeError:
79
+ logger.warning("skip memory extraction trigger: no running event loop")
80
+ return
81
+ task = loop.create_task(self.execute_extract_memories(context))
82
+ self._tasks.add(task)
83
+ task.add_done_callback(self._tasks.discard)
84
+
85
+ async def execute_extract_memories(self, context: MemoryExtractionContext) -> None:
86
+ if not self._can_extract(context):
87
+ return
88
+ if self._in_progress:
89
+ # 单槽覆盖语义:只保留最新上下文。
90
+ self._pending_context = context
91
+ return
92
+ self._in_progress = True
93
+ try:
94
+ current = context
95
+ while True:
96
+ await self._execute_once(current)
97
+ if self._pending_context is None:
98
+ break
99
+ current = self._pending_context
100
+ self._pending_context = None
101
+ finally:
102
+ self._in_progress = False
103
+
104
+ async def drain_pending_extraction(self, timeout_ms: int = 60_000) -> None:
105
+ if not self._tasks:
106
+ return
107
+ try:
108
+ await asyncio.wait_for(
109
+ asyncio.gather(*list(self._tasks), return_exceptions=True),
110
+ timeout=timeout_ms / 1000.0,
111
+ )
112
+ except asyncio.TimeoutError:
113
+ logger.warning("memory extraction drain timed out after %sms", timeout_ms)
114
+
115
+ async def _execute_once(self, context: MemoryExtractionContext) -> None:
116
+ new_messages = self._messages_since_cursor(context)
117
+ if not new_messages:
118
+ return
119
+ if self._has_memory_writes_since(context, new_messages):
120
+ self._advance_cursor(context, new_messages)
121
+ return
122
+ await self._executor(context, new_messages)
123
+ self._advance_cursor(context, new_messages)
124
+
125
+ def _can_extract(self, context: MemoryExtractionContext) -> bool:
126
+ if context.container_type not in ("interactive", "conversation"):
127
+ return False
128
+ if context.agent_id:
129
+ return False
130
+ return is_memory_enabled(context.workspace_cfg)
131
+
132
+ def _messages_since_cursor(self, context: MemoryExtractionContext) -> list[Any]:
133
+ if not context.messages:
134
+ return []
135
+ session_key = self._session_key(context)
136
+ cursor = self._last_message_id.get(session_key) or self._load_cursor(context)
137
+ if cursor is None:
138
+ return context.messages
139
+ for idx, message in enumerate(context.messages):
140
+ if self._message_id(message) == cursor:
141
+ return context.messages[idx + 1 :]
142
+ return context.messages
143
+
144
+ def _advance_cursor(self, context: MemoryExtractionContext, messages: list[Any]) -> None:
145
+ last_id = self._message_id(messages[-1])
146
+ if not last_id:
147
+ return
148
+ session_key = self._session_key(context)
149
+ self._last_message_id[session_key] = last_id
150
+ if context.container_type == "conversation":
151
+ cursor_path = context.workspace_cfg.sessions_dir / f"{context.session_id}.cursor"
152
+ cursor_path.parent.mkdir(parents=True, exist_ok=True)
153
+ cursor_path.write_text(last_id, encoding="utf-8")
154
+
155
+ def _load_cursor(self, context: MemoryExtractionContext) -> str | None:
156
+ if context.container_type != "conversation":
157
+ return None
158
+ path = context.workspace_cfg.sessions_dir / f"{context.session_id}.cursor"
159
+ if not path.exists():
160
+ return None
161
+ return path.read_text(encoding="utf-8").strip() or None
162
+
163
+ @staticmethod
164
+ def _session_key(context: MemoryExtractionContext) -> str:
165
+ return f"{context.container_type}:{context.session_id}"
166
+
167
+ @staticmethod
168
+ def _message_id(message: Any) -> str | None:
169
+ if isinstance(message, dict):
170
+ value = message.get("id")
171
+ return str(value) if value else None
172
+ value = getattr(message, "id", None)
173
+ return str(value) if value else None
174
+
175
+ @staticmethod
176
+ def _has_memory_writes_since(context: MemoryExtractionContext, messages: list[Any]) -> bool:
177
+ for message in messages:
178
+ calls = []
179
+ if isinstance(message, dict):
180
+ calls = list(message.get("tool_calls") or [])
181
+ else:
182
+ calls = list(getattr(message, "tool_calls", None) or [])
183
+ for call in calls:
184
+ if _is_memory_write_call(call, context.workspace_cfg):
185
+ return True
186
+ return False
187
+
188
+
189
+ def create_auto_mem_can_use_tool(memory_dir: Path) -> Callable[[str, dict[str, Any]], bool]:
190
+ resolved_memory_dir = memory_dir.resolve()
191
+
192
+ def _can_use(tool_name: str, tool_input: dict[str, Any]) -> bool:
193
+ if tool_name in _READ_TOOLS:
194
+ return True
195
+ if tool_name == "Bash":
196
+ command = str(tool_input.get("command") or "").strip()
197
+ return command.startswith(_READ_ONLY_BASH_PREFIX)
198
+ if tool_name in _WRITE_TOOLS:
199
+ candidate = tool_input.get("path") or tool_input.get("file_path")
200
+ if candidate is None:
201
+ return False
202
+ try:
203
+ Path(candidate).resolve().relative_to(resolved_memory_dir)
204
+ return True
205
+ except (ValueError, OSError, RuntimeError, TypeError):
206
+ return False
207
+ return False
208
+
209
+ return _can_use
210
+
211
+
212
+ async def _noop_extraction_executor(
213
+ context: MemoryExtractionContext, messages: list[Any]
214
+ ) -> None:
215
+ prompt = build_extract_prompt(
216
+ workspace_cfg=context.workspace_cfg,
217
+ window_messages=min(DEFAULT_EXTRACTION_WINDOW_MESSAGES, len(messages)),
218
+ )
219
+ logger.info(
220
+ "memory extraction placeholder executor triggered session=%s container=%s prompt_chars=%s message_count=%s",
221
+ context.session_id,
222
+ context.container_type,
223
+ len(prompt),
224
+ len(messages),
225
+ )
226
+ return None
227
+
228
+
229
+ _DEFAULT_COORDINATOR = MemoryExtractionCoordinator()
230
+
231
+
232
+ async def execute_extract_memories(context: MemoryExtractionContext) -> None:
233
+ await _DEFAULT_COORDINATOR.execute_extract_memories(context)
234
+
235
+
236
+ async def drain_pending_extraction(timeout_ms: int = 60_000) -> None:
237
+ await _DEFAULT_COORDINATOR.drain_pending_extraction(timeout_ms=timeout_ms)
238
+
239
+
240
+ def _is_memory_write_call(call: Any, workspace_cfg: AgentWorkspaceConfig) -> bool:
241
+ if isinstance(call, dict):
242
+ name = str(call.get("name") or call.get("tool_name") or "")
243
+ args = call.get("args") or call.get("tool_input") or {}
244
+ else:
245
+ name = str(getattr(call, "name", "") or getattr(call, "tool_name", ""))
246
+ args = getattr(call, "args", None) or getattr(call, "tool_input", None) or {}
247
+ if name not in _WRITE_TOOLS:
248
+ return False
249
+ if not isinstance(args, dict):
250
+ return False
251
+ candidate = args.get("path") or args.get("file_path")
252
+ if not candidate:
253
+ return False
254
+ try:
255
+ resolved = Path(candidate).resolve()
256
+ if is_agent_memory_path(workspace_cfg, resolved):
257
+ return True
258
+ resolved.relative_to(workspace_cfg.memory_dir.resolve())
259
+ return True
260
+ except (ValueError, OSError, RuntimeError, TypeError):
261
+ return False
262
+
263
+
264
+ def build_extract_prompt(
265
+ *,
266
+ workspace_cfg: AgentWorkspaceConfig,
267
+ window_messages: int = DEFAULT_EXTRACTION_WINDOW_MESSAGES,
268
+ ) -> str:
269
+ headers = scan_memory_files(workspace_cfg.memory_dir)
270
+ manifest = format_memory_manifest(headers)
271
+ opener = (
272
+ "You are now acting as the memory extraction subagent. Analyze the most recent\n"
273
+ f"~{window_messages} messages above and use them to update your persistent memory systems.\n\n"
274
+ "Available tools: Read, Grep, Glob, read-only Bash (ls/find/cat/stat/wc/head/tail\n"
275
+ "and similar), and Edit/Write for paths inside the memory directory only.\n"
276
+ "Bash rm is not permitted. All other tools — MCP, Agent, write-capable Bash,\n"
277
+ "etc — will be denied.\n\n"
278
+ "You have a limited turn budget. Edit requires a prior Read of the same file,\n"
279
+ "so the efficient strategy is: turn 1 — issue all Read calls in parallel for\n"
280
+ "every file you might update; turn 2 — issue all Write/Edit calls in parallel.\n"
281
+ "Do not interleave reads and writes across multiple turns.\n"
282
+ )
283
+ existing = ""
284
+ if manifest:
285
+ existing = (
286
+ "\n\n## Existing memory files\n\n"
287
+ f"{manifest}\n\n"
288
+ "Check this list before writing — update an existing file rather than creating\n"
289
+ "a duplicate."
290
+ )
291
+ body = (
292
+ "If the user explicitly asks you to remember something, save it immediately as\n"
293
+ "whichever type fits best. If they ask you to forget something, find and remove\n"
294
+ "the relevant entry.\n\n"
295
+ f"{TYPES_SECTION_INDIVIDUAL}\n\n"
296
+ f"{WHAT_NOT_TO_SAVE_SECTION}\n\n"
297
+ f"{HOW_TO_SAVE_SECTION}"
298
+ )
299
+ return opener + existing + "\n\n" + body
300
+
301
+
302
+ __all__ = [
303
+ "MemoryExtractionContext",
304
+ "MemoryExtractionCoordinator",
305
+ "build_extract_prompt",
306
+ "create_auto_mem_can_use_tool",
307
+ "drain_pending_extraction",
308
+ "execute_extract_memories",
309
+ ]