code-puppy 0.0.336__py3-none-any.whl → 0.0.348__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 (40) hide show
  1. code_puppy/agents/base_agent.py +41 -224
  2. code_puppy/agents/event_stream_handler.py +257 -0
  3. code_puppy/claude_cache_client.py +208 -2
  4. code_puppy/cli_runner.py +53 -35
  5. code_puppy/command_line/add_model_menu.py +8 -9
  6. code_puppy/command_line/autosave_menu.py +18 -24
  7. code_puppy/command_line/clipboard.py +527 -0
  8. code_puppy/command_line/core_commands.py +34 -0
  9. code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
  10. code_puppy/command_line/mcp/custom_server_form.py +54 -19
  11. code_puppy/command_line/mcp/custom_server_installer.py +8 -9
  12. code_puppy/command_line/mcp/handler.py +0 -2
  13. code_puppy/command_line/mcp/help_command.py +1 -5
  14. code_puppy/command_line/mcp/start_command.py +36 -18
  15. code_puppy/command_line/onboarding_slides.py +0 -1
  16. code_puppy/command_line/prompt_toolkit_completion.py +124 -0
  17. code_puppy/command_line/utils.py +54 -0
  18. code_puppy/http_utils.py +93 -130
  19. code_puppy/mcp_/async_lifecycle.py +35 -4
  20. code_puppy/mcp_/managed_server.py +49 -24
  21. code_puppy/mcp_/manager.py +81 -52
  22. code_puppy/messaging/message_queue.py +11 -23
  23. code_puppy/messaging/messages.py +3 -0
  24. code_puppy/messaging/rich_renderer.py +13 -3
  25. code_puppy/model_factory.py +16 -0
  26. code_puppy/models.json +2 -2
  27. code_puppy/plugins/antigravity_oauth/antigravity_model.py +17 -2
  28. code_puppy/plugins/claude_code_oauth/utils.py +126 -7
  29. code_puppy/terminal_utils.py +128 -1
  30. code_puppy/tools/agent_tools.py +66 -13
  31. code_puppy/tools/command_runner.py +1 -0
  32. code_puppy/tools/common.py +3 -9
  33. {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models.json +2 -2
  34. {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/METADATA +19 -71
  35. {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/RECORD +39 -38
  36. code_puppy/command_line/mcp/add_command.py +0 -170
  37. {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models_dev_api.json +0 -0
  38. {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/WHEEL +0 -0
  39. {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/entry_points.txt +0 -0
  40. {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/licenses/LICENSE +0 -0
@@ -329,31 +329,19 @@ def emit_divider(content: str = "─" * 100 + "\n", **metadata):
329
329
 
330
330
 
331
331
  def emit_prompt(prompt_text: str, timeout: float = None) -> str:
332
- """Emit a human input request and wait for response."""
333
- # TUI mode has been removed, always use interactive mode input
334
- if True:
335
- # Emit the prompt as a message for display
336
- from code_puppy.messaging import emit_info
332
+ """Emit a human input request and wait for response.
337
333
 
338
- emit_info(prompt_text)
334
+ Uses safe_input for cross-platform compatibility, especially on Windows
335
+ where raw input() can fail after prompt_toolkit Applications.
336
+ """
337
+ from code_puppy.command_line.utils import safe_input
338
+ from code_puppy.messaging import emit_info
339
339
 
340
- # Get input directly
341
- try:
342
- # Try to use rich console for better formatting
343
- from rich.console import Console
344
-
345
- console = Console()
346
- response = console.input("[cyan]>>> [/cyan]")
347
- return response
348
- except Exception:
349
- # Fallback to basic input
350
- response = input(">>> ")
351
- return response
352
-
353
- # In TUI mode, use the queue system
354
- queue = get_global_queue()
355
- prompt_id = queue.create_prompt_request(prompt_text)
356
- return queue.wait_for_prompt_response(prompt_id, timeout)
340
+ emit_info(prompt_text)
341
+
342
+ # Use safe_input which resets Windows console state before reading
343
+ response = safe_input(">>> ")
344
+ return response
357
345
 
358
346
 
359
347
  def provide_prompt_response(prompt_id: str, response: str):
@@ -209,6 +209,9 @@ class ShellStartMessage(BaseMessage):
209
209
  default=None, description="Working directory for the command"
210
210
  )
211
211
  timeout: int = Field(default=60, description="Timeout in seconds")
212
+ background: bool = Field(
213
+ default=False, description="Whether command runs in background mode"
214
+ )
212
215
 
213
216
 
214
217
  class ShellLineMessage(BaseMessage):
@@ -620,15 +620,25 @@ class RichConsoleRenderer:
620
620
  safe_command = escape_rich_markup(msg.command)
621
621
  # Header showing command is starting
622
622
  banner = self._format_banner("shell_command", "SHELL COMMAND")
623
- self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
623
+
624
+ # Add background indicator if running in background mode
625
+ if msg.background:
626
+ self._console.print(
627
+ f"\n{banner} 🚀 [dim]$ {safe_command}[/dim] [bold magenta][BACKGROUND 🌙][/bold magenta]"
628
+ )
629
+ else:
630
+ self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
624
631
 
625
632
  # Show working directory if specified
626
633
  if msg.cwd:
627
634
  safe_cwd = escape_rich_markup(msg.cwd)
628
635
  self._console.print(f"[dim]📂 Working directory: {safe_cwd}[/dim]")
629
636
 
630
- # Show timeout
631
- self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
637
+ # Show timeout or background status
638
+ if msg.background:
639
+ self._console.print("[dim]⏱ Runs detached (no timeout)[/dim]")
640
+ else:
641
+ self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
632
642
 
633
643
  def _render_shell_line(self, msg: ShellLineMessage) -> None:
634
644
  """Render shell output line preserving ANSI codes."""
@@ -388,6 +388,20 @@ class ModelFactory:
388
388
  return AnthropicModel(model_name=model_config["name"], provider=provider)
389
389
  elif model_type == "claude_code":
390
390
  url, headers, verify, api_key = get_custom_config(model_config)
391
+ if model_config.get("oauth_source") == "claude-code-plugin":
392
+ try:
393
+ from code_puppy.plugins.claude_code_oauth.utils import (
394
+ get_valid_access_token,
395
+ )
396
+
397
+ refreshed_token = get_valid_access_token()
398
+ if refreshed_token:
399
+ api_key = refreshed_token
400
+ custom_endpoint = model_config.get("custom_endpoint")
401
+ if isinstance(custom_endpoint, dict):
402
+ custom_endpoint["api_key"] = refreshed_token
403
+ except ImportError:
404
+ pass
391
405
  if not api_key:
392
406
  emit_warning(
393
407
  f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'."
@@ -663,6 +677,8 @@ class ModelFactory:
663
677
  f"API key is not set for Cerebras endpoint; skipping model '{model_config.get('name')}'."
664
678
  )
665
679
  return None
680
+ # Add Cerebras 3rd party integration header
681
+ headers["X-Cerebras-3rd-Party-Integration"] = "code-puppy"
666
682
  client = create_async_client(headers=headers, verify=verify)
667
683
  provider_args = dict(
668
684
  api_key=api_key,
code_puppy/models.json CHANGED
@@ -55,9 +55,9 @@
55
55
  "supported_settings": ["reasoning_effort", "verbosity"],
56
56
  "supports_xhigh_reasoning": true
57
57
  },
58
- "Cerebras-GLM-4.6": {
58
+ "Cerebras-GLM-4.7": {
59
59
  "type": "cerebras",
60
- "name": "zai-glm-4.6",
60
+ "name": "zai-glm-4.7",
61
61
  "custom_endpoint": {
62
62
  "url": "https://api.cerebras.ai/v1",
63
63
  "api_key": "$CEREBRAS_API_KEY"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
5
  import logging
5
6
  from collections.abc import AsyncIterator
@@ -75,7 +76,16 @@ class AntigravityModel(GoogleModel):
75
76
  system_parts.append({"text": part.content})
76
77
  elif isinstance(part, UserPromptPart):
77
78
  # Use parent's _map_user_prompt
78
- message_parts.extend(await self._map_user_prompt(part))
79
+ mapped_parts = await self._map_user_prompt(part)
80
+ # Sanitize bytes to base64 for JSON serialization
81
+ for mp in mapped_parts:
82
+ if "inline_data" in mp and "data" in mp["inline_data"]:
83
+ data = mp["inline_data"]["data"]
84
+ if isinstance(data, bytes):
85
+ mp["inline_data"]["data"] = base64.b64encode(
86
+ data
87
+ ).decode("utf-8")
88
+ message_parts.extend(mapped_parts)
79
89
  elif isinstance(part, ToolReturnPart):
80
90
  message_parts.append(
81
91
  {
@@ -542,8 +552,13 @@ def _antigravity_content_model_response(
542
552
 
543
553
  elif isinstance(item, FilePart):
544
554
  content = item.content
555
+ # Ensure data is base64 string, not bytes
556
+ data_val = content.data
557
+ if isinstance(data_val, bytes):
558
+ data_val = base64.b64encode(data_val).decode("utf-8")
559
+
545
560
  inline_data_dict: BlobDict = {
546
- "data": content.data,
561
+ "data": data_val,
547
562
  "mime_type": content.media_type,
548
563
  }
549
564
  part["inline_data"] = inline_data_dict
@@ -21,6 +21,8 @@ from .config import (
21
21
  get_token_storage_path,
22
22
  )
23
23
 
24
+ TOKEN_REFRESH_BUFFER_SECONDS = 60
25
+
24
26
  logger = logging.getLogger(__name__)
25
27
 
26
28
 
@@ -132,6 +134,124 @@ def load_stored_tokens() -> Optional[Dict[str, Any]]:
132
134
  return None
133
135
 
134
136
 
137
+ def _calculate_expires_at(expires_in: Optional[float]) -> Optional[float]:
138
+ if expires_in is None:
139
+ return None
140
+ try:
141
+ return time.time() + float(expires_in)
142
+ except (TypeError, ValueError):
143
+ return None
144
+
145
+
146
+ def is_token_expired(tokens: Dict[str, Any]) -> bool:
147
+ expires_at = tokens.get("expires_at")
148
+ if expires_at is None:
149
+ return False
150
+ try:
151
+ expires_at_value = float(expires_at)
152
+ except (TypeError, ValueError):
153
+ return False
154
+ return time.time() >= expires_at_value - TOKEN_REFRESH_BUFFER_SECONDS
155
+
156
+
157
+ def update_claude_code_model_tokens(access_token: str) -> bool:
158
+ try:
159
+ claude_models = load_claude_models()
160
+ if not claude_models:
161
+ return False
162
+
163
+ updated = False
164
+ for config in claude_models.values():
165
+ if config.get("oauth_source") != "claude-code-plugin":
166
+ continue
167
+ custom_endpoint = config.get("custom_endpoint")
168
+ if not isinstance(custom_endpoint, dict):
169
+ continue
170
+ custom_endpoint["api_key"] = access_token
171
+ updated = True
172
+
173
+ if updated:
174
+ return save_claude_models(claude_models)
175
+ except Exception as exc: # pragma: no cover - defensive logging
176
+ logger.error("Failed to update Claude model tokens: %s", exc)
177
+ return False
178
+
179
+
180
+ def refresh_access_token(force: bool = False) -> Optional[str]:
181
+ tokens = load_stored_tokens()
182
+ if not tokens:
183
+ return None
184
+
185
+ if not force and not is_token_expired(tokens):
186
+ return tokens.get("access_token")
187
+
188
+ refresh_token = tokens.get("refresh_token")
189
+ if not refresh_token:
190
+ logger.debug("No refresh_token available")
191
+ return None
192
+
193
+ payload = {
194
+ "grant_type": "refresh_token",
195
+ "client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
196
+ "refresh_token": refresh_token,
197
+ }
198
+
199
+ headers = {
200
+ "Content-Type": "application/json",
201
+ "Accept": "application/json",
202
+ "anthropic-beta": "oauth-2025-04-20",
203
+ }
204
+
205
+ try:
206
+ response = requests.post(
207
+ CLAUDE_CODE_OAUTH_CONFIG["token_url"],
208
+ json=payload,
209
+ headers=headers,
210
+ timeout=30,
211
+ )
212
+ if response.status_code == 200:
213
+ new_tokens = response.json()
214
+ tokens["access_token"] = new_tokens.get("access_token")
215
+ tokens["refresh_token"] = new_tokens.get("refresh_token", refresh_token)
216
+ if "expires_in" in new_tokens:
217
+ tokens["expires_in"] = new_tokens["expires_in"]
218
+ tokens["expires_at"] = _calculate_expires_at(
219
+ new_tokens.get("expires_in")
220
+ )
221
+ if save_tokens(tokens):
222
+ update_claude_code_model_tokens(tokens["access_token"])
223
+ return tokens["access_token"]
224
+ else:
225
+ logger.error(
226
+ "Token refresh failed: %s - %s", response.status_code, response.text
227
+ )
228
+ except Exception as exc: # pragma: no cover - defensive logging
229
+ logger.error("Token refresh error: %s", exc)
230
+ return None
231
+
232
+
233
+ def get_valid_access_token() -> Optional[str]:
234
+ tokens = load_stored_tokens()
235
+ if not tokens:
236
+ logger.debug("No stored Claude Code OAuth tokens found")
237
+ return None
238
+
239
+ access_token = tokens.get("access_token")
240
+ if not access_token:
241
+ logger.debug("No access_token in stored tokens")
242
+ return None
243
+
244
+ if is_token_expired(tokens):
245
+ logger.info("Claude Code OAuth token expired, attempting refresh")
246
+ refreshed = refresh_access_token()
247
+ if refreshed:
248
+ return refreshed
249
+ logger.warning("Claude Code token refresh failed")
250
+ return None
251
+
252
+ return access_token
253
+
254
+
135
255
  def save_tokens(tokens: Dict[str, Any]) -> bool:
136
256
  try:
137
257
  token_path = get_token_storage_path()
@@ -243,7 +363,11 @@ def exchange_code_for_tokens(
243
363
  logger.info("Token exchange response: %s", response.status_code)
244
364
  logger.debug("Response body: %s", response.text)
245
365
  if response.status_code == 200:
246
- return response.json()
366
+ token_data = response.json()
367
+ token_data["expires_at"] = _calculate_expires_at(
368
+ token_data.get("expires_in")
369
+ )
370
+ return token_data
247
371
  logger.error(
248
372
  "Token exchange failed: %s - %s",
249
373
  response.status_code,
@@ -341,12 +465,7 @@ def add_models_to_extra_config(models: List[str]) -> bool:
341
465
  # Start fresh - overwrite the file on every auth instead of loading existing
342
466
  claude_models = {}
343
467
  added = 0
344
- tokens = load_stored_tokens()
345
-
346
- # Handle case where tokens are None or empty
347
- access_token = ""
348
- if tokens and "access_token" in tokens:
349
- access_token = tokens["access_token"]
468
+ access_token = get_valid_access_token() or ""
350
469
 
351
470
  for model_name in filtered_models:
352
471
  prefixed = f"{CLAUDE_CODE_OAUTH_CONFIG['prefix']}{model_name}"
@@ -3,10 +3,14 @@
3
3
  Handles Windows console mode resets and Unix terminal sanity restoration.
4
4
  """
5
5
 
6
+ import os
6
7
  import platform
7
8
  import subprocess
8
9
  import sys
9
- from typing import Callable, Optional
10
+ from typing import TYPE_CHECKING, Callable, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from rich.console import Console
10
14
 
11
15
  # Store the original console ctrl handler so we can restore it if needed
12
16
  _original_ctrl_handler: Optional[Callable] = None
@@ -289,3 +293,126 @@ def ensure_ctrl_c_disabled() -> bool:
289
293
 
290
294
  except Exception:
291
295
  return False
296
+
297
+
298
+ def detect_truecolor_support() -> bool:
299
+ """Detect if the terminal supports truecolor (24-bit color).
300
+
301
+ Checks multiple indicators:
302
+ 1. COLORTERM environment variable (most reliable)
303
+ 2. TERM environment variable patterns
304
+ 3. Rich's Console color_system detection as fallback
305
+
306
+ Returns:
307
+ True if truecolor is supported, False otherwise.
308
+ """
309
+ # Check COLORTERM - this is the most reliable indicator
310
+ colorterm = os.environ.get("COLORTERM", "").lower()
311
+ if colorterm in ("truecolor", "24bit"):
312
+ return True
313
+
314
+ # Check TERM for known truecolor-capable terminals
315
+ term = os.environ.get("TERM", "").lower()
316
+ truecolor_terms = (
317
+ "xterm-direct",
318
+ "xterm-truecolor",
319
+ "iterm2",
320
+ "vte-256color", # Many modern terminals set this
321
+ )
322
+ if any(t in term for t in truecolor_terms):
323
+ return True
324
+
325
+ # Some terminals like iTerm2, Kitty, Alacritty set specific env vars
326
+ if os.environ.get("ITERM_SESSION_ID"):
327
+ return True
328
+ if os.environ.get("KITTY_WINDOW_ID"):
329
+ return True
330
+ if os.environ.get("ALACRITTY_SOCKET"):
331
+ return True
332
+ if os.environ.get("WT_SESSION"): # Windows Terminal
333
+ return True
334
+
335
+ # Use Rich's detection as a fallback
336
+ try:
337
+ from rich.console import Console
338
+
339
+ console = Console(force_terminal=True)
340
+ color_system = console.color_system
341
+ return color_system == "truecolor"
342
+ except Exception:
343
+ pass
344
+
345
+ return False
346
+
347
+
348
+ def print_truecolor_warning(console: Optional["Console"] = None) -> None:
349
+ """Print a big fat red warning if truecolor is not supported.
350
+
351
+ Args:
352
+ console: Optional Rich Console instance. If None, creates a new one.
353
+ """
354
+ if detect_truecolor_support():
355
+ return # All good, no warning needed
356
+
357
+ if console is None:
358
+ try:
359
+ from rich.console import Console
360
+
361
+ console = Console()
362
+ except ImportError:
363
+ # Rich not available, fall back to plain print
364
+ print("\n" + "=" * 70)
365
+ print("⚠️ WARNING: TERMINAL DOES NOT SUPPORT TRUECOLOR (24-BIT COLOR)")
366
+ print("=" * 70)
367
+ print("Code Puppy looks best with truecolor support.")
368
+ print("Consider using a modern terminal like:")
369
+ print(" • iTerm2 (macOS)")
370
+ print(" • Windows Terminal (Windows)")
371
+ print(" • Kitty, Alacritty, or any modern terminal emulator")
372
+ print("")
373
+ print("You can also try setting: export COLORTERM=truecolor")
374
+ print("")
375
+ print("Note: The built-in macOS Terminal.app does not support truecolor")
376
+ print("(Sequoia and earlier). You'll need a different terminal app.")
377
+ print("=" * 70 + "\n")
378
+ return
379
+
380
+ # Get detected color system for diagnostic info
381
+ color_system = console.color_system or "unknown"
382
+
383
+ # Build the warning box
384
+ warning_lines = [
385
+ "",
386
+ "[bold bright_red on red]" + "━" * 72 + "[/]",
387
+ "[bold bright_red on red]┃[/][bold bright_white on red]"
388
+ + " " * 70
389
+ + "[/][bold bright_red on red]┃[/]",
390
+ "[bold bright_red on red]┃[/][bold bright_white on red] ⚠️ WARNING: TERMINAL DOES NOT SUPPORT TRUECOLOR (24-BIT COLOR) ⚠️ [/][bold bright_red on red]┃[/]",
391
+ "[bold bright_red on red]┃[/][bold bright_white on red]"
392
+ + " " * 70
393
+ + "[/][bold bright_red on red]┃[/]",
394
+ "[bold bright_red on red]" + "━" * 72 + "[/]",
395
+ "",
396
+ f"[yellow]Detected color system:[/] [bold]{color_system}[/]",
397
+ "",
398
+ "[bold white]Code Puppy uses rich colors and will look degraded without truecolor.[/]",
399
+ "",
400
+ "[cyan]Consider using a modern terminal emulator:[/]",
401
+ " [green]•[/] [bold]iTerm2[/] (macOS) - https://iterm2.com",
402
+ " [green]•[/] [bold]Windows Terminal[/] (Windows) - Built into Windows 11",
403
+ " [green]•[/] [bold]Kitty[/] - https://sw.kovidgoyal.net/kitty",
404
+ " [green]•[/] [bold]Alacritty[/] - https://alacritty.org",
405
+ " [green]•[/] [bold]Warp[/] (macOS) - https://warp.dev",
406
+ "",
407
+ "[cyan]Or try setting the COLORTERM environment variable:[/]",
408
+ " [dim]export COLORTERM=truecolor[/]",
409
+ "",
410
+ "[dim italic]Note: The built-in macOS Terminal.app does not support truecolor (Sequoia and earlier).[/]",
411
+ "[dim italic]Setting COLORTERM=truecolor won't help - you'll need a different terminal app.[/]",
412
+ "",
413
+ "[bold bright_red]" + "─" * 72 + "[/]",
414
+ "",
415
+ ]
416
+
417
+ for line in warning_lines:
418
+ console.print(line)
@@ -21,6 +21,7 @@ from code_puppy.config import (
21
21
  DATA_DIR,
22
22
  get_message_limit,
23
23
  get_use_dbos,
24
+ get_value,
24
25
  )
25
26
  from code_puppy.messaging import (
26
27
  SubAgentInvocationMessage,
@@ -471,39 +472,90 @@ def register_invoke_agent(agent):
471
472
  subagent_name = f"temp-invoke-agent-{session_id}"
472
473
  model_settings = make_model_settings(model_name)
473
474
 
474
- temp_agent = Agent(
475
- model=model,
476
- instructions=instructions,
477
- output_type=str,
478
- retries=3,
479
- history_processors=[agent_config.message_history_accumulator],
480
- model_settings=model_settings,
481
- )
475
+ # Get MCP servers for sub-agents (same as main agent)
476
+ from code_puppy.mcp_ import get_mcp_manager
482
477
 
483
- # Register the tools that the agent needs
484
- from code_puppy.tools import register_tools_for_agent
478
+ mcp_servers = []
479
+ mcp_disabled = get_value("disable_mcp_servers")
480
+ if not (
481
+ mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on")
482
+ ):
483
+ manager = get_mcp_manager()
484
+ mcp_servers = manager.get_servers_for_agent()
485
485
 
486
- agent_tools = agent_config.get_available_tools()
487
- register_tools_for_agent(temp_agent, agent_tools)
486
+ # Get the event_stream_handler for streaming output
487
+ from code_puppy.agents.event_stream_handler import event_stream_handler
488
488
 
489
489
  if get_use_dbos():
490
490
  from pydantic_ai.durable_exec.dbos import DBOSAgent
491
491
 
492
- dbos_agent = DBOSAgent(temp_agent, name=subagent_name)
492
+ # For DBOS, create agent without MCP servers (to avoid serialization issues)
493
+ # and add them at runtime
494
+ temp_agent = Agent(
495
+ model=model,
496
+ instructions=instructions,
497
+ output_type=str,
498
+ retries=3,
499
+ toolsets=[], # MCP servers added separately for DBOS
500
+ history_processors=[agent_config.message_history_accumulator],
501
+ model_settings=model_settings,
502
+ )
503
+
504
+ # Register the tools that the agent needs
505
+ from code_puppy.tools import register_tools_for_agent
506
+
507
+ agent_tools = agent_config.get_available_tools()
508
+ register_tools_for_agent(temp_agent, agent_tools)
509
+
510
+ # Wrap with DBOS - pass event_stream_handler for streaming output
511
+ dbos_agent = DBOSAgent(
512
+ temp_agent,
513
+ name=subagent_name,
514
+ event_stream_handler=event_stream_handler,
515
+ )
493
516
  temp_agent = dbos_agent
494
517
 
518
+ # Store MCP servers to add at runtime
519
+ subagent_mcp_servers = mcp_servers
520
+ else:
521
+ # Non-DBOS path - include MCP servers directly in the agent
522
+ temp_agent = Agent(
523
+ model=model,
524
+ instructions=instructions,
525
+ output_type=str,
526
+ retries=3,
527
+ toolsets=mcp_servers,
528
+ history_processors=[agent_config.message_history_accumulator],
529
+ model_settings=model_settings,
530
+ )
531
+
532
+ # Register the tools that the agent needs
533
+ from code_puppy.tools import register_tools_for_agent
534
+
535
+ agent_tools = agent_config.get_available_tools()
536
+ register_tools_for_agent(temp_agent, agent_tools)
537
+
538
+ subagent_mcp_servers = None
539
+
495
540
  # Run the temporary agent with the provided prompt as an asyncio task
496
541
  # Pass the message_history from the session to continue the conversation
497
542
  workflow_id = None # Track for potential cancellation
498
543
  if get_use_dbos():
499
544
  # Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
500
545
  workflow_id = _generate_dbos_workflow_id(group_id)
546
+
547
+ # Add MCP servers to the DBOS agent's toolsets
548
+ # (temp_agent is discarded after this invocation, so no need to restore)
549
+ if subagent_mcp_servers:
550
+ temp_agent._toolsets = temp_agent._toolsets + subagent_mcp_servers
551
+
501
552
  with SetWorkflowID(workflow_id):
502
553
  task = asyncio.create_task(
503
554
  temp_agent.run(
504
555
  prompt,
505
556
  message_history=message_history,
506
557
  usage_limits=UsageLimits(request_limit=get_message_limit()),
558
+ event_stream_handler=event_stream_handler,
507
559
  )
508
560
  )
509
561
  _active_subagent_tasks.add(task)
@@ -513,6 +565,7 @@ def register_invoke_agent(agent):
513
565
  prompt,
514
566
  message_history=message_history,
515
567
  usage_limits=UsageLimits(request_limit=get_message_limit()),
568
+ event_stream_handler=event_stream_handler,
516
569
  )
517
570
  )
518
571
  _active_subagent_tasks.add(task)
@@ -880,6 +880,7 @@ async def run_shell_command(
880
880
  command=command,
881
881
  cwd=cwd,
882
882
  timeout=0, # No timeout for background processes
883
+ background=True,
883
884
  )
884
885
  )
885
886
 
@@ -727,15 +727,9 @@ def _format_diff_with_syntax_highlighting(
727
727
  result.append("\n")
728
728
  continue
729
729
 
730
- # Handle diff headers specially
731
- if line.startswith("---"):
732
- result.append(line, style="yellow")
733
- elif line.startswith("+++"):
734
- result.append(line, style="yellow")
735
- elif line.startswith("@@"):
736
- result.append(line, style="cyan")
737
- elif line.startswith(("diff ", "index ")):
738
- result.append(line, style="dim")
730
+ # Skip diff headers - they're redundant noise since we show the filename in the banner
731
+ if line.startswith(("---", "+++", "@@", "diff ", "index ")):
732
+ continue
739
733
  else:
740
734
  # Determine line type and extract code content
741
735
  if line.startswith("-"):
@@ -55,9 +55,9 @@
55
55
  "supported_settings": ["reasoning_effort", "verbosity"],
56
56
  "supports_xhigh_reasoning": true
57
57
  },
58
- "Cerebras-GLM-4.6": {
58
+ "Cerebras-GLM-4.7": {
59
59
  "type": "cerebras",
60
- "name": "zai-glm-4.6",
60
+ "name": "zai-glm-4.7",
61
61
  "custom_endpoint": {
62
62
  "url": "https://api.cerebras.ai/v1",
63
63
  "api_key": "$CEREBRAS_API_KEY"