codemaster-cli 2.2.0__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 (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. vibe/whats_new.md +5 -0
vibe/core/types.py ADDED
@@ -0,0 +1,405 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from collections import OrderedDict
5
+ from collections.abc import Awaitable, Callable
6
+ import copy
7
+ from enum import StrEnum, auto
8
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
9
+ from uuid import uuid4
10
+
11
+ if TYPE_CHECKING:
12
+ from vibe.core.tools.base import BaseTool
13
+ else:
14
+ BaseTool = Any
15
+
16
+ from pydantic import (
17
+ BaseModel,
18
+ BeforeValidator,
19
+ ConfigDict,
20
+ Field,
21
+ PrivateAttr,
22
+ computed_field,
23
+ model_validator,
24
+ )
25
+
26
+
27
+ class AgentStats(BaseModel):
28
+ steps: int = 0
29
+ session_prompt_tokens: int = 0
30
+ session_completion_tokens: int = 0
31
+ tool_calls_agreed: int = 0
32
+ tool_calls_rejected: int = 0
33
+ tool_calls_failed: int = 0
34
+ tool_calls_succeeded: int = 0
35
+
36
+ context_tokens: int = 0
37
+
38
+ last_turn_prompt_tokens: int = 0
39
+ last_turn_completion_tokens: int = 0
40
+ last_turn_duration: float = 0.0
41
+ tokens_per_second: float = 0.0
42
+
43
+ input_price_per_million: float = 0.0
44
+ output_price_per_million: float = 0.0
45
+
46
+ _listeners: dict[str, Callable[[AgentStats], None]] = PrivateAttr(
47
+ default_factory=dict
48
+ )
49
+
50
+ def __setattr__(self, name: str, value: Any) -> None:
51
+ super().__setattr__(name, value)
52
+ if name in self._listeners:
53
+ self._listeners[name](self)
54
+
55
+ def trigger_listeners(self) -> None:
56
+ for listener in self._listeners.values():
57
+ listener(self)
58
+
59
+ def add_listener(
60
+ self, attr_name: str, listener: Callable[[AgentStats], None]
61
+ ) -> None:
62
+ self._listeners[attr_name] = listener
63
+
64
+ @computed_field
65
+ @property
66
+ def session_total_llm_tokens(self) -> int:
67
+ return self.session_prompt_tokens + self.session_completion_tokens
68
+
69
+ @computed_field
70
+ @property
71
+ def last_turn_total_tokens(self) -> int:
72
+ return self.last_turn_prompt_tokens + self.last_turn_completion_tokens
73
+
74
+ @computed_field
75
+ @property
76
+ def session_cost(self) -> float:
77
+ """Calculate the total session cost in dollars based on token usage and pricing.
78
+
79
+ NOTE: This is a rough estimate and is worst-case scenario.
80
+ The actual cost may be lower due to prompt caching.
81
+ If the model changes mid-session, this uses current pricing for all tokens.
82
+ """
83
+ input_cost = (
84
+ self.session_prompt_tokens / 1_000_000
85
+ ) * self.input_price_per_million
86
+ output_cost = (
87
+ self.session_completion_tokens / 1_000_000
88
+ ) * self.output_price_per_million
89
+ return input_cost + output_cost
90
+
91
+ def update_pricing(self, input_price: float, output_price: float) -> None:
92
+ """Update pricing info when model changes.
93
+
94
+ NOTE: session_cost will be recalculated using new pricing for all
95
+ accumulated tokens. This is a known approximation when models change.
96
+ This should not be a big issue, pricing is only used for max_price which is in
97
+ programmatic mode, so user should not update models there.
98
+ """
99
+ self.input_price_per_million = input_price
100
+ self.output_price_per_million = output_price
101
+
102
+ def reset_context_state(self) -> None:
103
+ """Reset context-related fields while preserving cumulative session stats.
104
+
105
+ Used after config reload or similar operations where the context
106
+ changes but we want to preserve session totals.
107
+ """
108
+ self.context_tokens = 0
109
+ self.last_turn_prompt_tokens = 0
110
+ self.last_turn_completion_tokens = 0
111
+ self.last_turn_duration = 0.0
112
+ self.tokens_per_second = 0.0
113
+
114
+
115
+ class SessionInfo(BaseModel):
116
+ session_id: str
117
+ start_time: str
118
+ message_count: int
119
+ stats: AgentStats
120
+ save_dir: str
121
+
122
+
123
+ class SessionMetadata(BaseModel):
124
+ session_id: str
125
+ start_time: str
126
+ end_time: str | None
127
+ git_commit: str | None
128
+ git_branch: str | None
129
+ environment: dict[str, str | None]
130
+ username: str
131
+
132
+
133
+ StrToolChoice = Literal["auto", "none", "any", "required"]
134
+
135
+
136
+ class AvailableFunction(BaseModel):
137
+ name: str
138
+ description: str
139
+ parameters: dict[str, Any]
140
+
141
+
142
+ class AvailableTool(BaseModel):
143
+ type: Literal["function"] = "function"
144
+ function: AvailableFunction
145
+
146
+
147
+ class FunctionCall(BaseModel):
148
+ name: str | None = None
149
+ arguments: str | None = None
150
+
151
+
152
+ class ToolCall(BaseModel):
153
+ id: str | None = None
154
+ index: int | None = None
155
+ function: FunctionCall = Field(default_factory=FunctionCall)
156
+ type: str = "function"
157
+
158
+
159
+ def _content_before(v: Any) -> str:
160
+ if isinstance(v, str):
161
+ return v
162
+ if isinstance(v, list):
163
+ parts: list[str] = []
164
+ for p in v:
165
+ if isinstance(p, dict) and isinstance(p.get("text"), str):
166
+ parts.append(p["text"])
167
+ else:
168
+ parts.append(str(p))
169
+ return "\n".join(parts)
170
+ return str(v)
171
+
172
+
173
+ Content = Annotated[str, BeforeValidator(_content_before)]
174
+
175
+
176
+ class Role(StrEnum):
177
+ system = auto()
178
+ user = auto()
179
+ assistant = auto()
180
+ tool = auto()
181
+
182
+
183
+ class ApprovalResponse(StrEnum):
184
+ YES = "y"
185
+ NO = "n"
186
+
187
+
188
+ class LLMMessage(BaseModel):
189
+ model_config = ConfigDict(extra="ignore")
190
+
191
+ role: Role
192
+ content: Content | None = None
193
+ reasoning_content: Content | None = None
194
+ reasoning_signature: str | None = None
195
+ tool_calls: list[ToolCall] | None = None
196
+ name: str | None = None
197
+ tool_call_id: str | None = None
198
+ message_id: str | None = None
199
+
200
+ @model_validator(mode="before")
201
+ @classmethod
202
+ def _from_any(cls, v: Any) -> dict[str, Any] | Any:
203
+ if isinstance(v, dict):
204
+ v.setdefault("content", "")
205
+ v.setdefault("role", "assistant")
206
+ if "message_id" not in v and v.get("role") != "tool":
207
+ v["message_id"] = str(uuid4())
208
+ return v
209
+ role = str(getattr(v, "role", "assistant"))
210
+ return {
211
+ "role": role,
212
+ "content": getattr(v, "content", ""),
213
+ "reasoning_content": getattr(v, "reasoning_content", None),
214
+ "reasoning_signature": getattr(v, "reasoning_signature", None),
215
+ "tool_calls": getattr(v, "tool_calls", None),
216
+ "name": getattr(v, "name", None),
217
+ "tool_call_id": getattr(v, "tool_call_id", None),
218
+ "message_id": getattr(v, "message_id", None)
219
+ or (str(uuid4()) if role != "tool" else None),
220
+ }
221
+
222
+ def __add__(self, other: LLMMessage) -> LLMMessage:
223
+ """Careful: this is not commutative!"""
224
+ if self.role != other.role:
225
+ raise ValueError("Can't accumulate messages with different roles")
226
+
227
+ if self.name != other.name:
228
+ raise ValueError("Can't accumulate messages with different names")
229
+
230
+ if self.tool_call_id != other.tool_call_id:
231
+ raise ValueError("Can't accumulate messages with different tool_call_ids")
232
+
233
+ content = (self.content or "") + (other.content or "")
234
+ if not content:
235
+ content = None
236
+
237
+ reasoning_content = (self.reasoning_content or "") + (
238
+ other.reasoning_content or ""
239
+ )
240
+ if not reasoning_content:
241
+ reasoning_content = None
242
+
243
+ reasoning_signature = (self.reasoning_signature or "") + (
244
+ other.reasoning_signature or ""
245
+ )
246
+ if not reasoning_signature:
247
+ reasoning_signature = None
248
+
249
+ tool_calls_map = OrderedDict[int, ToolCall]()
250
+ for tool_calls in [self.tool_calls or [], other.tool_calls or []]:
251
+ for tc in tool_calls:
252
+ if tc.index is None:
253
+ raise ValueError("Tool call chunk missing index")
254
+ if tc.index not in tool_calls_map:
255
+ tool_calls_map[tc.index] = copy.deepcopy(tc)
256
+ else:
257
+ existing_name = tool_calls_map[tc.index].function.name
258
+ new_name = tc.function.name
259
+ if existing_name and new_name and existing_name != new_name:
260
+ raise ValueError(
261
+ "Can't accumulate messages with different tool call names"
262
+ )
263
+ if new_name and not existing_name:
264
+ tool_calls_map[tc.index].function.name = new_name
265
+ new_args = (tool_calls_map[tc.index].function.arguments or "") + (
266
+ tc.function.arguments or ""
267
+ )
268
+ tool_calls_map[tc.index].function.arguments = new_args
269
+
270
+ return LLMMessage(
271
+ role=self.role,
272
+ content=content,
273
+ reasoning_content=reasoning_content,
274
+ reasoning_signature=reasoning_signature,
275
+ tool_calls=list(tool_calls_map.values()) or None,
276
+ name=self.name,
277
+ tool_call_id=self.tool_call_id,
278
+ message_id=self.message_id,
279
+ )
280
+
281
+
282
+ class LLMUsage(BaseModel):
283
+ model_config = ConfigDict(frozen=True)
284
+ prompt_tokens: int = 0
285
+ completion_tokens: int = 0
286
+
287
+ def __add__(self, other: LLMUsage) -> LLMUsage:
288
+ return LLMUsage(
289
+ prompt_tokens=self.prompt_tokens + other.prompt_tokens,
290
+ completion_tokens=self.completion_tokens + other.completion_tokens,
291
+ )
292
+
293
+
294
+ class LLMChunk(BaseModel):
295
+ model_config = ConfigDict(frozen=True)
296
+ message: LLMMessage
297
+ usage: LLMUsage | None = None
298
+
299
+ def __add__(self, other: LLMChunk) -> LLMChunk:
300
+ if self.usage is None and other.usage is None:
301
+ new_usage = None
302
+ else:
303
+ new_usage = (self.usage or LLMUsage()) + (other.usage or LLMUsage())
304
+ return LLMChunk(message=self.message + other.message, usage=new_usage)
305
+
306
+
307
+ class BaseEvent(BaseModel, ABC):
308
+ model_config = ConfigDict(arbitrary_types_allowed=True)
309
+
310
+
311
+ class UserMessageEvent(BaseEvent):
312
+ content: str
313
+ message_id: str
314
+
315
+
316
+ class AssistantEvent(BaseEvent):
317
+ content: str
318
+ stopped_by_middleware: bool = False
319
+ message_id: str | None = None
320
+
321
+ def __add__(self, other: AssistantEvent) -> AssistantEvent:
322
+ return AssistantEvent(
323
+ content=self.content + other.content,
324
+ stopped_by_middleware=self.stopped_by_middleware
325
+ or other.stopped_by_middleware,
326
+ message_id=self.message_id or other.message_id,
327
+ )
328
+
329
+
330
+ class ReasoningEvent(BaseEvent):
331
+ content: str
332
+ message_id: str | None = None
333
+
334
+
335
+ class ToolCallEvent(BaseEvent):
336
+ tool_name: str
337
+ tool_class: type[BaseTool]
338
+ args: BaseModel
339
+ tool_call_id: str
340
+
341
+
342
+ class ToolResultEvent(BaseEvent):
343
+ tool_name: str
344
+ tool_class: type[BaseTool] | None
345
+ result: BaseModel | None = None
346
+ error: str | None = None
347
+ skipped: bool = False
348
+ skip_reason: str | None = None
349
+ duration: float | None = None
350
+ tool_call_id: str
351
+
352
+
353
+ class ToolStreamEvent(BaseEvent):
354
+ tool_name: str
355
+ message: str
356
+ tool_call_id: str
357
+
358
+
359
+ class CompactStartEvent(BaseEvent):
360
+ current_context_tokens: int
361
+ threshold: int
362
+ # WORKAROUND: Using tool_call to communicate compact events to the client.
363
+ # This should be revisited when the ACP protocol defines how compact events
364
+ # should be represented.
365
+ # [RFD](https://agentclientprotocol.com/rfds/session-usage)
366
+ tool_call_id: str
367
+
368
+
369
+ class CompactEndEvent(BaseEvent):
370
+ old_context_tokens: int
371
+ new_context_tokens: int
372
+ summary_length: int
373
+ # WORKAROUND: Using tool_call to communicate compact events to the client.
374
+ # This should be revisited when the ACP protocol defines how compact events
375
+ # should be represented.
376
+ # [RFD](https://agentclientprotocol.com/rfds/session-usage)
377
+ tool_call_id: str
378
+
379
+
380
+ class OutputFormat(StrEnum):
381
+ TEXT = auto()
382
+ JSON = auto()
383
+ STREAMING = auto()
384
+
385
+
386
+ type AsyncApprovalCallback = Callable[
387
+ [str, BaseModel, str], Awaitable[tuple[ApprovalResponse, str | None]]
388
+ ]
389
+
390
+ type SyncApprovalCallback = Callable[
391
+ [str, BaseModel, str], tuple[ApprovalResponse, str | None]
392
+ ]
393
+
394
+ type ApprovalCallback = AsyncApprovalCallback | SyncApprovalCallback
395
+
396
+ type UserInputCallback = Callable[[BaseModel], Awaitable[BaseModel]]
397
+
398
+
399
+ class RateLimitError(Exception):
400
+ def __init__(self, provider: str, model: str) -> None:
401
+ self.provider = provider
402
+ self.model = model
403
+ super().__init__(
404
+ "Rate limits exceeded. Please wait a moment before trying again."
405
+ )