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,208 @@
1
+ """
2
+ memory/session/compact_bridge.py — Session Memory 与 compact 桥接
3
+
4
+ 职责:
5
+ 在 compact 前等待提取完成,并在可用时基于 session memory 优先重建精简上下文。
6
+
7
+ 链路位置:
8
+ 由 loop compaction 编排层调用,作为 legacy compact 之前的优先尝试路径。
9
+
10
+ 当前裁剪范围:
11
+ 本阶段仅实现 Phase 3 的桥接核心能力,不接入具体 loop 节点。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ import time
20
+ from typing import Any
21
+
22
+ from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
23
+
24
+ from langchain_agentx.memory.session.prompts import (
25
+ DEFAULT_SESSION_MEMORY_TEMPLATE,
26
+ MAX_SECTION_LENGTH,
27
+ )
28
+ from langchain_agentx.memory.session.session_memory import SessionMemoryExtractionState
29
+
30
+ EXTRACTION_WAIT_TIMEOUT_SECONDS = 15.0
31
+ EXTRACTION_STALE_THRESHOLD_SECONDS = 60.0
32
+ MAX_SECTION_CHARS = MAX_SECTION_LENGTH * 4
33
+ TRUNCATION_MARKER = "[... section truncated for length ...]"
34
+ SUMMARY_PREFIX = "This session is being continued from saved session memory."
35
+
36
+
37
+ async def wait_for_session_memory_extraction(
38
+ extraction_state: SessionMemoryExtractionState,
39
+ *,
40
+ timeout_seconds: float = EXTRACTION_WAIT_TIMEOUT_SECONDS,
41
+ stale_threshold_seconds: float = EXTRACTION_STALE_THRESHOLD_SECONDS,
42
+ ) -> None:
43
+ """等待进行中的提取完成(带超时与 stale 保护)。"""
44
+ start = time.monotonic()
45
+ while extraction_state.extraction_started_at is not None:
46
+ extraction_age = start - extraction_state.extraction_started_at
47
+ if extraction_age >= stale_threshold_seconds:
48
+ return
49
+ if (time.monotonic() - start) >= timeout_seconds:
50
+ return
51
+ await asyncio.sleep(0.05)
52
+
53
+
54
+ def is_session_memory_empty(content: str, template: str = DEFAULT_SESSION_MEMORY_TEMPLATE) -> bool:
55
+ """判断 session memory 是否仍为模板占位内容。"""
56
+ return content.strip() == template.strip()
57
+
58
+
59
+ def _split_into_sections(content: str) -> list[str]:
60
+ sections: list[str] = []
61
+ current: list[str] = []
62
+ for line in content.splitlines():
63
+ if line.startswith("# ") and current:
64
+ sections.append("\n".join(current).strip())
65
+ current = [line]
66
+ continue
67
+ current.append(line)
68
+ if current:
69
+ sections.append("\n".join(current).strip())
70
+ return [section for section in sections if section]
71
+
72
+
73
+ def _truncate_at_line_boundary(text: str, max_chars: int) -> str:
74
+ if len(text) <= max_chars:
75
+ return text
76
+ cut = text.rfind("\n", 0, max_chars)
77
+ if cut == -1:
78
+ return text[:max_chars]
79
+ return text[:cut]
80
+
81
+
82
+ def truncate_session_memory_for_compact(content: str) -> str:
83
+ """按 section 逐段截断 session memory 内容。"""
84
+ sections = _split_into_sections(content)
85
+ if not sections:
86
+ return content
87
+
88
+ result: list[str] = []
89
+ for section in sections:
90
+ if len(section) <= MAX_SECTION_CHARS:
91
+ result.append(section)
92
+ continue
93
+ truncated = _truncate_at_line_boundary(section, MAX_SECTION_CHARS)
94
+ result.append(f"{truncated}\n{TRUNCATION_MARKER}")
95
+ return "\n\n".join(result)
96
+
97
+
98
+ def _is_safe_cut_point(messages: list[BaseMessage], index: int) -> bool:
99
+ if index >= len(messages):
100
+ return True
101
+ current = messages[index]
102
+ if isinstance(current, ToolMessage):
103
+ return False
104
+ if isinstance(current, AIMessage):
105
+ # OpenAI 对话约束:summary 之后首条消息不能是 assistant。
106
+ return False
107
+ if index > 0:
108
+ previous = messages[index - 1]
109
+ if isinstance(previous, AIMessage) and bool(previous.tool_calls):
110
+ return False
111
+ return True
112
+
113
+
114
+ def adjust_index_to_preserve_api_invariants(
115
+ messages: list[BaseMessage],
116
+ target_index: int,
117
+ ) -> int:
118
+ """向前调整截断点,避免拆分 tool_use/tool_result 对。"""
119
+ index = max(0, min(target_index, len(messages)))
120
+ while index > 0:
121
+ if _is_safe_cut_point(messages, index):
122
+ return index
123
+ index -= 1
124
+ return 0
125
+
126
+
127
+ def _build_summary_message(session_memory_summary: str) -> SystemMessage:
128
+ return SystemMessage(
129
+ content=(
130
+ f"{SUMMARY_PREFIX}\n\n"
131
+ f"<session_memory_summary>\n{session_memory_summary}\n</session_memory_summary>\n"
132
+ "Recent messages are preserved verbatim."
133
+ )
134
+ )
135
+
136
+
137
+ def try_session_memory_compaction(
138
+ *,
139
+ messages: list[BaseMessage],
140
+ session_memory_path: Path,
141
+ template: str = DEFAULT_SESSION_MEMORY_TEMPLATE,
142
+ ) -> list[BaseMessage] | None:
143
+ """优先使用 session memory 重建消息,失败返回 None。"""
144
+ try:
145
+ memory_content = session_memory_path.read_text(encoding="utf-8")
146
+ except OSError:
147
+ return None
148
+
149
+ if is_session_memory_empty(memory_content, template):
150
+ return None
151
+
152
+ if len(messages) <= 2:
153
+ return None
154
+
155
+ truncated_summary = truncate_session_memory_for_compact(memory_content)
156
+ target_index = max(1, len(messages) // 2)
157
+ cut_index = adjust_index_to_preserve_api_invariants(messages, target_index)
158
+ if cut_index == 0:
159
+ return None
160
+
161
+ head_system: list[BaseMessage] = []
162
+ remaining: list[BaseMessage] = messages
163
+ if isinstance(messages[0], SystemMessage):
164
+ head_system = [messages[0]]
165
+ remaining = messages[1:]
166
+ cut_index = max(0, cut_index - 1)
167
+ tail = remaining[cut_index:]
168
+ if not tail:
169
+ return None
170
+
171
+ rebuilt = [*head_system, _build_summary_message(truncated_summary), *tail]
172
+ return rebuilt
173
+
174
+
175
+ @dataclass
176
+ class SessionMemoryCompactBridge:
177
+ """Session memory compact 协作者。"""
178
+
179
+ session_memory_path: Path
180
+ extraction_state: SessionMemoryExtractionState
181
+ template: str = DEFAULT_SESSION_MEMORY_TEMPLATE
182
+ consecutive_failures: int = 0
183
+ max_failures_before_open_circuit: int = 3
184
+
185
+ async def wait_for_extraction(self) -> None:
186
+ await wait_for_session_memory_extraction(self.extraction_state)
187
+
188
+ def is_empty(self) -> bool:
189
+ try:
190
+ content = self.session_memory_path.read_text(encoding="utf-8")
191
+ except OSError:
192
+ return True
193
+ return is_session_memory_empty(content, self.template)
194
+
195
+ def try_compact(self, messages: list[BaseMessage]) -> list[BaseMessage] | None:
196
+ if self.consecutive_failures >= self.max_failures_before_open_circuit:
197
+ return None
198
+ compacted = try_session_memory_compaction(
199
+ messages=messages,
200
+ session_memory_path=self.session_memory_path,
201
+ template=self.template,
202
+ )
203
+ if compacted is None:
204
+ self.consecutive_failures += 1
205
+ return None
206
+ self.consecutive_failures = 0
207
+ return compacted
208
+
@@ -0,0 +1,172 @@
1
+ """
2
+ memory/session/prompts.py — Session Memory 提示词构建
3
+
4
+ 职责:
5
+ 提供会话记忆模板、提取提示词与提示词组装函数,确保 Layer 3 提取行为稳定可测。
6
+
7
+ 链路位置:
8
+ 由 SessionMemoryManager 在后台提取前调用,生成最终 update prompt。
9
+
10
+ 当前裁剪范围:
11
+ 本模块仅实现 Phase 1 的 prompt 与模板能力,不承担提取调度和 compact 集成。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ import re
18
+
19
+ MAX_SECTION_LENGTH = 2000
20
+ SECTION_REMINDER_THRESHOLD_RATIO = 0.8
21
+
22
+ DEFAULT_SESSION_MEMORY_TEMPLATE = """# Session Title
23
+ _A short and distinctive 5-10 word descriptive title for the session. Super info dense, no filler_
24
+
25
+ # Current State
26
+ _What is actively being worked on right now? Pending tasks not yet completed. Immediate next steps._
27
+
28
+ # Task specification
29
+ _What did the user ask to build? Any design decisions or other explanatory context_
30
+
31
+ # Files and Functions
32
+ _What are the important files? In short, what do they contain and why are they relevant?_
33
+
34
+ # Workflow
35
+ _What bash commands are usually run and in what order? How to interpret their output if not obvious?_
36
+
37
+ # Errors & Corrections
38
+ _Errors encountered and how they were fixed. What did the user correct? What approaches failed and should not be tried again?_
39
+
40
+ # Codebase and System Documentation
41
+ _What are the important system components? How do they work/fit together?_
42
+
43
+ # Learnings
44
+ _What has worked well? What has not? What to avoid? Do not duplicate items from other sections_
45
+
46
+ # Key results
47
+ _If the user asked a specific output such as an answer to a question, a table, or other document, repeat the exact result here_
48
+
49
+ # Worklog
50
+ _Step by step, what was attempted, done? Very terse summary for each step_
51
+ """
52
+
53
+ EXTRACT_SESSION_MEMORY_PROMPT = """IMPORTANT: This message and these instructions are NOT part of the actual user conversation. Do NOT include any references to "note-taking", "session notes extraction", or these update instructions in the notes content.
54
+
55
+ Based on the user conversation above (EXCLUDING this note-taking instruction message as well as system prompt, claude.md entries, or any past session summaries), update the session notes file.
56
+
57
+ The file {{notesPath}} has already been read for you. Here are its current contents:
58
+ <current_notes_content>
59
+ {{currentNotes}}
60
+ </current_notes_content>
61
+
62
+ Your ONLY task is to use the Edit tool to update the notes file, then stop. You can make multiple edits (update every section as needed) - make all Edit tool calls in parallel in a single message. Do not call any other tools.
63
+
64
+ CRITICAL RULES FOR EDITING:
65
+ - The file must maintain its exact structure with all sections, headers, and italic descriptions intact
66
+ -- NEVER modify, delete, or add section headers (the lines starting with '#' like # Task specification)
67
+ -- NEVER modify or delete the italic _section description_ lines
68
+ -- The italic _section descriptions_ are TEMPLATE INSTRUCTIONS that must be preserved exactly as-is
69
+ -- ONLY update the actual content that appears BELOW the italic _section descriptions_ within each existing section
70
+ -- Do NOT add any new sections, summaries, or information outside the existing structure
71
+ - Write DETAILED, INFO-DENSE content for each section - include specifics like file paths, function names, error messages, exact commands, technical details, etc.
72
+ - Keep each section under ~{{maxSectionLength}} tokens/words
73
+ - IMPORTANT: Always update "Current State" to reflect the most recent work
74
+
75
+ Use the Edit tool with file_path: {{notesPath}}
76
+ """
77
+
78
+
79
+ def substitute_variables(template: str, variables: dict[str, str]) -> str:
80
+ """单趟替换 {{variable}} 占位符,避免替换结果二次替换。"""
81
+ result = template
82
+ for key, value in variables.items():
83
+ result = result.replace("{{" + key + "}}", value)
84
+ return result
85
+
86
+
87
+ def _count_words(text: str) -> int:
88
+ return len(re.findall(r"\S+", text))
89
+
90
+
91
+ def analyze_section_sizes(content: str) -> dict[str, int]:
92
+ """按 H1 section 统计近似 token(以词数近似)。"""
93
+ section_sizes: dict[str, int] = {}
94
+ current_title: str | None = None
95
+ current_lines: list[str] = []
96
+
97
+ for line in content.splitlines():
98
+ if line.startswith("# "):
99
+ if current_title is not None:
100
+ section_sizes[current_title] = _count_words("\n".join(current_lines).strip())
101
+ current_title = line[2:].strip()
102
+ current_lines = []
103
+ continue
104
+ if current_title is not None:
105
+ current_lines.append(line)
106
+
107
+ if current_title is not None:
108
+ section_sizes[current_title] = _count_words("\n".join(current_lines).strip())
109
+ return section_sizes
110
+
111
+
112
+ def generate_section_reminders(section_sizes: dict[str, int]) -> str:
113
+ """为接近上限的 section 生成提醒,帮助模型主动收敛内容长度。"""
114
+ threshold = int(MAX_SECTION_LENGTH * SECTION_REMINDER_THRESHOLD_RATIO)
115
+ warnings: list[str] = []
116
+ for section_name, size in section_sizes.items():
117
+ if size >= threshold:
118
+ warnings.append(
119
+ f'- Section "{section_name}" is long ({size} words). Keep it concise and under ~{MAX_SECTION_LENGTH}.'
120
+ )
121
+ if not warnings:
122
+ return ""
123
+ return "\n".join(
124
+ [
125
+ "Section length reminders:",
126
+ *warnings,
127
+ ]
128
+ )
129
+
130
+
131
+ def load_session_memory_template(session_memory_config_dir: Path) -> str:
132
+ """优先加载用户模板,不存在时回退到默认模板。"""
133
+ custom = session_memory_config_dir / "template.md"
134
+ if custom.exists():
135
+ return custom.read_text(encoding="utf-8")
136
+ return DEFAULT_SESSION_MEMORY_TEMPLATE
137
+
138
+
139
+ def load_session_memory_prompt(session_memory_config_dir: Path) -> str | None:
140
+ """优先加载用户 prompt,不存在时返回 None。"""
141
+ custom = session_memory_config_dir / "prompt.md"
142
+ if custom.exists():
143
+ return custom.read_text(encoding="utf-8")
144
+ return None
145
+
146
+
147
+ def build_session_memory_update_prompt(
148
+ *,
149
+ current_notes: str,
150
+ notes_path: Path,
151
+ session_memory_config_dir: Path | None = None,
152
+ prompt_template: str | None = None,
153
+ ) -> str:
154
+ """构建最终提取 prompt,包含动态 section 长度提醒。"""
155
+ template = prompt_template
156
+ if template is None and session_memory_config_dir is not None:
157
+ template = load_session_memory_prompt(session_memory_config_dir)
158
+ if template is None:
159
+ template = EXTRACT_SESSION_MEMORY_PROMPT
160
+
161
+ reminders = generate_section_reminders(analyze_section_sizes(current_notes))
162
+ if reminders:
163
+ template = template + "\n\n" + reminders
164
+
165
+ return substitute_variables(
166
+ template=template,
167
+ variables={
168
+ "notesPath": str(notes_path),
169
+ "currentNotes": current_notes,
170
+ "maxSectionLength": str(MAX_SECTION_LENGTH),
171
+ },
172
+ )
@@ -0,0 +1,282 @@
1
+ """
2
+ memory/session/session_memory.py — Session Memory 触发与后台提取
3
+
4
+ 职责:
5
+ 提供 Layer 3 的触发判定、并发保护与后台提取编排能力。
6
+
7
+ 链路位置:
8
+ 由 HookGraphWiring.after_model_node 调用 SessionMemoryManager.maybe_trigger(),
9
+ 在满足阈值时异步触发会话笔记提取。
10
+
11
+ 当前裁剪范围:
12
+ 本阶段仅实现 Phase 2(触发与状态管理),不接入 loop/factory/compact。
13
+ _write_fallback_summary 仅用于 demo/test 验证链路通路,不替代 CC 真实提取语义。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from collections.abc import Awaitable, Callable, Mapping
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ import time
23
+ import logging
24
+ from typing import Any
25
+
26
+ from langchain_agentx.loop.config import TokenEstimator
27
+ from langchain_agentx.memory.session.prompts import (
28
+ DEFAULT_SESSION_MEMORY_TEMPLATE,
29
+ build_session_memory_update_prompt,
30
+ )
31
+
32
+ SESSION_MEMORY_INIT_TOKEN_THRESHOLD = 10_000
33
+ SESSION_MEMORY_UPDATE_TOKEN_THRESHOLD = 5_000
34
+ SESSION_MEMORY_TOOL_CALL_THRESHOLD = 3
35
+ logger = logging.getLogger(__name__)
36
+
37
+ ExtractionRunner = Callable[[list[Any], str, Path, Any], Awaitable[None]]
38
+
39
+
40
+ @dataclass
41
+ class SessionMemoryExtractionState:
42
+ """提取状态追踪与并发保护(对齐 CC sequential() 语义)。"""
43
+
44
+ extraction_started_at: float | None = None
45
+ tokens_at_last_extraction: int = 0
46
+ tool_calls_at_last_extraction: int = 0
47
+ session_memory_initialized: bool = False
48
+ last_summarized_message_id: str | None = None
49
+ _lock: asyncio.Lock = field(default_factory=asyncio.Lock)
50
+
51
+ async def run_exclusive(self, coro: Awaitable[None]) -> None:
52
+ """串行执行提取任务,防止并发重入。"""
53
+ async with self._lock:
54
+ await coro
55
+
56
+
57
+ def _as_int(value: Any) -> int:
58
+ try:
59
+ return int(value)
60
+ except (TypeError, ValueError):
61
+ return 0
62
+
63
+
64
+ def _compute_has_tool_calls_in_last_turn(messages: list[Any]) -> bool:
65
+ for message in reversed(messages):
66
+ if isinstance(message, Mapping):
67
+ role = str(message.get("role", ""))
68
+ if role not in {"assistant", "ai"}:
69
+ continue
70
+ tool_calls = message.get("tool_calls")
71
+ return bool(tool_calls)
72
+ role = getattr(message, "type", None) or getattr(message, "role", "")
73
+ if role in {"ai", "assistant"}:
74
+ return bool(getattr(message, "tool_calls", None))
75
+ return False
76
+
77
+
78
+ def _estimate_tool_calls_from_messages(messages: list[Any]) -> int:
79
+ count = 0
80
+ for message in messages:
81
+ if isinstance(message, Mapping):
82
+ tool_calls = message.get("tool_calls")
83
+ else:
84
+ tool_calls = getattr(message, "tool_calls", None)
85
+ if tool_calls:
86
+ count += len(tool_calls)
87
+ return count
88
+
89
+
90
+ def should_extract_session_memory(
91
+ *,
92
+ token_count: int,
93
+ tool_call_count: int,
94
+ last_extract_token_count: int,
95
+ last_extract_tool_call_count: int,
96
+ has_tool_calls_in_last_turn: bool,
97
+ ) -> bool:
98
+ """对齐 CC shouldExtractMemory 的三条件逻辑。"""
99
+ if last_extract_token_count == 0:
100
+ has_met_token_threshold = token_count >= SESSION_MEMORY_INIT_TOKEN_THRESHOLD
101
+ else:
102
+ has_met_token_threshold = (
103
+ token_count - last_extract_token_count
104
+ ) >= SESSION_MEMORY_UPDATE_TOKEN_THRESHOLD
105
+
106
+ if not has_met_token_threshold:
107
+ return False
108
+
109
+ has_met_tool_call_threshold = (
110
+ tool_call_count - last_extract_tool_call_count
111
+ ) >= SESSION_MEMORY_TOOL_CALL_THRESHOLD
112
+ if has_met_tool_call_threshold:
113
+ return True
114
+
115
+ return not has_tool_calls_in_last_turn
116
+
117
+
118
+ def create_memory_file_can_use_tool(session_memory_path: Path) -> list[dict[str, Any]]:
119
+ """生成提取 agent 的受限工具配置:仅允许 Edit 目标会话文件。"""
120
+ return [
121
+ {
122
+ "name": "Edit",
123
+ "allowed_paths": [str(session_memory_path)],
124
+ }
125
+ ]
126
+
127
+
128
+ @dataclass
129
+ class SessionMemoryManager:
130
+ """Session Memory 触发与后台提取编排器。"""
131
+
132
+ session_memory_path: Path
133
+ llm: Any
134
+ is_subagent: bool = False
135
+ extraction_state: SessionMemoryExtractionState = field(
136
+ default_factory=SessionMemoryExtractionState
137
+ )
138
+ subagent_runner: ExtractionRunner | None = None
139
+ _pending_tasks: set[asyncio.Task[None]] = field(default_factory=set, init=False)
140
+ _token_estimator: TokenEstimator = field(default_factory=TokenEstimator, init=False)
141
+
142
+ async def drain(self) -> None:
143
+ """等待所有 fire-and-forget 提取任务完成(测试与 demo 用)。"""
144
+ if self._pending_tasks:
145
+ await asyncio.gather(*list(self._pending_tasks), return_exceptions=True)
146
+
147
+ def maybe_trigger(self, state: Mapping[str, Any]) -> None:
148
+ """阈值满足后以 fire-and-forget 方式启动后台提取。"""
149
+ if self.is_subagent:
150
+ return
151
+
152
+ messages_raw = state.get("messages", [])
153
+ messages = list(messages_raw) if isinstance(messages_raw, list) else []
154
+ token_count = _as_int(state.get("token_count"))
155
+ tool_call_count = _as_int(state.get("tool_call_count"))
156
+ if token_count <= 0:
157
+ token_count = self._token_estimator.estimate_messages_tokens(messages)
158
+ if tool_call_count <= 0:
159
+ tool_call_count = _estimate_tool_calls_from_messages(messages)
160
+ has_tool_calls_in_last_turn = _compute_has_tool_calls_in_last_turn(messages)
161
+
162
+ if not should_extract_session_memory(
163
+ token_count=token_count,
164
+ tool_call_count=tool_call_count,
165
+ last_extract_token_count=self.extraction_state.tokens_at_last_extraction,
166
+ last_extract_tool_call_count=self.extraction_state.tool_calls_at_last_extraction,
167
+ has_tool_calls_in_last_turn=has_tool_calls_in_last_turn,
168
+ ):
169
+ return
170
+
171
+ try:
172
+ asyncio.get_running_loop()
173
+ except RuntimeError:
174
+ logger.warning(
175
+ "session_memory: no running event loop, skip extraction for this turn"
176
+ )
177
+ return
178
+
179
+ task = asyncio.ensure_future(
180
+ self._run_extraction(
181
+ messages=messages,
182
+ token_count=token_count,
183
+ tool_call_count=tool_call_count,
184
+ )
185
+ )
186
+ self._pending_tasks.add(task)
187
+ task.add_done_callback(self._pending_tasks.discard)
188
+
189
+ async def _run_extraction(
190
+ self,
191
+ *,
192
+ messages: list[Any],
193
+ token_count: int,
194
+ tool_call_count: int,
195
+ ) -> None:
196
+ async def _guarded() -> None:
197
+ self.extraction_state.extraction_started_at = time.monotonic()
198
+ try:
199
+ current_notes = self._read_or_init_notes()
200
+ prompt = build_session_memory_update_prompt(
201
+ current_notes=current_notes,
202
+ notes_path=self.session_memory_path,
203
+ )
204
+ if self.subagent_runner is not None:
205
+ await self.subagent_runner(
206
+ messages,
207
+ prompt,
208
+ self.session_memory_path,
209
+ self.llm,
210
+ )
211
+ else:
212
+ self._write_fallback_summary(messages=messages, prompt=prompt)
213
+ self.extraction_state.tokens_at_last_extraction = max(
214
+ self.extraction_state.tokens_at_last_extraction,
215
+ token_count,
216
+ )
217
+ self.extraction_state.tool_calls_at_last_extraction = max(
218
+ self.extraction_state.tool_calls_at_last_extraction,
219
+ tool_call_count,
220
+ )
221
+ self.extraction_state.session_memory_initialized = True
222
+ finally:
223
+ self.extraction_state.extraction_started_at = None
224
+
225
+ await self.extraction_state.run_exclusive(_guarded())
226
+
227
+ def _read_or_init_notes(self) -> str:
228
+ if not self.session_memory_path.exists():
229
+ self.session_memory_path.parent.mkdir(parents=True, exist_ok=True)
230
+ self.session_memory_path.write_text(
231
+ DEFAULT_SESSION_MEMORY_TEMPLATE,
232
+ encoding="utf-8",
233
+ )
234
+ return DEFAULT_SESSION_MEMORY_TEMPLATE
235
+ return self.session_memory_path.read_text(encoding="utf-8")
236
+
237
+ def _write_fallback_summary(self, *, messages: list[Any], prompt: str) -> None:
238
+ """无 subagent runner 时的兜底摘要写入,保证 session memory 可被验证。"""
239
+ snippets: list[str] = []
240
+ for msg in messages[-6:]:
241
+ if isinstance(msg, Mapping):
242
+ role = str(msg.get("role", "unknown"))
243
+ content = str(msg.get("content", ""))
244
+ else:
245
+ role = str(getattr(msg, "type", getattr(msg, "role", "unknown")))
246
+ content = str(getattr(msg, "content", ""))
247
+ if content:
248
+ snippets.append(f"- {role}: {content[:240]}")
249
+ body = "\n".join(snippets) if snippets else "- no recent messages captured"
250
+ fallback_blob = (
251
+ "\n\n## Auto Summary (fallback)\n"
252
+ "Session memory extraction runner is not configured.\n"
253
+ "Captured recent conversation snippets:\n"
254
+ f"{body}\n"
255
+ f"\nPrompt digest length: {len(prompt)}\n"
256
+ )
257
+ existing = self.session_memory_path.read_text(encoding="utf-8")
258
+ self.session_memory_path.write_text(existing + fallback_blob, encoding="utf-8")
259
+
260
+
261
+ def make_session_memory_section(session_memory_path: Path) -> Any:
262
+ """构建会话记忆注入段(volatile,避免缓存过期)。"""
263
+ from langchain_agentx.loop.prompt.sections import volatile_section
264
+
265
+ def _load() -> str | None:
266
+ from langchain_agentx.memory.session.compact_bridge import is_session_memory_empty
267
+
268
+ if not session_memory_path.exists():
269
+ return None
270
+ content = session_memory_path.read_text(encoding="utf-8")
271
+ if not content.strip():
272
+ return None
273
+ if is_session_memory_empty(content):
274
+ return None
275
+ return f"<session_memory>\n{content}\n</session_memory>"
276
+
277
+ return volatile_section(
278
+ name="session_memory",
279
+ compute=_load,
280
+ reason="session memory file may change between turns",
281
+ )
282
+