code-puppy 0.0.374__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_manager.py +34 -2
- code_puppy/agents/base_agent.py +61 -4
- code_puppy/callbacks.py +125 -0
- code_puppy/messaging/rich_renderer.py +13 -7
- 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/tools/__init__.py +31 -0
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/RECORD +28 -20
- {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Ralph plugin tools - registered via the register_tools callback."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pydantic_ai import RunContext
|
|
8
|
+
|
|
9
|
+
from .models import ProgressEntry
|
|
10
|
+
from .state_manager import get_state_manager
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# TOOL OUTPUT TYPES
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RalphStoryOutput:
|
|
19
|
+
"""Output for getting the current story."""
|
|
20
|
+
|
|
21
|
+
story_id: str | None
|
|
22
|
+
title: str | None
|
|
23
|
+
description: str | None
|
|
24
|
+
acceptance_criteria: List[str]
|
|
25
|
+
priority: int | None
|
|
26
|
+
requires_ui_verification: bool
|
|
27
|
+
all_complete: bool
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RalphStatusOutput:
|
|
33
|
+
"""Output for status checks."""
|
|
34
|
+
|
|
35
|
+
success: bool
|
|
36
|
+
message: str
|
|
37
|
+
progress_summary: str | None = None
|
|
38
|
+
stories_remaining: int = 0
|
|
39
|
+
all_complete: bool = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RalphPRDOutput:
|
|
44
|
+
"""Output for reading the full PRD."""
|
|
45
|
+
|
|
46
|
+
success: bool
|
|
47
|
+
project: str | None = None
|
|
48
|
+
branch_name: str | None = None
|
|
49
|
+
description: str | None = None
|
|
50
|
+
stories: List[dict] | None = None
|
|
51
|
+
progress_summary: str | None = None
|
|
52
|
+
error: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class RalphPatternsOutput:
|
|
57
|
+
"""Output for reading codebase patterns."""
|
|
58
|
+
|
|
59
|
+
patterns: str
|
|
60
|
+
has_patterns: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ============================================================================
|
|
64
|
+
# TOOL REGISTRATION FUNCTIONS
|
|
65
|
+
# ============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def register_ralph_get_current_story(agent) -> None:
|
|
69
|
+
"""Register the tool to get the current story to work on."""
|
|
70
|
+
|
|
71
|
+
@agent.tool
|
|
72
|
+
def ralph_get_current_story(context: RunContext) -> RalphStoryOutput:
|
|
73
|
+
"""Get the next user story to work on from prd.json.
|
|
74
|
+
|
|
75
|
+
This tool reads the prd.json file and returns the highest-priority
|
|
76
|
+
story that hasn't been completed yet (passes=false).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
RalphStoryOutput containing:
|
|
80
|
+
- story_id: The story ID (e.g., "US-001")
|
|
81
|
+
- title: Story title
|
|
82
|
+
- description: Full story description
|
|
83
|
+
- acceptance_criteria: List of criteria to satisfy
|
|
84
|
+
- priority: Story priority (lower = higher priority)
|
|
85
|
+
- requires_ui_verification: True if story needs browser testing
|
|
86
|
+
- all_complete: True if ALL stories are done
|
|
87
|
+
- error: Error message if something went wrong
|
|
88
|
+
"""
|
|
89
|
+
manager = get_state_manager()
|
|
90
|
+
|
|
91
|
+
if not manager.prd_exists():
|
|
92
|
+
return RalphStoryOutput(
|
|
93
|
+
story_id=None,
|
|
94
|
+
title=None,
|
|
95
|
+
description=None,
|
|
96
|
+
acceptance_criteria=[],
|
|
97
|
+
priority=None,
|
|
98
|
+
requires_ui_verification=False,
|
|
99
|
+
all_complete=False,
|
|
100
|
+
error="No prd.json found in current directory. Create one first with /ralph prd",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if manager.all_stories_complete():
|
|
104
|
+
return RalphStoryOutput(
|
|
105
|
+
story_id=None,
|
|
106
|
+
title=None,
|
|
107
|
+
description=None,
|
|
108
|
+
acceptance_criteria=[],
|
|
109
|
+
priority=None,
|
|
110
|
+
requires_ui_verification=False,
|
|
111
|
+
all_complete=True,
|
|
112
|
+
error=None,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
story = manager.get_next_story()
|
|
116
|
+
if story is None:
|
|
117
|
+
return RalphStoryOutput(
|
|
118
|
+
story_id=None,
|
|
119
|
+
title=None,
|
|
120
|
+
description=None,
|
|
121
|
+
acceptance_criteria=[],
|
|
122
|
+
priority=None,
|
|
123
|
+
requires_ui_verification=False,
|
|
124
|
+
all_complete=True,
|
|
125
|
+
error=None,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return RalphStoryOutput(
|
|
129
|
+
story_id=story.id,
|
|
130
|
+
title=story.title,
|
|
131
|
+
description=story.description,
|
|
132
|
+
acceptance_criteria=story.acceptance_criteria,
|
|
133
|
+
priority=story.priority,
|
|
134
|
+
requires_ui_verification=story.has_ui_verification(),
|
|
135
|
+
all_complete=False,
|
|
136
|
+
error=None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def register_ralph_mark_story_complete(agent) -> None:
|
|
141
|
+
"""Register the tool to mark a story as complete."""
|
|
142
|
+
|
|
143
|
+
@agent.tool
|
|
144
|
+
def ralph_mark_story_complete(
|
|
145
|
+
context: RunContext,
|
|
146
|
+
story_id: str,
|
|
147
|
+
notes: str | None = None,
|
|
148
|
+
) -> RalphStatusOutput:
|
|
149
|
+
"""Mark a user story as complete (passes=true) in prd.json.
|
|
150
|
+
|
|
151
|
+
Call this AFTER you have:
|
|
152
|
+
1. Implemented all the acceptance criteria
|
|
153
|
+
2. Verified the code compiles/typechecks
|
|
154
|
+
3. Run any required tests
|
|
155
|
+
4. Committed the changes
|
|
156
|
+
|
|
157
|
+
For UI stories, also ensure browser verification passed.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
story_id: The story ID to mark complete (e.g., "US-001")
|
|
161
|
+
notes: Optional notes about the implementation
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
RalphStatusOutput with success status and message
|
|
165
|
+
"""
|
|
166
|
+
manager = get_state_manager()
|
|
167
|
+
|
|
168
|
+
success, message = manager.mark_story_complete(story_id, notes or "")
|
|
169
|
+
|
|
170
|
+
prd = manager.read_prd()
|
|
171
|
+
remaining = 0
|
|
172
|
+
all_complete = False
|
|
173
|
+
progress = None
|
|
174
|
+
|
|
175
|
+
if prd:
|
|
176
|
+
remaining = sum(1 for s in prd.user_stories if not s.passes)
|
|
177
|
+
all_complete = prd.all_complete()
|
|
178
|
+
progress = prd.get_progress_summary()
|
|
179
|
+
|
|
180
|
+
return RalphStatusOutput(
|
|
181
|
+
success=success,
|
|
182
|
+
message=message,
|
|
183
|
+
progress_summary=progress,
|
|
184
|
+
stories_remaining=remaining,
|
|
185
|
+
all_complete=all_complete,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def register_ralph_log_progress(agent) -> None:
|
|
190
|
+
"""Register the tool to log progress to progress.txt."""
|
|
191
|
+
|
|
192
|
+
@agent.tool
|
|
193
|
+
def ralph_log_progress(
|
|
194
|
+
context: RunContext,
|
|
195
|
+
story_id: str,
|
|
196
|
+
summary: str,
|
|
197
|
+
files_changed: List[str] | None = None,
|
|
198
|
+
learnings: List[str] | None = None,
|
|
199
|
+
) -> RalphStatusOutput:
|
|
200
|
+
"""Append a progress entry to progress.txt.
|
|
201
|
+
|
|
202
|
+
Call this after completing a story to record:
|
|
203
|
+
- What was implemented
|
|
204
|
+
- Which files were changed
|
|
205
|
+
- Any learnings for future iterations
|
|
206
|
+
|
|
207
|
+
The learnings are especially important for helping future iterations
|
|
208
|
+
understand patterns and avoid mistakes.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
story_id: The story ID that was completed
|
|
212
|
+
summary: Brief summary of what was implemented
|
|
213
|
+
files_changed: List of files that were modified
|
|
214
|
+
learnings: List of learnings/patterns discovered
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
RalphStatusOutput with success status
|
|
218
|
+
"""
|
|
219
|
+
manager = get_state_manager()
|
|
220
|
+
|
|
221
|
+
entry = ProgressEntry(
|
|
222
|
+
timestamp=datetime.now(),
|
|
223
|
+
story_id=story_id,
|
|
224
|
+
summary=summary,
|
|
225
|
+
files_changed=files_changed or [],
|
|
226
|
+
learnings=learnings or [],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
success = manager.append_progress(entry)
|
|
230
|
+
|
|
231
|
+
return RalphStatusOutput(
|
|
232
|
+
success=success,
|
|
233
|
+
message="Progress logged successfully"
|
|
234
|
+
if success
|
|
235
|
+
else "Failed to log progress",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def register_ralph_check_all_complete(agent) -> None:
|
|
240
|
+
"""Register the tool to check if all stories are complete."""
|
|
241
|
+
|
|
242
|
+
@agent.tool
|
|
243
|
+
def ralph_check_all_complete(context: RunContext) -> RalphStatusOutput:
|
|
244
|
+
"""Check if all user stories in prd.json are complete.
|
|
245
|
+
|
|
246
|
+
Use this to determine if the Ralph loop should exit.
|
|
247
|
+
When all stories are complete, you should output:
|
|
248
|
+
<promise>COMPLETE</promise>
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
RalphStatusOutput with all_complete flag
|
|
252
|
+
"""
|
|
253
|
+
manager = get_state_manager()
|
|
254
|
+
|
|
255
|
+
if not manager.prd_exists():
|
|
256
|
+
return RalphStatusOutput(
|
|
257
|
+
success=False,
|
|
258
|
+
message="No prd.json found",
|
|
259
|
+
all_complete=False,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
prd = manager.read_prd()
|
|
263
|
+
if prd is None:
|
|
264
|
+
return RalphStatusOutput(
|
|
265
|
+
success=False,
|
|
266
|
+
message="Failed to read prd.json",
|
|
267
|
+
all_complete=False,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
all_complete = prd.all_complete()
|
|
271
|
+
remaining = sum(1 for s in prd.user_stories if not s.passes)
|
|
272
|
+
|
|
273
|
+
return RalphStatusOutput(
|
|
274
|
+
success=True,
|
|
275
|
+
message="All stories complete!"
|
|
276
|
+
if all_complete
|
|
277
|
+
else f"{remaining} stories remaining",
|
|
278
|
+
progress_summary=prd.get_progress_summary(),
|
|
279
|
+
stories_remaining=remaining,
|
|
280
|
+
all_complete=all_complete,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def register_ralph_read_prd(agent) -> None:
|
|
285
|
+
"""Register the tool to read the full PRD."""
|
|
286
|
+
|
|
287
|
+
@agent.tool
|
|
288
|
+
def ralph_read_prd(context: RunContext) -> RalphPRDOutput:
|
|
289
|
+
"""Read the full prd.json file and return its contents.
|
|
290
|
+
|
|
291
|
+
Use this to understand the overall project and see all stories.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
RalphPRDOutput with project details and all stories
|
|
295
|
+
"""
|
|
296
|
+
manager = get_state_manager()
|
|
297
|
+
|
|
298
|
+
if not manager.prd_exists():
|
|
299
|
+
return RalphPRDOutput(
|
|
300
|
+
success=False,
|
|
301
|
+
error="No prd.json found in current directory",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
prd = manager.read_prd()
|
|
305
|
+
if prd is None:
|
|
306
|
+
return RalphPRDOutput(
|
|
307
|
+
success=False,
|
|
308
|
+
error="Failed to parse prd.json",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return RalphPRDOutput(
|
|
312
|
+
success=True,
|
|
313
|
+
project=prd.project,
|
|
314
|
+
branch_name=prd.branch_name,
|
|
315
|
+
description=prd.description,
|
|
316
|
+
stories=[s.to_dict() for s in prd.user_stories],
|
|
317
|
+
progress_summary=prd.get_progress_summary(),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def register_ralph_read_patterns(agent) -> None:
|
|
322
|
+
"""Register the tool to read codebase patterns from progress.txt."""
|
|
323
|
+
|
|
324
|
+
@agent.tool
|
|
325
|
+
def ralph_read_patterns(context: RunContext) -> RalphPatternsOutput:
|
|
326
|
+
"""Read the Codebase Patterns section from progress.txt.
|
|
327
|
+
|
|
328
|
+
These patterns were discovered by previous iterations and contain
|
|
329
|
+
important context about the codebase. Read this BEFORE starting
|
|
330
|
+
work on a new story.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
RalphPatternsOutput with patterns text
|
|
334
|
+
"""
|
|
335
|
+
manager = get_state_manager()
|
|
336
|
+
patterns = manager.read_codebase_patterns()
|
|
337
|
+
|
|
338
|
+
return RalphPatternsOutput(
|
|
339
|
+
patterns=patterns if patterns else "No patterns recorded yet.",
|
|
340
|
+
has_patterns=bool(patterns),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def register_ralph_add_pattern(agent) -> None:
|
|
345
|
+
"""Register the tool to add a codebase pattern."""
|
|
346
|
+
|
|
347
|
+
@agent.tool
|
|
348
|
+
def ralph_add_pattern(context: RunContext, pattern: str) -> RalphStatusOutput:
|
|
349
|
+
"""Add a reusable pattern to the Codebase Patterns section.
|
|
350
|
+
|
|
351
|
+
Only add patterns that are GENERAL and REUSABLE, not story-specific.
|
|
352
|
+
|
|
353
|
+
Good examples:
|
|
354
|
+
- "Use `sql<number>` template for aggregations"
|
|
355
|
+
- "Always use `IF NOT EXISTS` for migrations"
|
|
356
|
+
- "Export types from actions.ts for UI components"
|
|
357
|
+
|
|
358
|
+
Bad examples (too specific):
|
|
359
|
+
- "Added login button to header" (story-specific)
|
|
360
|
+
- "Fixed bug in user.ts" (not a pattern)
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
pattern: The pattern to record
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
RalphStatusOutput with success status
|
|
367
|
+
"""
|
|
368
|
+
manager = get_state_manager()
|
|
369
|
+
success = manager.add_codebase_pattern(pattern)
|
|
370
|
+
|
|
371
|
+
return RalphStatusOutput(
|
|
372
|
+
success=success,
|
|
373
|
+
message="Pattern added" if success else "Failed to add pattern",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def register_ralph_run_loop(agent) -> None:
|
|
378
|
+
"""Register the tool to run the Ralph autonomous loop."""
|
|
379
|
+
|
|
380
|
+
@agent.tool
|
|
381
|
+
async def ralph_run_loop(
|
|
382
|
+
context: RunContext,
|
|
383
|
+
max_iterations: int = 10,
|
|
384
|
+
) -> RalphStatusOutput:
|
|
385
|
+
"""Start the Ralph autonomous loop to implement all pending stories.
|
|
386
|
+
|
|
387
|
+
This runs the full Ralph loop, executing one story per iteration
|
|
388
|
+
until all stories are complete or max_iterations is reached.
|
|
389
|
+
|
|
390
|
+
Each iteration:
|
|
391
|
+
1. Gets the next pending story from prd.json
|
|
392
|
+
2. Invokes the ralph-orchestrator agent with a fresh session
|
|
393
|
+
3. The orchestrator implements and commits the story
|
|
394
|
+
4. Checks if all stories are complete
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
max_iterations: Maximum number of iterations (default 10)
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
RalphStatusOutput with success status and final progress
|
|
401
|
+
"""
|
|
402
|
+
from .loop_controller import run_ralph_loop
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
result = await run_ralph_loop(max_iterations=max_iterations)
|
|
406
|
+
|
|
407
|
+
return RalphStatusOutput(
|
|
408
|
+
success=result.get("success", False),
|
|
409
|
+
message=result.get("message", "Loop completed"),
|
|
410
|
+
progress_summary=result.get("message"),
|
|
411
|
+
stories_remaining=0 if result.get("all_complete") else -1,
|
|
412
|
+
all_complete=result.get("all_complete", False),
|
|
413
|
+
)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
return RalphStatusOutput(
|
|
416
|
+
success=False,
|
|
417
|
+
message=f"Loop failed: {str(e)}",
|
|
418
|
+
all_complete=False,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ============================================================================
|
|
423
|
+
# TOOL PROVIDER FOR CALLBACK
|
|
424
|
+
# ============================================================================
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_ralph_tools() -> List[dict]:
|
|
428
|
+
"""Get all Ralph tools for registration via the register_tools callback.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
List of tool definitions with name and register_func.
|
|
432
|
+
"""
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
"name": "ralph_get_current_story",
|
|
436
|
+
"register_func": register_ralph_get_current_story,
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"name": "ralph_mark_story_complete",
|
|
440
|
+
"register_func": register_ralph_mark_story_complete,
|
|
441
|
+
},
|
|
442
|
+
{"name": "ralph_log_progress", "register_func": register_ralph_log_progress},
|
|
443
|
+
{
|
|
444
|
+
"name": "ralph_check_all_complete",
|
|
445
|
+
"register_func": register_ralph_check_all_complete,
|
|
446
|
+
},
|
|
447
|
+
{"name": "ralph_read_prd", "register_func": register_ralph_read_prd},
|
|
448
|
+
{"name": "ralph_read_patterns", "register_func": register_ralph_read_patterns},
|
|
449
|
+
{"name": "ralph_add_pattern", "register_func": register_ralph_add_pattern},
|
|
450
|
+
{"name": "ralph_run_loop", "register_func": register_ralph_run_loop},
|
|
451
|
+
]
|
code_puppy/tools/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from code_puppy.callbacks import on_register_tools
|
|
1
2
|
from code_puppy.messaging import emit_warning
|
|
2
3
|
from code_puppy.tools.agent_tools import register_invoke_agent, register_list_agents
|
|
3
4
|
|
|
@@ -169,6 +170,34 @@ TOOL_REGISTRY = {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
def _load_plugin_tools() -> None:
|
|
174
|
+
"""Load tools registered by plugins via the register_tools callback.
|
|
175
|
+
|
|
176
|
+
This merges plugin-provided tools into the TOOL_REGISTRY.
|
|
177
|
+
Called lazily when tools are first accessed.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
results = on_register_tools()
|
|
181
|
+
for result in results:
|
|
182
|
+
if result is None:
|
|
183
|
+
continue
|
|
184
|
+
# Each result should be a list of tool definitions
|
|
185
|
+
tools_list = result if isinstance(result, list) else [result]
|
|
186
|
+
for tool_def in tools_list:
|
|
187
|
+
if (
|
|
188
|
+
isinstance(tool_def, dict)
|
|
189
|
+
and "name" in tool_def
|
|
190
|
+
and "register_func" in tool_def
|
|
191
|
+
):
|
|
192
|
+
tool_name = tool_def["name"]
|
|
193
|
+
register_func = tool_def["register_func"]
|
|
194
|
+
if callable(register_func):
|
|
195
|
+
TOOL_REGISTRY[tool_name] = register_func
|
|
196
|
+
except Exception:
|
|
197
|
+
# Don't let plugin failures break core functionality
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
172
201
|
def register_tools_for_agent(agent, tool_names: list[str]):
|
|
173
202
|
"""Register specific tools for an agent based on tool names.
|
|
174
203
|
|
|
@@ -178,6 +207,7 @@ def register_tools_for_agent(agent, tool_names: list[str]):
|
|
|
178
207
|
"""
|
|
179
208
|
from code_puppy.config import get_universal_constructor_enabled
|
|
180
209
|
|
|
210
|
+
_load_plugin_tools()
|
|
181
211
|
for tool_name in tool_names:
|
|
182
212
|
# Handle UC tools (prefixed with "uc:")
|
|
183
213
|
if tool_name.startswith("uc:"):
|
|
@@ -337,4 +367,5 @@ def get_available_tool_names() -> list[str]:
|
|
|
337
367
|
Returns:
|
|
338
368
|
List of all tool names that can be registered.
|
|
339
369
|
"""
|
|
370
|
+
_load_plugin_tools()
|
|
340
371
|
return list(TOOL_REGISTRY.keys())
|
code_puppy/tools/agent_tools.py
CHANGED
|
@@ -469,7 +469,7 @@ def register_invoke_agent(agent):
|
|
|
469
469
|
model = ModelFactory.get_model(model_name, models_config)
|
|
470
470
|
|
|
471
471
|
# Create a temporary agent instance to avoid interfering with current agent state
|
|
472
|
-
instructions = agent_config.
|
|
472
|
+
instructions = agent_config.get_full_system_prompt()
|
|
473
473
|
|
|
474
474
|
# Add AGENTS.md content to subagents
|
|
475
475
|
puppy_rules = agent_config.load_puppy_rules()
|
|
@@ -647,7 +647,7 @@ def run_shell_command_streaming(
|
|
|
647
647
|
line = process.stdout.readline()
|
|
648
648
|
if not line: # EOF
|
|
649
649
|
break
|
|
650
|
-
line = line.rstrip("\n
|
|
650
|
+
line = line.rstrip("\n")
|
|
651
651
|
line = _truncate_line(line)
|
|
652
652
|
stdout_lines.append(line)
|
|
653
653
|
if not silent:
|
|
@@ -660,7 +660,7 @@ def run_shell_command_streaming(
|
|
|
660
660
|
try:
|
|
661
661
|
remaining = process.stdout.read()
|
|
662
662
|
if remaining:
|
|
663
|
-
for line in remaining.
|
|
663
|
+
for line in remaining.split("\n"):
|
|
664
664
|
line = _truncate_line(line)
|
|
665
665
|
stdout_lines.append(line)
|
|
666
666
|
if not silent:
|
|
@@ -683,7 +683,7 @@ def run_shell_command_streaming(
|
|
|
683
683
|
line = process.stdout.readline()
|
|
684
684
|
if not line: # EOF
|
|
685
685
|
break
|
|
686
|
-
line = line.rstrip("\n
|
|
686
|
+
line = line.rstrip("\n")
|
|
687
687
|
line = _truncate_line(line)
|
|
688
688
|
stdout_lines.append(line)
|
|
689
689
|
if not silent:
|
|
@@ -716,7 +716,7 @@ def run_shell_command_streaming(
|
|
|
716
716
|
line = process.stderr.readline()
|
|
717
717
|
if not line: # EOF
|
|
718
718
|
break
|
|
719
|
-
line = line.rstrip("\n
|
|
719
|
+
line = line.rstrip("\n")
|
|
720
720
|
line = _truncate_line(line)
|
|
721
721
|
stderr_lines.append(line)
|
|
722
722
|
if not silent:
|
|
@@ -729,7 +729,7 @@ def run_shell_command_streaming(
|
|
|
729
729
|
try:
|
|
730
730
|
remaining = process.stderr.read()
|
|
731
731
|
if remaining:
|
|
732
|
-
for line in remaining.
|
|
732
|
+
for line in remaining.split("\n"):
|
|
733
733
|
line = _truncate_line(line)
|
|
734
734
|
stderr_lines.append(line)
|
|
735
735
|
if not silent:
|
|
@@ -751,7 +751,7 @@ def run_shell_command_streaming(
|
|
|
751
751
|
line = process.stderr.readline()
|
|
752
752
|
if not line: # EOF
|
|
753
753
|
break
|
|
754
|
-
line = line.rstrip("\n
|
|
754
|
+
line = line.rstrip("\n")
|
|
755
755
|
line = _truncate_line(line)
|
|
756
756
|
stderr_lines.append(line)
|
|
757
757
|
if not silent:
|
|
@@ -1165,6 +1165,11 @@ async def _execute_shell_command(
|
|
|
1165
1165
|
)
|
|
1166
1166
|
)
|
|
1167
1167
|
|
|
1168
|
+
# Pause spinner during shell command so \r output can work properly
|
|
1169
|
+
from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
|
|
1170
|
+
|
|
1171
|
+
pause_all_spinners()
|
|
1172
|
+
|
|
1168
1173
|
# Acquire shared keyboard context - Ctrl-X/Ctrl-C will kill ALL running commands
|
|
1169
1174
|
# This is reference-counted: listener starts on first command, stops on last
|
|
1170
1175
|
_acquire_keyboard_context()
|
|
@@ -1172,6 +1177,7 @@ async def _execute_shell_command(
|
|
|
1172
1177
|
return await _run_command_inner(command, cwd, timeout, group_id, silent=silent)
|
|
1173
1178
|
finally:
|
|
1174
1179
|
_release_keyboard_context()
|
|
1180
|
+
resume_all_spinners()
|
|
1175
1181
|
|
|
1176
1182
|
|
|
1177
1183
|
def _run_command_sync(
|
|
@@ -1192,18 +1198,26 @@ def _run_command_sync(
|
|
|
1192
1198
|
else:
|
|
1193
1199
|
preexec_fn = os.setsid if hasattr(os, "setsid") else None
|
|
1194
1200
|
|
|
1201
|
+
import io
|
|
1202
|
+
|
|
1195
1203
|
process = subprocess.Popen(
|
|
1196
1204
|
command,
|
|
1197
1205
|
shell=True,
|
|
1198
1206
|
stdout=subprocess.PIPE,
|
|
1199
1207
|
stderr=subprocess.PIPE,
|
|
1200
|
-
text=True,
|
|
1201
1208
|
cwd=cwd,
|
|
1202
|
-
bufsize=
|
|
1203
|
-
universal_newlines=True,
|
|
1209
|
+
bufsize=0, # Unbuffered for real-time output
|
|
1204
1210
|
preexec_fn=preexec_fn,
|
|
1205
1211
|
creationflags=creationflags,
|
|
1206
1212
|
)
|
|
1213
|
+
|
|
1214
|
+
# Wrap pipes with TextIOWrapper that preserves \r (newline='' disables translation)
|
|
1215
|
+
process.stdout = io.TextIOWrapper(
|
|
1216
|
+
process.stdout, newline="", encoding="utf-8", errors="replace"
|
|
1217
|
+
)
|
|
1218
|
+
process.stderr = io.TextIOWrapper(
|
|
1219
|
+
process.stderr, newline="", encoding="utf-8", errors="replace"
|
|
1220
|
+
)
|
|
1207
1221
|
_register_process(process)
|
|
1208
1222
|
try:
|
|
1209
1223
|
return run_shell_command_streaming(
|