vtx-coding-agent 0.1.1__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 (117) hide show
  1. vtx/__init__.py +63 -0
  2. vtx/async_utils.py +40 -0
  3. vtx/builtin_skills/github/SKILL.md +139 -0
  4. vtx/builtin_skills/init/SKILL.md +74 -0
  5. vtx/builtin_skills/review/SKILL.md +73 -0
  6. vtx/builtin_skills/skill-builder/SKILL.md +133 -0
  7. vtx/cli.py +90 -0
  8. vtx/config.py +741 -0
  9. vtx/context/__init__.py +15 -0
  10. vtx/context/_xml.py +8 -0
  11. vtx/context/agent_mds.py +128 -0
  12. vtx/context/git.py +64 -0
  13. vtx/context/loader.py +41 -0
  14. vtx/context/skills.py +423 -0
  15. vtx/core/__init__.py +47 -0
  16. vtx/core/compaction.py +89 -0
  17. vtx/core/errors.py +17 -0
  18. vtx/core/handoff.py +51 -0
  19. vtx/core/scratchpad.py +54 -0
  20. vtx/core/types.py +197 -0
  21. vtx/defaults/__init__.py +0 -0
  22. vtx/defaults/config.yml +53 -0
  23. vtx/diff_display.py +12 -0
  24. vtx/events.py +224 -0
  25. vtx/gh_cli.py +82 -0
  26. vtx/git_branch.py +90 -0
  27. vtx/headless.py +127 -0
  28. vtx/llm/__init__.py +93 -0
  29. vtx/llm/base.py +217 -0
  30. vtx/llm/context_length.py +150 -0
  31. vtx/llm/dynamic_models.py +735 -0
  32. vtx/llm/model_fetcher.py +279 -0
  33. vtx/llm/models.py +78 -0
  34. vtx/llm/oauth/__init__.py +59 -0
  35. vtx/llm/oauth/copilot.py +358 -0
  36. vtx/llm/oauth/dynamic.py +236 -0
  37. vtx/llm/oauth/openai.py +400 -0
  38. vtx/llm/phase_parser.py +270 -0
  39. vtx/llm/provider.yaml +280 -0
  40. vtx/llm/provider_catalog.py +230 -0
  41. vtx/llm/providers/__init__.py +45 -0
  42. vtx/llm/providers/anthropic_sdk.py +256 -0
  43. vtx/llm/providers/mock.py +249 -0
  44. vtx/llm/providers/openai_sdk.py +246 -0
  45. vtx/llm/providers/sanitize.py +14 -0
  46. vtx/llm/sdk/__init__.py +13 -0
  47. vtx/llm/sdk/anthropic.py +382 -0
  48. vtx/llm/sdk/base.py +82 -0
  49. vtx/llm/sdk/openai.py +344 -0
  50. vtx/llm/tool_parser.py +161 -0
  51. vtx/loop.py +272 -0
  52. vtx/notify.py +109 -0
  53. vtx/permissions.py +114 -0
  54. vtx/prompts/__init__.py +45 -0
  55. vtx/prompts/builder.py +86 -0
  56. vtx/prompts/env.py +58 -0
  57. vtx/prompts/identity.py +166 -0
  58. vtx/prompts/tooling.py +36 -0
  59. vtx/py.typed +0 -0
  60. vtx/runtime.py +580 -0
  61. vtx/session.py +868 -0
  62. vtx/sounds/completion.wav +0 -0
  63. vtx/sounds/error.wav +0 -0
  64. vtx/sounds/permission.wav +0 -0
  65. vtx/themes.py +1104 -0
  66. vtx/tools/__init__.py +68 -0
  67. vtx/tools/_read_image.py +106 -0
  68. vtx/tools/_tool_utils.py +90 -0
  69. vtx/tools/base.py +36 -0
  70. vtx/tools/bash.py +371 -0
  71. vtx/tools/edit.py +261 -0
  72. vtx/tools/find.py +132 -0
  73. vtx/tools/read.py +238 -0
  74. vtx/tools/skill.py +278 -0
  75. vtx/tools/web.py +238 -0
  76. vtx/tools/write.py +88 -0
  77. vtx/tools_manager.py +216 -0
  78. vtx/turn.py +789 -0
  79. vtx/ui/__init__.py +0 -0
  80. vtx/ui/agent_runner.py +417 -0
  81. vtx/ui/app.py +665 -0
  82. vtx/ui/app_protocol.py +29 -0
  83. vtx/ui/autocomplete.py +440 -0
  84. vtx/ui/blocks.py +735 -0
  85. vtx/ui/chat.py +613 -0
  86. vtx/ui/clipboard.py +59 -0
  87. vtx/ui/commands/__init__.py +100 -0
  88. vtx/ui/commands/auth.py +306 -0
  89. vtx/ui/commands/base.py +122 -0
  90. vtx/ui/commands/models.py +144 -0
  91. vtx/ui/commands/sessions.py +388 -0
  92. vtx/ui/commands/settings.py +286 -0
  93. vtx/ui/completion_ui.py +313 -0
  94. vtx/ui/export.py +703 -0
  95. vtx/ui/floating_list.py +370 -0
  96. vtx/ui/formatting.py +287 -0
  97. vtx/ui/input.py +760 -0
  98. vtx/ui/latex.py +349 -0
  99. vtx/ui/launch.py +108 -0
  100. vtx/ui/path_complete.py +228 -0
  101. vtx/ui/prompt_history.py +102 -0
  102. vtx/ui/queue_ui.py +141 -0
  103. vtx/ui/selection_mode.py +18 -0
  104. vtx/ui/session_ui.py +235 -0
  105. vtx/ui/startup.py +124 -0
  106. vtx/ui/styles.py +327 -0
  107. vtx/ui/tool_output.py +34 -0
  108. vtx/ui/tree.py +437 -0
  109. vtx/ui/welcome.py +51 -0
  110. vtx/ui/widgets.py +558 -0
  111. vtx/update_check.py +49 -0
  112. vtx/version.py +22 -0
  113. vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
  114. vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
  115. vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
  116. vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
  117. vtx_coding_agent-0.1.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,256 @@
1
+ """Anthropic SDK provider - wraps the SDK layer into vtx's BaseProvider interface."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from typing import Any, ClassVar
5
+
6
+ from anthropic import APIConnectionError, APIStatusError, RateLimitError
7
+
8
+ from ...core.errors import format_error
9
+ from ...core.types import (
10
+ AssistantMessage,
11
+ ImageContent,
12
+ Message,
13
+ StopReason,
14
+ StreamDone,
15
+ StreamError,
16
+ StreamPart,
17
+ TextContent,
18
+ TextPart,
19
+ ThinkingContent,
20
+ ThinkPart,
21
+ ToolCall,
22
+ ToolCallDelta,
23
+ ToolCallStart,
24
+ ToolDefinition,
25
+ ToolResultMessage,
26
+ Usage,
27
+ UserMessage,
28
+ )
29
+ from ..base import BaseProvider, LLMStream, ProviderConfig, resolve_api_key
30
+ from ..sdk.anthropic import AnthropicSDK
31
+ from ..sdk.base import GenerationConfig
32
+ from ..sdk.base import Message as SDKMessage
33
+ from .sanitize import sanitize_surrogates
34
+
35
+
36
+ class AnthropicSDKProvider(BaseProvider):
37
+ name = "anthropic"
38
+ thinking_levels: ClassVar[list[str]] = ["none", "minimal", "low", "medium", "high", "xhigh"]
39
+
40
+ def __init__(self, config: ProviderConfig):
41
+ super().__init__(config)
42
+
43
+ api_key = resolve_api_key(
44
+ config.api_key,
45
+ env_vars=("ANTHROPIC_API_KEY",),
46
+ base_url=config.base_url,
47
+ auth_mode=config.anthropic_compat_auth_mode,
48
+ )
49
+ if not api_key:
50
+ raise ValueError(
51
+ f"No API key found for {self.name}. "
52
+ "Set ANTHROPIC_API_KEY environment variable or pass api_key in config, "
53
+ 'or configure llm.auth.anthropic_compat = "auto"/"none" for local endpoints.'
54
+ )
55
+
56
+ self._sdk = AnthropicSDK(api_key=api_key, base_url=config.base_url)
57
+
58
+ def _convert_messages(self, messages: list[Message]) -> list[SDKMessage]:
59
+ result: list[SDKMessage] = []
60
+ for msg in messages:
61
+ if isinstance(msg, UserMessage):
62
+ result.append(self._convert_user_message(msg))
63
+ elif isinstance(msg, AssistantMessage):
64
+ result.append(self._convert_assistant_message(msg))
65
+ elif isinstance(msg, ToolResultMessage):
66
+ result.append(self._convert_tool_result(msg))
67
+ return result
68
+
69
+ def _convert_user_message(self, msg: UserMessage) -> SDKMessage:
70
+ if isinstance(msg.content, str):
71
+ content = sanitize_surrogates(msg.content)
72
+ if not content or content.isspace():
73
+ raise ValueError("User message content cannot be empty or whitespace-only")
74
+ return SDKMessage(role="user", content=content)
75
+
76
+ parts: list[str] = []
77
+ image_parts: list[str] = []
78
+ for item in msg.content:
79
+ if isinstance(item, TextContent):
80
+ text = sanitize_surrogates(item.text)
81
+ if text and not text.isspace():
82
+ parts.append(text)
83
+ elif isinstance(item, ImageContent):
84
+ image_parts.append(f"data:{item.mime_type};base64,{item.data}")
85
+
86
+ content = "\n".join(parts) if parts else ""
87
+ if not content and not image_parts:
88
+ raise ValueError("User message content cannot be empty or whitespace-only")
89
+
90
+ return SDKMessage(role="user", content=content, image_parts=image_parts or None)
91
+
92
+ def _convert_assistant_message(self, msg: AssistantMessage) -> SDKMessage:
93
+ content_parts: list[str] = []
94
+ tool_calls: list[dict[str, Any]] = []
95
+
96
+ for item in msg.content:
97
+ if isinstance(item, TextContent):
98
+ if item.text.strip():
99
+ content_parts.append(sanitize_surrogates(item.text))
100
+ elif isinstance(item, ThinkingContent):
101
+ pass
102
+ elif isinstance(item, ToolCall):
103
+ tool_calls.append({"id": item.id, "name": item.name, "arguments": item.arguments})
104
+
105
+ return SDKMessage(
106
+ role="assistant",
107
+ content="".join(content_parts) if content_parts else "",
108
+ metadata={"tool_calls": tool_calls} if tool_calls else None,
109
+ )
110
+
111
+ def _convert_tool_result(self, msg: ToolResultMessage) -> SDKMessage:
112
+ text_parts = [item.text for item in msg.content if isinstance(item, TextContent)]
113
+ has_images = any(isinstance(item, ImageContent) for item in msg.content)
114
+
115
+ if text_parts:
116
+ content = "\n".join(text_parts)
117
+ elif has_images:
118
+ content = "(see attached image)"
119
+ else:
120
+ content = "(no output)"
121
+
122
+ return SDKMessage(
123
+ role="tool",
124
+ content=content,
125
+ metadata={"tool_call_id": msg.tool_call_id, "is_error": msg.is_error},
126
+ )
127
+
128
+ def _convert_tools(self, tools: list[ToolDefinition]) -> list[dict[str, Any]]:
129
+ return [
130
+ {
131
+ "type": "function",
132
+ "function": {
133
+ "name": tool.name,
134
+ "description": tool.description,
135
+ "parameters": tool.parameters,
136
+ },
137
+ }
138
+ for tool in tools
139
+ ]
140
+
141
+ async def _stream_impl(
142
+ self,
143
+ messages: list[Message],
144
+ *,
145
+ system_prompt: str | None = None,
146
+ tools: list[ToolDefinition] | None = None,
147
+ temperature: float | None = None,
148
+ max_tokens: int | None = None,
149
+ ) -> LLMStream:
150
+ sdk_messages = self._convert_messages(messages)
151
+ sdk_tools = self._convert_tools(tools) if tools else None
152
+ temp = temperature if temperature is not None else self.config.temperature
153
+ max_tok = max_tokens if max_tokens is not None else self.config.max_tokens
154
+
155
+ config = GenerationConfig(
156
+ model=self.config.model,
157
+ temperature=temp if temp is not None else 0.7,
158
+ max_tokens=max_tok,
159
+ )
160
+
161
+ response = await self._sdk.generate_with_tools(
162
+ sdk_messages, sdk_tools or [], config, stream=True
163
+ )
164
+
165
+ llm_stream = LLMStream()
166
+ llm_stream.set_iterator(self._process_stream(response, llm_stream))
167
+ return llm_stream
168
+
169
+ async def _process_stream(
170
+ self, response: Any, llm_stream: LLMStream
171
+ ) -> AsyncIterator[StreamPart]:
172
+ stop_reason: StopReason = StopReason.STOP
173
+ current_tool_index: int = -1
174
+ tool_use_blocks: dict[int, dict[str, Any]] = {}
175
+
176
+ try:
177
+ async for event in response:
178
+ event_type = event.get("type", "")
179
+
180
+ if event_type == "message_start":
181
+ if event.get("id"):
182
+ llm_stream._id = event["id"]
183
+ usage = event.get("usage") or {}
184
+ llm_stream._usage = Usage(
185
+ input_tokens=usage.get("input_tokens", 0),
186
+ output_tokens=usage.get("output_tokens", 0),
187
+ )
188
+ elif event_type == "content_block_start":
189
+ block = event.get("content_block", {})
190
+ if block.get("type") == "tool_use":
191
+ current_tool_index += 1
192
+ tool_use_blocks[event.get("index", 0)] = {
193
+ "id": block.get("id", ""),
194
+ "name": block.get("name", ""),
195
+ }
196
+ yield ToolCallStart(
197
+ id=block.get("id", ""),
198
+ name=block.get("name", ""),
199
+ index=current_tool_index,
200
+ )
201
+ elif event_type == "content_block_delta":
202
+ delta = event.get("delta", {})
203
+ delta_type = delta.get("type", "")
204
+
205
+ if delta_type == "text_delta":
206
+ yield TextPart(text=delta.get("text", ""))
207
+ elif delta_type == "thinking_delta":
208
+ yield ThinkPart(think=delta.get("thinking", ""))
209
+ elif delta_type == "signature_delta":
210
+ yield ThinkPart(think="", signature=delta.get("signature", ""))
211
+ elif delta_type == "input_json_delta":
212
+ tool_info = tool_use_blocks.get(event.get("index", 0))
213
+ if tool_info:
214
+ logical_index = list(tool_use_blocks.keys()).index(
215
+ event.get("index", 0)
216
+ )
217
+ yield ToolCallDelta(
218
+ index=logical_index, arguments_delta=delta.get("partial_json", "")
219
+ )
220
+ elif event_type == "message_delta":
221
+ delta = event.get("delta", {})
222
+ if delta.get("stop_reason"):
223
+ stop_reason = self._map_stop_reason(delta["stop_reason"])
224
+ usage = event.get("usage") or {}
225
+ if usage and llm_stream._usage:
226
+ llm_stream._usage = Usage(
227
+ input_tokens=llm_stream._usage.input_tokens,
228
+ output_tokens=usage.get("output_tokens", 0),
229
+ cache_read_tokens=llm_stream._usage.cache_read_tokens,
230
+ cache_write_tokens=llm_stream._usage.cache_write_tokens,
231
+ )
232
+
233
+ yield StreamDone(stop_reason=stop_reason)
234
+
235
+ except Exception as e:
236
+ yield StreamError(error=format_error(e))
237
+
238
+ def _map_stop_reason(self, reason: str) -> StopReason:
239
+ match reason:
240
+ case "end_turn":
241
+ return StopReason.STOP
242
+ case "max_tokens":
243
+ return StopReason.LENGTH
244
+ case "tool_use":
245
+ return StopReason.TOOL_USE
246
+ case _:
247
+ return StopReason.STOP
248
+
249
+ def should_retry_for_error(self, error: Exception) -> bool:
250
+ if isinstance(error, RateLimitError):
251
+ return True
252
+ if isinstance(error, APIConnectionError):
253
+ return True
254
+ if isinstance(error, APIStatusError):
255
+ return error.status_code >= 500
256
+ return False
@@ -0,0 +1,249 @@
1
+ """
2
+ Mock LLM provider for testing.
3
+
4
+ Yields a realistic streaming response (thinking, text, tool calls) for testing
5
+ agent loop and turn execution without making real API calls.
6
+
7
+ Scenarios (set via scenario parameter):
8
+ - "default": thinking → text → multiple tool calls
9
+ - "simple_text": just text, no thinking or tools
10
+ - "thinking_text_tool": thinking → text → single tool call
11
+ - "retries": fail twice, then succeed
12
+ - "retry_exhausted": always fail
13
+ - "non_retryable": fail with non-retryable error
14
+ - "stream_error": emit StreamError during streaming
15
+ - "unknown_tool": call unknown tool
16
+ - "long_text": multiple text chunks
17
+ - "tool_hang": emits a tool call and then never sends StreamDone
18
+ - "tool_hang_invalid_json": emits an incomplete tool-call JSON payload and hangs
19
+ - "tool_hang_with_initial_args": tool call with initial args, then an incomplete delta, then hangs
20
+ - "tool_with_many_chunks": tool call with many argument chunks for token counting tests
21
+ - "leading_empty_text_then_think": emits leading newlines before thinking
22
+ - "leading_empty_text_then_text": emits leading newlines before text
23
+ """
24
+
25
+ import asyncio
26
+ from collections.abc import AsyncIterator
27
+
28
+ from ...core.types import (
29
+ Message,
30
+ StopReason,
31
+ StreamDone,
32
+ StreamError,
33
+ TextPart,
34
+ ThinkPart,
35
+ ToolCallDelta,
36
+ ToolCallStart,
37
+ ToolDefinition,
38
+ Usage,
39
+ )
40
+ from ..base import BaseProvider, LLMStream, ProviderConfig
41
+
42
+
43
+ class MockProvider(BaseProvider):
44
+ name = "mock"
45
+
46
+ def __init__(self, config: ProviderConfig | None = None, scenario: str = "default"):
47
+ super().__init__(config or ProviderConfig())
48
+ self.scenario = scenario
49
+ self._attempt_count = 0
50
+
51
+ async def _stream_impl(
52
+ self,
53
+ messages: list[Message],
54
+ *,
55
+ system_prompt: str | None = None,
56
+ tools: list[ToolDefinition] | None = None,
57
+ temperature: float | None = None,
58
+ max_tokens: int | None = None,
59
+ ) -> LLMStream:
60
+ self._attempt_count += 1
61
+
62
+ if self.scenario == "retries":
63
+ if self._attempt_count < 3:
64
+ raise ConnectionError("Rate limit")
65
+ elif self.scenario == "retry_exhausted":
66
+ raise ConnectionError("Always fails")
67
+ elif self.scenario == "non_retryable":
68
+ raise ValueError("Invalid input")
69
+
70
+ llm_stream = LLMStream()
71
+ llm_stream.set_iterator(self._get_iterator())
72
+ llm_stream._id = "mock-1"
73
+ llm_stream._usage = Usage(input_tokens=10, output_tokens=5, cache_read_tokens=2)
74
+
75
+ return llm_stream
76
+
77
+ def _get_iterator(self) -> AsyncIterator:
78
+ match self.scenario:
79
+ case "default":
80
+
81
+ async def default_iter():
82
+ yield ThinkPart(think="Let me think about this...")
83
+ yield TextPart(text="I'll help you with that.")
84
+ yield ToolCallStart(id="call-1", name="read", index=0, arguments={})
85
+ yield ToolCallDelta(index=0, arguments_delta='{"path": "file.txt"}')
86
+ yield ToolCallStart(id="call-2", name="bash", index=1, arguments={})
87
+ yield ToolCallDelta(index=1, arguments_delta='{"command": "ls -la"}')
88
+ yield StreamDone(stop_reason=StopReason.TOOL_USE)
89
+
90
+ return default_iter()
91
+
92
+ case "simple_text":
93
+
94
+ async def simple_iter():
95
+ yield TextPart(text="Hello, world!")
96
+ yield StreamDone(stop_reason=StopReason.STOP)
97
+
98
+ return simple_iter()
99
+
100
+ case "thinking_text_tool":
101
+
102
+ async def flow_iter():
103
+ yield ThinkPart(think="I need to read the file")
104
+ yield TextPart(text="Let me check the file.")
105
+ yield ToolCallStart(id="call-1", name="read", index=0, arguments={})
106
+ yield ToolCallDelta(index=0, arguments_delta='{"path": "test.txt"}')
107
+ yield StreamDone(stop_reason=StopReason.TOOL_USE)
108
+
109
+ return flow_iter()
110
+
111
+ case "stream_error":
112
+
113
+ async def error_iter():
114
+ yield TextPart(text="Before error")
115
+ yield StreamError(error="Something went wrong")
116
+
117
+ return error_iter()
118
+
119
+ case "unknown_tool":
120
+
121
+ async def unknown_iter():
122
+ yield ToolCallStart(id="call-1", name="unknown_tool", index=0, arguments={})
123
+ yield ToolCallDelta(index=0, arguments_delta='{"arg": "value"}')
124
+ yield StreamDone(stop_reason=StopReason.TOOL_USE)
125
+
126
+ return unknown_iter()
127
+
128
+ case "long_text":
129
+
130
+ async def long_iter():
131
+ for chunk in ["This ", "is ", "a ", "long ", "response", "."]:
132
+ yield TextPart(text=chunk)
133
+ yield StreamDone(stop_reason=StopReason.STOP)
134
+
135
+ return long_iter()
136
+
137
+ case "tool_hang":
138
+
139
+ async def tool_hang_iter():
140
+ yield ToolCallStart(id="call-1", name="read", index=0, arguments={})
141
+ yield ToolCallDelta(index=0, arguments_delta='{"path": "test.txt"}')
142
+ await asyncio.sleep(3600)
143
+
144
+ return tool_hang_iter()
145
+
146
+ case "tool_hang_invalid_json":
147
+
148
+ async def tool_hang_invalid_json_iter():
149
+ yield ToolCallStart(id="call-1", name="write", index=0, arguments={})
150
+ yield ToolCallDelta(
151
+ index=0, arguments_delta='{"path": "/tmp/test.txt", "content": "incomplete'
152
+ )
153
+ await asyncio.sleep(3600)
154
+
155
+ return tool_hang_invalid_json_iter()
156
+
157
+ case "tool_hang_with_initial_args":
158
+
159
+ async def tool_hang_with_initial_args_iter():
160
+ yield ToolCallStart(
161
+ id="call-1",
162
+ name="write",
163
+ index=0,
164
+ arguments={"path": "/tmp/stale.txt", "content": "stale snapshot"},
165
+ )
166
+ yield ToolCallDelta(
167
+ index=0,
168
+ arguments_delta='{"path": "/tmp/test.txt", "content": "incomplete',
169
+ replace=True,
170
+ )
171
+ await asyncio.sleep(3600)
172
+
173
+ return tool_hang_with_initial_args_iter()
174
+
175
+ case "tool_with_many_chunks":
176
+
177
+ async def tool_with_many_chunks_iter():
178
+ # Tool call with many chunks to test token counting
179
+ # 24 chunks of 8 chars each = 192 chars = 48 tokens
180
+ # Should trigger token update events at chunks 12, 16, 20, 24
181
+ yield ToolCallStart(id="call-1", name="bash", index=0, arguments={})
182
+ chunks = [
183
+ "aaaaaaa",
184
+ "bbbbbbb",
185
+ "ccccccc",
186
+ "ddddddd",
187
+ "eeeeeee",
188
+ "fffffff",
189
+ "ggggggg",
190
+ "hhhhhhh",
191
+ "iiiiiii",
192
+ "jjjjjjj",
193
+ "kkkkkkk",
194
+ "lllllll",
195
+ "mmmmmmm",
196
+ "nnnnnnn",
197
+ "ooooooo",
198
+ "ppppppp",
199
+ "qqqqqqq",
200
+ "rrrrrrr",
201
+ "sssssss",
202
+ "ttttttt",
203
+ "uuuuuuu",
204
+ "vvvvvvv",
205
+ "wwwwwww",
206
+ "xxxxxxxx",
207
+ ]
208
+ for chunk in chunks:
209
+ yield ToolCallDelta(index=0, arguments_delta=chunk)
210
+ yield StreamDone(stop_reason=StopReason.TOOL_USE)
211
+
212
+ return tool_with_many_chunks_iter()
213
+
214
+ case "leading_empty_text_then_think":
215
+
216
+ async def leading_empty_text_then_think_iter():
217
+ yield TextPart(text="\n\n")
218
+ yield ThinkPart(think="Let me think about this...")
219
+ yield TextPart(text="I'll help you with that.")
220
+ yield StreamDone(stop_reason=StopReason.STOP)
221
+
222
+ return leading_empty_text_then_think_iter()
223
+
224
+ case "leading_empty_text_then_text":
225
+
226
+ async def leading_empty_text_then_text_iter():
227
+ yield TextPart(text="\n\n")
228
+ yield TextPart(text="Hello, world!")
229
+ yield StreamDone(stop_reason=StopReason.STOP)
230
+
231
+ return leading_empty_text_then_text_iter()
232
+
233
+ case _:
234
+ # Fallback to default
235
+ async def default_iter():
236
+ yield ThinkPart(think="Let me think about this...")
237
+ yield TextPart(text="I'll help you with that.")
238
+ yield ToolCallStart(id="call-1", name="read", index=0, arguments={})
239
+ yield ToolCallDelta(index=0, arguments_delta='{"path": "file.txt"}')
240
+ yield ToolCallStart(id="call-2", name="bash", index=1, arguments={})
241
+ yield ToolCallDelta(index=1, arguments_delta='{"command": "ls -la"}')
242
+ yield StreamDone(stop_reason=StopReason.TOOL_USE)
243
+
244
+ return default_iter()
245
+
246
+ def should_retry_for_error(self, error: Exception) -> bool:
247
+ if self.scenario == "retries" or self.scenario == "retry_exhausted":
248
+ return isinstance(error, ConnectionError)
249
+ return False