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,295 @@
1
+ """
2
+ trace_callback.py — LLM/Tool 调用回调采集器(Phase 3)。
3
+
4
+ 职责:
5
+ 基于 LangChain BaseCallbackHandler 采集 llm_call/tool_call latency 与错误归因。
6
+
7
+ 链路位置:
8
+ factory.compile() -> compiled.with_config({"callbacks": [TraceCallbackHandler(...)]}).
9
+
10
+ 当前裁剪范围:
11
+ 单 run 级回调;不承担 session 生命周期管理。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ from langchain_core.callbacks import BaseCallbackHandler
21
+
22
+ from .collector import TraceCollector
23
+
24
+
25
+ @dataclass
26
+ class TraceCallbackHandler(BaseCallbackHandler):
27
+ """LLM/Tool 事件回调到 TraceCollector。"""
28
+
29
+ collector: TraceCollector
30
+ session_id: str
31
+ capture_llm: bool = True
32
+ capture_tool: bool = True
33
+ _llm_spans: dict[str, str] = field(default_factory=dict)
34
+ _tool_spans: dict[str, str] = field(default_factory=dict)
35
+ _llm_started_at: dict[str, float] = field(default_factory=dict)
36
+ _tool_started_at: dict[str, float] = field(default_factory=dict)
37
+
38
+ def _ensure_session(self) -> None:
39
+ if self.collector.has_session(self.session_id):
40
+ return
41
+ self.collector.start_session(session_id=self.session_id, graph_name="agent", user_query=None)
42
+
43
+ @staticmethod
44
+ def _llm_name(serialized: dict[str, Any]) -> str:
45
+ return str(serialized.get("name") or serialized.get("id") or "llm")
46
+
47
+ def _start_llm_span(self, *, run_id: Any, name: str, prompt_count: int) -> None:
48
+ self._ensure_session()
49
+ span_id = self.collector.span_start(
50
+ span_type="llm_call",
51
+ name=name,
52
+ session_id=self.session_id,
53
+ input_data={"prompt_count": prompt_count},
54
+ )
55
+ key = str(run_id)
56
+ self._llm_spans[key] = span_id
57
+ self._llm_started_at[key] = time.time()
58
+
59
+ def on_llm_start(self, serialized: dict[str, Any], prompts: list[str], *, run_id: Any, **kwargs: Any) -> None:
60
+ if not self.capture_llm:
61
+ return
62
+ self._start_llm_span(
63
+ run_id=run_id,
64
+ name=self._llm_name(serialized),
65
+ prompt_count=len(prompts),
66
+ )
67
+
68
+ def on_chat_model_start(
69
+ self,
70
+ serialized: dict[str, Any],
71
+ messages: list[list[Any]],
72
+ *,
73
+ run_id: Any,
74
+ **kwargs: Any,
75
+ ) -> None:
76
+ """兼容 ChatModel 回调入口,避免 provider 仅触发 chat 路径时漏采集。"""
77
+ if not self.capture_llm:
78
+ return
79
+ # ChatModel 的 messages 是 batch 结构(List[List[BaseMessage]])
80
+ prompt_count = len(messages)
81
+ self._start_llm_span(
82
+ run_id=run_id,
83
+ name=self._llm_name(serialized),
84
+ prompt_count=prompt_count,
85
+ )
86
+
87
+ def on_llm_end(self, response: Any, *, run_id: Any, **kwargs: Any) -> None:
88
+ if not self.capture_llm:
89
+ return
90
+ key = str(run_id)
91
+ span_id = self._llm_spans.pop(key, None)
92
+ started_at = self._llm_started_at.pop(key, None)
93
+ if not span_id:
94
+ return
95
+ metadata: dict[str, Any] = self._extract_llm_metadata(response)
96
+ if started_at is not None:
97
+ metadata["latency_ms"] = (time.time() - started_at) * 1000
98
+ self.collector.span_end(span_id=span_id, session_id=self.session_id, metadata=metadata)
99
+
100
+ def on_llm_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> None:
101
+ if not self.capture_llm:
102
+ return
103
+ key = str(run_id)
104
+ span_id = self._llm_spans.pop(key, None)
105
+ self._llm_started_at.pop(key, None)
106
+ if not span_id:
107
+ return
108
+ self.collector.span_end(span_id=span_id, session_id=self.session_id, error=str(error))
109
+
110
+ def on_tool_start(self, serialized: dict[str, Any], input_str: str, *, run_id: Any, **kwargs: Any) -> None:
111
+ if not self.capture_tool:
112
+ return
113
+ self._ensure_session()
114
+ name = str(serialized.get("name") or "tool")
115
+ input_data, metadata = self._extract_tool_input_and_metadata(input_str=input_str, kwargs=kwargs)
116
+ span_id = self.collector.span_start(
117
+ span_type="tool_call",
118
+ name=name,
119
+ session_id=self.session_id,
120
+ input_data=input_data,
121
+ metadata=metadata,
122
+ )
123
+ key = str(run_id)
124
+ self._tool_spans[key] = span_id
125
+ self._tool_started_at[key] = time.time()
126
+
127
+ def on_tool_end(self, output: Any, *, run_id: Any, **kwargs: Any) -> None:
128
+ if not self.capture_tool:
129
+ return
130
+ key = str(run_id)
131
+ span_id = self._tool_spans.pop(key, None)
132
+ started_at = self._tool_started_at.pop(key, None)
133
+ if not span_id:
134
+ return
135
+ metadata: dict[str, Any] = {}
136
+ if started_at is not None:
137
+ metadata["latency_ms"] = (time.time() - started_at) * 1000
138
+ self.collector.span_end(span_id=span_id, session_id=self.session_id, metadata=metadata)
139
+
140
+ def on_tool_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> None:
141
+ if not self.capture_tool:
142
+ return
143
+ key = str(run_id)
144
+ span_id = self._tool_spans.pop(key, None)
145
+ self._tool_started_at.pop(key, None)
146
+ if not span_id:
147
+ return
148
+ self.collector.span_end(span_id=span_id, session_id=self.session_id, error=str(error))
149
+
150
+ @staticmethod
151
+ def _extract_tool_input_and_metadata(*, input_str: str, kwargs: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
152
+ """
153
+ 优先使用 LangChain 回调透传的结构化 inputs,避免把 tool_runtime 等内部对象写入 trace。
154
+ 仅当结构化输入不可用时,回退到 input_str 快照(保留上限防止极端体积)。
155
+ """
156
+ metadata: dict[str, Any] = {}
157
+ raw_inputs = kwargs.get("inputs")
158
+ if isinstance(raw_inputs, dict):
159
+ clean_inputs: dict[str, Any] = {}
160
+ for key, value in raw_inputs.items():
161
+ # LangGraph 注入运行时对象(ToolRuntime),不属于用户态业务参数。
162
+ if key == "tool_runtime":
163
+ tool_call_id = getattr(value, "tool_call_id", None)
164
+ if isinstance(tool_call_id, str) and tool_call_id:
165
+ metadata["tool_call_id"] = tool_call_id
166
+ continue
167
+ clean_inputs[key] = value
168
+
169
+ if clean_inputs:
170
+ return clean_inputs, metadata
171
+
172
+ # fallback: 历史链路仍可能只提供 input_str。
173
+ return {"input": input_str[:500]}, metadata
174
+
175
+ def _extract_llm_metadata(self, response: Any) -> dict[str, Any]:
176
+ metadata: dict[str, Any] = {}
177
+ finish_reason = self._extract_finish_reason(response)
178
+ if isinstance(finish_reason, str) and finish_reason:
179
+ metadata["finish_reason"] = finish_reason
180
+ usage = self._extract_token_usage(response)
181
+ metadata.update(usage)
182
+ return metadata
183
+
184
+ @staticmethod
185
+ def _extract_finish_reason(response: Any) -> str | None:
186
+ first_message = TraceCallbackHandler._extract_first_generation_message(response)
187
+ if first_message is not None:
188
+ response_metadata = getattr(first_message, "response_metadata", None)
189
+ if isinstance(response_metadata, dict):
190
+ value = response_metadata.get("finish_reason")
191
+ if isinstance(value, str) and value:
192
+ return value
193
+
194
+ # ChatModel 常见结构:AIMessage.response_metadata.finish_reason
195
+ response_metadata = getattr(response, "response_metadata", None)
196
+ if isinstance(response_metadata, dict):
197
+ value = response_metadata.get("finish_reason")
198
+ if isinstance(value, str) and value:
199
+ return value
200
+
201
+ # LLMResult 常见结构:llm_output.finish_reason
202
+ llm_output = getattr(response, "llm_output", None)
203
+ if isinstance(llm_output, dict):
204
+ value = llm_output.get("finish_reason")
205
+ if isinstance(value, str) and value:
206
+ return value
207
+
208
+ return None
209
+
210
+ @staticmethod
211
+ def _extract_token_usage(response: Any) -> dict[str, int]:
212
+ first_message = TraceCallbackHandler._extract_first_generation_message(response)
213
+ if first_message is not None:
214
+ usage_metadata = getattr(first_message, "usage_metadata", None)
215
+ if isinstance(usage_metadata, dict):
216
+ input_tokens = usage_metadata.get("input_tokens")
217
+ output_tokens = usage_metadata.get("output_tokens")
218
+ total_tokens = usage_metadata.get("total_tokens")
219
+ result: dict[str, int] = {}
220
+ if isinstance(input_tokens, int):
221
+ result["input_tokens"] = input_tokens
222
+ if isinstance(output_tokens, int):
223
+ result["output_tokens"] = output_tokens
224
+ if isinstance(total_tokens, int):
225
+ result["total_tokens"] = total_tokens
226
+ if result:
227
+ return result
228
+
229
+ # ChatModel 常见结构:AIMessage.usage_metadata
230
+ usage_metadata = getattr(response, "usage_metadata", None)
231
+ if isinstance(usage_metadata, dict):
232
+ input_tokens = usage_metadata.get("input_tokens")
233
+ output_tokens = usage_metadata.get("output_tokens")
234
+ total_tokens = usage_metadata.get("total_tokens")
235
+ result: dict[str, int] = {}
236
+ if isinstance(input_tokens, int):
237
+ result["input_tokens"] = input_tokens
238
+ if isinstance(output_tokens, int):
239
+ result["output_tokens"] = output_tokens
240
+ if isinstance(total_tokens, int):
241
+ result["total_tokens"] = total_tokens
242
+ if result:
243
+ return result
244
+
245
+ # LLMResult/OpenAI 常见结构:response_metadata.token_usage
246
+ response_metadata = getattr(response, "response_metadata", None)
247
+ if isinstance(response_metadata, dict):
248
+ token_usage = response_metadata.get("token_usage")
249
+ if isinstance(token_usage, dict):
250
+ result = {}
251
+ prompt_tokens = token_usage.get("prompt_tokens")
252
+ completion_tokens = token_usage.get("completion_tokens")
253
+ total_tokens = token_usage.get("total_tokens")
254
+ if isinstance(prompt_tokens, int):
255
+ result["input_tokens"] = prompt_tokens
256
+ if isinstance(completion_tokens, int):
257
+ result["output_tokens"] = completion_tokens
258
+ if isinstance(total_tokens, int):
259
+ result["total_tokens"] = total_tokens
260
+ if result:
261
+ return result
262
+
263
+ # 部分 LLMResult:response.llm_output.token_usage
264
+ llm_output = getattr(response, "llm_output", None)
265
+ if isinstance(llm_output, dict):
266
+ token_usage = llm_output.get("token_usage")
267
+ if isinstance(token_usage, dict):
268
+ result = {}
269
+ prompt_tokens = token_usage.get("prompt_tokens")
270
+ completion_tokens = token_usage.get("completion_tokens")
271
+ total_tokens = token_usage.get("total_tokens")
272
+ if isinstance(prompt_tokens, int):
273
+ result["input_tokens"] = prompt_tokens
274
+ if isinstance(completion_tokens, int):
275
+ result["output_tokens"] = completion_tokens
276
+ if isinstance(total_tokens, int):
277
+ result["total_tokens"] = total_tokens
278
+ if result:
279
+ return result
280
+
281
+ return {}
282
+
283
+ @staticmethod
284
+ def _extract_first_generation_message(response: Any) -> Any | None:
285
+ generations = getattr(response, "generations", None)
286
+ if not isinstance(generations, list) or not generations:
287
+ return None
288
+ first_batch = generations[0]
289
+ if not isinstance(first_batch, list) or not first_batch:
290
+ return None
291
+ first_generation = first_batch[0]
292
+ return getattr(first_generation, "message", None)
293
+
294
+
295
+ __all__ = ["TraceCallbackHandler"]
@@ -0,0 +1,114 @@
1
+ """
2
+ trace_lifecycle_collector.py — Loop 图生命周期 Trace 采集(Phase 3)。
3
+
4
+ 职责:
5
+ 在固定 Loop 图节点上采集 loop 生命周期事件,统一写入 TraceCollector。
6
+
7
+ 链路位置:
8
+ HookGraphWiring(loop_start/before_model/after_model/loop_end) -> TraceLifecycleCollector。
9
+
10
+ 当前裁剪范围:
11
+ 仅覆盖框架内置生命周期节点采集(非 HookRegistry 用户扩展体系);
12
+ llm/tool 细粒度调用由 TraceCallbackHandler 负责。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+ from uuid import uuid4
20
+
21
+ from .collector import TraceCollector
22
+
23
+
24
+ @dataclass
25
+ class TraceLifecycleCollector:
26
+ """框架内置生命周期 Trace 采集器,由 HookGraphWiring 调用。"""
27
+
28
+ collector: TraceCollector
29
+ graph_name: str = "agent"
30
+ default_run_id: str | None = None
31
+ unit_type: str = "main_loop"
32
+ container_type: str = "agent_session"
33
+ origin: str = "user"
34
+ visibility: str = "default"
35
+ parent_run_id: str | None = None
36
+ parent_tool_call_id: str | None = None
37
+ conversation_session_id: str | None = None
38
+
39
+ def loop_start_node(self, state: dict[str, Any]) -> dict[str, Any]:
40
+ run_id = state.get("_run_id")
41
+ if not isinstance(run_id, str) or not run_id:
42
+ run_id = self.default_run_id or uuid4().hex
43
+ if not self.collector.has_session(run_id):
44
+ user_query = self._extract_user_query(state)
45
+ self.collector.start_session(
46
+ session_id=run_id,
47
+ graph_name=self.graph_name,
48
+ user_query=user_query,
49
+ conversation_session_id=self.conversation_session_id or run_id,
50
+ parent_session_id=self.parent_run_id,
51
+ parent_tool_call_id=self.parent_tool_call_id,
52
+ unit_type=self.unit_type,
53
+ container_type=self.container_type,
54
+ origin=self.origin,
55
+ visibility=self.visibility,
56
+ )
57
+ return {"_trace_collector": self.collector, "_run_id": run_id}
58
+
59
+ def before_model_node(self, state: dict[str, Any]) -> dict[str, Any]:
60
+ run_id = state.get("_run_id")
61
+ if not isinstance(run_id, str) or not run_id or not self.collector.has_session(run_id):
62
+ return {}
63
+ span_id = self.collector.span_start(
64
+ span_type="loop_step",
65
+ name=f"step_{int(state.get('step', 0) or 0)}",
66
+ session_id=run_id,
67
+ metadata={"context_length": len(state.get("messages", []))},
68
+ )
69
+ return {"_trace_loop_step_span_id": span_id}
70
+
71
+ def after_model_node(self, state: dict[str, Any]) -> dict[str, Any]:
72
+ run_id = state.get("_run_id")
73
+ span_id = state.get("_trace_loop_step_span_id")
74
+ if (
75
+ not isinstance(run_id, str)
76
+ or not run_id
77
+ or not isinstance(span_id, str)
78
+ or not span_id
79
+ or not self.collector.has_session(run_id)
80
+ ):
81
+ return {}
82
+ self.collector.span_end(
83
+ span_id=span_id,
84
+ session_id=run_id,
85
+ metadata={"finish_reason": state.get("finish_reason")},
86
+ )
87
+ return {"_trace_loop_step_span_id": None}
88
+
89
+ def loop_end_node(self, state: dict[str, Any]) -> dict[str, Any]:
90
+ run_id = state.get("_run_id")
91
+ if not isinstance(run_id, str) or not run_id or not self.collector.has_session(run_id):
92
+ return {}
93
+ self.collector.end_session(
94
+ session_id=run_id,
95
+ finish_reason=state.get("finish_reason"),
96
+ terminal_reason=state.get("terminal_reason"),
97
+ )
98
+ return {}
99
+
100
+ # A07 文档对齐别名:保留 loop_* 原命名,避免 HookGraphWiring 接线调整。
101
+ session_start_node = loop_start_node
102
+ session_end_node = loop_end_node
103
+
104
+ @staticmethod
105
+ def _extract_user_query(state: dict[str, Any]) -> str | None:
106
+ messages = list(state.get("messages") or [])
107
+ if not messages:
108
+ return None
109
+ first = messages[0]
110
+ content = getattr(first, "content", None)
111
+ return str(content)[:200] if isinstance(content, str) else None
112
+
113
+
114
+ __all__ = ["TraceLifecycleCollector"]
@@ -0,0 +1,26 @@
1
+ """
2
+ plugin/__init__.py — Plugin 子系统公共导出。
3
+
4
+ 职责:
5
+ 暴露 Plugin 子系统第一阶段可复用的数据类型与配置读取入口。
6
+
7
+ 链路位置:
8
+ 被 PluginLoader、容器集成层与测试代码统一导入。
9
+
10
+ 当前裁剪范围:
11
+ 仅包含 Phase B 的数据层导出,不含 Loader/Registry 实现。
12
+ """
13
+
14
+ from .builtin import BuiltinPluginDefinition, BuiltinPluginRegistry
15
+ from .config import PluginConfig, PluginConfigLoader, RepositoryEntry
16
+ from .types import LoadedPlugin, PluginLoadResult
17
+
18
+ __all__ = [
19
+ "BuiltinPluginDefinition",
20
+ "BuiltinPluginRegistry",
21
+ "LoadedPlugin",
22
+ "PluginConfig",
23
+ "PluginConfigLoader",
24
+ "PluginLoadResult",
25
+ "RepositoryEntry",
26
+ ]
@@ -0,0 +1,53 @@
1
+ """
2
+ plugin/builtin.py — 内置插件定义注册表。
3
+
4
+ 职责:
5
+ 维护随框架发布的内置插件定义,并提供全局注册表访问。
6
+
7
+ 链路位置:
8
+ PluginLoader._discover_builtin() 通过本模块获取内置插件声明。
9
+
10
+ 当前裁剪范围:
11
+ 仅做定义注册与读取,不包含 hooks 实际注册逻辑。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Callable
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class BuiltinPluginDefinition:
22
+ name: str
23
+ description: str
24
+ version: str = "0.1.0"
25
+ hooks: dict[str, Any] | None = None
26
+ mcp_servers: dict[str, dict[str, Any]] = field(default_factory=dict)
27
+ applicable_containers: list[str] = field(default_factory=list)
28
+ default_enabled: bool = True
29
+ is_available: Callable[[], bool] | None = None
30
+
31
+
32
+ class BuiltinPluginRegistry:
33
+ """内置插件注册表。"""
34
+
35
+ _singleton: "BuiltinPluginRegistry | None" = None
36
+
37
+ def __init__(self) -> None:
38
+ self._definitions: dict[str, BuiltinPluginDefinition] = {}
39
+
40
+ def register(self, definition: BuiltinPluginDefinition) -> None:
41
+ self._definitions[definition.name] = definition
42
+
43
+ def all(self) -> list[BuiltinPluginDefinition]:
44
+ return list(self._definitions.values())
45
+
46
+ @classmethod
47
+ def global_instance(cls) -> "BuiltinPluginRegistry":
48
+ if cls._singleton is None:
49
+ cls._singleton = cls()
50
+ return cls._singleton
51
+
52
+
53
+ __all__ = ["BuiltinPluginDefinition", "BuiltinPluginRegistry"]
@@ -0,0 +1,113 @@
1
+ """
2
+ plugin/config.py — plugin_config.json 读取与内存表示。
3
+
4
+ 职责:
5
+ 定义 PluginConfig/RepositoryEntry,并提供 PluginConfigLoader 读取配置文件。
6
+
7
+ 链路位置:
8
+ PluginLoader 在 Discovery 阶段通过本模块读取插件启停状态和仓库坐标。
9
+
10
+ 当前裁剪范围:
11
+ 仅包含读取逻辑,不包含配置写入管理(PluginConfigManager 在后续阶段实现)。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ import json
18
+ import logging
19
+
20
+ from langchain_agentx.workspace.config import AgentWorkspaceConfig
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RepositoryEntry:
27
+ url: str
28
+ branch: str = "main"
29
+ commit_sha: str | None = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PluginConfig:
34
+ """plugin_config.json 的内存表示。"""
35
+
36
+ enabled_plugins: dict[str, bool] = field(default_factory=dict)
37
+ repositories: dict[str, RepositoryEntry] = field(default_factory=dict)
38
+
39
+ def is_enabled(self, plugin_id: str, default: bool = True) -> bool:
40
+ return self.enabled_plugins.get(plugin_id, default)
41
+
42
+
43
+ class PluginConfigLoader:
44
+ """加载 plugin_config.json。"""
45
+
46
+ @staticmethod
47
+ def load(cfg: AgentWorkspaceConfig) -> PluginConfig:
48
+ config_path = cfg.plugin_config_path
49
+ if not config_path.exists():
50
+ return PluginConfig()
51
+
52
+ try:
53
+ raw = json.loads(config_path.read_text(encoding="utf-8"))
54
+ except json.JSONDecodeError as exc:
55
+ logger.warning(
56
+ "plugin config parse failed at %s: %s; fallback to empty config",
57
+ config_path,
58
+ exc,
59
+ )
60
+ return PluginConfig()
61
+ except OSError as exc:
62
+ logger.warning(
63
+ "plugin config read failed at %s: %s; fallback to empty config",
64
+ config_path,
65
+ exc,
66
+ )
67
+ return PluginConfig()
68
+
69
+ if not isinstance(raw, dict):
70
+ logger.warning(
71
+ "plugin config root must be object at %s; fallback to empty config",
72
+ config_path,
73
+ )
74
+ return PluginConfig()
75
+
76
+ enabled_plugins = PluginConfigLoader._parse_enabled_plugins(raw)
77
+ repositories = PluginConfigLoader._parse_repositories(raw)
78
+ return PluginConfig(enabled_plugins=enabled_plugins, repositories=repositories)
79
+
80
+ @staticmethod
81
+ def _parse_enabled_plugins(raw: dict[str, object]) -> dict[str, bool]:
82
+ payload = raw.get("enabled_plugins")
83
+ if not isinstance(payload, dict):
84
+ return {}
85
+ parsed: dict[str, bool] = {}
86
+ for plugin_id, enabled in payload.items():
87
+ if isinstance(plugin_id, str) and isinstance(enabled, bool):
88
+ parsed[plugin_id] = enabled
89
+ return parsed
90
+
91
+ @staticmethod
92
+ def _parse_repositories(raw: dict[str, object]) -> dict[str, RepositoryEntry]:
93
+ payload = raw.get("repositories")
94
+ if not isinstance(payload, dict):
95
+ return {}
96
+ parsed: dict[str, RepositoryEntry] = {}
97
+ for name, entry in payload.items():
98
+ if not isinstance(name, str) or not isinstance(entry, dict):
99
+ continue
100
+ url = entry.get("url")
101
+ if not isinstance(url, str) or not url:
102
+ continue
103
+ branch = entry.get("branch")
104
+ commit_sha = entry.get("commit_sha")
105
+ parsed[name] = RepositoryEntry(
106
+ url=url,
107
+ branch=branch if isinstance(branch, str) and branch else "main",
108
+ commit_sha=commit_sha if isinstance(commit_sha, str) else None,
109
+ )
110
+ return parsed
111
+
112
+
113
+ __all__ = ["PluginConfig", "PluginConfigLoader", "RepositoryEntry"]