ralph-workflow 0.8.0b1__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 (276) hide show
  1. ralph/__init__.py +18 -0
  2. ralph/__main__.py +6 -0
  3. ralph/agents/__init__.py +15 -0
  4. ralph/agents/chain.py +296 -0
  5. ralph/agents/executor.py +32 -0
  6. ralph/agents/invoke.py +924 -0
  7. ralph/agents/parsers/__init__.py +45 -0
  8. ralph/agents/parsers/base.py +54 -0
  9. ralph/agents/parsers/claude.py +460 -0
  10. ralph/agents/parsers/codex.py +281 -0
  11. ralph/agents/parsers/gemini.py +270 -0
  12. ralph/agents/parsers/generic.py +133 -0
  13. ralph/agents/parsers/opencode.py +277 -0
  14. ralph/agents/registry.py +243 -0
  15. ralph/agents/subprocess_executor.py +143 -0
  16. ralph/agents/transport_emit.py +323 -0
  17. ralph/api/__init__.py +14 -0
  18. ralph/api/cloud.py +29 -0
  19. ralph/api/opencode.py +152 -0
  20. ralph/banner.py +103 -0
  21. ralph/checkpoint/__init__.py +19 -0
  22. ralph/checkpoint/builder.py +125 -0
  23. ralph/checkpoint/execution_history.py +182 -0
  24. ralph/checkpoint/run_context.py +60 -0
  25. ralph/checkpoint/size_monitor.py +75 -0
  26. ralph/cli/__init__.py +9 -0
  27. ralph/cli/commands/__init__.py +13 -0
  28. ralph/cli/commands/cleanup.py +84 -0
  29. ralph/cli/commands/commit.py +941 -0
  30. ralph/cli/commands/diagnose.py +231 -0
  31. ralph/cli/commands/init.py +92 -0
  32. ralph/cli/commands/run.py +150 -0
  33. ralph/cli/main.py +643 -0
  34. ralph/cli/options.py +187 -0
  35. ralph/cloud/__init__.py +28 -0
  36. ralph/cloud/client.py +221 -0
  37. ralph/config/__init__.py +37 -0
  38. ralph/config/enums.py +149 -0
  39. ralph/config/loader.py +276 -0
  40. ralph/config/mcp_loader.py +128 -0
  41. ralph/config/mcp_models.py +97 -0
  42. ralph/config/models.py +289 -0
  43. ralph/diagnostics/__init__.py +275 -0
  44. ralph/display/__init__.py +39 -0
  45. ralph/display/activity_model.py +145 -0
  46. ralph/display/activity_router.py +140 -0
  47. ralph/display/artifact_reader.py +164 -0
  48. ralph/display/artifact_renderer.py +254 -0
  49. ralph/display/completion_summary.py +172 -0
  50. ralph/display/line_sanitizer.py +26 -0
  51. ralph/display/mode.py +18 -0
  52. ralph/display/parallel_display.py +135 -0
  53. ralph/display/phase_banner.py +204 -0
  54. ralph/display/plain_renderer.py +254 -0
  55. ralph/display/progress.py +394 -0
  56. ralph/display/prompt_reader.py +37 -0
  57. ralph/display/ring_buffer.py +58 -0
  58. ralph/display/snapshot.py +195 -0
  59. ralph/display/status.py +108 -0
  60. ralph/display/subscriber.py +316 -0
  61. ralph/display/tables.py +140 -0
  62. ralph/display/theme.py +105 -0
  63. ralph/executor/__init__.py +5 -0
  64. ralph/executor/process.py +209 -0
  65. ralph/exit_pause/__init__.py +182 -0
  66. ralph/files/__init__.py +29 -0
  67. ralph/files/operations.py +130 -0
  68. ralph/git/__init__.py +63 -0
  69. ralph/git/__init__.pyi +14 -0
  70. ralph/git/executor.py +62 -0
  71. ralph/git/hooks.py +249 -0
  72. ralph/git/operations.py +311 -0
  73. ralph/git/rebase/__init__.py +69 -0
  74. ralph/git/rebase/rebase.py +346 -0
  75. ralph/git/rebase/rebase_checkpoint.py +333 -0
  76. ralph/git/rebase/rebase_continuation.py +200 -0
  77. ralph/git/rebase/rebase_kinds.py +389 -0
  78. ralph/git/rebase/rebase_preconditions.py +265 -0
  79. ralph/git/rebase/rebase_state_machine.py +236 -0
  80. ralph/git/worktree_manager.py +92 -0
  81. ralph/git/worktree_preflight.py +89 -0
  82. ralph/git/wrapper.py +180 -0
  83. ralph/git/wrapper.pyi +14 -0
  84. ralph/guidelines/__init__.py +24 -0
  85. ralph/guidelines/go.py +169 -0
  86. ralph/guidelines/java.py +157 -0
  87. ralph/guidelines/javascript.py +285 -0
  88. ralph/guidelines/php.py +178 -0
  89. ralph/guidelines/python.py +168 -0
  90. ralph/guidelines/ruby.py +176 -0
  91. ralph/guidelines/rust.py +158 -0
  92. ralph/guidelines/stack.py +398 -0
  93. ralph/install.py +60 -0
  94. ralph/interrupt/__init__.py +32 -0
  95. ralph/interrupt/asyncio_bridge.py +53 -0
  96. ralph/language_detector/__init__.py +80 -0
  97. ralph/language_detector/extensions.py +52 -0
  98. ralph/language_detector/models.py +44 -0
  99. ralph/language_detector/scanner.py +197 -0
  100. ralph/language_detector/signatures.py +281 -0
  101. ralph/logging.py +326 -0
  102. ralph/main.py +6 -0
  103. ralph/mcp/ARCHITECTURE.md +122 -0
  104. ralph/mcp/__init__.py +90 -0
  105. ralph/mcp/agent_transport_probe.py +76 -0
  106. ralph/mcp/artifacts/__init__.py +40 -0
  107. ralph/mcp/artifacts/audit_adapter.py +227 -0
  108. ralph/mcp/artifacts/bridge.py +352 -0
  109. ralph/mcp/artifacts/commit_message.py +312 -0
  110. ralph/mcp/artifacts/development_result.py +56 -0
  111. ralph/mcp/artifacts/file_backend.py +44 -0
  112. ralph/mcp/artifacts/plan.py +377 -0
  113. ralph/mcp/artifacts/policy_outcomes.py +33 -0
  114. ralph/mcp/artifacts/store.py +264 -0
  115. ralph/mcp/artifacts.py +33 -0
  116. ralph/mcp/audit_adapter.py +29 -0
  117. ralph/mcp/bridge.py +78 -0
  118. ralph/mcp/capability_mapping.py +61 -0
  119. ralph/mcp/commit_message.py +31 -0
  120. ralph/mcp/development_result_artifact.py +13 -0
  121. ralph/mcp/env.py +25 -0
  122. ralph/mcp/file_backend.py +13 -0
  123. ralph/mcp/plan_artifact.py +71 -0
  124. ralph/mcp/policy_outcomes.py +11 -0
  125. ralph/mcp/protocol/__init__.py +9 -0
  126. ralph/mcp/protocol/capability_mapping.py +521 -0
  127. ralph/mcp/protocol/env.py +26 -0
  128. ralph/mcp/protocol/session.py +61 -0
  129. ralph/mcp/protocol/startup.py +623 -0
  130. ralph/mcp/protocol/transport.py +212 -0
  131. ralph/mcp/server/__init__.py +31 -0
  132. ralph/mcp/server/__main__.py +6 -0
  133. ralph/mcp/server/factory.py +18 -0
  134. ralph/mcp/server/factory_impl.py +93 -0
  135. ralph/mcp/server/lifecycle.py +218 -0
  136. ralph/mcp/server/runtime.py +840 -0
  137. ralph/mcp/session.py +15 -0
  138. ralph/mcp/startup.py +71 -0
  139. ralph/mcp/tool_artifact.py +23 -0
  140. ralph/mcp/tool_bridge.py +33 -0
  141. ralph/mcp/tool_coordination.py +43 -0
  142. ralph/mcp/tool_exec.py +53 -0
  143. ralph/mcp/tool_git_read.py +81 -0
  144. ralph/mcp/tool_names.py +89 -0
  145. ralph/mcp/tool_websearch.py +43 -0
  146. ralph/mcp/tool_workspace.py +41 -0
  147. ralph/mcp/tools/__init__.py +8 -0
  148. ralph/mcp/tools/artifact.py +475 -0
  149. ralph/mcp/tools/bridge.py +798 -0
  150. ralph/mcp/tools/coordination.py +245 -0
  151. ralph/mcp/tools/exec.py +474 -0
  152. ralph/mcp/tools/git_read.py +226 -0
  153. ralph/mcp/tools/names.py +192 -0
  154. ralph/mcp/tools/websearch.py +117 -0
  155. ralph/mcp/tools/workspace.py +283 -0
  156. ralph/mcp/transport.py +15 -0
  157. ralph/mcp/upstream/__init__.py +9 -0
  158. ralph/mcp/upstream/agent_probe.py +332 -0
  159. ralph/mcp/upstream/client.py +222 -0
  160. ralph/mcp/upstream/config.py +146 -0
  161. ralph/mcp/upstream/models.py +20 -0
  162. ralph/mcp/upstream/registry.py +107 -0
  163. ralph/mcp/upstream/validation.py +233 -0
  164. ralph/mcp/upstream_client.py +13 -0
  165. ralph/mcp/upstream_config.py +19 -0
  166. ralph/mcp/upstream_models.py +11 -0
  167. ralph/mcp/upstream_registry.py +13 -0
  168. ralph/mcp/upstream_validation.py +67 -0
  169. ralph/mcp/websearch/__init__.py +14 -0
  170. ralph/mcp/websearch/backends/__init__.py +21 -0
  171. ralph/mcp/websearch/backends/base.py +28 -0
  172. ralph/mcp/websearch/backends/brave.py +86 -0
  173. ralph/mcp/websearch/backends/ddgs.py +71 -0
  174. ralph/mcp/websearch/backends/exa.py +87 -0
  175. ralph/mcp/websearch/backends/searxng.py +72 -0
  176. ralph/mcp/websearch/backends/tavily.py +77 -0
  177. ralph/mcp/websearch/secrets.py +45 -0
  178. ralph/phases/__init__.py +167 -0
  179. ralph/phases/analysis.py +187 -0
  180. ralph/phases/artifacts.py +89 -0
  181. ralph/phases/commit.py +96 -0
  182. ralph/phases/commit_logging.py +329 -0
  183. ralph/phases/development.py +184 -0
  184. ralph/phases/fix.py +64 -0
  185. ralph/phases/integrity.py +118 -0
  186. ralph/phases/planning.py +109 -0
  187. ralph/phases/review.py +181 -0
  188. ralph/phases/timing.py +67 -0
  189. ralph/pipeline/__init__.py +19 -0
  190. ralph/pipeline/checkpoint.py +183 -0
  191. ralph/pipeline/effects.py +187 -0
  192. ralph/pipeline/events.py +86 -0
  193. ralph/pipeline/handoffs.py +95 -0
  194. ralph/pipeline/orchestrator.py +324 -0
  195. ralph/pipeline/parallel/__init__.py +0 -0
  196. ralph/pipeline/parallel/coordinator.py +377 -0
  197. ralph/pipeline/parallel/merge_integrator.py +113 -0
  198. ralph/pipeline/parallel/scheduler.py +22 -0
  199. ralph/pipeline/parallel/worker_session.py +46 -0
  200. ralph/pipeline/reducer.py +846 -0
  201. ralph/pipeline/runner.py +1837 -0
  202. ralph/pipeline/state.py +315 -0
  203. ralph/pipeline/work_units.py +142 -0
  204. ralph/pipeline/worker_state.py +48 -0
  205. ralph/platform/__init__.py +29 -0
  206. ralph/platform/detection.py +164 -0
  207. ralph/platform/models.py +102 -0
  208. ralph/policy/__init__.py +69 -0
  209. ralph/policy/defaults/agents.toml +78 -0
  210. ralph/policy/defaults/artifacts.toml +95 -0
  211. ralph/policy/defaults/mcp.toml +51 -0
  212. ralph/policy/defaults/pipeline.toml +122 -0
  213. ralph/policy/loader.py +282 -0
  214. ralph/policy/models.py +432 -0
  215. ralph/policy/validation.py +230 -0
  216. ralph/prompts/__init__.py +29 -0
  217. ralph/prompts/commit/__init__.py +141 -0
  218. ralph/prompts/debug_dump.py +19 -0
  219. ralph/prompts/developer/__init__.py +164 -0
  220. ralph/prompts/materialize.py +409 -0
  221. ralph/prompts/payload_refs.py +53 -0
  222. ralph/prompts/reviewer/__init__.py +78 -0
  223. ralph/prompts/system_prompt.py +46 -0
  224. ralph/prompts/template_context.py +34 -0
  225. ralph/prompts/template_engine.py +116 -0
  226. ralph/prompts/template_parsing.py +297 -0
  227. ralph/prompts/template_registry.py +96 -0
  228. ralph/prompts/template_variables.py +633 -0
  229. ralph/prompts/templates/analysis_system_prompt.jinja +5 -0
  230. ralph/prompts/templates/commit_message.jinja +68 -0
  231. ralph/prompts/templates/commit_simplified.jinja +7 -0
  232. ralph/prompts/templates/conflict_resolution.jinja +11 -0
  233. ralph/prompts/templates/conflict_resolution_fallback.jinja +5 -0
  234. ralph/prompts/templates/developer_iteration.jinja +24 -0
  235. ralph/prompts/templates/developer_iteration_continuation.jinja +15 -0
  236. ralph/prompts/templates/developer_iteration_fallback.jinja +22 -0
  237. ralph/prompts/templates/development_analysis.jinja +52 -0
  238. ralph/prompts/templates/development_commit_message.jinja +10 -0
  239. ralph/prompts/templates/fix_analysis_system_prompt.jinja +5 -0
  240. ralph/prompts/templates/fix_mode.jinja +17 -0
  241. ralph/prompts/templates/parallel_dev_worker.jinja +5 -0
  242. ralph/prompts/templates/parallel_planning.jinja +12 -0
  243. ralph/prompts/templates/parallel_verifier.jinja +5 -0
  244. ralph/prompts/templates/planning.jinja +127 -0
  245. ralph/prompts/templates/planning_fallback.jinja +35 -0
  246. ralph/prompts/templates/review.jinja +57 -0
  247. ralph/prompts/templates/review_analysis.jinja +52 -0
  248. ralph/prompts/templates/shared/_context_section.jinja +5 -0
  249. ralph/prompts/templates/shared/_critical_header.jinja +1 -0
  250. ralph/prompts/templates/shared/_developer_iteration_guidance.jinja +11 -0
  251. ralph/prompts/templates/shared/_diff_section.jinja +3 -0
  252. ralph/prompts/templates/shared/_mcp_tools.jinja +31 -0
  253. ralph/prompts/templates/shared/_no_git_commit.jinja +1 -0
  254. ralph/prompts/templates/shared/_output_checklist.jinja +4 -0
  255. ralph/prompts/templates/shared/_payload_section.jinja +10 -0
  256. ralph/prompts/templates/shared/_safety_no_execute.jinja +4 -0
  257. ralph/prompts/templates/shared/_session_capabilities.jinja +8 -0
  258. ralph/prompts/templates/shared/_unattended_mode.jinja +12 -0
  259. ralph/prompts/templates/worker_developer.jinja +20 -0
  260. ralph/prompts/types.py +111 -0
  261. ralph/runtime/__init__.py +35 -0
  262. ralph/runtime/environment.py +182 -0
  263. ralph/runtime/verify_timeout.py +23 -0
  264. ralph/testing/__init__.py +21 -0
  265. ralph/testing/fake_agent_executor.py +67 -0
  266. ralph/verify_timeout.py +198 -0
  267. ralph/workspace/__init__.py +19 -0
  268. ralph/workspace/fs.py +165 -0
  269. ralph/workspace/memory.py +193 -0
  270. ralph/workspace/protocol.py +109 -0
  271. ralph/workspace/scope.py +94 -0
  272. ralph_workflow-0.8.0b1.dist-info/METADATA +175 -0
  273. ralph_workflow-0.8.0b1.dist-info/RECORD +276 -0
  274. ralph_workflow-0.8.0b1.dist-info/WHEEL +4 -0
  275. ralph_workflow-0.8.0b1.dist-info/entry_points.txt +3 -0
  276. ralph_workflow-0.8.0b1.dist-info/licenses/LICENSE +659 -0
ralph/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Top-level package for Ralph Workflow.
2
+
3
+ The public Python package is intentionally small at the root: it exposes version
4
+ metadata and points users toward the major subpackages that make up the system.
5
+
6
+ Useful pydoc entry points:
7
+
8
+ - ``ralph.cli`` for the Typer CLI application
9
+ - ``ralph.config`` for configuration models and loading
10
+ - ``ralph.pipeline`` for orchestration state and reducer/orchestrator logic
11
+ - ``ralph.phases`` for phase dispatch
12
+ - ``ralph.mcp`` for the MCP bridge and standalone server helpers
13
+ - ``ralph.git`` for GitPython-backed repository operations
14
+ - ``ralph.workspace`` for filesystem abstractions used by production code and tests
15
+ """
16
+
17
+ __version__ = "0.8.0b1"
18
+ __all__ = ["__version__"]
ralph/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m ralph`."""
2
+
3
+ from ralph.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,15 @@
1
+ """Public agent-management exports.
2
+
3
+ This package exposes the small set of agent abstractions most callers need:
4
+ registry lookup, chain composition, and process invocation.
5
+ """
6
+
7
+ from ralph.agents.chain import AgentChain
8
+ from ralph.agents.invoke import invoke_agent
9
+ from ralph.agents.registry import AgentRegistry
10
+
11
+ __all__ = [
12
+ "AgentChain",
13
+ "AgentRegistry",
14
+ "invoke_agent",
15
+ ]
ralph/agents/chain.py ADDED
@@ -0,0 +1,296 @@
1
+ """Agent fallback chain management with strict drain-to-chain binding.
2
+
3
+ This module handles the agent fallback chain — the ordered list of agents
4
+ to try when an agent fails. It supports retry logic and exponential backoff.
5
+
6
+ IMPORTANT: This module implements STRICT drain-to-chain binding. Every drain
7
+ must have an explicit binding in AgentsPolicy or startup validation fails.
8
+ There is NO permissive fallback resolution — no sibling fallback, no inference,
9
+ no default chains. If a drain is not bound, DrainNotBoundError is raised.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from typing import TYPE_CHECKING, cast
16
+
17
+ from loguru import logger
18
+
19
+ from ralph.policy.models import AgentChainConfig, AgentDrainConfig, AgentsPolicy, DrainName
20
+
21
+ if TYPE_CHECKING:
22
+ from ralph.config.models import UnifiedConfig
23
+
24
+
25
+ class DrainNotBoundError(Exception):
26
+ """Raised when a drain has no explicit chain binding.
27
+
28
+ Attributes:
29
+ drain: The unbound drain name.
30
+ available_drains: Names of all bound drains.
31
+ """
32
+
33
+ def __init__(self, drain: str, available_drains: set[str]) -> None:
34
+ self.drain = drain
35
+ self.available_drains = available_drains
36
+ available = sorted(available_drains)
37
+ msg = (
38
+ f"Drain '{drain}' is not bound to any agent chain in agents.toml. "
39
+ f"Available drains: {available}. "
40
+ f"Add a binding for '{drain}' in agent_drains or use a bound drain."
41
+ )
42
+ super().__init__(msg)
43
+
44
+
45
+ class UnknownAgentError(Exception):
46
+ """Raised when an agent name is not found in the registry.
47
+
48
+ Attributes:
49
+ agent_name: The unknown agent name.
50
+ """
51
+
52
+ def __init__(self, agent_name: str) -> None:
53
+ self.agent_name = agent_name
54
+ msg = f"Unknown agent: '{agent_name}'. Register the agent in the configuration."
55
+ super().__init__(msg)
56
+
57
+
58
+ class AgentChain:
59
+ """Manages agent fallback chain with retry logic.
60
+
61
+ The chain maintains an ordered list of agents and handles:
62
+ - Current agent selection
63
+ - Retry counting and limits
64
+ - Exponential backoff between retries
65
+ - Fallback to next agent on exhaustion
66
+
67
+ Attributes:
68
+ agents: List of agent names in the chain.
69
+ current_index: Index of the currently selected agent.
70
+ retries: Number of retries for current agent.
71
+ max_retries: Maximum retries before falling back.
72
+ retry_delay_ms: Base delay between retries in milliseconds.
73
+ backoff_multiplier: Multiplier for exponential backoff.
74
+ max_backoff_ms: Maximum backoff delay in milliseconds.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ agents: list[str],
80
+ max_retries: int = 3,
81
+ retry_delay_ms: int = 1000,
82
+ backoff_multiplier: float = 2.0,
83
+ max_backoff_ms: int = 60000,
84
+ ) -> None:
85
+ """Initialize agent chain.
86
+
87
+ Args:
88
+ agents: List of agent names in fallback order.
89
+ max_retries: Maximum retries per agent before fallback.
90
+ retry_delay_ms: Base delay between retries in milliseconds.
91
+ backoff_multiplier: Multiplier for exponential backoff.
92
+ max_backoff_ms: Maximum backoff delay in milliseconds.
93
+ """
94
+ self.agents = agents
95
+ self.current_index = 0
96
+ self.retries = 0
97
+ self.max_retries = max_retries
98
+ self.retry_delay_ms = retry_delay_ms
99
+ self.backoff_multiplier = backoff_multiplier
100
+ self.max_backoff_ms = max_backoff_ms
101
+
102
+ @property
103
+ def current_agent(self) -> str | None:
104
+ """Get the current agent name.
105
+
106
+ Returns:
107
+ Agent name or None if chain is exhausted.
108
+ """
109
+ if not self.agents or self.current_index >= len(self.agents):
110
+ return None
111
+ return self.agents[self.current_index]
112
+
113
+ @property
114
+ def is_exhausted(self) -> bool:
115
+ """Check if all agents in chain are exhausted.
116
+
117
+ Returns:
118
+ True if no agents remain.
119
+ """
120
+ return self.current_agent is None
121
+
122
+ def can_retry(self) -> bool:
123
+ """Check if current agent can be retried.
124
+
125
+ Returns:
126
+ True if retries remain for current agent.
127
+ """
128
+ return self.retries < self.max_retries
129
+
130
+ def advance(self) -> bool:
131
+ """Advance to the next agent in the chain.
132
+
133
+ Returns:
134
+ True if advanced successfully, False if chain exhausted.
135
+ """
136
+ if self.current_index + 1 < len(self.agents):
137
+ self.current_index += 1
138
+ self.retries = 0
139
+ logger.debug("Advanced to next agent: {}", self.current_agent)
140
+ return True
141
+ logger.debug("Agent chain exhausted")
142
+ return False
143
+
144
+ def record_retry(self) -> None:
145
+ """Record a retry attempt for current agent."""
146
+ self.retries += 1
147
+ logger.debug(
148
+ "Retry {} of {} for agent {}",
149
+ self.retries,
150
+ self.max_retries,
151
+ self.current_agent,
152
+ )
153
+
154
+ def calculate_backoff(self) -> float:
155
+ """Calculate backoff delay in seconds.
156
+
157
+ Returns:
158
+ Backoff delay in seconds.
159
+ """
160
+ delay = self.retry_delay_ms * (self.backoff_multiplier**self.retries)
161
+ return min(delay, self.max_backoff_ms) / 1000.0
162
+
163
+ def wait_backoff(self) -> None:
164
+ """Wait for the backoff period."""
165
+ backoff = self.calculate_backoff()
166
+ logger.debug("Backing off for {:.2f} seconds", backoff)
167
+ time.sleep(backoff)
168
+
169
+
170
+ class ChainManager:
171
+ """Manages agent chains with strict drain-to-chain binding.
172
+
173
+ ChainManager is constructed with an AgentsPolicy and provides lookup of
174
+ chains by drain name. Drain resolution is STRICT — there is no fallback
175
+ or inference. If a drain is not explicitly bound, DrainNotBoundError
176
+ is raised.
177
+
178
+ Attributes:
179
+ agents_policy: The agents policy containing chains and drain bindings.
180
+ """
181
+
182
+ def __init__(self, agents_policy: AgentsPolicy) -> None:
183
+ """Initialize ChainManager with an AgentsPolicy.
184
+
185
+ Args:
186
+ agents_policy: Validated agents policy with chain and drain definitions.
187
+ """
188
+ self._policy = agents_policy
189
+
190
+ @classmethod
191
+ def from_config(cls, config: UnifiedConfig) -> ChainManager:
192
+ """Create ChainManager from a legacy UnifiedConfig.
193
+
194
+ This is a compatibility shim that converts the old UnifiedConfig
195
+ format to the new AgentsPolicy format.
196
+
197
+ Args:
198
+ config: Legacy unified configuration.
199
+
200
+ Returns:
201
+ ChainManager instance.
202
+ """
203
+ agent_chains: dict[str, AgentChainConfig] = {}
204
+ for name, agents in config.agent_chains.items():
205
+ agent_chains[name] = AgentChainConfig(agents=agents)
206
+
207
+ agent_drains: dict[DrainName, AgentDrainConfig] = {}
208
+ for drain, chain in config.agent_drains.items():
209
+ agent_drains[cast("DrainName", drain)] = AgentDrainConfig(chain=chain)
210
+
211
+ policy = AgentsPolicy(
212
+ agent_chains=agent_chains,
213
+ agent_drains=agent_drains,
214
+ )
215
+ return cls(policy)
216
+
217
+ def chain_for_drain(self, drain: DrainName) -> AgentChainConfig:
218
+ """Get the chain configuration for a drain.
219
+
220
+ This is the STRICT drain resolution — no fallback, no inference.
221
+ If the drain is not explicitly bound in agents.toml, DrainNotBoundError
222
+ is raised at startup before any agent is invoked.
223
+
224
+ Args:
225
+ drain: Drain name to look up.
226
+
227
+ Returns:
228
+ AgentChainConfig for the bound chain.
229
+
230
+ Raises:
231
+ DrainNotBoundError: If the drain is not explicitly bound.
232
+ """
233
+ binding = self._policy.agent_drains.get(drain)
234
+ if binding is None:
235
+ raise DrainNotBoundError(
236
+ drain=drain,
237
+ available_drains=set(self._policy.agent_drains.keys()),
238
+ )
239
+
240
+ chain = self._policy.agent_chains.get(binding.chain)
241
+ if chain is None:
242
+ msg = (
243
+ f"Drain '{drain}' references chain '{binding.chain}' "
244
+ f"which is not defined in agent_chains"
245
+ )
246
+ raise ValueError(msg)
247
+
248
+ return chain
249
+
250
+ def chain_config_for_drain(self, drain: DrainName) -> AgentChainConfig:
251
+ """Alias for chain_for_drain for clarity."""
252
+ return self.chain_for_drain(drain)
253
+
254
+ def validate(self) -> list[str]:
255
+ """Validate the policy for internal consistency.
256
+
257
+ Returns:
258
+ List of validation error messages (empty if valid).
259
+ """
260
+ errors: list[str] = []
261
+
262
+ for drain, binding in self._policy.agent_drains.items():
263
+ if binding.chain not in self._policy.agent_chains:
264
+ errors.append(f"Drain '{drain}' references unknown chain '{binding.chain}'")
265
+
266
+ for name, chain in self._policy.agent_chains.items():
267
+ if not chain.agents:
268
+ errors.append(f"Chain '{name}' has no agents")
269
+
270
+ return errors
271
+
272
+
273
+ def create_chain_from_config(
274
+ config: UnifiedConfig,
275
+ chain_name: str,
276
+ ) -> AgentChain | None:
277
+ """Create an AgentChain from UnifiedConfig.
278
+
279
+ Args:
280
+ config: Unified configuration.
281
+ chain_name: Name of the chain in agent_chains.
282
+
283
+ Returns:
284
+ AgentChain instance or None if chain not found.
285
+ """
286
+ agent_names = config.agent_chains.get(chain_name)
287
+ if not agent_names:
288
+ return None
289
+
290
+ return AgentChain(
291
+ agents=agent_names,
292
+ max_retries=config.general.max_retries,
293
+ retry_delay_ms=config.general.retry_delay_ms,
294
+ backoff_multiplier=config.general.backoff_multiplier,
295
+ max_backoff_ms=config.general.max_backoff_ms,
296
+ )
@@ -0,0 +1,32 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from ralph.pipeline.work_units import WorkUnit
6
+ from ralph.pipeline.worker_state import WorkerStatus
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class WorkerResult:
11
+ unit_id: str
12
+ exit_code: int
13
+ final_message: str
14
+ duration_ms: int
15
+
16
+
17
+ class ExecutorError(Exception):
18
+ pass
19
+
20
+
21
+ @runtime_checkable
22
+ class AgentExecutor(Protocol):
23
+ async def run(
24
+ self,
25
+ unit: WorkUnit,
26
+ *,
27
+ on_output: Callable[[str], None],
28
+ on_status: Callable[[WorkerStatus], None],
29
+ ) -> WorkerResult: ...
30
+
31
+
32
+ __all__ = ["AgentExecutor", "ExecutorError", "WorkerResult"]