code-puppy 0.0.373__py3-none-any.whl → 0.0.375__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.
- code_puppy/agents/agent_creator_agent.py +49 -1
- code_puppy/agents/agent_helios.py +122 -0
- code_puppy/agents/agent_manager.py +60 -4
- code_puppy/agents/base_agent.py +61 -4
- code_puppy/agents/json_agent.py +30 -7
- code_puppy/callbacks.py +125 -0
- code_puppy/command_line/colors_menu.py +2 -0
- code_puppy/command_line/command_handler.py +1 -0
- code_puppy/command_line/config_commands.py +3 -1
- code_puppy/command_line/uc_menu.py +890 -0
- code_puppy/config.py +29 -0
- code_puppy/messaging/messages.py +18 -0
- code_puppy/messaging/rich_renderer.py +48 -7
- code_puppy/messaging/subagent_console.py +0 -1
- code_puppy/model_factory.py +63 -258
- code_puppy/model_utils.py +33 -1
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
- code_puppy/plugins/antigravity_oauth/utils.py +2 -3
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
- code_puppy/plugins/ralph/__init__.py +13 -0
- code_puppy/plugins/ralph/agents.py +433 -0
- code_puppy/plugins/ralph/commands.py +208 -0
- code_puppy/plugins/ralph/loop_controller.py +285 -0
- code_puppy/plugins/ralph/models.py +125 -0
- code_puppy/plugins/ralph/register_callbacks.py +133 -0
- code_puppy/plugins/ralph/state_manager.py +322 -0
- code_puppy/plugins/ralph/tools.py +451 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +304 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/tools/__init__.py +169 -1
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- code_puppy/tools/universal_constructor.py +889 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Ralph Loop Controller - Manages the autonomous iteration loop.
|
|
2
|
+
|
|
3
|
+
This is the "outer loop" that spawns fresh agent instances per iteration,
|
|
4
|
+
just like the original ralph.sh bash script does.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Awaitable, Callable, Optional
|
|
10
|
+
|
|
11
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
12
|
+
|
|
13
|
+
from .state_manager import get_state_manager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RalphLoopController:
|
|
19
|
+
"""Controls the Ralph autonomous loop.
|
|
20
|
+
|
|
21
|
+
Each iteration:
|
|
22
|
+
1. Checks if there's work to do
|
|
23
|
+
2. Invokes the ralph-orchestrator agent with a FRESH session
|
|
24
|
+
3. Waits for completion
|
|
25
|
+
4. Checks if all stories are done or if we should continue
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, max_iterations: int = 10):
|
|
29
|
+
self.max_iterations = max_iterations
|
|
30
|
+
self.current_iteration = 0
|
|
31
|
+
self.is_complete = False
|
|
32
|
+
self.is_running = False
|
|
33
|
+
self._stop_requested = False
|
|
34
|
+
|
|
35
|
+
def request_stop(self) -> None:
|
|
36
|
+
"""Request the loop to stop after current iteration."""
|
|
37
|
+
self._stop_requested = True
|
|
38
|
+
emit_warning("🛑 Stop requested - will halt after current iteration")
|
|
39
|
+
|
|
40
|
+
async def run(
|
|
41
|
+
self,
|
|
42
|
+
invoke_func: Callable[[str, str, Optional[str]], Awaitable[dict]],
|
|
43
|
+
) -> dict:
|
|
44
|
+
"""Run the Ralph loop until completion or max iterations.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
invoke_func: Async function to invoke an agent.
|
|
48
|
+
Signature: (agent_name, prompt, session_id) -> result_dict
|
|
49
|
+
The result_dict should have 'response' and 'error' keys.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
dict with 'success', 'iterations', 'message' keys
|
|
53
|
+
"""
|
|
54
|
+
self.is_running = True
|
|
55
|
+
self.is_complete = False
|
|
56
|
+
self._stop_requested = False
|
|
57
|
+
|
|
58
|
+
manager = get_state_manager()
|
|
59
|
+
|
|
60
|
+
# Pre-flight checks
|
|
61
|
+
if not manager.prd_exists():
|
|
62
|
+
self.is_running = False
|
|
63
|
+
return {
|
|
64
|
+
"success": False,
|
|
65
|
+
"iterations": 0,
|
|
66
|
+
"message": "No prd.json found. Create one with /ralph prd first.",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if manager.all_stories_complete():
|
|
70
|
+
self.is_running = False
|
|
71
|
+
return {
|
|
72
|
+
"success": True,
|
|
73
|
+
"iterations": 0,
|
|
74
|
+
"message": "All stories already complete!",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
prd = manager.read_prd()
|
|
78
|
+
emit_info(f"🐺 Starting Ralph Loop")
|
|
79
|
+
emit_info(f"📋 Project: {prd.project if prd else 'Unknown'}")
|
|
80
|
+
emit_info(f"📊 Progress: {prd.get_progress_summary() if prd else 'Unknown'}")
|
|
81
|
+
emit_info(f"🔄 Max iterations: {self.max_iterations}")
|
|
82
|
+
emit_info("─" * 50)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
86
|
+
self.current_iteration = iteration
|
|
87
|
+
|
|
88
|
+
# Check for stop request
|
|
89
|
+
if self._stop_requested:
|
|
90
|
+
emit_warning(f"🛑 Stopped at iteration {iteration}")
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
# Check if already complete
|
|
94
|
+
if manager.all_stories_complete():
|
|
95
|
+
self.is_complete = True
|
|
96
|
+
emit_success("🎉 All stories complete!")
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
# Get current story for logging
|
|
100
|
+
story = manager.get_next_story()
|
|
101
|
+
if story is None:
|
|
102
|
+
self.is_complete = True
|
|
103
|
+
emit_success("🎉 All stories complete!")
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
emit_info(f"\n{'='*60}")
|
|
107
|
+
emit_info(f"🐺 RALPH ITERATION {iteration} of {self.max_iterations}")
|
|
108
|
+
emit_info(f"📌 Working on: [{story.id}] {story.title}")
|
|
109
|
+
emit_info(f"{'='*60}\n")
|
|
110
|
+
|
|
111
|
+
# Build the prompt for this iteration
|
|
112
|
+
iteration_prompt = self._build_iteration_prompt(story)
|
|
113
|
+
|
|
114
|
+
# Invoke orchestrator with FRESH session (unique per iteration)
|
|
115
|
+
session_id = f"ralph-iter-{iteration}"
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = await invoke_func(
|
|
119
|
+
"ralph-orchestrator",
|
|
120
|
+
iteration_prompt,
|
|
121
|
+
session_id,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
response = result.get("response", "")
|
|
125
|
+
error = result.get("error")
|
|
126
|
+
|
|
127
|
+
if error:
|
|
128
|
+
emit_error(f"Iteration {iteration} error: {error}")
|
|
129
|
+
# Continue to next iteration despite error
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Check for completion signal in response
|
|
133
|
+
if response and "<promise>COMPLETE</promise>" in response:
|
|
134
|
+
self.is_complete = True
|
|
135
|
+
emit_success("🎉 Ralph signaled COMPLETE - all stories done!")
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
except asyncio.CancelledError:
|
|
139
|
+
emit_warning(f"🛑 Iteration {iteration} cancelled")
|
|
140
|
+
break
|
|
141
|
+
except Exception as e:
|
|
142
|
+
emit_error(f"Iteration {iteration} failed: {e}")
|
|
143
|
+
logger.exception(f"Ralph iteration {iteration} failed")
|
|
144
|
+
# Continue to next iteration
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Brief pause between iterations
|
|
148
|
+
await asyncio.sleep(1)
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
# Loop completed without break (max iterations reached)
|
|
152
|
+
emit_warning(f"⚠️ Reached max iterations ({self.max_iterations})")
|
|
153
|
+
|
|
154
|
+
finally:
|
|
155
|
+
self.is_running = False
|
|
156
|
+
|
|
157
|
+
# Final status
|
|
158
|
+
prd = manager.read_prd()
|
|
159
|
+
final_progress = prd.get_progress_summary() if prd else "Unknown"
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"success": self.is_complete,
|
|
163
|
+
"iterations": self.current_iteration,
|
|
164
|
+
"message": f"Completed {self.current_iteration} iterations. {final_progress}",
|
|
165
|
+
"all_complete": self.is_complete,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def _build_iteration_prompt(self, story) -> str:
|
|
169
|
+
"""Build the prompt for a single iteration."""
|
|
170
|
+
# Find VERIFY criteria
|
|
171
|
+
verify_criteria = [c for c in story.acceptance_criteria if c.startswith("VERIFY:")]
|
|
172
|
+
other_criteria = [c for c in story.acceptance_criteria if not c.startswith("VERIFY:")]
|
|
173
|
+
|
|
174
|
+
verify_section = ""
|
|
175
|
+
if verify_criteria:
|
|
176
|
+
verify_section = f"""
|
|
177
|
+
## MANDATORY VERIFICATION COMMANDS
|
|
178
|
+
You MUST run these commands and they MUST succeed before marking complete:
|
|
179
|
+
{chr(10).join(f' {c}' for c in verify_criteria)}
|
|
180
|
+
|
|
181
|
+
If ANY verification fails, fix the code and re-run until it passes!
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
return f"""Execute ONE iteration of the Ralph loop.
|
|
185
|
+
|
|
186
|
+
## Current Story
|
|
187
|
+
- **ID:** {story.id}
|
|
188
|
+
- **Title:** {story.title}
|
|
189
|
+
- **Description:** {story.description}
|
|
190
|
+
|
|
191
|
+
## Acceptance Criteria (implement ALL of these):
|
|
192
|
+
{chr(10).join(f' - {c}' for c in other_criteria)}
|
|
193
|
+
{verify_section}
|
|
194
|
+
## Requires UI Verification: {story.has_ui_verification()}
|
|
195
|
+
{"If yes, invoke qa-kitten to verify UI changes work correctly." if story.has_ui_verification() else ""}
|
|
196
|
+
|
|
197
|
+
## Your Task
|
|
198
|
+
|
|
199
|
+
1. Call `ralph_read_patterns()` to get context from previous iterations
|
|
200
|
+
2. Implement this ONE story completely
|
|
201
|
+
3. **RUN ALL VERIFY COMMANDS** - they must pass!
|
|
202
|
+
4. If checks pass, commit with: `git commit -m "feat: {story.id} - {story.title}"`
|
|
203
|
+
5. Call `ralph_mark_story_complete("{story.id}", "Verified: <what you tested>")`
|
|
204
|
+
6. Call `ralph_log_progress(...)` with what you learned
|
|
205
|
+
7. Call `ralph_check_all_complete()` to see if we're done
|
|
206
|
+
|
|
207
|
+
If ALL stories are complete, output: <promise>COMPLETE</promise>
|
|
208
|
+
|
|
209
|
+
⚠️ DO NOT mark complete until verification passes! Actually run the VERIFY commands!
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Global controller instance
|
|
214
|
+
_controller: Optional[RalphLoopController] = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_loop_controller(max_iterations: int = 10) -> RalphLoopController:
|
|
218
|
+
"""Get or create the loop controller."""
|
|
219
|
+
global _controller
|
|
220
|
+
if _controller is None or not _controller.is_running:
|
|
221
|
+
_controller = RalphLoopController(max_iterations)
|
|
222
|
+
return _controller
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def run_ralph_loop(
|
|
226
|
+
max_iterations: int = 10,
|
|
227
|
+
invoke_func: Optional[Callable] = None,
|
|
228
|
+
) -> dict:
|
|
229
|
+
"""Convenience function to run the Ralph loop.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
max_iterations: Maximum number of iterations
|
|
233
|
+
invoke_func: Function to invoke agents. If None, uses default.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Result dict from the controller
|
|
237
|
+
"""
|
|
238
|
+
if invoke_func is None:
|
|
239
|
+
# Use the default agent invocation mechanism
|
|
240
|
+
invoke_func = _default_invoke_agent
|
|
241
|
+
|
|
242
|
+
controller = get_loop_controller(max_iterations)
|
|
243
|
+
return await controller.run(invoke_func)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def _default_invoke_agent(
|
|
247
|
+
agent_name: str,
|
|
248
|
+
prompt: str,
|
|
249
|
+
session_id: Optional[str] = None,
|
|
250
|
+
) -> dict:
|
|
251
|
+
"""Default agent invocation using code_puppy's agent system."""
|
|
252
|
+
try:
|
|
253
|
+
from code_puppy.agents import get_current_agent, load_agent, set_current_agent
|
|
254
|
+
|
|
255
|
+
# Save current agent to restore later
|
|
256
|
+
original_agent = get_current_agent()
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Load the target agent
|
|
260
|
+
target_agent = load_agent(agent_name)
|
|
261
|
+
if target_agent is None:
|
|
262
|
+
return {"response": None, "error": f"Agent '{agent_name}' not found"}
|
|
263
|
+
|
|
264
|
+
# Run the agent with the prompt
|
|
265
|
+
# Note: This creates a fresh run with no message history
|
|
266
|
+
result = await target_agent.run_with_mcp(prompt)
|
|
267
|
+
|
|
268
|
+
# Extract response text
|
|
269
|
+
response_text = ""
|
|
270
|
+
if result is not None:
|
|
271
|
+
if hasattr(result, "data"):
|
|
272
|
+
response_text = str(result.data) if result.data else ""
|
|
273
|
+
else:
|
|
274
|
+
response_text = str(result)
|
|
275
|
+
|
|
276
|
+
return {"response": response_text, "error": None}
|
|
277
|
+
|
|
278
|
+
finally:
|
|
279
|
+
# Restore original agent
|
|
280
|
+
if original_agent:
|
|
281
|
+
set_current_agent(original_agent.name)
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.exception(f"Failed to invoke agent {agent_name}")
|
|
285
|
+
return {"response": None, "error": str(e)}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Data models for the Ralph plugin."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class UserStory:
|
|
11
|
+
"""A single user story in the PRD."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
title: str
|
|
15
|
+
description: str
|
|
16
|
+
acceptance_criteria: List[str]
|
|
17
|
+
priority: int
|
|
18
|
+
passes: bool = False
|
|
19
|
+
notes: str = ""
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"id": self.id,
|
|
24
|
+
"title": self.title,
|
|
25
|
+
"description": self.description,
|
|
26
|
+
"acceptanceCriteria": self.acceptance_criteria,
|
|
27
|
+
"priority": self.priority,
|
|
28
|
+
"passes": self.passes,
|
|
29
|
+
"notes": self.notes,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, data: dict) -> "UserStory":
|
|
34
|
+
return cls(
|
|
35
|
+
id=data.get("id", ""),
|
|
36
|
+
title=data.get("title", ""),
|
|
37
|
+
description=data.get("description", ""),
|
|
38
|
+
acceptance_criteria=data.get("acceptanceCriteria", []),
|
|
39
|
+
priority=data.get("priority", 0),
|
|
40
|
+
passes=data.get("passes", False),
|
|
41
|
+
notes=data.get("notes", ""),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def has_ui_verification(self) -> bool:
|
|
45
|
+
"""Check if this story requires browser/UI verification."""
|
|
46
|
+
ui_keywords = ["browser", "ui", "verify in browser", "qa-kitten", "visual"]
|
|
47
|
+
criteria_text = " ".join(self.acceptance_criteria).lower()
|
|
48
|
+
return any(kw in criteria_text for kw in ui_keywords)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PRDConfig:
|
|
53
|
+
"""Configuration for a PRD project."""
|
|
54
|
+
|
|
55
|
+
project: str
|
|
56
|
+
branch_name: str
|
|
57
|
+
description: str
|
|
58
|
+
user_stories: List[UserStory] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
return {
|
|
62
|
+
"project": self.project,
|
|
63
|
+
"branchName": self.branch_name,
|
|
64
|
+
"description": self.description,
|
|
65
|
+
"userStories": [s.to_dict() for s in self.user_stories],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: dict) -> "PRDConfig":
|
|
70
|
+
return cls(
|
|
71
|
+
project=data.get("project", ""),
|
|
72
|
+
branch_name=data.get("branchName", ""),
|
|
73
|
+
description=data.get("description", ""),
|
|
74
|
+
user_stories=[UserStory.from_dict(s) for s in data.get("userStories", [])],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def to_json(self, indent: int = 2) -> str:
|
|
78
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_json(cls, json_str: str) -> "PRDConfig":
|
|
82
|
+
return cls.from_dict(json.loads(json_str))
|
|
83
|
+
|
|
84
|
+
def get_next_story(self) -> Optional[UserStory]:
|
|
85
|
+
"""Get the highest priority story that hasn't passed yet."""
|
|
86
|
+
pending = [s for s in self.user_stories if not s.passes]
|
|
87
|
+
if not pending:
|
|
88
|
+
return None
|
|
89
|
+
return min(pending, key=lambda s: s.priority)
|
|
90
|
+
|
|
91
|
+
def all_complete(self) -> bool:
|
|
92
|
+
"""Check if all stories have passed."""
|
|
93
|
+
return all(s.passes for s in self.user_stories)
|
|
94
|
+
|
|
95
|
+
def get_progress_summary(self) -> str:
|
|
96
|
+
"""Get a summary of progress."""
|
|
97
|
+
total = len(self.user_stories)
|
|
98
|
+
done = sum(1 for s in self.user_stories if s.passes)
|
|
99
|
+
return f"{done}/{total} stories complete"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ProgressEntry:
|
|
104
|
+
"""An entry in the progress log."""
|
|
105
|
+
|
|
106
|
+
timestamp: datetime
|
|
107
|
+
story_id: str
|
|
108
|
+
summary: str
|
|
109
|
+
files_changed: List[str] = field(default_factory=list)
|
|
110
|
+
learnings: List[str] = field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
def to_markdown(self) -> str:
|
|
113
|
+
"""Convert to markdown format for progress.txt."""
|
|
114
|
+
lines = [
|
|
115
|
+
f"## {self.timestamp.strftime('%Y-%m-%d %H:%M')} - {self.story_id}",
|
|
116
|
+
f"- {self.summary}",
|
|
117
|
+
]
|
|
118
|
+
if self.files_changed:
|
|
119
|
+
lines.append(f"- Files changed: {', '.join(self.files_changed)}")
|
|
120
|
+
if self.learnings:
|
|
121
|
+
lines.append("- **Learnings for future iterations:**")
|
|
122
|
+
for learning in self.learnings:
|
|
123
|
+
lines.append(f" - {learning}")
|
|
124
|
+
lines.append("---")
|
|
125
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Ralph Plugin - Autonomous AI agent loop for completing PRDs.
|
|
2
|
+
|
|
3
|
+
This module registers all Ralph callbacks:
|
|
4
|
+
- register_tools: Ralph-specific tools for PRD management
|
|
5
|
+
- register_agents: PRD Generator, Converter, and Orchestrator agents
|
|
6
|
+
- custom_command: /ralph slash commands
|
|
7
|
+
- custom_command_help: Help entries for Ralph commands
|
|
8
|
+
- agent_response_complete: Detect completion signal for loop termination
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.messaging import emit_info, emit_success
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# TOOL REGISTRATION
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _provide_tools() -> List[Dict[str, Any]]:
|
|
26
|
+
"""Provide Ralph tools via the register_tools callback."""
|
|
27
|
+
from .tools import get_ralph_tools
|
|
28
|
+
|
|
29
|
+
logger.debug("Ralph plugin: providing tools")
|
|
30
|
+
return get_ralph_tools()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# AGENT REGISTRATION
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _provide_agents() -> List[Dict[str, Any]]:
|
|
39
|
+
"""Provide Ralph agents via the register_agents callback."""
|
|
40
|
+
from .agents import get_ralph_agents
|
|
41
|
+
|
|
42
|
+
logger.debug("Ralph plugin: providing agents")
|
|
43
|
+
return get_ralph_agents()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# COMMAND REGISTRATION
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _provide_command_help() -> List[Tuple[str, str]]:
|
|
52
|
+
"""Provide help entries for Ralph commands."""
|
|
53
|
+
from .commands import get_ralph_help
|
|
54
|
+
|
|
55
|
+
return get_ralph_help()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _handle_command(command: str, name: str) -> Optional[Any]:
|
|
59
|
+
"""Handle /ralph commands."""
|
|
60
|
+
from .commands import handle_ralph_command
|
|
61
|
+
|
|
62
|
+
return handle_ralph_command(command, name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# COMPLETION DETECTION
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
# Track completion state for the loop controller
|
|
70
|
+
_ralph_completion_detected = False
|
|
71
|
+
_ralph_last_session_id: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_ralph_complete() -> bool:
|
|
75
|
+
"""Check if Ralph has signaled completion."""
|
|
76
|
+
return _ralph_completion_detected
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def reset_ralph_completion() -> None:
|
|
80
|
+
"""Reset the completion flag for a new run."""
|
|
81
|
+
global _ralph_completion_detected, _ralph_last_session_id
|
|
82
|
+
_ralph_completion_detected = False
|
|
83
|
+
_ralph_last_session_id = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _on_agent_complete(
|
|
87
|
+
agent_name: str,
|
|
88
|
+
response_text: str,
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
metadata: Optional[dict] = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Handle agent response completion.
|
|
93
|
+
|
|
94
|
+
This detects the <promise>COMPLETE</promise> signal from the
|
|
95
|
+
Ralph Orchestrator and sets the completion flag.
|
|
96
|
+
"""
|
|
97
|
+
global _ralph_completion_detected, _ralph_last_session_id
|
|
98
|
+
|
|
99
|
+
# Only track ralph-orchestrator completions
|
|
100
|
+
if agent_name != "ralph-orchestrator":
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
logger.debug(f"Ralph plugin: orchestrator completed (session={session_id})")
|
|
104
|
+
|
|
105
|
+
# Check for completion signal
|
|
106
|
+
if response_text and "<promise>COMPLETE</promise>" in response_text:
|
|
107
|
+
_ralph_completion_detected = True
|
|
108
|
+
_ralph_last_session_id = session_id
|
|
109
|
+
|
|
110
|
+
emit_success("🎉 Ralph has completed all user stories!")
|
|
111
|
+
emit_info("All tasks in prd.json are now marked as passes: true")
|
|
112
|
+
logger.info("Ralph completion signal detected - all stories complete")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# REGISTER ALL CALLBACKS
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
# Tools
|
|
120
|
+
register_callback("register_tools", _provide_tools)
|
|
121
|
+
|
|
122
|
+
# Agents
|
|
123
|
+
register_callback("register_agents", _provide_agents)
|
|
124
|
+
|
|
125
|
+
# Commands
|
|
126
|
+
register_callback("custom_command", _handle_command)
|
|
127
|
+
register_callback("custom_command_help", _provide_command_help)
|
|
128
|
+
|
|
129
|
+
# Completion detection
|
|
130
|
+
register_callback("agent_response_complete", _on_agent_complete)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
logger.info("Ralph plugin: all callbacks registered successfully")
|