code-puppy 0.0.375__py3-none-any.whl → 0.0.376__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/base_agent.py +88 -64
- code_puppy/callbacks.py +57 -9
- code_puppy/plugins/claude_code_oauth/__init__.py +19 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +72 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +242 -0
- code_puppy/plugins/ralph/loop_controller.py +52 -48
- code_puppy/plugins/ralph/register_callbacks.py +13 -6
- {code_puppy-0.0.375.dist-info → code_puppy-0.0.376.dist-info}/METADATA +1 -1
- {code_puppy-0.0.375.dist-info → code_puppy-0.0.376.dist-info}/RECORD +14 -13
- {code_puppy-0.0.375.data → code_puppy-0.0.376.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.375.data → code_puppy-0.0.376.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.375.dist-info → code_puppy-0.0.376.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.375.dist-info → code_puppy-0.0.376.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.375.dist-info → code_puppy-0.0.376.dist-info}/licenses/LICENSE +0 -0
code_puppy/agents/base_agent.py
CHANGED
|
@@ -47,7 +47,10 @@ from pydantic_ai.messages import (
|
|
|
47
47
|
from rich.text import Text
|
|
48
48
|
|
|
49
49
|
from code_puppy.agents.event_stream_handler import event_stream_handler
|
|
50
|
-
from code_puppy.callbacks import
|
|
50
|
+
from code_puppy.callbacks import (
|
|
51
|
+
on_agent_run_end,
|
|
52
|
+
on_agent_run_start,
|
|
53
|
+
)
|
|
51
54
|
|
|
52
55
|
# Consolidated relative imports
|
|
53
56
|
from code_puppy.config import (
|
|
@@ -404,35 +407,27 @@ class BaseAgent(ABC):
|
|
|
404
407
|
total_tokens = 0
|
|
405
408
|
|
|
406
409
|
# 1. Estimate tokens for system prompt / instructions
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
# fixed instructions. For other models,
|
|
410
|
+
# Use prepare_prompt_for_model() to get the correct instructions for token counting.
|
|
411
|
+
# For models that prepend system prompt to user message (claude-code, antigravity),
|
|
412
|
+
# this returns the short fixed instructions. For other models, returns full prompt.
|
|
410
413
|
try:
|
|
411
|
-
from code_puppy.model_utils import
|
|
412
|
-
get_antigravity_instructions,
|
|
413
|
-
get_claude_code_instructions,
|
|
414
|
-
is_antigravity_model,
|
|
415
|
-
is_claude_code_model,
|
|
416
|
-
)
|
|
414
|
+
from code_puppy.model_utils import prepare_prompt_for_model
|
|
417
415
|
|
|
418
416
|
model_name = (
|
|
419
417
|
self.get_model_name() if hasattr(self, "get_model_name") else ""
|
|
420
418
|
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
system_prompt = self.get_full_system_prompt()
|
|
434
|
-
if system_prompt:
|
|
435
|
-
total_tokens += self.estimate_token_count(system_prompt)
|
|
419
|
+
system_prompt = self.get_full_system_prompt()
|
|
420
|
+
|
|
421
|
+
# Get the instructions that will be used (handles model-specific logic via hooks)
|
|
422
|
+
prepared = prepare_prompt_for_model(
|
|
423
|
+
model_name=model_name,
|
|
424
|
+
system_prompt=system_prompt,
|
|
425
|
+
user_prompt="", # Empty - we just need the instructions
|
|
426
|
+
prepend_system_to_user=False, # Don't modify prompt, just get instructions
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if prepared.instructions:
|
|
430
|
+
total_tokens += self.estimate_token_count(prepared.instructions)
|
|
436
431
|
except Exception:
|
|
437
432
|
pass # If we can't get system prompt, skip it
|
|
438
433
|
|
|
@@ -1590,21 +1585,25 @@ class BaseAgent(ABC):
|
|
|
1590
1585
|
if output_type is not None:
|
|
1591
1586
|
pydantic_agent = self._create_agent_with_output_type(output_type)
|
|
1592
1587
|
|
|
1593
|
-
# Handle
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
is_claude_code_model,
|
|
1597
|
-
)
|
|
1588
|
+
# Handle model-specific prompt transformations via prepare_prompt_for_model()
|
|
1589
|
+
# This uses the get_model_system_prompt hook, so plugins can register their own handlers
|
|
1590
|
+
from code_puppy.model_utils import prepare_prompt_for_model
|
|
1598
1591
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1592
|
+
# Only prepend system prompt on first message (empty history)
|
|
1593
|
+
should_prepend = len(self.get_message_history()) == 0
|
|
1594
|
+
if should_prepend:
|
|
1595
|
+
system_prompt = self.get_full_system_prompt()
|
|
1596
|
+
puppy_rules = self.load_puppy_rules()
|
|
1597
|
+
if puppy_rules:
|
|
1598
|
+
system_prompt += f"\n{puppy_rules}"
|
|
1599
|
+
|
|
1600
|
+
prepared = prepare_prompt_for_model(
|
|
1601
|
+
model_name=self.get_model_name(),
|
|
1602
|
+
system_prompt=system_prompt,
|
|
1603
|
+
user_prompt=prompt,
|
|
1604
|
+
prepend_system_to_user=True,
|
|
1605
|
+
)
|
|
1606
|
+
prompt = prepared.user_prompt
|
|
1608
1607
|
|
|
1609
1608
|
# Build combined prompt payload when attachments are provided.
|
|
1610
1609
|
attachment_parts: List[Any] = []
|
|
@@ -1751,6 +1750,17 @@ class BaseAgent(ABC):
|
|
|
1751
1750
|
# Create the task FIRST
|
|
1752
1751
|
agent_task = asyncio.create_task(run_agent_task())
|
|
1753
1752
|
|
|
1753
|
+
# Fire agent_run_start hook - plugins can use this to start background tasks
|
|
1754
|
+
# (e.g., token refresh heartbeats for OAuth models)
|
|
1755
|
+
try:
|
|
1756
|
+
await on_agent_run_start(
|
|
1757
|
+
agent_name=self.name,
|
|
1758
|
+
model_name=self.get_model_name(),
|
|
1759
|
+
session_id=group_id,
|
|
1760
|
+
)
|
|
1761
|
+
except Exception:
|
|
1762
|
+
pass # Don't fail agent run if hook fails
|
|
1763
|
+
|
|
1754
1764
|
# Import shell process status helper
|
|
1755
1765
|
|
|
1756
1766
|
loop = asyncio.get_running_loop()
|
|
@@ -1832,39 +1842,53 @@ class BaseAgent(ABC):
|
|
|
1832
1842
|
except Exception:
|
|
1833
1843
|
pass # Don't fail the run if cache update fails
|
|
1834
1844
|
|
|
1835
|
-
#
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
else:
|
|
1845
|
-
response_text = str(result)
|
|
1846
|
-
|
|
1847
|
-
# Fire the callback - don't await to avoid blocking return
|
|
1848
|
-
# Use asyncio.create_task to run it in background
|
|
1849
|
-
asyncio.create_task(
|
|
1850
|
-
on_agent_response_complete(
|
|
1851
|
-
agent_name=self.name,
|
|
1852
|
-
response_text=response_text,
|
|
1853
|
-
session_id=group_id,
|
|
1854
|
-
metadata={"model": self.get_model_name()},
|
|
1855
|
-
)
|
|
1856
|
-
)
|
|
1857
|
-
except Exception:
|
|
1858
|
-
pass # Don't fail the run if callback fails
|
|
1845
|
+
# Extract response text for the callback
|
|
1846
|
+
_run_response_text = ""
|
|
1847
|
+
if result is not None:
|
|
1848
|
+
if hasattr(result, "data"):
|
|
1849
|
+
_run_response_text = str(result.data) if result.data else ""
|
|
1850
|
+
elif hasattr(result, "output"):
|
|
1851
|
+
_run_response_text = str(result.output) if result.output else ""
|
|
1852
|
+
else:
|
|
1853
|
+
_run_response_text = str(result)
|
|
1859
1854
|
|
|
1855
|
+
_run_success = True
|
|
1856
|
+
_run_error = None
|
|
1860
1857
|
return result
|
|
1861
1858
|
except asyncio.CancelledError:
|
|
1859
|
+
_run_success = False
|
|
1860
|
+
_run_error = None # Cancellation is not an error
|
|
1861
|
+
_run_response_text = ""
|
|
1862
1862
|
agent_task.cancel()
|
|
1863
1863
|
except KeyboardInterrupt:
|
|
1864
|
-
|
|
1864
|
+
_run_success = False
|
|
1865
|
+
_run_error = None # User interrupt is not an error
|
|
1866
|
+
_run_response_text = ""
|
|
1865
1867
|
if not agent_task.done():
|
|
1866
1868
|
agent_task.cancel()
|
|
1869
|
+
except Exception as e:
|
|
1870
|
+
_run_success = False
|
|
1871
|
+
_run_error = e
|
|
1872
|
+
_run_response_text = ""
|
|
1873
|
+
raise
|
|
1867
1874
|
finally:
|
|
1875
|
+
# Fire agent_run_end hook - plugins can use this for:
|
|
1876
|
+
# - Stopping background tasks (token refresh heartbeats)
|
|
1877
|
+
# - Workflow orchestration (Ralph's autonomous loop)
|
|
1878
|
+
# - Logging/analytics
|
|
1879
|
+
try:
|
|
1880
|
+
await on_agent_run_end(
|
|
1881
|
+
agent_name=self.name,
|
|
1882
|
+
model_name=self.get_model_name(),
|
|
1883
|
+
session_id=group_id,
|
|
1884
|
+
success=_run_success,
|
|
1885
|
+
error=_run_error,
|
|
1886
|
+
response_text=_run_response_text,
|
|
1887
|
+
metadata={"model": self.get_model_name()},
|
|
1888
|
+
)
|
|
1889
|
+
except Exception:
|
|
1890
|
+
pass # Don't fail cleanup if hook fails
|
|
1891
|
+
|
|
1868
1892
|
# Stop keyboard listener if it was started
|
|
1869
1893
|
if key_listener_stop_event is not None:
|
|
1870
1894
|
key_listener_stop_event.set()
|
code_puppy/callbacks.py
CHANGED
|
@@ -25,7 +25,8 @@ PhaseType = Literal[
|
|
|
25
25
|
"register_agents",
|
|
26
26
|
"register_model_type",
|
|
27
27
|
"get_model_system_prompt",
|
|
28
|
-
"
|
|
28
|
+
"agent_run_start",
|
|
29
|
+
"agent_run_end",
|
|
29
30
|
]
|
|
30
31
|
CallbackFunc = Callable[..., Any]
|
|
31
32
|
|
|
@@ -51,7 +52,8 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
|
|
|
51
52
|
"register_agents": [],
|
|
52
53
|
"register_model_type": [],
|
|
53
54
|
"get_model_system_prompt": [],
|
|
54
|
-
"
|
|
55
|
+
"agent_run_start": [],
|
|
56
|
+
"agent_run_end": [],
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
logger = logging.getLogger(__name__)
|
|
@@ -446,26 +448,72 @@ def on_get_model_system_prompt(
|
|
|
446
448
|
)
|
|
447
449
|
|
|
448
450
|
|
|
449
|
-
async def
|
|
451
|
+
async def on_agent_run_start(
|
|
450
452
|
agent_name: str,
|
|
451
|
-
|
|
453
|
+
model_name: str,
|
|
452
454
|
session_id: str | None = None,
|
|
455
|
+
) -> List[Any]:
|
|
456
|
+
"""Trigger callbacks when an agent run starts.
|
|
457
|
+
|
|
458
|
+
This fires at the beginning of run_with_mcp, before the agent task is created.
|
|
459
|
+
Useful for:
|
|
460
|
+
- Starting background tasks (like token refresh heartbeats)
|
|
461
|
+
- Logging/analytics
|
|
462
|
+
- Resource allocation
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
agent_name: Name of the agent starting
|
|
466
|
+
model_name: Name of the model being used
|
|
467
|
+
session_id: Optional session identifier
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
List of results from registered callbacks.
|
|
471
|
+
"""
|
|
472
|
+
return await _trigger_callbacks(
|
|
473
|
+
"agent_run_start", agent_name, model_name, session_id
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def on_agent_run_end(
|
|
478
|
+
agent_name: str,
|
|
479
|
+
model_name: str,
|
|
480
|
+
session_id: str | None = None,
|
|
481
|
+
success: bool = True,
|
|
482
|
+
error: Exception | None = None,
|
|
483
|
+
response_text: str | None = None,
|
|
453
484
|
metadata: dict | None = None,
|
|
454
485
|
) -> List[Any]:
|
|
455
|
-
"""Trigger callbacks
|
|
486
|
+
"""Trigger callbacks when an agent run ends.
|
|
487
|
+
|
|
488
|
+
This fires at the end of run_with_mcp, in the finally block.
|
|
489
|
+
Always fires regardless of success/failure/cancellation.
|
|
456
490
|
|
|
457
|
-
This fires after all tool calls are resolved and the agent has finished.
|
|
458
491
|
Useful for:
|
|
492
|
+
- Stopping background tasks (like token refresh heartbeats)
|
|
459
493
|
- Workflow orchestration (like Ralph's autonomous loop)
|
|
460
494
|
- Logging/analytics
|
|
495
|
+
- Resource cleanup
|
|
461
496
|
- Detecting completion signals in responses
|
|
462
497
|
|
|
463
498
|
Args:
|
|
464
|
-
agent_name: Name of the agent that
|
|
465
|
-
|
|
499
|
+
agent_name: Name of the agent that finished
|
|
500
|
+
model_name: Name of the model that was used
|
|
466
501
|
session_id: Optional session identifier
|
|
502
|
+
success: Whether the run completed successfully
|
|
503
|
+
error: Exception if the run failed, None otherwise
|
|
504
|
+
response_text: The final text response from the agent (if successful)
|
|
467
505
|
metadata: Optional dict with additional context (tokens used, etc.)
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
List of results from registered callbacks.
|
|
468
509
|
"""
|
|
469
510
|
return await _trigger_callbacks(
|
|
470
|
-
"
|
|
511
|
+
"agent_run_end",
|
|
512
|
+
agent_name,
|
|
513
|
+
model_name,
|
|
514
|
+
session_id,
|
|
515
|
+
success,
|
|
516
|
+
error,
|
|
517
|
+
response_text,
|
|
518
|
+
metadata,
|
|
471
519
|
)
|
|
@@ -3,4 +3,23 @@ Claude Code OAuth Plugin for Code Puppy
|
|
|
3
3
|
|
|
4
4
|
This plugin provides OAuth authentication for Claude Code and automatically
|
|
5
5
|
adds available models to the extra_models.json configuration.
|
|
6
|
+
|
|
7
|
+
The plugin also includes a token refresh heartbeat for maintaining fresh
|
|
8
|
+
tokens during long-running agentic operations.
|
|
6
9
|
"""
|
|
10
|
+
|
|
11
|
+
from .token_refresh_heartbeat import (
|
|
12
|
+
TokenRefreshHeartbeat,
|
|
13
|
+
force_token_refresh,
|
|
14
|
+
get_current_heartbeat,
|
|
15
|
+
is_heartbeat_running,
|
|
16
|
+
token_refresh_heartbeat_context,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"TokenRefreshHeartbeat",
|
|
21
|
+
"token_refresh_heartbeat_context",
|
|
22
|
+
"is_heartbeat_running",
|
|
23
|
+
"get_current_heartbeat",
|
|
24
|
+
"force_token_refresh",
|
|
25
|
+
]
|
|
@@ -363,6 +363,78 @@ def _register_model_types() -> List[Dict[str, Any]]:
|
|
|
363
363
|
return [{"type": "claude_code", "handler": _create_claude_code_model}]
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
# Global storage for the token refresh heartbeat
|
|
367
|
+
# Using a dict to allow multiple concurrent agent runs (keyed by session_id)
|
|
368
|
+
_active_heartbeats: Dict[str, Any] = {}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def _on_agent_run_start(
|
|
372
|
+
agent_name: str,
|
|
373
|
+
model_name: str,
|
|
374
|
+
session_id: Optional[str] = None,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Start token refresh heartbeat for Claude Code OAuth models.
|
|
377
|
+
|
|
378
|
+
This callback is triggered when an agent run starts. If the model is a
|
|
379
|
+
Claude Code OAuth model, we start a background heartbeat to keep the
|
|
380
|
+
token fresh during long-running operations.
|
|
381
|
+
"""
|
|
382
|
+
# Only start heartbeat for Claude Code models
|
|
383
|
+
if not model_name.startswith("claude-code"):
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
from .token_refresh_heartbeat import TokenRefreshHeartbeat
|
|
388
|
+
|
|
389
|
+
heartbeat = TokenRefreshHeartbeat()
|
|
390
|
+
await heartbeat.start()
|
|
391
|
+
|
|
392
|
+
# Store heartbeat for cleanup, keyed by session_id
|
|
393
|
+
key = session_id or "default"
|
|
394
|
+
_active_heartbeats[key] = heartbeat
|
|
395
|
+
logger.debug(
|
|
396
|
+
"Started token refresh heartbeat for session %s (model: %s)",
|
|
397
|
+
key,
|
|
398
|
+
model_name,
|
|
399
|
+
)
|
|
400
|
+
except ImportError:
|
|
401
|
+
logger.debug("Token refresh heartbeat module not available")
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
logger.debug("Failed to start token refresh heartbeat: %s", exc)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def _on_agent_run_end(
|
|
407
|
+
agent_name: str,
|
|
408
|
+
model_name: str,
|
|
409
|
+
session_id: Optional[str] = None,
|
|
410
|
+
success: bool = True,
|
|
411
|
+
error: Optional[Exception] = None,
|
|
412
|
+
response_text: Optional[str] = None,
|
|
413
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Stop token refresh heartbeat when agent run ends.
|
|
416
|
+
|
|
417
|
+
This callback is triggered when an agent run completes (success or failure).
|
|
418
|
+
We stop any heartbeat that was started for this session.
|
|
419
|
+
"""
|
|
420
|
+
# We don't use response_text or metadata, just cleanup the heartbeat
|
|
421
|
+
key = session_id or "default"
|
|
422
|
+
heartbeat = _active_heartbeats.pop(key, None)
|
|
423
|
+
|
|
424
|
+
if heartbeat is not None:
|
|
425
|
+
try:
|
|
426
|
+
await heartbeat.stop()
|
|
427
|
+
logger.debug(
|
|
428
|
+
"Stopped token refresh heartbeat for session %s (refreshed %d times)",
|
|
429
|
+
key,
|
|
430
|
+
heartbeat.refresh_count,
|
|
431
|
+
)
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
logger.debug("Error stopping token refresh heartbeat: %s", exc)
|
|
434
|
+
|
|
435
|
+
|
|
366
436
|
register_callback("custom_command_help", _custom_help)
|
|
367
437
|
register_callback("custom_command", _handle_custom_command)
|
|
368
438
|
register_callback("register_model_type", _register_model_types)
|
|
439
|
+
register_callback("agent_run_start", _on_agent_run_start)
|
|
440
|
+
register_callback("agent_run_end", _on_agent_run_end)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Token refresh heartbeat for long-running Claude Code OAuth sessions.
|
|
2
|
+
|
|
3
|
+
This module provides a background task that periodically checks and refreshes
|
|
4
|
+
Claude Code OAuth tokens during long-running agentic operations. This ensures
|
|
5
|
+
that tokens don't expire during extended streaming responses or tool processing.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
async with token_refresh_heartbeat_context():
|
|
9
|
+
# Long running agent operation
|
|
10
|
+
await agent.run(...)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Heartbeat interval in seconds - check token every 2 minutes
|
|
24
|
+
# This is frequent enough to catch expiring tokens before they cause issues
|
|
25
|
+
# but not so frequent as to spam the token endpoint
|
|
26
|
+
HEARTBEAT_INTERVAL_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
# Minimum time between refresh attempts to avoid hammering the endpoint
|
|
29
|
+
MIN_REFRESH_INTERVAL_SECONDS = 60
|
|
30
|
+
|
|
31
|
+
# Global tracking of last refresh time to coordinate across heartbeats
|
|
32
|
+
_last_refresh_time: float = 0.0
|
|
33
|
+
_heartbeat_lock = asyncio.Lock()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TokenRefreshHeartbeat:
|
|
37
|
+
"""Background task that periodically refreshes Claude Code OAuth tokens.
|
|
38
|
+
|
|
39
|
+
This runs as an asyncio task during agent operations and checks if the
|
|
40
|
+
token needs refreshing at regular intervals.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
46
|
+
min_refresh_interval: float = MIN_REFRESH_INTERVAL_SECONDS,
|
|
47
|
+
):
|
|
48
|
+
self._interval = interval
|
|
49
|
+
self._min_refresh_interval = min_refresh_interval
|
|
50
|
+
self._task: Optional[asyncio.Task] = None
|
|
51
|
+
self._stop_event = asyncio.Event()
|
|
52
|
+
self._refresh_count = 0
|
|
53
|
+
|
|
54
|
+
async def start(self) -> None:
|
|
55
|
+
"""Start the heartbeat background task."""
|
|
56
|
+
if self._task is not None:
|
|
57
|
+
logger.debug("Heartbeat already running")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self._stop_event.clear()
|
|
61
|
+
self._task = asyncio.create_task(self._heartbeat_loop())
|
|
62
|
+
logger.debug("Token refresh heartbeat started")
|
|
63
|
+
|
|
64
|
+
async def stop(self) -> None:
|
|
65
|
+
"""Stop the heartbeat background task."""
|
|
66
|
+
if self._task is None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self._stop_event.set()
|
|
70
|
+
self._task.cancel()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
await self._task
|
|
74
|
+
except asyncio.CancelledError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
self._task = None
|
|
78
|
+
logger.debug(
|
|
79
|
+
"Token refresh heartbeat stopped (refreshed %d times)",
|
|
80
|
+
self._refresh_count,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def _heartbeat_loop(self) -> None:
|
|
84
|
+
"""Main heartbeat loop that periodically checks token status."""
|
|
85
|
+
global _last_refresh_time
|
|
86
|
+
|
|
87
|
+
while not self._stop_event.is_set():
|
|
88
|
+
try:
|
|
89
|
+
# Wait for the interval or until stopped
|
|
90
|
+
try:
|
|
91
|
+
await asyncio.wait_for(
|
|
92
|
+
self._stop_event.wait(), timeout=self._interval
|
|
93
|
+
)
|
|
94
|
+
# If we got here, stop event was set
|
|
95
|
+
break
|
|
96
|
+
except asyncio.TimeoutError:
|
|
97
|
+
# Normal timeout - time to check token
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Check if we should attempt refresh
|
|
101
|
+
async with _heartbeat_lock:
|
|
102
|
+
now = time.time()
|
|
103
|
+
if now - _last_refresh_time < self._min_refresh_interval:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Skipping refresh - last refresh was %.1f seconds ago",
|
|
106
|
+
now - _last_refresh_time,
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Attempt the refresh
|
|
111
|
+
refreshed = await self._attempt_refresh()
|
|
112
|
+
if refreshed:
|
|
113
|
+
_last_refresh_time = now
|
|
114
|
+
self._refresh_count += 1
|
|
115
|
+
|
|
116
|
+
except asyncio.CancelledError:
|
|
117
|
+
break
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.debug("Error in heartbeat loop: %s", exc)
|
|
120
|
+
# Continue running - don't let errors kill the heartbeat
|
|
121
|
+
await asyncio.sleep(5) # Brief pause before retrying
|
|
122
|
+
|
|
123
|
+
async def _attempt_refresh(self) -> bool:
|
|
124
|
+
"""Attempt to refresh the token if needed.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if a refresh was performed, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# Import here to avoid circular imports
|
|
131
|
+
from .utils import (
|
|
132
|
+
is_token_expired,
|
|
133
|
+
load_stored_tokens,
|
|
134
|
+
refresh_access_token,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
tokens = load_stored_tokens()
|
|
138
|
+
if not tokens:
|
|
139
|
+
logger.debug("No stored tokens found")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if not is_token_expired(tokens):
|
|
143
|
+
logger.debug("Token not yet expired, skipping refresh")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Token is expiring soon, refresh it
|
|
147
|
+
logger.info("Heartbeat: Token expiring soon, refreshing proactively")
|
|
148
|
+
refreshed_token = refresh_access_token(force=False)
|
|
149
|
+
|
|
150
|
+
if refreshed_token:
|
|
151
|
+
logger.info("Heartbeat: Successfully refreshed token")
|
|
152
|
+
return True
|
|
153
|
+
else:
|
|
154
|
+
logger.warning("Heartbeat: Token refresh returned None")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.error("Heartbeat: Error during token refresh: %s", exc)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def refresh_count(self) -> int:
|
|
163
|
+
"""Get the number of successful refreshes performed by this heartbeat."""
|
|
164
|
+
return self._refresh_count
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def is_running(self) -> bool:
|
|
168
|
+
"""Check if the heartbeat is currently running."""
|
|
169
|
+
return self._task is not None and not self._task.done()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Global heartbeat instance for the current session
|
|
173
|
+
_current_heartbeat: Optional[TokenRefreshHeartbeat] = None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def token_refresh_heartbeat_context(
|
|
178
|
+
interval: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
179
|
+
):
|
|
180
|
+
"""Context manager that runs token refresh heartbeat during its scope.
|
|
181
|
+
|
|
182
|
+
Use this around long-running agent operations to ensure tokens stay fresh.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
interval: Seconds between heartbeat checks. Default is 2 minutes.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
async with token_refresh_heartbeat_context():
|
|
189
|
+
result = await agent.run(prompt)
|
|
190
|
+
"""
|
|
191
|
+
global _current_heartbeat
|
|
192
|
+
|
|
193
|
+
heartbeat = TokenRefreshHeartbeat(interval=interval)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
await heartbeat.start()
|
|
197
|
+
_current_heartbeat = heartbeat
|
|
198
|
+
yield heartbeat
|
|
199
|
+
finally:
|
|
200
|
+
await heartbeat.stop()
|
|
201
|
+
_current_heartbeat = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_heartbeat_running() -> bool:
|
|
205
|
+
"""Check if a token refresh heartbeat is currently active."""
|
|
206
|
+
return _current_heartbeat is not None and _current_heartbeat.is_running
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_current_heartbeat() -> Optional[TokenRefreshHeartbeat]:
|
|
210
|
+
"""Get the currently running heartbeat instance, if any."""
|
|
211
|
+
return _current_heartbeat
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def force_token_refresh() -> bool:
|
|
215
|
+
"""Force an immediate token refresh.
|
|
216
|
+
|
|
217
|
+
This can be called from anywhere to trigger a token refresh,
|
|
218
|
+
regardless of whether a heartbeat is running.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if refresh was successful, False otherwise.
|
|
222
|
+
"""
|
|
223
|
+
global _last_refresh_time
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from .utils import refresh_access_token
|
|
227
|
+
|
|
228
|
+
logger.info("Forcing token refresh")
|
|
229
|
+
refreshed_token = refresh_access_token(force=True)
|
|
230
|
+
|
|
231
|
+
if refreshed_token:
|
|
232
|
+
async with _heartbeat_lock:
|
|
233
|
+
_last_refresh_time = time.time()
|
|
234
|
+
logger.info("Force refresh successful")
|
|
235
|
+
return True
|
|
236
|
+
else:
|
|
237
|
+
logger.warning("Force refresh returned None")
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
logger.error("Force refresh error: %s", exc)
|
|
242
|
+
return False
|
|
@@ -17,46 +17,46 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
class RalphLoopController:
|
|
19
19
|
"""Controls the Ralph autonomous loop.
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
Each iteration:
|
|
22
22
|
1. Checks if there's work to do
|
|
23
23
|
2. Invokes the ralph-orchestrator agent with a FRESH session
|
|
24
24
|
3. Waits for completion
|
|
25
25
|
4. Checks if all stories are done or if we should continue
|
|
26
26
|
"""
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
def __init__(self, max_iterations: int = 10):
|
|
29
29
|
self.max_iterations = max_iterations
|
|
30
30
|
self.current_iteration = 0
|
|
31
31
|
self.is_complete = False
|
|
32
32
|
self.is_running = False
|
|
33
33
|
self._stop_requested = False
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
def request_stop(self) -> None:
|
|
36
36
|
"""Request the loop to stop after current iteration."""
|
|
37
37
|
self._stop_requested = True
|
|
38
38
|
emit_warning("🛑 Stop requested - will halt after current iteration")
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
async def run(
|
|
41
41
|
self,
|
|
42
42
|
invoke_func: Callable[[str, str, Optional[str]], Awaitable[dict]],
|
|
43
43
|
) -> dict:
|
|
44
44
|
"""Run the Ralph loop until completion or max iterations.
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
Args:
|
|
47
|
-
invoke_func: Async function to invoke an agent.
|
|
47
|
+
invoke_func: Async function to invoke an agent.
|
|
48
48
|
Signature: (agent_name, prompt, session_id) -> result_dict
|
|
49
49
|
The result_dict should have 'response' and 'error' keys.
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
Returns:
|
|
52
52
|
dict with 'success', 'iterations', 'message' keys
|
|
53
53
|
"""
|
|
54
54
|
self.is_running = True
|
|
55
55
|
self.is_complete = False
|
|
56
56
|
self._stop_requested = False
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
manager = get_state_manager()
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
# Pre-flight checks
|
|
61
61
|
if not manager.prd_exists():
|
|
62
62
|
self.is_running = False
|
|
@@ -65,7 +65,7 @@ class RalphLoopController:
|
|
|
65
65
|
"iterations": 0,
|
|
66
66
|
"message": "No prd.json found. Create one with /ralph prd first.",
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
if manager.all_stories_complete():
|
|
70
70
|
self.is_running = False
|
|
71
71
|
return {
|
|
@@ -73,68 +73,68 @@ class RalphLoopController:
|
|
|
73
73
|
"iterations": 0,
|
|
74
74
|
"message": "All stories already complete!",
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
prd = manager.read_prd()
|
|
78
|
-
emit_info(
|
|
78
|
+
emit_info("🐺 Starting Ralph Loop")
|
|
79
79
|
emit_info(f"📋 Project: {prd.project if prd else 'Unknown'}")
|
|
80
80
|
emit_info(f"📊 Progress: {prd.get_progress_summary() if prd else 'Unknown'}")
|
|
81
81
|
emit_info(f"🔄 Max iterations: {self.max_iterations}")
|
|
82
82
|
emit_info("─" * 50)
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
try:
|
|
85
85
|
for iteration in range(1, self.max_iterations + 1):
|
|
86
86
|
self.current_iteration = iteration
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
# Check for stop request
|
|
89
89
|
if self._stop_requested:
|
|
90
90
|
emit_warning(f"🛑 Stopped at iteration {iteration}")
|
|
91
91
|
break
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
# Check if already complete
|
|
94
94
|
if manager.all_stories_complete():
|
|
95
95
|
self.is_complete = True
|
|
96
96
|
emit_success("🎉 All stories complete!")
|
|
97
97
|
break
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
# Get current story for logging
|
|
100
100
|
story = manager.get_next_story()
|
|
101
101
|
if story is None:
|
|
102
102
|
self.is_complete = True
|
|
103
103
|
emit_success("🎉 All stories complete!")
|
|
104
104
|
break
|
|
105
|
-
|
|
106
|
-
emit_info(f"\n{'='*60}")
|
|
105
|
+
|
|
106
|
+
emit_info(f"\n{'=' * 60}")
|
|
107
107
|
emit_info(f"🐺 RALPH ITERATION {iteration} of {self.max_iterations}")
|
|
108
108
|
emit_info(f"📌 Working on: [{story.id}] {story.title}")
|
|
109
|
-
emit_info(f"{'='*60}\n")
|
|
110
|
-
|
|
109
|
+
emit_info(f"{'=' * 60}\n")
|
|
110
|
+
|
|
111
111
|
# Build the prompt for this iteration
|
|
112
112
|
iteration_prompt = self._build_iteration_prompt(story)
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
# Invoke orchestrator with FRESH session (unique per iteration)
|
|
115
115
|
session_id = f"ralph-iter-{iteration}"
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
try:
|
|
118
118
|
result = await invoke_func(
|
|
119
119
|
"ralph-orchestrator",
|
|
120
120
|
iteration_prompt,
|
|
121
121
|
session_id,
|
|
122
122
|
)
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
response = result.get("response", "")
|
|
125
125
|
error = result.get("error")
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
if error:
|
|
128
128
|
emit_error(f"Iteration {iteration} error: {error}")
|
|
129
129
|
# Continue to next iteration despite error
|
|
130
130
|
continue
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
# Check for completion signal in response
|
|
133
133
|
if response and "<promise>COMPLETE</promise>" in response:
|
|
134
134
|
self.is_complete = True
|
|
135
135
|
emit_success("🎉 Ralph signaled COMPLETE - all stories done!")
|
|
136
136
|
break
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
except asyncio.CancelledError:
|
|
139
139
|
emit_warning(f"🛑 Iteration {iteration} cancelled")
|
|
140
140
|
break
|
|
@@ -143,44 +143,48 @@ class RalphLoopController:
|
|
|
143
143
|
logger.exception(f"Ralph iteration {iteration} failed")
|
|
144
144
|
# Continue to next iteration
|
|
145
145
|
continue
|
|
146
|
-
|
|
146
|
+
|
|
147
147
|
# Brief pause between iterations
|
|
148
148
|
await asyncio.sleep(1)
|
|
149
|
-
|
|
149
|
+
|
|
150
150
|
else:
|
|
151
151
|
# Loop completed without break (max iterations reached)
|
|
152
152
|
emit_warning(f"⚠️ Reached max iterations ({self.max_iterations})")
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
finally:
|
|
155
155
|
self.is_running = False
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
# Final status
|
|
158
158
|
prd = manager.read_prd()
|
|
159
159
|
final_progress = prd.get_progress_summary() if prd else "Unknown"
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
return {
|
|
162
162
|
"success": self.is_complete,
|
|
163
163
|
"iterations": self.current_iteration,
|
|
164
164
|
"message": f"Completed {self.current_iteration} iterations. {final_progress}",
|
|
165
165
|
"all_complete": self.is_complete,
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
def _build_iteration_prompt(self, story) -> str:
|
|
169
169
|
"""Build the prompt for a single iteration."""
|
|
170
170
|
# Find VERIFY criteria
|
|
171
|
-
verify_criteria = [
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
verify_criteria = [
|
|
172
|
+
c for c in story.acceptance_criteria if c.startswith("VERIFY:")
|
|
173
|
+
]
|
|
174
|
+
other_criteria = [
|
|
175
|
+
c for c in story.acceptance_criteria if not c.startswith("VERIFY:")
|
|
176
|
+
]
|
|
177
|
+
|
|
174
178
|
verify_section = ""
|
|
175
179
|
if verify_criteria:
|
|
176
180
|
verify_section = f"""
|
|
177
181
|
## MANDATORY VERIFICATION COMMANDS
|
|
178
182
|
You MUST run these commands and they MUST succeed before marking complete:
|
|
179
|
-
{chr(10).join(f
|
|
183
|
+
{chr(10).join(f" {c}" for c in verify_criteria)}
|
|
180
184
|
|
|
181
185
|
If ANY verification fails, fix the code and re-run until it passes!
|
|
182
186
|
"""
|
|
183
|
-
|
|
187
|
+
|
|
184
188
|
return f"""Execute ONE iteration of the Ralph loop.
|
|
185
189
|
|
|
186
190
|
## Current Story
|
|
@@ -189,7 +193,7 @@ If ANY verification fails, fix the code and re-run until it passes!
|
|
|
189
193
|
- **Description:** {story.description}
|
|
190
194
|
|
|
191
195
|
## Acceptance Criteria (implement ALL of these):
|
|
192
|
-
{chr(10).join(f
|
|
196
|
+
{chr(10).join(f" - {c}" for c in other_criteria)}
|
|
193
197
|
{verify_section}
|
|
194
198
|
## Requires UI Verification: {story.has_ui_verification()}
|
|
195
199
|
{"If yes, invoke qa-kitten to verify UI changes work correctly." if story.has_ui_verification() else ""}
|
|
@@ -227,18 +231,18 @@ async def run_ralph_loop(
|
|
|
227
231
|
invoke_func: Optional[Callable] = None,
|
|
228
232
|
) -> dict:
|
|
229
233
|
"""Convenience function to run the Ralph loop.
|
|
230
|
-
|
|
234
|
+
|
|
231
235
|
Args:
|
|
232
236
|
max_iterations: Maximum number of iterations
|
|
233
237
|
invoke_func: Function to invoke agents. If None, uses default.
|
|
234
|
-
|
|
238
|
+
|
|
235
239
|
Returns:
|
|
236
240
|
Result dict from the controller
|
|
237
241
|
"""
|
|
238
242
|
if invoke_func is None:
|
|
239
243
|
# Use the default agent invocation mechanism
|
|
240
244
|
invoke_func = _default_invoke_agent
|
|
241
|
-
|
|
245
|
+
|
|
242
246
|
controller = get_loop_controller(max_iterations)
|
|
243
247
|
return await controller.run(invoke_func)
|
|
244
248
|
|
|
@@ -251,20 +255,20 @@ async def _default_invoke_agent(
|
|
|
251
255
|
"""Default agent invocation using code_puppy's agent system."""
|
|
252
256
|
try:
|
|
253
257
|
from code_puppy.agents import get_current_agent, load_agent, set_current_agent
|
|
254
|
-
|
|
258
|
+
|
|
255
259
|
# Save current agent to restore later
|
|
256
260
|
original_agent = get_current_agent()
|
|
257
|
-
|
|
261
|
+
|
|
258
262
|
try:
|
|
259
263
|
# Load the target agent
|
|
260
264
|
target_agent = load_agent(agent_name)
|
|
261
265
|
if target_agent is None:
|
|
262
266
|
return {"response": None, "error": f"Agent '{agent_name}' not found"}
|
|
263
|
-
|
|
267
|
+
|
|
264
268
|
# Run the agent with the prompt
|
|
265
269
|
# Note: This creates a fresh run with no message history
|
|
266
270
|
result = await target_agent.run_with_mcp(prompt)
|
|
267
|
-
|
|
271
|
+
|
|
268
272
|
# Extract response text
|
|
269
273
|
response_text = ""
|
|
270
274
|
if result is not None:
|
|
@@ -272,14 +276,14 @@ async def _default_invoke_agent(
|
|
|
272
276
|
response_text = str(result.data) if result.data else ""
|
|
273
277
|
else:
|
|
274
278
|
response_text = str(result)
|
|
275
|
-
|
|
279
|
+
|
|
276
280
|
return {"response": response_text, "error": None}
|
|
277
|
-
|
|
281
|
+
|
|
278
282
|
finally:
|
|
279
283
|
# Restore original agent
|
|
280
284
|
if original_agent:
|
|
281
285
|
set_current_agent(original_agent.name)
|
|
282
|
-
|
|
286
|
+
|
|
283
287
|
except Exception as e:
|
|
284
288
|
logger.exception(f"Failed to invoke agent {agent_name}")
|
|
285
289
|
return {"response": None, "error": str(e)}
|
|
@@ -5,7 +5,7 @@ This module registers all Ralph callbacks:
|
|
|
5
5
|
- register_agents: PRD Generator, Converter, and Orchestrator agents
|
|
6
6
|
- custom_command: /ralph slash commands
|
|
7
7
|
- custom_command_help: Help entries for Ralph commands
|
|
8
|
-
-
|
|
8
|
+
- agent_run_end: Detect completion signal for loop termination
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import logging
|
|
@@ -83,13 +83,16 @@ def reset_ralph_completion() -> None:
|
|
|
83
83
|
_ralph_last_session_id = None
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
async def
|
|
86
|
+
async def _on_agent_run_end(
|
|
87
87
|
agent_name: str,
|
|
88
|
-
|
|
88
|
+
model_name: str,
|
|
89
89
|
session_id: Optional[str] = None,
|
|
90
|
+
success: bool = True,
|
|
91
|
+
error: Optional[Exception] = None,
|
|
92
|
+
response_text: Optional[str] = None,
|
|
90
93
|
metadata: Optional[dict] = None,
|
|
91
94
|
) -> None:
|
|
92
|
-
"""Handle agent
|
|
95
|
+
"""Handle agent run completion.
|
|
93
96
|
|
|
94
97
|
This detects the <promise>COMPLETE</promise> signal from the
|
|
95
98
|
Ralph Orchestrator and sets the completion flag.
|
|
@@ -100,10 +103,14 @@ async def _on_agent_complete(
|
|
|
100
103
|
if agent_name != "ralph-orchestrator":
|
|
101
104
|
return
|
|
102
105
|
|
|
106
|
+
# Only process successful runs with response text
|
|
107
|
+
if not success or not response_text:
|
|
108
|
+
return
|
|
109
|
+
|
|
103
110
|
logger.debug(f"Ralph plugin: orchestrator completed (session={session_id})")
|
|
104
111
|
|
|
105
112
|
# Check for completion signal
|
|
106
|
-
if
|
|
113
|
+
if "<promise>COMPLETE</promise>" in response_text:
|
|
107
114
|
_ralph_completion_detected = True
|
|
108
115
|
_ralph_last_session_id = session_id
|
|
109
116
|
|
|
@@ -127,7 +134,7 @@ register_callback("custom_command", _handle_command)
|
|
|
127
134
|
register_callback("custom_command_help", _provide_command_help)
|
|
128
135
|
|
|
129
136
|
# Completion detection
|
|
130
|
-
register_callback("
|
|
137
|
+
register_callback("agent_run_end", _on_agent_run_end)
|
|
131
138
|
|
|
132
139
|
|
|
133
140
|
logger.info("Ralph plugin: all callbacks registered successfully")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
code_puppy/__init__.py,sha256=xMPewo9RNHb3yfFNIk5WCbv2cvSPtJOCgK2-GqLbNnU,373
|
|
2
2
|
code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
|
|
3
|
-
code_puppy/callbacks.py,sha256=
|
|
3
|
+
code_puppy/callbacks.py,sha256=A7aqykjNVB2HorewzerCEb3YYnqeDIdQG_2wNTt1paE,16745
|
|
4
4
|
code_puppy/chatgpt_codex_client.py,sha256=upMuAfOhMB7SEpVw4CU4GjgaeZ8X65ri3yNM-dnlmYA,12308
|
|
5
5
|
code_puppy/claude_cache_client.py,sha256=GtwYrxcTe0pE-JGtl1ysR2qskfeE73_x4w7q_u-kR1k,24026
|
|
6
6
|
code_puppy/cli_runner.py,sha256=w5CLKgQYYaT7My3Cga2StXYol-u6DBxNzzUuhhsfhsA,34952
|
|
@@ -45,7 +45,7 @@ code_puppy/agents/agent_qa_kitten.py,sha256=qvry-1u_CiXi8eRueHTax4OtqsS_mQrtXHsb
|
|
|
45
45
|
code_puppy/agents/agent_security_auditor.py,sha256=SpiYNA0XAsIwBj7S2_EQPRslRUmF_-b89pIJyW7DYtY,12022
|
|
46
46
|
code_puppy/agents/agent_terminal_qa.py,sha256=U-iyP7OBWdAmchW_oUU8k6asH2aignTMmgqqYDyf-ms,10343
|
|
47
47
|
code_puppy/agents/agent_typescript_reviewer.py,sha256=vsnpp98xg6cIoFAEJrRTUM_i4wLEWGm5nJxs6fhHobM,10275
|
|
48
|
-
code_puppy/agents/base_agent.py,sha256=
|
|
48
|
+
code_puppy/agents/base_agent.py,sha256=gZvACpqH8L2Wp2xjn_v5D4Azlt6yvGnQO6UApN8HCEo,76272
|
|
49
49
|
code_puppy/agents/event_stream_handler.py,sha256=JttLZJpNADE5HXiXY-GZ6tpwaBeFRODcy34KiquPOvU,14952
|
|
50
50
|
code_puppy/agents/json_agent.py,sha256=FtbZxO8mo563kvXgpgRM4b-c9VA3G3cty7r-O0nBZQk,5690
|
|
51
51
|
code_puppy/agents/prompt_reviewer.py,sha256=JJrJ0m5q0Puxl8vFsyhAbY9ftU9n6c6UxEVdNct1E-Q,5558
|
|
@@ -169,10 +169,11 @@ code_puppy/plugins/chatgpt_oauth/test_plugin.py,sha256=oHX7Eb_Hb4rgRpOWdhtFp8Jj6
|
|
|
169
169
|
code_puppy/plugins/chatgpt_oauth/utils.py,sha256=fzpsCQOv0kqPWmG5vNEV_GLSUrMQh8cF7tdIjSOt1Dc,16504
|
|
170
170
|
code_puppy/plugins/claude_code_oauth/README.md,sha256=fOSDDzCdm2JCKjU5J82IRHIAhxYxl8_UmHo7uH4AbFo,5469
|
|
171
171
|
code_puppy/plugins/claude_code_oauth/SETUP.md,sha256=DCNLkSU9nf86S1rsrIg8HBe87NZrF8YND8P4ettWeEM,3289
|
|
172
|
-
code_puppy/plugins/claude_code_oauth/__init__.py,sha256=
|
|
172
|
+
code_puppy/plugins/claude_code_oauth/__init__.py,sha256=DgYQ1zQy-Wf-tSphD4wYrvtztdr-Dubhf4AqsSi315I,659
|
|
173
173
|
code_puppy/plugins/claude_code_oauth/config.py,sha256=DjGySCkvjSGZds6DYErLMAi3TItt8iSLGvyJN98nSEM,2013
|
|
174
|
-
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=
|
|
174
|
+
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=uvNA2wpSifeCD_o2CTENCuXQiq2ntk0uzQw8iAhZYng,15080
|
|
175
175
|
code_puppy/plugins/claude_code_oauth/test_plugin.py,sha256=yQy4EeZl4bjrcog1d8BjknoDTRK75mRXXvkSQJYSSEM,9286
|
|
176
|
+
code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py,sha256=ErkrquUUTaqHkrLuPxTZymLkMOcju-wr5zAuO4fyN-s,7865
|
|
176
177
|
code_puppy/plugins/claude_code_oauth/utils.py,sha256=2ioGG-4FCh4WdHrN2MJvWKbPWA-YVg_WTEeddc1xv4U,18557
|
|
177
178
|
code_puppy/plugins/customizable_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
178
179
|
code_puppy/plugins/customizable_commands/register_callbacks.py,sha256=zVMfIzr--hVn0IOXxIicbmgj2s-HZUgtrOc0NCDOnDw,5183
|
|
@@ -186,9 +187,9 @@ code_puppy/plugins/frontend_emitter/register_callbacks.py,sha256=3j7Emn3VeWSJSN9
|
|
|
186
187
|
code_puppy/plugins/ralph/__init__.py,sha256=drPRgJ2LGLh8v_8dk7TJIklv-51XHjqW7SMfNTzwiGI,448
|
|
187
188
|
code_puppy/plugins/ralph/agents.py,sha256=EQ2pvb8tb8aFfYZYtBuo9rBIvVp18XCieeK5PRvH3K4,11914
|
|
188
189
|
code_puppy/plugins/ralph/commands.py,sha256=J1sVNrm4SBj_ujkO51ILWzdH5L3px6NPR_mn0mXD_FQ,6736
|
|
189
|
-
code_puppy/plugins/ralph/loop_controller.py,sha256=
|
|
190
|
+
code_puppy/plugins/ralph/loop_controller.py,sha256=uhEOKpmkxlfj5MK6_zVvSHH9D79JRx4RRkNtBkrBkNk,10107
|
|
190
191
|
code_puppy/plugins/ralph/models.py,sha256=IrAJW85CgKA6yYSrWRn3VHx_65ukFkrasUhbVM1emF0,3983
|
|
191
|
-
code_puppy/plugins/ralph/register_callbacks.py,sha256=
|
|
192
|
+
code_puppy/plugins/ralph/register_callbacks.py,sha256=_q1SILAYc8BHyWpXZoA8MOVsLCFnOvQ3vU8PzjfzZxc,4429
|
|
192
193
|
code_puppy/plugins/ralph/state_manager.py,sha256=jv7bnQBiy_XQTj7Kfl3zBIMLV3XN5U-IS_U-fIdpHe0,10215
|
|
193
194
|
code_puppy/plugins/ralph/tools.py,sha256=p767ZdjkeRmfWTqUYdFmXnKEHC15ek1Ho3tkLcjWb20,14391
|
|
194
195
|
code_puppy/plugins/shell_safety/__init__.py,sha256=B-RYLWKlvrws9XCHG1Z99mBMC3VC394HAlMOhhCoGGI,243
|
|
@@ -224,10 +225,10 @@ code_puppy/tools/browser/chromium_terminal_manager.py,sha256=w1thQ_ACb6oV45L93TS
|
|
|
224
225
|
code_puppy/tools/browser/terminal_command_tools.py,sha256=9byOZku-dwvTtCl532xt7Lumed_jTn0sLvUe_X75XCQ,19068
|
|
225
226
|
code_puppy/tools/browser/terminal_screenshot_tools.py,sha256=J_21YO_495NvYgNFu9KQP6VYg2K_f8CtSdZuF94Yhnw,18448
|
|
226
227
|
code_puppy/tools/browser/terminal_tools.py,sha256=F5LjVH3udSCFHmqC3O1UJLoLozZFZsEdX42jOmkqkW0,17853
|
|
227
|
-
code_puppy-0.0.
|
|
228
|
-
code_puppy-0.0.
|
|
229
|
-
code_puppy-0.0.
|
|
230
|
-
code_puppy-0.0.
|
|
231
|
-
code_puppy-0.0.
|
|
232
|
-
code_puppy-0.0.
|
|
233
|
-
code_puppy-0.0.
|
|
228
|
+
code_puppy-0.0.376.data/data/code_puppy/models.json,sha256=jAHRsCl3trysP4vU_k_ltA8GcFU2APd4lxFl8-4Jnvc,3243
|
|
229
|
+
code_puppy-0.0.376.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
|
|
230
|
+
code_puppy-0.0.376.dist-info/METADATA,sha256=8zBZ-_vl5I-bVjco6ZxTH1Vsiz7NQQO1AqtYAiisweA,27604
|
|
231
|
+
code_puppy-0.0.376.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
232
|
+
code_puppy-0.0.376.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
|
|
233
|
+
code_puppy-0.0.376.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
|
|
234
|
+
code_puppy-0.0.376.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|