code-puppy 0.0.361__py3-none-any.whl → 0.0.372__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 (41) hide show
  1. code_puppy/agents/__init__.py +6 -0
  2. code_puppy/agents/agent_manager.py +223 -1
  3. code_puppy/agents/base_agent.py +2 -12
  4. code_puppy/chatgpt_codex_client.py +53 -0
  5. code_puppy/claude_cache_client.py +45 -7
  6. code_puppy/command_line/add_model_menu.py +13 -4
  7. code_puppy/command_line/agent_menu.py +662 -0
  8. code_puppy/command_line/core_commands.py +4 -112
  9. code_puppy/command_line/model_picker_completion.py +3 -20
  10. code_puppy/command_line/model_settings_menu.py +21 -3
  11. code_puppy/config.py +79 -8
  12. code_puppy/gemini_model.py +706 -0
  13. code_puppy/http_utils.py +6 -3
  14. code_puppy/model_factory.py +50 -16
  15. code_puppy/model_switching.py +63 -0
  16. code_puppy/model_utils.py +1 -52
  17. code_puppy/models.json +12 -12
  18. code_puppy/plugins/antigravity_oauth/antigravity_model.py +128 -165
  19. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  20. code_puppy/plugins/antigravity_oauth/transport.py +235 -45
  21. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  22. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  23. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  24. code_puppy/pydantic_patches.py +52 -0
  25. code_puppy/tools/agent_tools.py +3 -3
  26. code_puppy/tools/browser/__init__.py +1 -1
  27. code_puppy/tools/browser/browser_control.py +1 -1
  28. code_puppy/tools/browser/browser_interactions.py +1 -1
  29. code_puppy/tools/browser/browser_locators.py +1 -1
  30. code_puppy/tools/browser/{camoufox_manager.py → browser_manager.py} +29 -110
  31. code_puppy/tools/browser/browser_navigation.py +1 -1
  32. code_puppy/tools/browser/browser_screenshot.py +1 -1
  33. code_puppy/tools/browser/browser_scripts.py +1 -1
  34. {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  35. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/METADATA +5 -6
  36. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/RECORD +40 -38
  37. code_puppy/prompts/codex_system_prompt.md +0 -310
  38. {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  39. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  40. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  41. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -23,20 +23,78 @@ from .constants import (
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- def _inline_refs(
27
- schema: dict, convert_unions: bool = False, simplify_for_claude: bool = False
28
- ) -> dict:
26
+ def _flatten_union_to_object(union_items: list, defs: dict, resolve_fn) -> dict:
27
+ """Flatten a union of object types into a single object with all properties.
28
+
29
+ For discriminated unions like EditFilePayload (ContentPayload | ReplacementsPayload | DeleteSnippetPayload),
30
+ we merge all object types into one with all properties marked as optional.
31
+ """
32
+ merged_properties = {}
33
+ has_string_type = False
34
+
35
+ for item in union_items:
36
+ if not isinstance(item, dict):
37
+ continue
38
+
39
+ # Resolve $ref first
40
+ if "$ref" in item:
41
+ ref_path = item["$ref"]
42
+ ref_name = None
43
+ if ref_path.startswith("#/$defs/"):
44
+ ref_name = ref_path[8:]
45
+ elif ref_path.startswith("#/definitions/"):
46
+ ref_name = ref_path[14:]
47
+ if ref_name and ref_name in defs:
48
+ item = copy.deepcopy(defs[ref_name])
49
+ else:
50
+ continue
51
+
52
+ # Check for string type (common fallback)
53
+ if item.get("type") == "string":
54
+ has_string_type = True
55
+ continue
56
+
57
+ # Skip null types
58
+ if item.get("type") == "null":
59
+ continue
60
+
61
+ # Merge properties from object types
62
+ if item.get("type") == "object" or "properties" in item:
63
+ props = item.get("properties", {})
64
+ for prop_name, prop_schema in props.items():
65
+ if prop_name not in merged_properties:
66
+ # Resolve the property schema
67
+ merged_properties[prop_name] = resolve_fn(
68
+ copy.deepcopy(prop_schema)
69
+ )
70
+
71
+ if not merged_properties:
72
+ # No object properties found, return string type as fallback
73
+ return {"type": "string"} if has_string_type else {"type": "object"}
74
+
75
+ # Build merged object - no required fields since any subset is valid
76
+ result = {
77
+ "type": "object",
78
+ "properties": merged_properties,
79
+ }
80
+
81
+ return result
82
+
83
+
84
+ def _inline_refs(schema: dict, simplify_unions: bool = False) -> dict:
29
85
  """Inline $ref references and transform schema for Antigravity compatibility.
30
86
 
31
87
  - Inlines $ref references
32
88
  - Removes $defs, definitions, $schema, $id
33
- - Optionally converts anyOf/oneOf/allOf to any_of/one_of/all_of (only for Gemini)
34
89
  - Removes unsupported fields like 'default', 'examples', 'const'
35
- - For Claude: simplifies anyOf unions to single types
90
+ - When simplify_unions=True: flattens anyOf/oneOf unions:
91
+ - For unions of objects: merges into single object with all properties
92
+ - For simple unions (string | null): picks first non-null type
93
+ (required for both Gemini AND Claude - neither supports union types in function schemas!)
36
94
 
37
95
  Args:
38
- convert_unions: If True, convert anyOf->any_of etc. (for Gemini).
39
- simplify_for_claude: If True, simplify anyOf to single types.
96
+ simplify_unions: If True, simplify anyOf/oneOf unions.
97
+ Required for Gemini and Claude models.
40
98
  """
41
99
  if not isinstance(schema, dict):
42
100
  return schema
@@ -47,31 +105,73 @@ def _inline_refs(
47
105
  # Extract $defs for reference resolution
48
106
  defs = schema.pop("$defs", schema.pop("definitions", {}))
49
107
 
50
- def resolve_refs(
51
- obj, convert_unions=convert_unions, simplify_for_claude=simplify_for_claude
52
- ):
108
+ def resolve_refs(obj, simplify_unions=simplify_unions):
53
109
  """Recursively resolve $ref references and transform schema."""
54
110
  if isinstance(obj, dict):
55
- # For Claude: simplify anyOf/oneOf unions to first non-null type
56
- if simplify_for_claude:
111
+ # Handle anyOf/oneOf unions
112
+ if simplify_unions:
57
113
  for union_key in ["anyOf", "oneOf"]:
58
114
  if union_key in obj:
59
115
  union = obj[union_key]
60
116
  if isinstance(union, list):
61
- # Find first non-null type
117
+ # Check if this is a complex union of objects (discriminated union)
118
+ # vs a simple nullable type (string | null)
119
+ object_count = 0
120
+ has_refs = False
121
+ for item in union:
122
+ if isinstance(item, dict):
123
+ if "$ref" in item:
124
+ has_refs = True
125
+ object_count += 1
126
+ elif (
127
+ item.get("type") == "object"
128
+ or "properties" in item
129
+ ):
130
+ object_count += 1
131
+
132
+ # If multiple objects or has refs, flatten to single object
133
+ if object_count > 1 or has_refs:
134
+ flattened = _flatten_union_to_object(
135
+ union, defs, resolve_refs
136
+ )
137
+ # Keep description if present
138
+ if "description" in obj:
139
+ flattened["description"] = obj["description"]
140
+ return flattened
141
+
142
+ # Simple union - pick first non-null type
62
143
  for item in union:
63
144
  if (
64
145
  isinstance(item, dict)
65
146
  and item.get("type") != "null"
66
147
  ):
67
- # Replace the whole object with this type
68
148
  result = dict(item)
69
- # Keep description if present
70
149
  if "description" in obj:
71
150
  result["description"] = obj["description"]
72
- return resolve_refs(
73
- result, convert_unions, simplify_for_claude
74
- )
151
+ return resolve_refs(result, simplify_unions)
152
+
153
+ # Also handle allOf by merging all schemas
154
+ if simplify_unions and "allOf" in obj:
155
+ all_of = obj["allOf"]
156
+ if isinstance(all_of, list):
157
+ merged = {}
158
+ merged_properties = {}
159
+ for item in all_of:
160
+ if isinstance(item, dict):
161
+ resolved_item = resolve_refs(item, simplify_unions)
162
+ # Deep merge properties dicts
163
+ if "properties" in resolved_item:
164
+ merged_properties.update(
165
+ resolved_item.pop("properties")
166
+ )
167
+ merged.update(resolved_item)
168
+ if merged_properties:
169
+ merged["properties"] = merged_properties
170
+ # Keep other fields from original object (except allOf)
171
+ for k, v in obj.items():
172
+ if k != "allOf":
173
+ merged[k] = v
174
+ return resolve_refs(merged, simplify_unions)
75
175
 
76
176
  # Check for $ref
77
177
  if "$ref" in obj:
@@ -111,34 +211,23 @@ def _inline_refs(
111
211
  ):
112
212
  continue
113
213
 
114
- # For Claude: skip additionalProperties
115
- if simplify_for_claude and key == "additionalProperties":
214
+ # Skip additionalProperties (not supported by Gemini or Claude)
215
+ if simplify_unions and key == "additionalProperties":
116
216
  continue
117
217
 
118
- # Optionally transform union types for Gemini
119
- new_key = key
120
- if convert_unions:
121
- if key == "anyOf":
122
- new_key = "any_of"
123
- elif key == "oneOf":
124
- new_key = "one_of"
125
- elif key == "allOf":
126
- new_key = "all_of"
127
- elif key == "additionalProperties":
128
- new_key = "additional_properties"
129
-
130
- result[new_key] = resolve_refs(
131
- value, convert_unions, simplify_for_claude
132
- )
218
+ # Skip any remaining union type keys that weren't simplified above
219
+ # (This shouldn't happen normally, but just in case)
220
+ if simplify_unions and key in ("anyOf", "oneOf", "allOf"):
221
+ continue
222
+
223
+ result[key] = resolve_refs(value, simplify_unions)
133
224
  return result
134
225
  elif isinstance(obj, list):
135
- return [
136
- resolve_refs(item, convert_unions, simplify_for_claude) for item in obj
137
- ]
226
+ return [resolve_refs(item, simplify_unions) for item in obj]
138
227
  else:
139
228
  return obj
140
229
 
141
- return resolve_refs(schema, convert_unions, simplify_for_claude)
230
+ return resolve_refs(schema, simplify_unions)
142
231
 
143
232
 
144
233
  class UnwrappedResponse(httpx.Response):
@@ -264,17 +353,91 @@ class UnwrappedSSEResponse(httpx.Response):
264
353
 
265
354
 
266
355
  class AntigravityClient(httpx.AsyncClient):
267
- """Custom httpx client that handles Antigravity request/response wrapping."""
356
+ """Custom httpx client that handles Antigravity request/response wrapping.
357
+
358
+ Supports proactive token refresh to prevent expiry during long sessions.
359
+ """
268
360
 
269
361
  def __init__(
270
362
  self,
271
363
  project_id: str = "",
272
364
  model_name: str = "",
365
+ refresh_token: str = "",
366
+ expires_at: Optional[float] = None,
367
+ on_token_refreshed: Optional[Any] = None,
273
368
  **kwargs: Any,
274
369
  ):
275
370
  super().__init__(**kwargs)
276
371
  self.project_id = project_id
277
372
  self.model_name = model_name
373
+ self._refresh_token = refresh_token
374
+ self._expires_at = expires_at
375
+ self._on_token_refreshed = on_token_refreshed
376
+ self._refresh_lock = None # Lazy init for async lock
377
+
378
+ async def _ensure_valid_token(self) -> None:
379
+ """Proactively refresh the access token if it's expired or about to expire.
380
+
381
+ This prevents 401 errors during long-running sessions by checking and
382
+ refreshing the token BEFORE making requests, not after they fail.
383
+ """
384
+ import asyncio
385
+
386
+ from .token import is_token_expired, refresh_access_token
387
+
388
+ # Skip if no refresh token configured
389
+ if not self._refresh_token:
390
+ return
391
+
392
+ # Check if token needs refresh (includes 60-second buffer)
393
+ if not is_token_expired(self._expires_at):
394
+ return
395
+
396
+ # Lazy init the async lock
397
+ if self._refresh_lock is None:
398
+ self._refresh_lock = asyncio.Lock()
399
+
400
+ async with self._refresh_lock:
401
+ # Double-check after acquiring lock (another coroutine may have refreshed)
402
+ if not is_token_expired(self._expires_at):
403
+ return
404
+
405
+ logger.debug("Proactively refreshing Antigravity access token...")
406
+
407
+ try:
408
+ # Run the synchronous refresh in a thread pool to avoid blocking
409
+ loop = asyncio.get_event_loop()
410
+ new_tokens = await loop.run_in_executor(
411
+ None, refresh_access_token, self._refresh_token
412
+ )
413
+
414
+ if new_tokens:
415
+ # Update internal state
416
+ new_access_token = new_tokens.access_token
417
+ self._expires_at = new_tokens.expires_at
418
+ self._refresh_token = new_tokens.refresh_token
419
+
420
+ # Update the Authorization header
421
+ self.headers["Authorization"] = f"Bearer {new_access_token}"
422
+
423
+ logger.info(
424
+ "Proactively refreshed Antigravity token (expires in %ds)",
425
+ int(self._expires_at - __import__("time").time()),
426
+ )
427
+
428
+ # Notify callback (e.g., to persist updated tokens)
429
+ if self._on_token_refreshed:
430
+ try:
431
+ self._on_token_refreshed(new_tokens)
432
+ except Exception as e:
433
+ logger.warning("Token refresh callback failed: %s", e)
434
+ else:
435
+ logger.warning(
436
+ "Failed to proactively refresh token - request may fail with 401"
437
+ )
438
+
439
+ except Exception as e:
440
+ logger.warning("Proactive token refresh error: %s", e)
278
441
 
279
442
  def _wrap_request(self, content: bytes, url: str) -> tuple[bytes, str, str, bool]:
280
443
  """Wrap request body in Antigravity envelope and transform URL.
@@ -331,15 +494,14 @@ class AntigravityClient(httpx.AsyncClient):
331
494
  )
332
495
 
333
496
  # Inline $refs and remove $defs from parameters
334
- # Convert unions (anyOf->any_of) only for Gemini
335
- # Simplify schemas for Claude (no anyOf, no additionalProperties)
497
+ # Simplify union types (anyOf/oneOf/allOf) for BOTH Gemini and Claude
498
+ # Neither API supports union types in function schemas!
336
499
  if "parameters" in func_decl:
337
500
  is_gemini = "gemini" in model.lower()
338
501
  is_claude = "claude" in model.lower()
339
502
  func_decl["parameters"] = _inline_refs(
340
503
  func_decl["parameters"],
341
- convert_unions=is_gemini,
342
- simplify_for_claude=is_claude,
504
+ simplify_unions=(is_gemini or is_claude),
343
505
  )
344
506
 
345
507
  # Fix generationConfig for Antigravity compatibility
@@ -425,6 +587,9 @@ class AntigravityClient(httpx.AsyncClient):
425
587
  """Override send to intercept at the lowest level with endpoint fallback."""
426
588
  import asyncio
427
589
 
590
+ # Proactively refresh token BEFORE making the request
591
+ await self._ensure_valid_token()
592
+
428
593
  # Transform POST requests to Antigravity format
429
594
  if request.method == "POST" and request.content:
430
595
  new_content, new_path, new_query, is_claude_thinking = self._wrap_request(
@@ -638,14 +803,36 @@ class AntigravityClient(httpx.AsyncClient):
638
803
  return None
639
804
 
640
805
 
806
+ # Type alias for token refresh callback
807
+ TokenRefreshCallback = Any # Callable[[OAuthTokens], None]
808
+
809
+
641
810
  def create_antigravity_client(
642
811
  access_token: str,
643
812
  project_id: str = "",
644
813
  model_name: str = "",
645
814
  base_url: str = "https://daily-cloudcode-pa.sandbox.googleapis.com",
646
815
  headers: Optional[Dict[str, str]] = None,
816
+ refresh_token: str = "",
817
+ expires_at: Optional[float] = None,
818
+ on_token_refreshed: Optional[TokenRefreshCallback] = None,
647
819
  ) -> AntigravityClient:
648
- """Create an httpx client configured for Antigravity API."""
820
+ """Create an httpx client configured for Antigravity API.
821
+
822
+ Args:
823
+ access_token: The OAuth access token for authentication
824
+ project_id: The GCP project ID
825
+ model_name: The model name being used
826
+ base_url: The API base URL
827
+ headers: Additional headers to include
828
+ refresh_token: The OAuth refresh token for proactive token refresh
829
+ expires_at: Unix timestamp when the access token expires
830
+ on_token_refreshed: Callback called when token is proactively refreshed,
831
+ receives OAuthTokens object to persist the new tokens
832
+
833
+ Returns:
834
+ An AntigravityClient configured for API requests with proactive token refresh
835
+ """
649
836
  # Start with Antigravity-specific headers
650
837
  default_headers = {
651
838
  "Authorization": f"Bearer {access_token}",
@@ -659,6 +846,9 @@ def create_antigravity_client(
659
846
  return AntigravityClient(
660
847
  project_id=project_id,
661
848
  model_name=model_name,
849
+ refresh_token=refresh_token,
850
+ expires_at=expires_at,
851
+ on_token_refreshed=on_token_refreshed,
662
852
  base_url=base_url,
663
853
  headers=default_headers,
664
854
  timeout=httpx.Timeout(180.0, connect=30.0),
@@ -6,8 +6,8 @@ import os
6
6
  from typing import List, Optional, Tuple
7
7
 
8
8
  from code_puppy.callbacks import register_callback
9
- from code_puppy.config import set_model_name
10
9
  from code_puppy.messaging import emit_info, emit_success, emit_warning
10
+ from code_puppy.model_switching import set_model_and_reload_agent
11
11
 
12
12
  from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
13
13
  from .oauth_flow import run_oauth_flow
@@ -76,7 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
76
76
 
77
77
  if name == "chatgpt-auth":
78
78
  run_oauth_flow()
79
- set_model_name("chatgpt-gpt-5.2-codex")
79
+ set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
80
80
  return True
81
81
 
82
82
  if name == "chatgpt-status":
@@ -12,8 +12,8 @@ from typing import Any, Dict, List, Optional, Tuple
12
12
  from urllib.parse import parse_qs, urlparse
13
13
 
14
14
  from code_puppy.callbacks import register_callback
15
- from code_puppy.config import set_model_name
16
15
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
16
+ from code_puppy.model_switching import set_model_and_reload_agent
17
17
 
18
18
  from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
19
19
  from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
@@ -181,31 +181,6 @@ def _custom_help() -> List[Tuple[str, str]]:
181
181
  ]
182
182
 
183
183
 
184
- def _reload_current_agent() -> None:
185
- """Reload the current agent so new auth tokens are picked up immediately."""
186
- try:
187
- from code_puppy.agents import get_current_agent
188
-
189
- current_agent = get_current_agent()
190
- if current_agent is None:
191
- logger.debug("No current agent to reload")
192
- return
193
-
194
- # JSON agents may need to refresh their config before reload
195
- if hasattr(current_agent, "refresh_config"):
196
- try:
197
- current_agent.refresh_config()
198
- except Exception:
199
- # Non-fatal, continue to reload
200
- pass
201
-
202
- current_agent.reload_code_generation_agent()
203
- emit_info("Active agent reloaded with new authentication")
204
- except Exception as e:
205
- emit_warning(f"Authentication succeeded but agent reload failed: {e}")
206
- logger.exception("Failed to reload agent after authentication")
207
-
208
-
209
184
  def _perform_authentication() -> None:
210
185
  context = prepare_oauth_context()
211
186
  code = _await_callback(context)
@@ -245,9 +220,6 @@ def _perform_authentication() -> None:
245
220
  "Claude Code models added to your configuration. Use the `claude-code-` prefix!"
246
221
  )
247
222
 
248
- # Reload the current agent so the new auth token is picked up immediately
249
- _reload_current_agent()
250
-
251
223
 
252
224
  def _handle_custom_command(command: str, name: str) -> Optional[bool]:
253
225
  if not name:
@@ -261,7 +233,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
261
233
  "Existing Claude Code tokens found. Continuing will overwrite them."
262
234
  )
263
235
  _perform_authentication()
264
- set_model_name("claude-code-claude-opus-4-5-20251101")
236
+ set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
265
237
  return True
266
238
 
267
239
  if name == "claude-code-status":
@@ -21,7 +21,10 @@ from .config import (
21
21
  get_token_storage_path,
22
22
  )
23
23
 
24
- TOKEN_REFRESH_BUFFER_SECONDS = 60
24
+ # Proactive refresh buffer: refresh tokens 1 hour before expiration
25
+ # This ensures smooth operation by refreshing during model requests,
26
+ # well before the token actually expires
27
+ TOKEN_REFRESH_BUFFER_SECONDS = 3600 # 1 hour
25
28
 
26
29
  logger = logging.getLogger(__name__)
27
30
 
@@ -121,6 +121,57 @@ def patch_process_message_history() -> None:
121
121
  pass
122
122
 
123
123
 
124
+ def patch_tool_call_json_repair() -> None:
125
+ """Patch pydantic-ai's _call_tool to auto-repair malformed JSON arguments.
126
+
127
+ LLMs sometimes produce slightly broken JSON in tool calls (trailing commas,
128
+ missing quotes, etc.). This patch intercepts tool calls and runs json_repair
129
+ on the arguments before validation, preventing unnecessary retries.
130
+ """
131
+ try:
132
+ import json_repair
133
+ from pydantic_ai._tool_manager import ToolManager
134
+
135
+ # Store the original method
136
+ _original_call_tool = ToolManager._call_tool
137
+
138
+ async def _patched_call_tool(
139
+ self,
140
+ call,
141
+ *,
142
+ allow_partial: bool,
143
+ wrap_validation_errors: bool,
144
+ approved: bool,
145
+ ):
146
+ """Patched _call_tool that repairs malformed JSON before validation."""
147
+ # Only attempt repair if args is a string (JSON)
148
+ if isinstance(call.args, str) and call.args:
149
+ try:
150
+ repaired = json_repair.repair_json(call.args)
151
+ if repaired != call.args:
152
+ # Update the call args with repaired JSON
153
+ call.args = repaired
154
+ except Exception:
155
+ pass # If repair fails, let original validation handle it
156
+
157
+ # Call the original method
158
+ return await _original_call_tool(
159
+ self,
160
+ call,
161
+ allow_partial=allow_partial,
162
+ wrap_validation_errors=wrap_validation_errors,
163
+ approved=approved,
164
+ )
165
+
166
+ # Apply the patch
167
+ ToolManager._call_tool = _patched_call_tool
168
+
169
+ except ImportError:
170
+ pass # json_repair or pydantic_ai not available
171
+ except Exception:
172
+ pass # Don't crash on patch failure
173
+
174
+
124
175
  def apply_all_patches() -> None:
125
176
  """Apply all pydantic-ai monkey patches.
126
177
 
@@ -129,3 +180,4 @@ def apply_all_patches() -> None:
129
180
  patch_user_agent()
130
181
  patch_message_history_cleaning()
131
182
  patch_process_message_history()
183
+ patch_tool_call_json_repair()
@@ -443,9 +443,9 @@ def register_invoke_agent(agent):
443
443
 
444
444
  terminal_session_token = set_terminal_session(f"terminal-{session_id}")
445
445
 
446
- # Set browser session for Camoufox browser tools (qa-kitten, etc.)
446
+ # Set browser session for browser tools (qa-kitten, etc.)
447
447
  # This allows parallel agent invocations to each have their own browser
448
- from code_puppy.tools.browser.camoufox_manager import (
448
+ from code_puppy.tools.browser.browser_manager import (
449
449
  set_browser_session,
450
450
  )
451
451
 
@@ -665,7 +665,7 @@ def register_invoke_agent(agent):
665
665
  # Reset terminal session context
666
666
  _terminal_session_var.reset(terminal_session_token)
667
667
  # Reset browser session context
668
- from code_puppy.tools.browser.camoufox_manager import (
668
+ from code_puppy.tools.browser.browser_manager import (
669
669
  _browser_session_var,
670
670
  )
671
671
 
@@ -5,7 +5,7 @@ This module provides browser-based terminal automation tools.
5
5
 
6
6
  from code_puppy.config import get_banner_color
7
7
 
8
- from .camoufox_manager import (
8
+ from .browser_manager import (
9
9
  cleanup_all_browsers,
10
10
  get_browser_session,
11
11
  get_session_browser_manager,
@@ -7,7 +7,7 @@ from pydantic_ai import RunContext
7
7
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
8
8
  from code_puppy.tools.common import generate_group_id
9
9
 
10
- from .camoufox_manager import get_session_browser_manager
10
+ from .browser_manager import get_session_browser_manager
11
11
 
12
12
 
13
13
  async def initialize_browser(
@@ -7,7 +7,7 @@ from pydantic_ai import RunContext
7
7
  from code_puppy.messaging import emit_error, emit_info, emit_success
8
8
  from code_puppy.tools.common import generate_group_id
9
9
 
10
- from .camoufox_manager import get_session_browser_manager
10
+ from .browser_manager import get_session_browser_manager
11
11
 
12
12
 
13
13
  async def click_element(
@@ -7,7 +7,7 @@ from pydantic_ai import RunContext
7
7
  from code_puppy.messaging import emit_info, emit_success
8
8
  from code_puppy.tools.common import generate_group_id
9
9
 
10
- from .camoufox_manager import get_session_browser_manager
10
+ from .browser_manager import get_session_browser_manager
11
11
 
12
12
 
13
13
  async def find_by_role(