monkeybot 2.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 (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,644 @@
1
+ """Typed conversation content blocks (goose-style discriminated union).
2
+
3
+ Each message turn is a list of ContentBlock values serialized as JSON objects
4
+ with a ``type`` discriminator. Wire field names use camelCase; Python
5
+ dataclass fields use snake_case. This module owns serde invariants shared by
6
+ SQLite history (Story 2), provider adapters (Story 3), and gateway payloads.
7
+
8
+ The variant set is closed; unknown ``type`` tags fail deserialization with
9
+ ValueError. Optional fields are omitted from JSON when None.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from typing import ClassVar, Literal, Self, TypeAlias, cast
18
+
19
+ _DISPATCH: dict[str, type[ContentBlock]] = {}
20
+
21
+
22
+ def _drop_none(values: dict[str, object | None]) -> dict[str, object]:
23
+ """Return a copy with key/value pairs whose value is not None."""
24
+ return {k: v for k, v in values.items() if v is not None}
25
+
26
+
27
+ def _require_str(d: Mapping[str, object], key: str, *, context: str) -> str:
28
+ """Return a required string field or raise ValueError."""
29
+ if key not in d:
30
+ raise ValueError(f"{context}: missing required field {key!r}")
31
+ val = d[key]
32
+ if not isinstance(val, str):
33
+ raise ValueError(
34
+ f"{context}: field {key!r} must be a string, got {type(val).__name__}"
35
+ )
36
+ return val
37
+
38
+
39
+ def _optional_str(d: Mapping[str, object], key: str) -> str | None:
40
+ """Return optional string: absent or JSON null -> None."""
41
+ if key not in d or d[key] is None:
42
+ return None
43
+ val = d[key]
44
+ if not isinstance(val, str):
45
+ raise ValueError(
46
+ f"optional field {key!r}: expected string or null, got {type(val).__name__}"
47
+ )
48
+ return val
49
+
50
+
51
+ def _require_dict(d: Mapping[str, object], key: str, context: str) -> dict[str, object]:
52
+ """Return a required JSON object at key."""
53
+ if key not in d:
54
+ raise ValueError(f"{context}: missing required field {key!r}")
55
+ val = d[key]
56
+ if not isinstance(val, dict):
57
+ raise ValueError(
58
+ f"{context}: field {key!r} must be a JSON object, got {type(val).__name__}"
59
+ )
60
+ return val
61
+
62
+
63
+ def _optional_dict(
64
+ d: Mapping[str, object], key: str, *, context: str
65
+ ) -> dict[str, object] | None:
66
+ """Return optional JSON object; absent or null -> None."""
67
+ if key not in d or d[key] is None:
68
+ return None
69
+ val = d[key]
70
+ if not isinstance(val, dict):
71
+ raise ValueError(
72
+ f"{context}: field {key!r} must be a JSON object or null, "
73
+ f"got {type(val).__name__}"
74
+ )
75
+ return val
76
+
77
+
78
+ def _require_bool(d: Mapping[str, object], key: str, *, context: str) -> bool:
79
+ """Return a required boolean field."""
80
+ if key not in d:
81
+ raise ValueError(f"{context}: missing required field {key!r}")
82
+ val = d[key]
83
+ if not isinstance(val, bool):
84
+ raise ValueError(
85
+ f"{context}: field {key!r} must be a boolean, got {type(val).__name__}"
86
+ )
87
+ return val
88
+
89
+
90
+ def _parse_tool_response_is_error(d: Mapping[str, object], *, context: str) -> bool:
91
+ """Parse ``isError``; default False when absent (read tolerant)."""
92
+ if "isError" not in d or d["isError"] is None:
93
+ return False
94
+ return _require_bool(d, "isError", context=context)
95
+
96
+
97
+ def _require_block_list(
98
+ d: Mapping[str, object], key: str, *, context: str
99
+ ) -> list[ContentBlock]:
100
+ """Parse a list of content block objects."""
101
+ if key not in d:
102
+ raise ValueError(f"{context}: missing required field {key!r}")
103
+ val = d[key]
104
+ if not isinstance(val, list):
105
+ raise ValueError(
106
+ f"{context}: field {key!r} must be a list, got {type(val).__name__}"
107
+ )
108
+ blocks: list[ContentBlock] = []
109
+ for i, item in enumerate(val):
110
+ if not isinstance(item, dict):
111
+ raise ValueError(
112
+ f"{context}: {key!r}[{i}] must be a JSON object, "
113
+ f"got {type(item).__name__}"
114
+ )
115
+ blocks.append(ContentBlock.from_dict(item))
116
+ return blocks
117
+
118
+
119
+ def _require_args_dict(d: Mapping[str, object], key: str, *, context: str) -> dict[str, object]:
120
+ """Parse ``args`` / ``arguments`` as a JSON object."""
121
+ return _require_dict(d, key, context)
122
+
123
+
124
+ def _action_data_from_mapping(data: Mapping[str, object]) -> ActionRequiredData:
125
+ """Parse ActionRequired inner ``data`` object."""
126
+ action_type = _require_str(data, "actionType", context="ActionRequired.data")
127
+ ctx = "ActionRequired.data"
128
+ if action_type == ToolConfirmationAction.ACTION_TYPE:
129
+ return ToolConfirmationAction(
130
+ id=_require_str(data, "id", context=f"{ctx}.toolConfirmation"),
131
+ tool_name=_require_str(data, "toolName", context=f"{ctx}.toolConfirmation"),
132
+ arguments=_require_args_dict(
133
+ data, "arguments", context=f"{ctx}.toolConfirmation"
134
+ ),
135
+ prompt=_optional_str(data, "prompt"),
136
+ )
137
+ if action_type == ElicitationAction.ACTION_TYPE:
138
+ return ElicitationAction(
139
+ id=_require_str(data, "id", context=f"{ctx}.elicitation"),
140
+ message=_require_str(data, "message", context=f"{ctx}.elicitation"),
141
+ requested_schema=_require_dict(
142
+ data, "requestedSchema", context=f"{ctx}.elicitation"
143
+ ),
144
+ )
145
+ if action_type == ElicitationResponseAction.ACTION_TYPE:
146
+ return ElicitationResponseAction(
147
+ id=_require_str(data, "id", context=f"{ctx}.elicitationResponse"),
148
+ user_data=_require_dict(data, "userData", context=f"{ctx}.elicitationResponse"),
149
+ )
150
+ raise ValueError(f"{ctx}: unknown actionType: {action_type!r}")
151
+
152
+
153
+ def _action_data_to_dict(data: ActionRequiredData) -> dict[str, object]:
154
+ """Serialize ActionRequired inner data to wire JSON (camelCase)."""
155
+ if isinstance(data, ToolConfirmationAction):
156
+ inner: dict[str, object | None] = {
157
+ "actionType": data.ACTION_TYPE,
158
+ "id": data.id,
159
+ "toolName": data.tool_name,
160
+ "arguments": data.arguments,
161
+ "prompt": data.prompt,
162
+ }
163
+ return _drop_none(inner)
164
+ if isinstance(data, ElicitationAction):
165
+ return {
166
+ "actionType": data.ACTION_TYPE,
167
+ "id": data.id,
168
+ "message": data.message,
169
+ "requestedSchema": data.requested_schema,
170
+ }
171
+ if isinstance(data, ElicitationResponseAction):
172
+ return {
173
+ "actionType": data.ACTION_TYPE,
174
+ "id": data.id,
175
+ "userData": data.user_data,
176
+ }
177
+ raise TypeError(f"unsupported ActionRequiredData: {type(data)!r}")
178
+
179
+
180
+ def _notification_type_from_wire(
181
+ d: Mapping[str, object], *, context: str
182
+ ) -> SystemNotificationType:
183
+ """Parse and validate ``notificationType``."""
184
+ raw = _require_str(d, "notificationType", context=context)
185
+ if raw not in ("thinkingMessage", "inlineMessage", "creditsExhausted"):
186
+ raise ValueError(f"{context}: invalid notificationType: {raw!r}")
187
+ return raw # type: ignore[return-value]
188
+
189
+
190
+ class ContentBlock(ABC):
191
+ """Abstract base for all typed blocks (see subclasses for variants)."""
192
+
193
+ TYPE: ClassVar[str]
194
+
195
+ @abstractmethod
196
+ def to_dict(self) -> dict[str, object]:
197
+ """Serialize this block to a JSON-compatible dict (camelCase keys)."""
198
+
199
+ @classmethod
200
+ @abstractmethod
201
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
202
+ """Parse validated payload (``type`` already checked by dispatcher)."""
203
+
204
+ @classmethod
205
+ def from_dict(cls, d: Mapping[str, object]) -> Self:
206
+ """Deserialize a content block or dispatch from the abstract base."""
207
+ if cls is ContentBlock:
208
+ tag = d.get("type")
209
+ if not isinstance(tag, str) or tag not in _DISPATCH:
210
+ raise ValueError(f"unknown ContentBlock type: {tag!r}")
211
+ concrete = _DISPATCH[tag]
212
+ return cast(Self, concrete._from_dict_payload(d))
213
+ tag = d.get("type")
214
+ if not isinstance(tag, str) or tag != cls.TYPE:
215
+ raise ValueError(f"expected ContentBlock type {cls.TYPE!r}, got {tag!r}")
216
+ return cls._from_dict_payload(d)
217
+
218
+
219
+ @dataclass(frozen=True, kw_only=True)
220
+ class ToolConfirmationAction:
221
+ """Tool confirmation payload for :class:`ActionRequired`."""
222
+
223
+ ACTION_TYPE: ClassVar[str] = "toolConfirmation"
224
+ id: str
225
+ tool_name: str
226
+ arguments: dict[str, object]
227
+ prompt: str | None = None
228
+
229
+
230
+ @dataclass(frozen=True, kw_only=True)
231
+ class ElicitationAction:
232
+ """Elicitation request payload for :class:`ActionRequired`."""
233
+
234
+ ACTION_TYPE: ClassVar[str] = "elicitation"
235
+ id: str
236
+ message: str
237
+ requested_schema: dict[str, object]
238
+
239
+
240
+ @dataclass(frozen=True, kw_only=True)
241
+ class ElicitationResponseAction:
242
+ """Elicitation response payload for :class:`ActionRequired`."""
243
+
244
+ ACTION_TYPE: ClassVar[str] = "elicitationResponse"
245
+ id: str
246
+ user_data: dict[str, object]
247
+
248
+
249
+ ActionRequiredData: TypeAlias = (
250
+ ToolConfirmationAction | ElicitationAction | ElicitationResponseAction
251
+ )
252
+
253
+ SystemNotificationType = Literal["thinkingMessage", "inlineMessage", "creditsExhausted"]
254
+
255
+
256
+ @dataclass(frozen=True, kw_only=True)
257
+ class Text(ContentBlock):
258
+ """Plain text segment."""
259
+
260
+ TYPE: ClassVar[str] = "text"
261
+ text: str
262
+
263
+ def to_dict(self) -> dict[str, object]:
264
+ return {"type": self.TYPE, "text": self.text}
265
+
266
+ @classmethod
267
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
268
+ return cls(text=_require_str(payload, "text", context="Text"))
269
+
270
+
271
+ @dataclass(frozen=True, kw_only=True)
272
+ class Image(ContentBlock):
273
+ """Base64 image payload."""
274
+
275
+ TYPE: ClassVar[str] = "image"
276
+ mime_type: str
277
+ data: str
278
+ metadata: dict[str, object] | None = None
279
+
280
+ def to_dict(self) -> dict[str, object]:
281
+ return _drop_none(
282
+ {
283
+ "type": self.TYPE,
284
+ "mimeType": self.mime_type,
285
+ "data": self.data,
286
+ "metadata": self.metadata,
287
+ }
288
+ )
289
+
290
+ @classmethod
291
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
292
+ return cls(
293
+ mime_type=_require_str(payload, "mimeType", context="Image"),
294
+ data=_require_str(payload, "data", context="Image"),
295
+ metadata=_optional_dict(payload, "metadata", context="Image"),
296
+ )
297
+
298
+
299
+ @dataclass(frozen=True, kw_only=True)
300
+ class File(ContentBlock):
301
+ """Base64 document payload (e.g. PDF)."""
302
+
303
+ TYPE: ClassVar[str] = "file"
304
+ mime_type: str
305
+ data: str
306
+ metadata: dict[str, object] | None = None
307
+
308
+ def to_dict(self) -> dict[str, object]:
309
+ return _drop_none(
310
+ {
311
+ "type": self.TYPE,
312
+ "mimeType": self.mime_type,
313
+ "data": self.data,
314
+ "metadata": self.metadata,
315
+ }
316
+ )
317
+
318
+ @classmethod
319
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
320
+ return cls(
321
+ mime_type=_require_str(payload, "mimeType", context="File"),
322
+ data=_require_str(payload, "data", context="File"),
323
+ metadata=_optional_dict(payload, "metadata", context="File"),
324
+ )
325
+
326
+
327
+ @dataclass(frozen=True, kw_only=True)
328
+ class AttachmentRef(ContentBlock):
329
+ """Reference to a session-scoped uploaded attachment (client / pre-freeze user turn)."""
330
+
331
+ TYPE: ClassVar[str] = "attachmentRef"
332
+ attachment_id: str
333
+ mime_type: str
334
+ metadata: dict[str, object] | None = None
335
+
336
+ def to_dict(self) -> dict[str, object]:
337
+ return _drop_none(
338
+ {
339
+ "type": self.TYPE,
340
+ "attachmentId": self.attachment_id,
341
+ "mimeType": self.mime_type,
342
+ "metadata": self.metadata,
343
+ }
344
+ )
345
+
346
+ @classmethod
347
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
348
+ return cls(
349
+ attachment_id=_require_str(payload, "attachmentId", context="AttachmentRef"),
350
+ mime_type=_require_str(payload, "mimeType", context="AttachmentRef"),
351
+ metadata=_optional_dict(payload, "metadata", context="AttachmentRef"),
352
+ )
353
+
354
+
355
+ @dataclass(frozen=True, kw_only=True)
356
+ class AttachmentDescriptor(ContentBlock):
357
+ """Structured attachment metadata for SSE/UI (not persisted in history)."""
358
+
359
+ TYPE: ClassVar[str] = "attachmentDescriptor"
360
+ attachment_id: str
361
+ mime_type: str
362
+ description: str
363
+ metadata: dict[str, object] | None = None
364
+
365
+ def to_dict(self) -> dict[str, object]:
366
+ return _drop_none(
367
+ {
368
+ "type": self.TYPE,
369
+ "attachmentId": self.attachment_id,
370
+ "mimeType": self.mime_type,
371
+ "description": self.description,
372
+ "metadata": self.metadata,
373
+ }
374
+ )
375
+
376
+ @classmethod
377
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
378
+ return cls(
379
+ attachment_id=_require_str(payload, "attachmentId", context="AttachmentDescriptor"),
380
+ mime_type=_require_str(payload, "mimeType", context="AttachmentDescriptor"),
381
+ description=_require_str(payload, "description", context="AttachmentDescriptor"),
382
+ metadata=_optional_dict(payload, "metadata", context="AttachmentDescriptor"),
383
+ )
384
+
385
+
386
+ @dataclass(frozen=True, kw_only=True)
387
+ class ToolRequest(ContentBlock):
388
+ """Model-issued tool invocation request.
389
+
390
+ ``metadata`` carries opaque, provider-specific round-trip data (e.g. Vertex
391
+ Gemini's ``thoughtSignature``). It is omitted from JSON when None or empty.
392
+ """
393
+
394
+ TYPE: ClassVar[str] = "toolRequest"
395
+ id: str
396
+ name: str
397
+ args: dict[str, object]
398
+ parse_error: str | None = None
399
+ metadata: dict[str, object] | None = None
400
+
401
+ def to_dict(self) -> dict[str, object]:
402
+ meta = self.metadata if self.metadata else None
403
+ return _drop_none(
404
+ {
405
+ "type": self.TYPE,
406
+ "id": self.id,
407
+ "name": self.name,
408
+ "args": self.args,
409
+ "parseError": self.parse_error,
410
+ "metadata": meta,
411
+ }
412
+ )
413
+
414
+ @classmethod
415
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
416
+ return cls(
417
+ id=_require_str(payload, "id", context="ToolRequest"),
418
+ name=_require_str(payload, "name", context="ToolRequest"),
419
+ args=_require_args_dict(payload, "args", context="ToolRequest"),
420
+ parse_error=_optional_str(payload, "parseError"),
421
+ metadata=_optional_dict(payload, "metadata", context="ToolRequest"),
422
+ )
423
+
424
+
425
+ @dataclass(frozen=True, kw_only=True)
426
+ class ToolResponse(ContentBlock):
427
+ """Tool execution result as nested content blocks."""
428
+
429
+ TYPE: ClassVar[str] = "toolResponse"
430
+ id: str
431
+ tool_name: str
432
+ result: list[ContentBlock]
433
+ is_error: bool = False
434
+
435
+ def to_dict(self) -> dict[str, object]:
436
+ return {
437
+ "type": self.TYPE,
438
+ "id": self.id,
439
+ "toolName": self.tool_name,
440
+ "result": [b.to_dict() for b in self.result],
441
+ "isError": self.is_error,
442
+ }
443
+
444
+ @classmethod
445
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
446
+ return cls(
447
+ id=_require_str(payload, "id", context="ToolResponse"),
448
+ tool_name=_require_str(payload, "toolName", context="ToolResponse"),
449
+ result=_require_block_list(payload, "result", context="ToolResponse"),
450
+ is_error=_parse_tool_response_is_error(payload, context="ToolResponse"),
451
+ )
452
+
453
+
454
+ @dataclass(frozen=True, kw_only=True)
455
+ class ToolConfirmationRequest(ContentBlock):
456
+ """Request for user confirmation before running a tool."""
457
+
458
+ TYPE: ClassVar[str] = "toolConfirmationRequest"
459
+ id: str
460
+ tool_name: str
461
+ arguments: dict[str, object]
462
+ prompt: str | None = None
463
+
464
+ def to_dict(self) -> dict[str, object]:
465
+ return _drop_none(
466
+ {
467
+ "type": self.TYPE,
468
+ "id": self.id,
469
+ "toolName": self.tool_name,
470
+ "arguments": self.arguments,
471
+ "prompt": self.prompt,
472
+ }
473
+ )
474
+
475
+ @classmethod
476
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
477
+ return cls(
478
+ id=_require_str(payload, "id", context="ToolConfirmationRequest"),
479
+ tool_name=_require_str(payload, "toolName", context="ToolConfirmationRequest"),
480
+ arguments=_require_args_dict(
481
+ payload, "arguments", context="ToolConfirmationRequest"
482
+ ),
483
+ prompt=_optional_str(payload, "prompt"),
484
+ )
485
+
486
+
487
+ @dataclass(frozen=True, kw_only=True)
488
+ class FrontendToolRequest(ContentBlock):
489
+ """Tool request directed at frontend tooling."""
490
+
491
+ TYPE: ClassVar[str] = "frontendToolRequest"
492
+ id: str
493
+ name: str
494
+ args: dict[str, object]
495
+ parse_error: str | None = None
496
+
497
+ def to_dict(self) -> dict[str, object]:
498
+ return _drop_none(
499
+ {
500
+ "type": self.TYPE,
501
+ "id": self.id,
502
+ "name": self.name,
503
+ "args": self.args,
504
+ "parseError": self.parse_error,
505
+ }
506
+ )
507
+
508
+ @classmethod
509
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
510
+ return cls(
511
+ id=_require_str(payload, "id", context="FrontendToolRequest"),
512
+ name=_require_str(payload, "name", context="FrontendToolRequest"),
513
+ args=_require_args_dict(payload, "args", context="FrontendToolRequest"),
514
+ parse_error=_optional_str(payload, "parseError"),
515
+ )
516
+
517
+
518
+ @dataclass(frozen=True, kw_only=True)
519
+ class Thinking(ContentBlock):
520
+ """Model reasoning text (opaque to most consumers)."""
521
+
522
+ TYPE: ClassVar[str] = "thinking"
523
+ thinking: str
524
+ signature: str = ""
525
+
526
+ def to_dict(self) -> dict[str, object]:
527
+ return {"type": self.TYPE, "thinking": self.thinking, "signature": self.signature}
528
+
529
+ @classmethod
530
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
531
+ thinking = _require_str(payload, "thinking", context="Thinking")
532
+ sig_raw = payload.get("signature")
533
+ if sig_raw is None:
534
+ signature = ""
535
+ elif not isinstance(sig_raw, str):
536
+ raise ValueError(
537
+ f"Thinking: field 'signature' must be a string, got {type(sig_raw).__name__}"
538
+ )
539
+ else:
540
+ signature = sig_raw
541
+ return cls(thinking=thinking, signature=signature)
542
+
543
+
544
+ @dataclass(frozen=True, kw_only=True)
545
+ class RedactedThinking(ContentBlock):
546
+ """Opaque redacted reasoning blob."""
547
+
548
+ TYPE: ClassVar[str] = "redactedThinking"
549
+ data: str
550
+
551
+ def to_dict(self) -> dict[str, object]:
552
+ return {"type": self.TYPE, "data": self.data}
553
+
554
+ @classmethod
555
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
556
+ return cls(data=_require_str(payload, "data", context="RedactedThinking"))
557
+
558
+
559
+ @dataclass(frozen=True, kw_only=True)
560
+ class SystemNotification(ContentBlock):
561
+ """System-originated notification block."""
562
+
563
+ TYPE: ClassVar[str] = "systemNotification"
564
+ notification_type: SystemNotificationType
565
+ msg: str
566
+ data: dict[str, object] | None = None
567
+
568
+ def to_dict(self) -> dict[str, object]:
569
+ return _drop_none(
570
+ {
571
+ "type": self.TYPE,
572
+ "notificationType": self.notification_type,
573
+ "msg": self.msg,
574
+ "data": self.data,
575
+ }
576
+ )
577
+
578
+ @classmethod
579
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
580
+ return cls(
581
+ notification_type=_notification_type_from_wire(
582
+ payload, context="SystemNotification"
583
+ ),
584
+ msg=_require_str(payload, "msg", context="SystemNotification"),
585
+ data=_optional_dict(payload, "data", context="SystemNotification"),
586
+ )
587
+
588
+
589
+ @dataclass(frozen=True, kw_only=True)
590
+ class ActionRequired(ContentBlock):
591
+ """User or UI must take action (sub-tagged by ``data.actionType``)."""
592
+
593
+ TYPE: ClassVar[str] = "actionRequired"
594
+ data: ActionRequiredData
595
+
596
+ def to_dict(self) -> dict[str, object]:
597
+ return {"type": self.TYPE, "data": _action_data_to_dict(self.data)}
598
+
599
+ @classmethod
600
+ def _from_dict_payload(cls, payload: Mapping[str, object]) -> Self:
601
+ inner = _require_dict(payload, "data", "ActionRequired")
602
+ return cls(data=_action_data_from_mapping(inner))
603
+
604
+
605
+ _DISPATCH.update(
606
+ {
607
+ "text": Text,
608
+ "image": Image,
609
+ "file": File,
610
+ "attachmentRef": AttachmentRef,
611
+ "attachmentDescriptor": AttachmentDescriptor,
612
+ "toolRequest": ToolRequest,
613
+ "toolResponse": ToolResponse,
614
+ "toolConfirmationRequest": ToolConfirmationRequest,
615
+ "actionRequired": ActionRequired,
616
+ "frontendToolRequest": FrontendToolRequest,
617
+ "thinking": Thinking,
618
+ "redactedThinking": RedactedThinking,
619
+ "systemNotification": SystemNotification,
620
+ }
621
+ )
622
+
623
+
624
+ __all__ = [
625
+ "ActionRequired",
626
+ "ActionRequiredData",
627
+ "AttachmentDescriptor",
628
+ "AttachmentRef",
629
+ "ContentBlock",
630
+ "ElicitationAction",
631
+ "ElicitationResponseAction",
632
+ "File",
633
+ "FrontendToolRequest",
634
+ "Image",
635
+ "RedactedThinking",
636
+ "SystemNotification",
637
+ "SystemNotificationType",
638
+ "Text",
639
+ "Thinking",
640
+ "ToolConfirmationAction",
641
+ "ToolConfirmationRequest",
642
+ "ToolRequest",
643
+ "ToolResponse",
644
+ ]