code-puppy 0.0.325__py3-none-any.whl → 0.0.336__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 (44) hide show
  1. code_puppy/agents/base_agent.py +41 -103
  2. code_puppy/cli_runner.py +105 -2
  3. code_puppy/command_line/add_model_menu.py +4 -0
  4. code_puppy/command_line/autosave_menu.py +5 -0
  5. code_puppy/command_line/colors_menu.py +5 -0
  6. code_puppy/command_line/config_commands.py +24 -1
  7. code_puppy/command_line/core_commands.py +51 -0
  8. code_puppy/command_line/diff_menu.py +5 -0
  9. code_puppy/command_line/mcp/custom_server_form.py +4 -0
  10. code_puppy/command_line/mcp/install_menu.py +5 -1
  11. code_puppy/command_line/model_settings_menu.py +5 -0
  12. code_puppy/command_line/motd.py +13 -7
  13. code_puppy/command_line/onboarding_slides.py +180 -0
  14. code_puppy/command_line/onboarding_wizard.py +340 -0
  15. code_puppy/config.py +3 -2
  16. code_puppy/http_utils.py +155 -196
  17. code_puppy/keymap.py +10 -8
  18. code_puppy/messaging/rich_renderer.py +101 -19
  19. code_puppy/model_factory.py +86 -15
  20. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  21. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  22. code_puppy/plugins/antigravity_oauth/antigravity_model.py +653 -0
  23. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  24. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  25. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  26. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  27. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  28. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  29. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  30. code_puppy/plugins/antigravity_oauth/transport.py +664 -0
  31. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  32. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
  33. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
  34. code_puppy/reopenable_async_client.py +8 -8
  35. code_puppy/terminal_utils.py +168 -3
  36. code_puppy/tools/command_runner.py +42 -54
  37. code_puppy/uvx_detection.py +242 -0
  38. {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/METADATA +30 -1
  39. {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/RECORD +44 -29
  40. {code_puppy-0.0.325.data → code_puppy-0.0.336.data}/data/code_puppy/models.json +0 -0
  41. {code_puppy-0.0.325.data → code_puppy-0.0.336.data}/data/code_puppy/models_dev_api.json +0 -0
  42. {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/WHEEL +0 -0
  43. {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/entry_points.txt +0 -0
  44. {code_puppy-0.0.325.dist-info → code_puppy-0.0.336.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,169 @@
1
+ """Utility helpers for the Antigravity OAuth plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from .config import (
10
+ ANTIGRAVITY_OAUTH_CONFIG,
11
+ get_antigravity_models_path,
12
+ get_token_storage_path,
13
+ )
14
+ from .constants import ANTIGRAVITY_ENDPOINT, ANTIGRAVITY_HEADERS, ANTIGRAVITY_MODELS
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def load_stored_tokens() -> Optional[Dict[str, Any]]:
20
+ """Load stored OAuth tokens from disk."""
21
+ try:
22
+ token_path = get_token_storage_path()
23
+ if token_path.exists():
24
+ with open(token_path, "r", encoding="utf-8") as f:
25
+ return json.load(f)
26
+ except Exception as e:
27
+ logger.error("Failed to load tokens: %s", e)
28
+ return None
29
+
30
+
31
+ def save_tokens(tokens: Dict[str, Any]) -> bool:
32
+ """Save OAuth tokens to disk."""
33
+ try:
34
+ token_path = get_token_storage_path()
35
+ with open(token_path, "w", encoding="utf-8") as f:
36
+ json.dump(tokens, f, indent=2)
37
+ token_path.chmod(0o600)
38
+ return True
39
+ except Exception as e:
40
+ logger.error("Failed to save tokens: %s", e)
41
+ return False
42
+
43
+
44
+ def load_antigravity_models() -> Dict[str, Any]:
45
+ """Load configured Antigravity models from disk."""
46
+ try:
47
+ models_path = get_antigravity_models_path()
48
+ if models_path.exists():
49
+ with open(models_path, "r", encoding="utf-8") as f:
50
+ return json.load(f)
51
+ except Exception as e:
52
+ logger.error("Failed to load Antigravity models: %s", e)
53
+ return {}
54
+
55
+
56
+ def save_antigravity_models(models: Dict[str, Any]) -> bool:
57
+ """Save Antigravity models configuration to disk."""
58
+ try:
59
+ models_path = get_antigravity_models_path()
60
+ with open(models_path, "w", encoding="utf-8") as f:
61
+ json.dump(models, f, indent=2)
62
+ return True
63
+ except Exception as e:
64
+ logger.error("Failed to save Antigravity models: %s", e)
65
+ return False
66
+
67
+
68
+ def add_models_to_config(access_token: str, project_id: str = "") -> bool:
69
+ """Add all available Antigravity models to the configuration."""
70
+ try:
71
+ models_config: Dict[str, Any] = {}
72
+ prefix = ANTIGRAVITY_OAUTH_CONFIG["prefix"]
73
+
74
+ for model_id, model_info in ANTIGRAVITY_MODELS.items():
75
+ prefixed_name = f"{prefix}{model_id}"
76
+
77
+ # Build custom headers
78
+ headers = dict(ANTIGRAVITY_HEADERS)
79
+
80
+ # Use custom_gemini type with Antigravity transport
81
+ models_config[prefixed_name] = {
82
+ "type": "custom_gemini",
83
+ "name": model_id,
84
+ "custom_endpoint": {
85
+ "url": ANTIGRAVITY_ENDPOINT,
86
+ "api_key": access_token,
87
+ "headers": headers,
88
+ },
89
+ "project_id": project_id,
90
+ "context_length": model_info.get("context_length", 200000),
91
+ "family": model_info.get("family", "other"),
92
+ "oauth_source": "antigravity-plugin",
93
+ "antigravity": True, # Flag to use Antigravity transport
94
+ }
95
+
96
+ # Add thinking budget if present
97
+ if model_info.get("thinking_budget"):
98
+ models_config[prefixed_name]["thinking_budget"] = model_info[
99
+ "thinking_budget"
100
+ ]
101
+
102
+ if save_antigravity_models(models_config):
103
+ logger.info("Added %d Antigravity models", len(models_config))
104
+ return True
105
+
106
+ except Exception as e:
107
+ logger.error("Error adding models to config: %s", e)
108
+ return False
109
+
110
+
111
+ def remove_antigravity_models() -> int:
112
+ """Remove all Antigravity models from configuration."""
113
+ try:
114
+ models = load_antigravity_models()
115
+ to_remove = [
116
+ name
117
+ for name, config in models.items()
118
+ if config.get("oauth_source") == "antigravity-plugin"
119
+ ]
120
+
121
+ if not to_remove:
122
+ return 0
123
+
124
+ for model_name in to_remove:
125
+ models.pop(model_name, None)
126
+
127
+ if save_antigravity_models(models):
128
+ return len(to_remove)
129
+ except Exception as e:
130
+ logger.error("Error removing Antigravity models: %s", e)
131
+ return 0
132
+
133
+
134
+ def get_model_families_summary() -> Dict[str, List[str]]:
135
+ """Get a summary of available models by family."""
136
+ families: Dict[str, List[str]] = {
137
+ "gemini": [],
138
+ "claude": [],
139
+ "other": [],
140
+ }
141
+
142
+ for model_id, info in ANTIGRAVITY_MODELS.items():
143
+ family = info.get("family", "other")
144
+ if family in families:
145
+ families[family].append(model_id)
146
+
147
+ return families
148
+
149
+
150
+ def reload_current_agent() -> None:
151
+ """Reload the current agent so new auth tokens are picked up immediately."""
152
+ try:
153
+ from code_puppy.agents import get_current_agent
154
+
155
+ current_agent = get_current_agent()
156
+ if current_agent is None:
157
+ logger.debug("No current agent to reload")
158
+ return
159
+
160
+ if hasattr(current_agent, "refresh_config"):
161
+ try:
162
+ current_agent.refresh_config()
163
+ except Exception:
164
+ pass
165
+
166
+ current_agent.reload_code_generation_agent()
167
+ logger.info("Active agent reloaded with new authentication")
168
+ except Exception as e:
169
+ logger.warning("Agent reload failed: %s", e)
@@ -6,6 +6,7 @@ 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
9
10
  from code_puppy.messaging import emit_info, emit_success, emit_warning
10
11
 
11
12
  from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
@@ -75,6 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
75
76
 
76
77
  if name == "chatgpt-auth":
77
78
  run_oauth_flow()
79
+ set_model_name("chatgpt-gpt-5.2-codex")
78
80
  return True
79
81
 
80
82
  if name == "chatgpt-status":
@@ -12,6 +12,7 @@ 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
15
16
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
16
17
 
17
18
  from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
@@ -260,6 +261,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
260
261
  "Existing Claude Code tokens found. Continuing will overwrite them."
261
262
  )
262
263
  _perform_authentication()
264
+ set_model_name("claude-code-claude-opus-4-5-20251101")
263
265
  return True
264
266
 
265
267
  if name == "claude-code-status":
@@ -54,13 +54,15 @@ class ReopenableAsyncClient:
54
54
  if self._stream_context:
55
55
  return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
56
56
 
57
- def __init__(self, **kwargs):
57
+ def __init__(self, client_class=None, **kwargs):
58
58
  """
59
59
  Initialize the ReopenableAsyncClient.
60
60
 
61
61
  Args:
62
- **kwargs: All arguments that would be passed to httpx.AsyncClient()
62
+ client_class: Class to use for creating the internal client (defaults to httpx.AsyncClient)
63
+ **kwargs: All arguments that would be passed to the client constructor
63
64
  """
65
+ self._client_class = client_class or httpx.AsyncClient
64
66
  self._client_kwargs = kwargs.copy()
65
67
  self._client: Optional[httpx.AsyncClient] = None
66
68
  self._is_closed = True
@@ -70,7 +72,7 @@ class ReopenableAsyncClient:
70
72
  Ensure the underlying client is open and ready to use.
71
73
 
72
74
  Returns:
73
- The active httpx.AsyncClient instance
75
+ The active client instance
74
76
 
75
77
  Raises:
76
78
  RuntimeError: If client cannot be opened
@@ -80,12 +82,12 @@ class ReopenableAsyncClient:
80
82
  return self._client
81
83
 
82
84
  async def _create_client(self) -> None:
83
- """Create a new httpx.AsyncClient with the stored configuration."""
85
+ """Create a new client with the stored configuration."""
84
86
  if self._client is not None and not self._is_closed:
85
87
  # Close existing client first
86
88
  await self._client.aclose()
87
89
 
88
- self._client = httpx.AsyncClient(**self._client_kwargs)
90
+ self._client = self._client_class(**self._client_kwargs)
89
91
  self._is_closed = False
90
92
 
91
93
  async def reopen(self) -> None:
@@ -171,14 +173,12 @@ class ReopenableAsyncClient:
171
173
  """
172
174
  if self._client is None or self._is_closed:
173
175
  # Create a temporary client just for building the request
174
- temp_client = httpx.AsyncClient(**self._client_kwargs)
176
+ temp_client = self._client_class(**self._client_kwargs)
175
177
  try:
176
178
  request = temp_client.build_request(method, url, **kwargs)
177
179
  return request
178
180
  finally:
179
181
  # Clean up the temporary client synchronously if possible
180
- # Note: This might leave a connection open, but it's better than
181
- # making this method async just for building requests
182
182
  pass
183
183
  return self._client.build_request(method, url, **kwargs)
184
184
 
@@ -6,6 +6,10 @@ Handles Windows console mode resets and Unix terminal sanity restoration.
6
6
  import platform
7
7
  import subprocess
8
8
  import sys
9
+ from typing import Callable, Optional
10
+
11
+ # Store the original console ctrl handler so we can restore it if needed
12
+ _original_ctrl_handler: Optional[Callable] = None
9
13
 
10
14
 
11
15
  def reset_windows_terminal_ansi() -> None:
@@ -86,17 +90,36 @@ def reset_windows_console_mode() -> None:
86
90
  pass # Silently ignore errors - best effort reset
87
91
 
88
92
 
93
+ def flush_windows_keyboard_buffer() -> None:
94
+ """Flush the Windows keyboard buffer.
95
+
96
+ Clears any pending keyboard input that could interfere with
97
+ subsequent input operations after an interrupt.
98
+ """
99
+ if platform.system() != "Windows":
100
+ return
101
+
102
+ try:
103
+ import msvcrt
104
+
105
+ while msvcrt.kbhit():
106
+ msvcrt.getch()
107
+ except Exception:
108
+ pass # Silently ignore errors - best effort flush
109
+
110
+
89
111
  def reset_windows_terminal_full() -> None:
90
- """Perform a full Windows terminal reset (ANSI + console mode).
112
+ """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
91
113
 
92
- Combines both ANSI reset and console mode reset for complete
93
- terminal state restoration after interrupts.
114
+ Combines ANSI reset, console mode reset, and keyboard buffer flush
115
+ for complete terminal state restoration after interrupts.
94
116
  """
95
117
  if platform.system() != "Windows":
96
118
  return
97
119
 
98
120
  reset_windows_terminal_ansi()
99
121
  reset_windows_console_mode()
122
+ flush_windows_keyboard_buffer()
100
123
 
101
124
 
102
125
  def reset_unix_terminal() -> None:
@@ -124,3 +147,145 @@ def reset_terminal() -> None:
124
147
  reset_windows_terminal_full()
125
148
  else:
126
149
  reset_unix_terminal()
150
+
151
+
152
+ def disable_windows_ctrl_c() -> bool:
153
+ """Disable Ctrl+C processing at the Windows console input level.
154
+
155
+ This removes ENABLE_PROCESSED_INPUT from stdin, which prevents
156
+ Ctrl+C from being interpreted as a signal at all. Instead, it
157
+ becomes just a regular character (^C) that gets ignored.
158
+
159
+ This is more reliable than SetConsoleCtrlHandler because it
160
+ prevents Ctrl+C from being processed before it reaches any handler.
161
+
162
+ Returns:
163
+ True if successfully disabled, False otherwise.
164
+ """
165
+ global _original_ctrl_handler
166
+
167
+ if platform.system() != "Windows":
168
+ return False
169
+
170
+ try:
171
+ import ctypes
172
+
173
+ kernel32 = ctypes.windll.kernel32
174
+
175
+ # Get stdin handle
176
+ STD_INPUT_HANDLE = -10
177
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
178
+
179
+ # Get current console mode
180
+ mode = ctypes.c_ulong()
181
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
182
+ return False
183
+
184
+ # Save original mode for potential restoration
185
+ _original_ctrl_handler = mode.value
186
+
187
+ # Console mode flags
188
+ ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals
189
+
190
+ # Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation
191
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
192
+
193
+ if kernel32.SetConsoleMode(stdin_handle, new_mode):
194
+ return True
195
+ return False
196
+
197
+ except Exception:
198
+ return False
199
+
200
+
201
+ def enable_windows_ctrl_c() -> bool:
202
+ """Re-enable Ctrl+C at the Windows console level.
203
+
204
+ Restores the original console mode saved by disable_windows_ctrl_c().
205
+
206
+ Returns:
207
+ True if successfully re-enabled, False otherwise.
208
+ """
209
+ global _original_ctrl_handler
210
+
211
+ if platform.system() != "Windows":
212
+ return False
213
+
214
+ if _original_ctrl_handler is None:
215
+ return True # Nothing to restore
216
+
217
+ try:
218
+ import ctypes
219
+
220
+ kernel32 = ctypes.windll.kernel32
221
+
222
+ # Get stdin handle
223
+ STD_INPUT_HANDLE = -10
224
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
225
+
226
+ # Restore original mode
227
+ if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler):
228
+ _original_ctrl_handler = None
229
+ return True
230
+ return False
231
+
232
+ except Exception:
233
+ return False
234
+
235
+
236
+ # Flag to track if we should keep Ctrl+C disabled
237
+ _keep_ctrl_c_disabled: bool = False
238
+
239
+
240
+ def set_keep_ctrl_c_disabled(value: bool) -> None:
241
+ """Set whether Ctrl+C should be kept disabled.
242
+
243
+ When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C
244
+ even if something else (like prompt_toolkit) re-enables it.
245
+ """
246
+ global _keep_ctrl_c_disabled
247
+ _keep_ctrl_c_disabled = value
248
+
249
+
250
+ def ensure_ctrl_c_disabled() -> bool:
251
+ """Ensure Ctrl+C is disabled if it should be.
252
+
253
+ Call this after operations that might restore console mode
254
+ (like prompt_toolkit input).
255
+
256
+ Returns:
257
+ True if Ctrl+C is now disabled (or wasn't needed), False on error.
258
+ """
259
+ if not _keep_ctrl_c_disabled:
260
+ return True
261
+
262
+ if platform.system() != "Windows":
263
+ return True
264
+
265
+ try:
266
+ import ctypes
267
+
268
+ kernel32 = ctypes.windll.kernel32
269
+
270
+ # Get stdin handle
271
+ STD_INPUT_HANDLE = -10
272
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
273
+
274
+ # Get current console mode
275
+ mode = ctypes.c_ulong()
276
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
277
+ return False
278
+
279
+ # Console mode flags
280
+ ENABLE_PROCESSED_INPUT = 0x0001
281
+
282
+ # Check if Ctrl+C processing is enabled
283
+ if mode.value & ENABLE_PROCESSED_INPUT:
284
+ # Disable it
285
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
286
+ return bool(kernel32.SetConsoleMode(stdin_handle, new_mode))
287
+
288
+ return True # Already disabled
289
+
290
+ except Exception:
291
+ return False
@@ -9,7 +9,7 @@ import threading
9
9
  import time
10
10
  import traceback
11
11
  from contextlib import contextmanager
12
- from typing import Callable, Literal, Optional, Set
12
+ from typing import Callable, List, Literal, Optional, Set
13
13
 
14
14
  from pydantic import BaseModel
15
15
  from pydantic_ai import RunContext
@@ -192,11 +192,6 @@ def kill_all_running_shell_processes() -> int:
192
192
  """Kill all currently tracked running shell processes and stop reader threads.
193
193
 
194
194
  Returns the number of processes signaled.
195
-
196
- Implementation notes:
197
- - Atomically snapshot and clear the registry to prevent race conditions
198
- - Deduplicate by PID to ensure each process is killed at most once
199
- - Let exceptions from _kill_process_group propagate (tests expect this)
200
195
  """
201
196
  global _READER_STOP_EVENT
202
197
 
@@ -204,52 +199,30 @@ def kill_all_running_shell_processes() -> int:
204
199
  if _READER_STOP_EVENT:
205
200
  _READER_STOP_EVENT.set()
206
201
 
207
- # Atomically take snapshot and clear registry
208
- # This prevents other threads from seeing/processing the same processes
202
+ procs: list[subprocess.Popen]
209
203
  with _RUNNING_PROCESSES_LOCK:
210
- procs_snapshot = list(_RUNNING_PROCESSES)
211
- _RUNNING_PROCESSES.clear()
212
-
213
- # Deduplicate by pid to ensure at-most-one kill per process
214
- seen_pids: set = set()
215
- killed_count = 0
216
-
217
- for proc in procs_snapshot:
218
- if proc is None:
219
- continue
220
-
221
- pid = getattr(proc, "pid", None)
222
- key = pid if pid is not None else id(proc)
223
-
224
- if key in seen_pids:
225
- continue
226
- seen_pids.add(key)
227
-
228
- # Close pipes first to unblock readline()
204
+ procs = list(_RUNNING_PROCESSES)
205
+ count = 0
206
+ for p in procs:
229
207
  try:
230
- if proc.stdout and not proc.stdout.closed:
231
- proc.stdout.close()
232
- if proc.stderr and not proc.stderr.closed:
233
- proc.stderr.close()
234
- if proc.stdin and not proc.stdin.closed:
235
- proc.stdin.close()
236
- except (OSError, ValueError):
237
- pass
238
-
239
- # Only attempt to kill processes that are still running
240
- if proc.poll() is None:
241
- # Let exceptions bubble up (tests expect this behavior)
242
- _kill_process_group(proc)
243
- killed_count += 1
244
-
245
- # Track user-killed PIDs
246
- if pid is not None:
247
- try:
248
- _USER_KILLED_PROCESSES.add(pid)
249
- except Exception:
250
- pass # Non-fatal bookkeeping
208
+ # Close pipes first to unblock readline()
209
+ try:
210
+ if p.stdout and not p.stdout.closed:
211
+ p.stdout.close()
212
+ if p.stderr and not p.stderr.closed:
213
+ p.stderr.close()
214
+ if p.stdin and not p.stdin.closed:
215
+ p.stdin.close()
216
+ except (OSError, ValueError):
217
+ pass
251
218
 
252
- return killed_count
219
+ if p.poll() is None:
220
+ _kill_process_group(p)
221
+ count += 1
222
+ _USER_KILLED_PROCESSES.add(p.pid)
223
+ finally:
224
+ _unregister_process(p)
225
+ return count
253
226
 
254
227
 
255
228
  def get_running_shell_process_count() -> int:
@@ -1104,12 +1077,21 @@ class ReasoningOutput(BaseModel):
1104
1077
 
1105
1078
 
1106
1079
  def share_your_reasoning(
1107
- context: RunContext, reasoning: str, next_steps: str | None = None
1080
+ context: RunContext, reasoning: str, next_steps: str | List[str] | None = None
1108
1081
  ) -> ReasoningOutput:
1082
+ # Handle list of next steps by formatting them
1083
+ formatted_next_steps = next_steps
1084
+ if isinstance(next_steps, list):
1085
+ formatted_next_steps = "\n".join(
1086
+ [f"{i + 1}. {step}" for i, step in enumerate(next_steps)]
1087
+ )
1088
+
1109
1089
  # Emit structured AgentReasoningMessage for the UI
1110
1090
  reasoning_msg = AgentReasoningMessage(
1111
1091
  reasoning=reasoning,
1112
- next_steps=next_steps if next_steps and next_steps.strip() else None,
1092
+ next_steps=formatted_next_steps
1093
+ if formatted_next_steps and formatted_next_steps.strip()
1094
+ else None,
1113
1095
  )
1114
1096
  get_message_bus().emit(reasoning_msg)
1115
1097
 
@@ -1197,7 +1179,9 @@ def register_agent_share_your_reasoning(agent):
1197
1179
 
1198
1180
  @agent.tool
1199
1181
  def agent_share_your_reasoning(
1200
- context: RunContext, reasoning: str = "", next_steps: str | None = None
1182
+ context: RunContext,
1183
+ reasoning: str = "",
1184
+ next_steps: str | List[str] | None = None,
1201
1185
  ) -> ReasoningOutput:
1202
1186
  """Share the agent's current reasoning and planned next steps with the user.
1203
1187
 
@@ -1211,8 +1195,8 @@ def register_agent_share_your_reasoning(agent):
1211
1195
  reasoning for the current situation. This should be clear,
1212
1196
  comprehensive, and explain the 'why' behind decisions.
1213
1197
  next_steps: Planned upcoming actions or steps
1214
- the agent intends to take. Can be None if no specific next steps
1215
- are determined. Defaults to None.
1198
+ the agent intends to take. Can be a string or a list of strings.
1199
+ Can be None if no specific next steps are determined. Defaults to None.
1216
1200
 
1217
1201
  Returns:
1218
1202
  ReasoningOutput: A simple response object containing:
@@ -1223,6 +1207,10 @@ def register_agent_share_your_reasoning(agent):
1223
1207
  >>> next_steps = "First, I'll list the directory contents, then read key files"
1224
1208
  >>> result = agent_share_your_reasoning(ctx, reasoning, next_steps)
1225
1209
 
1210
+ >>> # Using a list for next steps
1211
+ >>> next_steps_list = ["List files", "Read README.md", "Run tests"]
1212
+ >>> result = agent_share_your_reasoning(ctx, reasoning, next_steps_list)
1213
+
1226
1214
  Best Practice:
1227
1215
  Use this tool frequently to maintain transparency. Call it:
1228
1216
  - Before starting complex operations