code-puppy 0.0.341__py3-none-any.whl → 0.0.361__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/agent_manager.py +49 -0
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +34 -252
  7. code_puppy/agents/event_stream_handler.py +350 -0
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/claude_cache_client.py +249 -34
  29. code_puppy/cli_runner.py +4 -3
  30. code_puppy/command_line/add_model_menu.py +8 -9
  31. code_puppy/command_line/core_commands.py +85 -0
  32. code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
  33. code_puppy/command_line/mcp/custom_server_form.py +54 -19
  34. code_puppy/command_line/mcp/custom_server_installer.py +8 -9
  35. code_puppy/command_line/mcp/handler.py +0 -2
  36. code_puppy/command_line/mcp/help_command.py +1 -5
  37. code_puppy/command_line/mcp/start_command.py +36 -18
  38. code_puppy/command_line/onboarding_slides.py +0 -1
  39. code_puppy/command_line/prompt_toolkit_completion.py +16 -10
  40. code_puppy/command_line/utils.py +54 -0
  41. code_puppy/config.py +66 -62
  42. code_puppy/mcp_/async_lifecycle.py +35 -4
  43. code_puppy/mcp_/managed_server.py +49 -20
  44. code_puppy/mcp_/manager.py +81 -52
  45. code_puppy/messaging/__init__.py +15 -0
  46. code_puppy/messaging/message_queue.py +11 -23
  47. code_puppy/messaging/messages.py +27 -0
  48. code_puppy/messaging/queue_console.py +1 -1
  49. code_puppy/messaging/rich_renderer.py +36 -1
  50. code_puppy/messaging/spinner/__init__.py +20 -2
  51. code_puppy/messaging/subagent_console.py +461 -0
  52. code_puppy/model_utils.py +54 -0
  53. code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
  54. code_puppy/plugins/antigravity_oauth/transport.py +1 -0
  55. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  56. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  57. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  58. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +139 -36
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_navigation.py +7 -7
  67. code_puppy/tools/browser/browser_screenshot.py +78 -140
  68. code_puppy/tools/browser/browser_scripts.py +15 -13
  69. code_puppy/tools/browser/camoufox_manager.py +226 -64
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
  79. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
  80. code_puppy/command_line/mcp/add_command.py +0 -170
  81. code_puppy/tools/browser/vqa_agent.py +0 -90
  82. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
  84. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
  85. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
  86. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,6 @@ import signal
7
7
  import threading
8
8
  import uuid
9
9
  from abc import ABC, abstractmethod
10
- from collections.abc import AsyncIterable
11
10
  from typing import (
12
11
  Any,
13
12
  Callable,
@@ -30,7 +29,6 @@ from pydantic_ai import (
30
29
  BinaryContent,
31
30
  DocumentUrl,
32
31
  ImageUrl,
33
- PartEndEvent,
34
32
  RunContext,
35
33
  UsageLimitExceeded,
36
34
  UsageLimits,
@@ -48,6 +46,8 @@ from pydantic_ai.messages import (
48
46
  )
49
47
  from rich.text import Text
50
48
 
49
+ from code_puppy.agents.event_stream_handler import event_stream_handler
50
+
51
51
  # Consolidated relative imports
52
52
  from code_puppy.config import (
53
53
  get_agent_pinned_model,
@@ -100,9 +100,6 @@ class BaseAgent(ABC):
100
100
  # Cache for MCP tool definitions (for token estimation)
101
101
  # This is populated after the first successful run when MCP tools are retrieved
102
102
  self._mcp_tool_definitions_cache: List[Dict[str, Any]] = []
103
- # Shared console for streaming output - should be set by cli_runner
104
- # to avoid conflicts between spinner's Live display and response streaming
105
- self._console: Optional[Any] = None
106
103
 
107
104
  @property
108
105
  @abstractmethod
@@ -380,8 +377,10 @@ class BaseAgent(ABC):
380
377
  # fixed instructions. For other models, count the full system prompt.
381
378
  try:
382
379
  from code_puppy.model_utils import (
380
+ get_antigravity_instructions,
383
381
  get_chatgpt_codex_instructions,
384
382
  get_claude_code_instructions,
383
+ is_antigravity_model,
385
384
  is_chatgpt_codex_model,
386
385
  is_claude_code_model,
387
386
  )
@@ -399,6 +398,11 @@ class BaseAgent(ABC):
399
398
  # The full system prompt is already in the message history
400
399
  instructions = get_chatgpt_codex_instructions()
401
400
  total_tokens += self.estimate_token_count(instructions)
401
+ elif is_antigravity_model(model_name):
402
+ # For Antigravity models, only count the short fixed instructions
403
+ # The full system prompt is already in the message history
404
+ instructions = get_antigravity_instructions()
405
+ total_tokens += self.estimate_token_count(instructions)
402
406
  else:
403
407
  # For other models, count the full system prompt
404
408
  system_prompt = self.get_system_prompt()
@@ -1233,9 +1237,12 @@ class BaseAgent(ABC):
1233
1237
  agent_tools = self.get_available_tools()
1234
1238
  register_tools_for_agent(agent_without_mcp, agent_tools)
1235
1239
 
1236
- # Wrap with DBOS
1240
+ # Wrap with DBOS - pass event_stream_handler at construction time
1241
+ # so DBOSModel gets the handler for streaming output
1237
1242
  dbos_agent = DBOSAgent(
1238
- agent_without_mcp, name=f"{self.name}-{_reload_count}"
1243
+ agent_without_mcp,
1244
+ name=f"{self.name}-{_reload_count}",
1245
+ event_stream_handler=event_stream_handler,
1239
1246
  )
1240
1247
  self.pydantic_agent = dbos_agent
1241
1248
  self._code_generation_agent = dbos_agent
@@ -1314,8 +1321,11 @@ class BaseAgent(ABC):
1314
1321
  )
1315
1322
  agent_tools = self.get_available_tools()
1316
1323
  register_tools_for_agent(temp_agent, agent_tools)
1324
+ # Pass event_stream_handler at construction time for streaming output
1317
1325
  dbos_agent = DBOSAgent(
1318
- temp_agent, name=f"{self.name}-structured-{_reload_count}"
1326
+ temp_agent,
1327
+ name=f"{self.name}-structured-{_reload_count}",
1328
+ event_stream_handler=event_stream_handler,
1319
1329
  )
1320
1330
  return dbos_agent
1321
1331
  else:
@@ -1357,241 +1367,6 @@ class BaseAgent(ABC):
1357
1367
  self.set_message_history(result_messages_filtered_empty_thinking)
1358
1368
  return self.get_message_history()
1359
1369
 
1360
- async def _event_stream_handler(
1361
- self, ctx: RunContext, events: AsyncIterable[Any]
1362
- ) -> None:
1363
- """Handle streaming events from the agent run.
1364
-
1365
- This method processes streaming events and emits TextPart, ThinkingPart,
1366
- and ToolCallPart content with styled banners/tokens as they stream in.
1367
-
1368
- Args:
1369
- ctx: The run context.
1370
- events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
1371
- """
1372
- from pydantic_ai import PartDeltaEvent, PartStartEvent
1373
- from pydantic_ai.messages import (
1374
- TextPartDelta,
1375
- ThinkingPartDelta,
1376
- ToolCallPartDelta,
1377
- )
1378
- from rich.console import Console
1379
- from rich.markup import escape
1380
-
1381
- from code_puppy.messaging.spinner import pause_all_spinners
1382
-
1383
- # IMPORTANT: Use the shared console (set by cli_runner) to avoid conflicts
1384
- # with the spinner's Live display. Multiple Console instances with separate
1385
- # Live displays cause cursor positioning chaos and line duplication.
1386
- if self._console is not None:
1387
- console = self._console
1388
- else:
1389
- # Fallback if console not set (shouldn't happen in normal use)
1390
- console = Console()
1391
-
1392
- # Track which part indices we're currently streaming (for Text/Thinking/Tool parts)
1393
- streaming_parts: set[int] = set()
1394
- thinking_parts: set[int] = (
1395
- set()
1396
- ) # Track which parts are thinking (for dim style)
1397
- text_parts: set[int] = set() # Track which parts are text
1398
- tool_parts: set[int] = set() # Track which parts are tool calls
1399
- banner_printed: set[int] = set() # Track if banner was already printed
1400
- token_count: dict[int, int] = {} # Track token count per text/tool part
1401
- did_stream_anything = False # Track if we streamed any content
1402
-
1403
- # Termflow streaming state for text parts
1404
- from termflow import Parser as TermflowParser
1405
- from termflow import Renderer as TermflowRenderer
1406
-
1407
- termflow_parsers: dict[int, TermflowParser] = {}
1408
- termflow_renderers: dict[int, TermflowRenderer] = {}
1409
- termflow_line_buffers: dict[int, str] = {} # Buffer incomplete lines
1410
-
1411
- def _print_thinking_banner() -> None:
1412
- """Print the THINKING banner with spinner pause and line clear."""
1413
- nonlocal did_stream_anything
1414
- import time
1415
-
1416
- from code_puppy.config import get_banner_color
1417
-
1418
- pause_all_spinners()
1419
- time.sleep(0.1) # Delay to let spinner fully clear
1420
- # Clear line and print newline before banner
1421
- console.print(" " * 50, end="\r")
1422
- console.print() # Newline before banner
1423
- # Bold banner with configurable color and lightning bolt
1424
- thinking_color = get_banner_color("thinking")
1425
- console.print(
1426
- Text.from_markup(
1427
- f"[bold white on {thinking_color}] THINKING [/bold white on {thinking_color}] [dim]⚡ "
1428
- ),
1429
- end="",
1430
- )
1431
- did_stream_anything = True
1432
-
1433
- def _print_response_banner() -> None:
1434
- """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1435
- nonlocal did_stream_anything
1436
- import time
1437
-
1438
- from code_puppy.config import get_banner_color
1439
-
1440
- pause_all_spinners()
1441
- time.sleep(0.1) # Delay to let spinner fully clear
1442
- # Clear line and print newline before banner
1443
- console.print(" " * 50, end="\r")
1444
- console.print() # Newline before banner
1445
- response_color = get_banner_color("agent_response")
1446
- console.print(
1447
- Text.from_markup(
1448
- f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1449
- )
1450
- )
1451
- did_stream_anything = True
1452
-
1453
- async for event in events:
1454
- # PartStartEvent - register the part but defer banner until content arrives
1455
- if isinstance(event, PartStartEvent):
1456
- part = event.part
1457
- if isinstance(part, ThinkingPart):
1458
- streaming_parts.add(event.index)
1459
- thinking_parts.add(event.index)
1460
- # If there's initial content, print banner + content now
1461
- if part.content and part.content.strip():
1462
- _print_thinking_banner()
1463
- escaped = escape(part.content)
1464
- console.print(f"[dim]{escaped}[/dim]", end="")
1465
- banner_printed.add(event.index)
1466
- elif isinstance(part, TextPart):
1467
- streaming_parts.add(event.index)
1468
- text_parts.add(event.index)
1469
- # Initialize termflow streaming for this text part
1470
- termflow_parsers[event.index] = TermflowParser()
1471
- termflow_renderers[event.index] = TermflowRenderer(
1472
- output=console.file, width=console.width
1473
- )
1474
- termflow_line_buffers[event.index] = ""
1475
- # Handle initial content if present
1476
- if part.content and part.content.strip():
1477
- _print_response_banner()
1478
- banner_printed.add(event.index)
1479
- termflow_line_buffers[event.index] = part.content
1480
- elif isinstance(part, ToolCallPart):
1481
- streaming_parts.add(event.index)
1482
- tool_parts.add(event.index)
1483
- token_count[event.index] = 0 # Initialize token counter
1484
- # Track tool name for display
1485
- banner_printed.add(
1486
- event.index
1487
- ) # Use banner_printed to track if we've shown tool info
1488
-
1489
- # PartDeltaEvent - stream the content as it arrives
1490
- elif isinstance(event, PartDeltaEvent):
1491
- if event.index in streaming_parts:
1492
- delta = event.delta
1493
- if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1494
- if delta.content_delta:
1495
- # For text parts, stream markdown with termflow
1496
- if event.index in text_parts:
1497
- # Print banner on first content
1498
- if event.index not in banner_printed:
1499
- _print_response_banner()
1500
- banner_printed.add(event.index)
1501
-
1502
- # Add content to line buffer
1503
- termflow_line_buffers[event.index] += (
1504
- delta.content_delta
1505
- )
1506
-
1507
- # Process complete lines
1508
- parser = termflow_parsers[event.index]
1509
- renderer = termflow_renderers[event.index]
1510
- buffer = termflow_line_buffers[event.index]
1511
-
1512
- while "\n" in buffer:
1513
- line, buffer = buffer.split("\n", 1)
1514
- events_to_render = parser.parse_line(line)
1515
- renderer.render_all(events_to_render)
1516
-
1517
- termflow_line_buffers[event.index] = buffer
1518
- else:
1519
- # For thinking parts, stream immediately (dim)
1520
- if event.index not in banner_printed:
1521
- _print_thinking_banner()
1522
- banner_printed.add(event.index)
1523
- escaped = escape(delta.content_delta)
1524
- console.print(f"[dim]{escaped}[/dim]", end="")
1525
- elif isinstance(delta, ToolCallPartDelta):
1526
- # For tool calls, count chunks received
1527
- token_count[event.index] += 1
1528
- # Get tool name if available
1529
- tool_name = getattr(delta, "tool_name_delta", "")
1530
- count = token_count[event.index]
1531
- # Display with tool wrench icon and tool name
1532
- if tool_name:
1533
- console.print(
1534
- f" 🔧 Calling {tool_name}... {count} chunks ",
1535
- end="\r",
1536
- )
1537
- else:
1538
- console.print(
1539
- f" 🔧 Calling tool... {count} chunks ",
1540
- end="\r",
1541
- )
1542
-
1543
- # PartEndEvent - finish the streaming with a newline
1544
- elif isinstance(event, PartEndEvent):
1545
- if event.index in streaming_parts:
1546
- # For text parts, finalize termflow rendering
1547
- if event.index in text_parts:
1548
- # Render any remaining buffered content
1549
- if event.index in termflow_parsers:
1550
- parser = termflow_parsers[event.index]
1551
- renderer = termflow_renderers[event.index]
1552
- remaining = termflow_line_buffers.get(event.index, "")
1553
-
1554
- # Parse and render any remaining partial line
1555
- if remaining.strip():
1556
- events_to_render = parser.parse_line(remaining)
1557
- renderer.render_all(events_to_render)
1558
-
1559
- # Finalize the parser to close any open blocks
1560
- final_events = parser.finalize()
1561
- renderer.render_all(final_events)
1562
-
1563
- # Clean up termflow state
1564
- del termflow_parsers[event.index]
1565
- del termflow_renderers[event.index]
1566
- del termflow_line_buffers[event.index]
1567
- # For tool parts, clear the chunk counter line
1568
- elif event.index in tool_parts:
1569
- # Clear the chunk counter line by printing spaces and returning
1570
- console.print(" " * 50, end="\r")
1571
- # For thinking parts, just print newline
1572
- elif event.index in banner_printed:
1573
- console.print() # Final newline after streaming
1574
-
1575
- # Clean up token count
1576
- token_count.pop(event.index, None)
1577
- # Clean up all tracking sets
1578
- streaming_parts.discard(event.index)
1579
- thinking_parts.discard(event.index)
1580
- text_parts.discard(event.index)
1581
- tool_parts.discard(event.index)
1582
- banner_printed.discard(event.index)
1583
-
1584
- # Resume spinner if next part is NOT text/thinking/tool (avoid race condition)
1585
- # If next part is None or handled differently, it's safe to resume
1586
- # Note: spinner itself handles blank line before appearing
1587
- from code_puppy.messaging.spinner import resume_all_spinners
1588
-
1589
- next_kind = getattr(event, "next_part_kind", None)
1590
- if next_kind not in ("text", "thinking", "tool-call"):
1591
- resume_all_spinners()
1592
-
1593
- # Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
1594
-
1595
1370
  def _spawn_ctrl_x_key_listener(
1596
1371
  self,
1597
1372
  stop_event: threading.Event,
@@ -1790,11 +1565,17 @@ class BaseAgent(ABC):
1790
1565
  if output_type is not None:
1791
1566
  pydantic_agent = self._create_agent_with_output_type(output_type)
1792
1567
 
1793
- # Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
1794
- from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
1568
+ # Handle claude-code, chatgpt-codex, and antigravity models: prepend system prompt to first user message
1569
+ from code_puppy.model_utils import (
1570
+ is_antigravity_model,
1571
+ is_chatgpt_codex_model,
1572
+ is_claude_code_model,
1573
+ )
1795
1574
 
1796
- if is_claude_code_model(self.get_model_name()) or is_chatgpt_codex_model(
1797
- self.get_model_name()
1575
+ if (
1576
+ is_claude_code_model(self.get_model_name())
1577
+ or is_chatgpt_codex_model(self.get_model_name())
1578
+ or is_antigravity_model(self.get_model_name())
1798
1579
  ):
1799
1580
  if len(self.get_message_history()) == 0:
1800
1581
  system_prompt = self.get_system_prompt()
@@ -1859,32 +1640,33 @@ class BaseAgent(ABC):
1859
1640
  prompt_payload,
1860
1641
  message_history=self.get_message_history(),
1861
1642
  usage_limits=usage_limits,
1862
- event_stream_handler=self._event_stream_handler,
1643
+ event_stream_handler=event_stream_handler,
1863
1644
  **kwargs,
1864
1645
  )
1646
+ return result_
1865
1647
  finally:
1866
1648
  # Always restore original toolsets
1867
1649
  pydantic_agent._toolsets = original_toolsets
1868
1650
  elif get_use_dbos():
1869
- # DBOS without MCP servers
1870
1651
  with SetWorkflowID(group_id):
1871
1652
  result_ = await pydantic_agent.run(
1872
1653
  prompt_payload,
1873
1654
  message_history=self.get_message_history(),
1874
1655
  usage_limits=usage_limits,
1875
- event_stream_handler=self._event_stream_handler,
1656
+ event_stream_handler=event_stream_handler,
1876
1657
  **kwargs,
1877
1658
  )
1659
+ return result_
1878
1660
  else:
1879
1661
  # Non-DBOS path (MCP servers are already included)
1880
1662
  result_ = await pydantic_agent.run(
1881
1663
  prompt_payload,
1882
1664
  message_history=self.get_message_history(),
1883
1665
  usage_limits=usage_limits,
1884
- event_stream_handler=self._event_stream_handler,
1666
+ event_stream_handler=event_stream_handler,
1885
1667
  **kwargs,
1886
1668
  )
1887
- return result_
1669
+ return result_
1888
1670
  except* UsageLimitExceeded as ule:
1889
1671
  emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
1890
1672
  emit_info(