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.
- ralph/__init__.py +18 -0
- ralph/__main__.py +6 -0
- ralph/agents/__init__.py +15 -0
- ralph/agents/chain.py +296 -0
- ralph/agents/executor.py +32 -0
- ralph/agents/invoke.py +924 -0
- ralph/agents/parsers/__init__.py +45 -0
- ralph/agents/parsers/base.py +54 -0
- ralph/agents/parsers/claude.py +460 -0
- ralph/agents/parsers/codex.py +281 -0
- ralph/agents/parsers/gemini.py +270 -0
- ralph/agents/parsers/generic.py +133 -0
- ralph/agents/parsers/opencode.py +277 -0
- ralph/agents/registry.py +243 -0
- ralph/agents/subprocess_executor.py +143 -0
- ralph/agents/transport_emit.py +323 -0
- ralph/api/__init__.py +14 -0
- ralph/api/cloud.py +29 -0
- ralph/api/opencode.py +152 -0
- ralph/banner.py +103 -0
- ralph/checkpoint/__init__.py +19 -0
- ralph/checkpoint/builder.py +125 -0
- ralph/checkpoint/execution_history.py +182 -0
- ralph/checkpoint/run_context.py +60 -0
- ralph/checkpoint/size_monitor.py +75 -0
- ralph/cli/__init__.py +9 -0
- ralph/cli/commands/__init__.py +13 -0
- ralph/cli/commands/cleanup.py +84 -0
- ralph/cli/commands/commit.py +941 -0
- ralph/cli/commands/diagnose.py +231 -0
- ralph/cli/commands/init.py +92 -0
- ralph/cli/commands/run.py +150 -0
- ralph/cli/main.py +643 -0
- ralph/cli/options.py +187 -0
- ralph/cloud/__init__.py +28 -0
- ralph/cloud/client.py +221 -0
- ralph/config/__init__.py +37 -0
- ralph/config/enums.py +149 -0
- ralph/config/loader.py +276 -0
- ralph/config/mcp_loader.py +128 -0
- ralph/config/mcp_models.py +97 -0
- ralph/config/models.py +289 -0
- ralph/diagnostics/__init__.py +275 -0
- ralph/display/__init__.py +39 -0
- ralph/display/activity_model.py +145 -0
- ralph/display/activity_router.py +140 -0
- ralph/display/artifact_reader.py +164 -0
- ralph/display/artifact_renderer.py +254 -0
- ralph/display/completion_summary.py +172 -0
- ralph/display/line_sanitizer.py +26 -0
- ralph/display/mode.py +18 -0
- ralph/display/parallel_display.py +135 -0
- ralph/display/phase_banner.py +204 -0
- ralph/display/plain_renderer.py +254 -0
- ralph/display/progress.py +394 -0
- ralph/display/prompt_reader.py +37 -0
- ralph/display/ring_buffer.py +58 -0
- ralph/display/snapshot.py +195 -0
- ralph/display/status.py +108 -0
- ralph/display/subscriber.py +316 -0
- ralph/display/tables.py +140 -0
- ralph/display/theme.py +105 -0
- ralph/executor/__init__.py +5 -0
- ralph/executor/process.py +209 -0
- ralph/exit_pause/__init__.py +182 -0
- ralph/files/__init__.py +29 -0
- ralph/files/operations.py +130 -0
- ralph/git/__init__.py +63 -0
- ralph/git/__init__.pyi +14 -0
- ralph/git/executor.py +62 -0
- ralph/git/hooks.py +249 -0
- ralph/git/operations.py +311 -0
- ralph/git/rebase/__init__.py +69 -0
- ralph/git/rebase/rebase.py +346 -0
- ralph/git/rebase/rebase_checkpoint.py +333 -0
- ralph/git/rebase/rebase_continuation.py +200 -0
- ralph/git/rebase/rebase_kinds.py +389 -0
- ralph/git/rebase/rebase_preconditions.py +265 -0
- ralph/git/rebase/rebase_state_machine.py +236 -0
- ralph/git/worktree_manager.py +92 -0
- ralph/git/worktree_preflight.py +89 -0
- ralph/git/wrapper.py +180 -0
- ralph/git/wrapper.pyi +14 -0
- ralph/guidelines/__init__.py +24 -0
- ralph/guidelines/go.py +169 -0
- ralph/guidelines/java.py +157 -0
- ralph/guidelines/javascript.py +285 -0
- ralph/guidelines/php.py +178 -0
- ralph/guidelines/python.py +168 -0
- ralph/guidelines/ruby.py +176 -0
- ralph/guidelines/rust.py +158 -0
- ralph/guidelines/stack.py +398 -0
- ralph/install.py +60 -0
- ralph/interrupt/__init__.py +32 -0
- ralph/interrupt/asyncio_bridge.py +53 -0
- ralph/language_detector/__init__.py +80 -0
- ralph/language_detector/extensions.py +52 -0
- ralph/language_detector/models.py +44 -0
- ralph/language_detector/scanner.py +197 -0
- ralph/language_detector/signatures.py +281 -0
- ralph/logging.py +326 -0
- ralph/main.py +6 -0
- ralph/mcp/ARCHITECTURE.md +122 -0
- ralph/mcp/__init__.py +90 -0
- ralph/mcp/agent_transport_probe.py +76 -0
- ralph/mcp/artifacts/__init__.py +40 -0
- ralph/mcp/artifacts/audit_adapter.py +227 -0
- ralph/mcp/artifacts/bridge.py +352 -0
- ralph/mcp/artifacts/commit_message.py +312 -0
- ralph/mcp/artifacts/development_result.py +56 -0
- ralph/mcp/artifacts/file_backend.py +44 -0
- ralph/mcp/artifacts/plan.py +377 -0
- ralph/mcp/artifacts/policy_outcomes.py +33 -0
- ralph/mcp/artifacts/store.py +264 -0
- ralph/mcp/artifacts.py +33 -0
- ralph/mcp/audit_adapter.py +29 -0
- ralph/mcp/bridge.py +78 -0
- ralph/mcp/capability_mapping.py +61 -0
- ralph/mcp/commit_message.py +31 -0
- ralph/mcp/development_result_artifact.py +13 -0
- ralph/mcp/env.py +25 -0
- ralph/mcp/file_backend.py +13 -0
- ralph/mcp/plan_artifact.py +71 -0
- ralph/mcp/policy_outcomes.py +11 -0
- ralph/mcp/protocol/__init__.py +9 -0
- ralph/mcp/protocol/capability_mapping.py +521 -0
- ralph/mcp/protocol/env.py +26 -0
- ralph/mcp/protocol/session.py +61 -0
- ralph/mcp/protocol/startup.py +623 -0
- ralph/mcp/protocol/transport.py +212 -0
- ralph/mcp/server/__init__.py +31 -0
- ralph/mcp/server/__main__.py +6 -0
- ralph/mcp/server/factory.py +18 -0
- ralph/mcp/server/factory_impl.py +93 -0
- ralph/mcp/server/lifecycle.py +218 -0
- ralph/mcp/server/runtime.py +840 -0
- ralph/mcp/session.py +15 -0
- ralph/mcp/startup.py +71 -0
- ralph/mcp/tool_artifact.py +23 -0
- ralph/mcp/tool_bridge.py +33 -0
- ralph/mcp/tool_coordination.py +43 -0
- ralph/mcp/tool_exec.py +53 -0
- ralph/mcp/tool_git_read.py +81 -0
- ralph/mcp/tool_names.py +89 -0
- ralph/mcp/tool_websearch.py +43 -0
- ralph/mcp/tool_workspace.py +41 -0
- ralph/mcp/tools/__init__.py +8 -0
- ralph/mcp/tools/artifact.py +475 -0
- ralph/mcp/tools/bridge.py +798 -0
- ralph/mcp/tools/coordination.py +245 -0
- ralph/mcp/tools/exec.py +474 -0
- ralph/mcp/tools/git_read.py +226 -0
- ralph/mcp/tools/names.py +192 -0
- ralph/mcp/tools/websearch.py +117 -0
- ralph/mcp/tools/workspace.py +283 -0
- ralph/mcp/transport.py +15 -0
- ralph/mcp/upstream/__init__.py +9 -0
- ralph/mcp/upstream/agent_probe.py +332 -0
- ralph/mcp/upstream/client.py +222 -0
- ralph/mcp/upstream/config.py +146 -0
- ralph/mcp/upstream/models.py +20 -0
- ralph/mcp/upstream/registry.py +107 -0
- ralph/mcp/upstream/validation.py +233 -0
- ralph/mcp/upstream_client.py +13 -0
- ralph/mcp/upstream_config.py +19 -0
- ralph/mcp/upstream_models.py +11 -0
- ralph/mcp/upstream_registry.py +13 -0
- ralph/mcp/upstream_validation.py +67 -0
- ralph/mcp/websearch/__init__.py +14 -0
- ralph/mcp/websearch/backends/__init__.py +21 -0
- ralph/mcp/websearch/backends/base.py +28 -0
- ralph/mcp/websearch/backends/brave.py +86 -0
- ralph/mcp/websearch/backends/ddgs.py +71 -0
- ralph/mcp/websearch/backends/exa.py +87 -0
- ralph/mcp/websearch/backends/searxng.py +72 -0
- ralph/mcp/websearch/backends/tavily.py +77 -0
- ralph/mcp/websearch/secrets.py +45 -0
- ralph/phases/__init__.py +167 -0
- ralph/phases/analysis.py +187 -0
- ralph/phases/artifacts.py +89 -0
- ralph/phases/commit.py +96 -0
- ralph/phases/commit_logging.py +329 -0
- ralph/phases/development.py +184 -0
- ralph/phases/fix.py +64 -0
- ralph/phases/integrity.py +118 -0
- ralph/phases/planning.py +109 -0
- ralph/phases/review.py +181 -0
- ralph/phases/timing.py +67 -0
- ralph/pipeline/__init__.py +19 -0
- ralph/pipeline/checkpoint.py +183 -0
- ralph/pipeline/effects.py +187 -0
- ralph/pipeline/events.py +86 -0
- ralph/pipeline/handoffs.py +95 -0
- ralph/pipeline/orchestrator.py +324 -0
- ralph/pipeline/parallel/__init__.py +0 -0
- ralph/pipeline/parallel/coordinator.py +377 -0
- ralph/pipeline/parallel/merge_integrator.py +113 -0
- ralph/pipeline/parallel/scheduler.py +22 -0
- ralph/pipeline/parallel/worker_session.py +46 -0
- ralph/pipeline/reducer.py +846 -0
- ralph/pipeline/runner.py +1837 -0
- ralph/pipeline/state.py +315 -0
- ralph/pipeline/work_units.py +142 -0
- ralph/pipeline/worker_state.py +48 -0
- ralph/platform/__init__.py +29 -0
- ralph/platform/detection.py +164 -0
- ralph/platform/models.py +102 -0
- ralph/policy/__init__.py +69 -0
- ralph/policy/defaults/agents.toml +78 -0
- ralph/policy/defaults/artifacts.toml +95 -0
- ralph/policy/defaults/mcp.toml +51 -0
- ralph/policy/defaults/pipeline.toml +122 -0
- ralph/policy/loader.py +282 -0
- ralph/policy/models.py +432 -0
- ralph/policy/validation.py +230 -0
- ralph/prompts/__init__.py +29 -0
- ralph/prompts/commit/__init__.py +141 -0
- ralph/prompts/debug_dump.py +19 -0
- ralph/prompts/developer/__init__.py +164 -0
- ralph/prompts/materialize.py +409 -0
- ralph/prompts/payload_refs.py +53 -0
- ralph/prompts/reviewer/__init__.py +78 -0
- ralph/prompts/system_prompt.py +46 -0
- ralph/prompts/template_context.py +34 -0
- ralph/prompts/template_engine.py +116 -0
- ralph/prompts/template_parsing.py +297 -0
- ralph/prompts/template_registry.py +96 -0
- ralph/prompts/template_variables.py +633 -0
- ralph/prompts/templates/analysis_system_prompt.jinja +5 -0
- ralph/prompts/templates/commit_message.jinja +68 -0
- ralph/prompts/templates/commit_simplified.jinja +7 -0
- ralph/prompts/templates/conflict_resolution.jinja +11 -0
- ralph/prompts/templates/conflict_resolution_fallback.jinja +5 -0
- ralph/prompts/templates/developer_iteration.jinja +24 -0
- ralph/prompts/templates/developer_iteration_continuation.jinja +15 -0
- ralph/prompts/templates/developer_iteration_fallback.jinja +22 -0
- ralph/prompts/templates/development_analysis.jinja +52 -0
- ralph/prompts/templates/development_commit_message.jinja +10 -0
- ralph/prompts/templates/fix_analysis_system_prompt.jinja +5 -0
- ralph/prompts/templates/fix_mode.jinja +17 -0
- ralph/prompts/templates/parallel_dev_worker.jinja +5 -0
- ralph/prompts/templates/parallel_planning.jinja +12 -0
- ralph/prompts/templates/parallel_verifier.jinja +5 -0
- ralph/prompts/templates/planning.jinja +127 -0
- ralph/prompts/templates/planning_fallback.jinja +35 -0
- ralph/prompts/templates/review.jinja +57 -0
- ralph/prompts/templates/review_analysis.jinja +52 -0
- ralph/prompts/templates/shared/_context_section.jinja +5 -0
- ralph/prompts/templates/shared/_critical_header.jinja +1 -0
- ralph/prompts/templates/shared/_developer_iteration_guidance.jinja +11 -0
- ralph/prompts/templates/shared/_diff_section.jinja +3 -0
- ralph/prompts/templates/shared/_mcp_tools.jinja +31 -0
- ralph/prompts/templates/shared/_no_git_commit.jinja +1 -0
- ralph/prompts/templates/shared/_output_checklist.jinja +4 -0
- ralph/prompts/templates/shared/_payload_section.jinja +10 -0
- ralph/prompts/templates/shared/_safety_no_execute.jinja +4 -0
- ralph/prompts/templates/shared/_session_capabilities.jinja +8 -0
- ralph/prompts/templates/shared/_unattended_mode.jinja +12 -0
- ralph/prompts/templates/worker_developer.jinja +20 -0
- ralph/prompts/types.py +111 -0
- ralph/runtime/__init__.py +35 -0
- ralph/runtime/environment.py +182 -0
- ralph/runtime/verify_timeout.py +23 -0
- ralph/testing/__init__.py +21 -0
- ralph/testing/fake_agent_executor.py +67 -0
- ralph/verify_timeout.py +198 -0
- ralph/workspace/__init__.py +19 -0
- ralph/workspace/fs.py +165 -0
- ralph/workspace/memory.py +193 -0
- ralph/workspace/protocol.py +109 -0
- ralph/workspace/scope.py +94 -0
- ralph_workflow-0.8.0b1.dist-info/METADATA +175 -0
- ralph_workflow-0.8.0b1.dist-info/RECORD +276 -0
- ralph_workflow-0.8.0b1.dist-info/WHEEL +4 -0
- ralph_workflow-0.8.0b1.dist-info/entry_points.txt +3 -0
- 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
ralph/agents/__init__.py
ADDED
|
@@ -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
|
+
)
|
ralph/agents/executor.py
ADDED
|
@@ -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"]
|