crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,334 @@
1
+ """Event types for Core <-> Frontend communication.
2
+
3
+ The Core emits these events as an async stream. Frontends (CLI, Web UI, SDK)
4
+ consume them to render output, handle permission prompts, etc.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any, Union
11
+
12
+ if TYPE_CHECKING:
13
+ from crabcode_core.types.message import Message
14
+
15
+
16
+ @dataclass
17
+ class StreamTextEvent:
18
+ """A chunk of streamed text from the model."""
19
+ text: str
20
+
21
+
22
+ @dataclass
23
+ class ThinkingEvent:
24
+ """A chunk of thinking/reasoning from the model."""
25
+ text: str
26
+
27
+
28
+ @dataclass
29
+ class ToolUseEvent:
30
+ """The model wants to use a tool."""
31
+ tool_name: str
32
+ tool_input: dict[str, Any]
33
+ tool_use_id: str
34
+ agent_id: str | None = None
35
+
36
+
37
+ @dataclass
38
+ class ToolResultEvent:
39
+ """Result from a tool execution."""
40
+ tool_use_id: str
41
+ tool_name: str
42
+ result: str
43
+ is_error: bool = False
44
+ result_for_display: str | None = None
45
+ tool_input: dict[str, Any] = field(default_factory=dict)
46
+ agent_id: str | None = None
47
+ images: list[dict[str, str]] = field(default_factory=list)
48
+
49
+
50
+ @dataclass
51
+ class PermissionRequestEvent:
52
+ """Core is requesting permission from the frontend to use a tool."""
53
+ tool_name: str
54
+ tool_input: dict[str, Any]
55
+ tool_use_id: str
56
+ reason: str | None = None
57
+ permission_key: str | None = None
58
+ agent_id: str | None = None
59
+ request_kind: str = "tool"
60
+
61
+
62
+ @dataclass
63
+ class PermissionResponseEvent:
64
+ """Frontend's response to a permission request."""
65
+ tool_use_id: str
66
+ allowed: bool
67
+ always_allow: bool = False
68
+ agent_id: str | None = None
69
+ feedback: str | None = None
70
+
71
+
72
+ @dataclass
73
+ class CompactEvent:
74
+ """Conversation was compacted."""
75
+ summary: str
76
+ messages_before: int = 0
77
+ messages_after: int = 0
78
+ trigger: str = "auto"
79
+ agent_id: str | None = None
80
+ # Internal frozen snapshots used by CoreSession for ordered persistence.
81
+ # Frontend payload converters intentionally expose only the fields above.
82
+ source_messages: list[Message] | None = field(default=None, repr=False)
83
+ checkpoint_messages: list[Message] | None = field(default=None, repr=False)
84
+
85
+
86
+ @dataclass
87
+ class ErrorEvent:
88
+ """An error occurred."""
89
+ message: str
90
+ recoverable: bool = True
91
+ error_type: str = ""
92
+ agent_id: str | None = None
93
+
94
+
95
+ @dataclass
96
+ class TurnCompleteEvent:
97
+ """An agentic turn completed."""
98
+ reason: str = "end_turn"
99
+ turn_count: int = 0
100
+ usage: dict[str, Any] = field(default_factory=dict)
101
+ context_used_tokens: int = 0
102
+ context_window_tokens: int = 0
103
+ context_remaining_tokens: int = 0
104
+ context_used_percent: float = 0.0
105
+ context_token_source: str = "estimated"
106
+ assistant_message_uuid: str | None = None
107
+ turn_details: dict[str, Any] | None = None
108
+ prompt_budget: dict[str, Any] = field(default_factory=dict)
109
+
110
+
111
+ @dataclass
112
+ class ChoiceRequestEvent:
113
+ """Core is requesting the user to make a choice from a list of options."""
114
+ tool_use_id: str
115
+ question: str
116
+ options: list[str]
117
+ multiple: bool = False
118
+ agent_id: str | None = None
119
+
120
+
121
+ @dataclass
122
+ class ChoiceResponseEvent:
123
+ """Frontend's response to a choice request."""
124
+ tool_use_id: str
125
+ selected: list[str]
126
+ cancelled: bool = False
127
+ agent_id: str | None = None
128
+
129
+
130
+ @dataclass
131
+ class StreamModeEvent:
132
+ """Signals a phase transition in the streaming lifecycle.
133
+
134
+ Modes:
135
+ - "requesting": API request sent, waiting for first chunk
136
+ - "compacting": conversation checkpoint generation is in progress
137
+ - "thinking": model entered extended thinking
138
+ - "responding": model is streaming text output
139
+ """
140
+ mode: str
141
+ agent_id: str | None = None
142
+
143
+
144
+ @dataclass
145
+ class StreamRetryEvent:
146
+ """A retryable model stream failure that has scheduled a reconnect."""
147
+ message: str
148
+ error: str
149
+ retry_count: int
150
+ max_retries: int
151
+ delay_seconds: float
152
+ unbounded: bool = False
153
+ transport_fallback: bool = False
154
+ discarded_text_chars: int = 0
155
+ agent_id: str | None = None
156
+
157
+
158
+ @dataclass
159
+ class SteeringAppliedEvent:
160
+ """Queued user guidance entered the conversation at a safe boundary."""
161
+ count: int = 1
162
+
163
+
164
+ @dataclass
165
+ class QueuedMessageStartedEvent:
166
+ """A queued follow-up is starting a separate user turn."""
167
+ text: str
168
+ images: list[dict[str, Any]] = field(default_factory=list)
169
+ request_id: str | None = None
170
+
171
+
172
+ @dataclass
173
+ class QueuedMessageUpdatedEvent:
174
+ """A pending follow-up was recalled, deleted, or promoted to guidance."""
175
+ request_id: str
176
+ action: str
177
+
178
+
179
+ @dataclass
180
+ class AgentStateEvent:
181
+ """Lifecycle update for a managed sub-agent."""
182
+ agent_id: str
183
+ parent_agent_id: str | None
184
+ status: str
185
+ subagent_type: str
186
+ title: str
187
+ message: str = ""
188
+ usage: dict[str, Any] = field(default_factory=dict)
189
+
190
+
191
+ @dataclass
192
+ class AgentOutputEvent:
193
+ """Streamed output or tool activity from a managed sub-agent."""
194
+ agent_id: str
195
+ stream: str
196
+ text: str
197
+ tool_name: str | None = None
198
+
199
+
200
+ @dataclass
201
+ class ModeChangeEvent:
202
+ """Agent mode has changed (e.g. plan <-> agent)."""
203
+ mode: str # "plan" | "agent"
204
+ reason: str = ""
205
+
206
+
207
+ @dataclass
208
+ class PlanReadyEvent:
209
+ """A structured execution plan has been produced by plan mode."""
210
+ plan: dict[str, Any]
211
+
212
+
213
+ @dataclass
214
+ class TeamMessageEvent:
215
+ """A message was sent between teammates."""
216
+ team_id: str
217
+ from_agent: str
218
+ to_agent: str
219
+ text: str
220
+ msg_type: str = "text"
221
+ message_id: str = ""
222
+
223
+
224
+ @dataclass
225
+ class PeerMessageEvent:
226
+ """A message arrived from an independent CrabCode session."""
227
+ message_id: str
228
+ from_session_id: str
229
+ from_name: str
230
+ from_cwd: str
231
+ text: str
232
+
233
+
234
+ @dataclass
235
+ class TeamStateEvent:
236
+ """A teammate's state changed within a team."""
237
+ team_id: str
238
+ agent_id: str
239
+ old_state: str
240
+ new_state: str
241
+ role: str = ""
242
+
243
+
244
+ @dataclass
245
+ class TaskUpdateEvent:
246
+ """A task on the shared task board was updated."""
247
+ team_id: str
248
+ task_id: str
249
+ status: str
250
+ assignee: str | None = None
251
+ description: str = ""
252
+
253
+
254
+ @dataclass
255
+ class SnapshotEvent:
256
+ """A file system snapshot was created."""
257
+ snapshot_id: str
258
+ tool_name: str
259
+ files: list[str] # files affected
260
+
261
+
262
+ @dataclass
263
+ class RevertEvent:
264
+ """File system state was reverted."""
265
+ snapshot_id: str
266
+ files_restored: list[str]
267
+
268
+
269
+ @dataclass
270
+ class ScheduleRunEvent:
271
+ """A scheduled job has been executed (or failed/timed out)."""
272
+ job_id: str
273
+ run_id: str
274
+ status: str # "success" | "failed" | "timeout"
275
+ duration_seconds: float = 0.0
276
+ error_message: str = ""
277
+ result_summary: str = ""
278
+ next_run: str | None = None
279
+
280
+
281
+ @dataclass
282
+ class DocumentJobEvent:
283
+ """Progress for a structured document translation or Blog operation."""
284
+ action: str
285
+ status: str
286
+ locale: str = ""
287
+ language: str = ""
288
+ source: str = ""
289
+ current: int = 0
290
+ total: int = 0
291
+ message: str = ""
292
+ engine: str = "legacy"
293
+
294
+
295
+ @dataclass
296
+ class DocumentSelectionTranslationEvent:
297
+ """Translation result for a user-selected document excerpt."""
298
+ operation_id: str
299
+ locale: str
300
+ source_text: str
301
+ translated_text: str = ""
302
+ status: str = "completed"
303
+ message: str = ""
304
+
305
+
306
+ CoreEvent = Union[
307
+ StreamTextEvent,
308
+ ThinkingEvent,
309
+ ToolUseEvent,
310
+ ToolResultEvent,
311
+ PermissionRequestEvent,
312
+ ChoiceRequestEvent,
313
+ CompactEvent,
314
+ ErrorEvent,
315
+ TurnCompleteEvent,
316
+ StreamModeEvent,
317
+ StreamRetryEvent,
318
+ SteeringAppliedEvent,
319
+ QueuedMessageStartedEvent,
320
+ QueuedMessageUpdatedEvent,
321
+ AgentStateEvent,
322
+ AgentOutputEvent,
323
+ ModeChangeEvent,
324
+ PlanReadyEvent,
325
+ PeerMessageEvent,
326
+ TeamMessageEvent,
327
+ TeamStateEvent,
328
+ TaskUpdateEvent,
329
+ SnapshotEvent,
330
+ RevertEvent,
331
+ ScheduleRunEvent,
332
+ DocumentJobEvent,
333
+ DocumentSelectionTranslationEvent,
334
+ ]
@@ -0,0 +1,263 @@
1
+ """Message types for the CrabCode conversation protocol.
2
+
3
+ Internal format follows Anthropic's message structure. API adapters
4
+ translate to/from provider-specific formats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from enum import Enum
12
+ from typing import Any, Literal, Union
13
+
14
+ from pydantic import BaseModel, Field
15
+ from crabcode_core.logging_utils import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class MessageRole(str, Enum):
21
+ USER = "user"
22
+ ASSISTANT = "assistant"
23
+ SYSTEM = "system"
24
+
25
+
26
+ # --- Content blocks ---
27
+
28
+
29
+ class TextBlock(BaseModel):
30
+ type: Literal["text"] = "text"
31
+ text: str
32
+
33
+
34
+ class ToolUseBlock(BaseModel):
35
+ type: Literal["tool_use"] = "tool_use"
36
+ id: str
37
+ name: str
38
+ input: dict[str, Any]
39
+
40
+
41
+ class ToolResultBlock(BaseModel):
42
+ type: Literal["tool_result"] = "tool_result"
43
+ tool_use_id: str
44
+ content: str
45
+ is_error: bool = False
46
+
47
+
48
+ class ThinkingBlock(BaseModel):
49
+ type: Literal["thinking"] = "thinking"
50
+ thinking: str
51
+
52
+
53
+ class SignatureBlock(BaseModel):
54
+ type: Literal["signature"] = "signature"
55
+ signature: str
56
+
57
+
58
+ class ImageBlock(BaseModel):
59
+ """An image content block (base64-encoded).
60
+
61
+ Follows Anthropic's image block format. API adapters translate
62
+ to provider-specific representations.
63
+ """
64
+ type: Literal["image"] = "image"
65
+ source: dict[str, Any] # {"type": "base64", "media_type": "image/png", "data": "..."}
66
+ # Persist UI captions outside source so provider image payloads stay valid.
67
+ description: str = ""
68
+
69
+
70
+ ContentBlock = Union[TextBlock, ToolUseBlock, ToolResultBlock, ThinkingBlock, SignatureBlock, ImageBlock]
71
+
72
+
73
+ # --- Messages ---
74
+
75
+
76
+ class Message(BaseModel):
77
+ """Base message in a conversation."""
78
+
79
+ uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
80
+ parent_uuid: str | None = None
81
+ role: MessageRole
82
+ content: list[ContentBlock] | str
83
+ timestamp: str = Field(
84
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
85
+ )
86
+ is_compact_summary: bool = False
87
+ origin: str | None = None
88
+
89
+ @property
90
+ def text_content(self) -> str:
91
+ """Extract plain text from content blocks."""
92
+ if isinstance(self.content, str):
93
+ return self.content
94
+ parts: list[str] = []
95
+ for block in self.content:
96
+ if isinstance(block, TextBlock):
97
+ parts.append(block.text)
98
+ return "".join(parts)
99
+
100
+ @property
101
+ def tool_use_blocks(self) -> list[ToolUseBlock]:
102
+ if isinstance(self.content, str):
103
+ return []
104
+ return [b for b in self.content if isinstance(b, ToolUseBlock)]
105
+
106
+ @property
107
+ def has_tool_use(self) -> bool:
108
+ return len(self.tool_use_blocks) > 0
109
+
110
+
111
+ class UserMessage(Message):
112
+ role: Literal[MessageRole.USER] = MessageRole.USER
113
+ tool_use_result: str | None = None
114
+ source_tool_assistant_uuid: str | None = None
115
+
116
+
117
+ class AssistantMessage(Message):
118
+ role: Literal[MessageRole.ASSISTANT] = MessageRole.ASSISTANT
119
+ reply_to_uuid: str | None = None
120
+ api_error: str | None = None
121
+ usage: dict[str, Any] | None = None
122
+ request_id: str | None = None
123
+ turn_details: dict[str, Any] | None = None
124
+
125
+
126
+ class SystemMessage(Message):
127
+ role: Literal[MessageRole.SYSTEM] = MessageRole.SYSTEM
128
+
129
+
130
+ def create_user_message(
131
+ content: list[ContentBlock] | str,
132
+ **kwargs: Any,
133
+ ) -> UserMessage:
134
+ return UserMessage(content=content, **kwargs)
135
+
136
+
137
+ def create_assistant_message(
138
+ content: list[ContentBlock] | str,
139
+ **kwargs: Any,
140
+ ) -> AssistantMessage:
141
+ return AssistantMessage(content=content, **kwargs)
142
+
143
+
144
+ def create_tool_result_message(
145
+ tool_use_id: str,
146
+ result: str,
147
+ is_error: bool = False,
148
+ source_tool_assistant_uuid: str | None = None,
149
+ images: list[dict[str, str]] | None = None,
150
+ ) -> UserMessage:
151
+ """Create a user message containing a tool result."""
152
+ return UserMessage(
153
+ content=[
154
+ ToolResultBlock(
155
+ tool_use_id=tool_use_id,
156
+ content=result,
157
+ is_error=is_error,
158
+ )
159
+ ] + [
160
+ ImageBlock(
161
+ source={
162
+ "type": "base64",
163
+ "media_type": image.get("media_type", "image/png"),
164
+ "data": image.get("data", ""),
165
+ },
166
+ description=image.get("description", ""),
167
+ )
168
+ for image in (images or [])
169
+ if image.get("data")
170
+ ],
171
+ parent_uuid=source_tool_assistant_uuid,
172
+ tool_use_result=result,
173
+ source_tool_assistant_uuid=source_tool_assistant_uuid,
174
+ )
175
+
176
+
177
+ def message_from_entry(raw: dict[str, Any]) -> Message | None:
178
+ """Rebuild a durable message entry without dropping subtype metadata."""
179
+ role = raw.get("type")
180
+ model: type[Message]
181
+ if role == MessageRole.USER.value:
182
+ model = UserMessage
183
+ elif role == MessageRole.ASSISTANT.value:
184
+ model = AssistantMessage
185
+ elif role == MessageRole.SYSTEM.value:
186
+ model = SystemMessage
187
+ else:
188
+ return None
189
+
190
+ payload = dict(raw)
191
+ payload.pop("type", None)
192
+ payload["role"] = role
193
+ payload["content"] = deserialize_content(payload.get("content", ""))
194
+ try:
195
+ return model.model_validate(payload)
196
+ except Exception:
197
+ logger.warning("Failed to restore durable %s message", role, exc_info=True)
198
+ return None
199
+
200
+
201
+ def find_assistant_reply(
202
+ messages: list[Message],
203
+ message_id: str,
204
+ ) -> AssistantMessage | None:
205
+ """Find a successful final assistant reply correlated to one input message."""
206
+ related_uuids = {message_id}
207
+ message_seen = False
208
+ for message in messages:
209
+ if message.uuid == message_id:
210
+ message_seen = True
211
+ continue
212
+
213
+ directly_related = (
214
+ isinstance(message, AssistantMessage)
215
+ and message.reply_to_uuid == message_id
216
+ )
217
+ ancestrally_related = message_seen and message.parent_uuid in related_uuids
218
+ if not directly_related and not ancestrally_related:
219
+ continue
220
+
221
+ related_uuids.add(message.uuid)
222
+ if (
223
+ isinstance(message, AssistantMessage)
224
+ and not message.api_error
225
+ and not message.has_tool_use
226
+ ):
227
+ return message
228
+ return None
229
+
230
+
231
+ def deserialize_content(raw: Any) -> list[ContentBlock] | str:
232
+ """Deserialize raw JSON content back into typed ContentBlock list."""
233
+ if isinstance(raw, str):
234
+ return raw
235
+ if not isinstance(raw, list):
236
+ return str(raw) if raw else ""
237
+
238
+ _BLOCK_MAP: dict[str, type[BaseModel]] = {
239
+ "text": TextBlock,
240
+ "tool_use": ToolUseBlock,
241
+ "tool_result": ToolResultBlock,
242
+ "thinking": ThinkingBlock,
243
+ "signature": SignatureBlock,
244
+ "image": ImageBlock,
245
+ }
246
+
247
+ blocks: list[ContentBlock] = []
248
+ for item in raw:
249
+ if not isinstance(item, dict):
250
+ continue
251
+ block_type = item.get("type", "")
252
+ cls = _BLOCK_MAP.get(block_type)
253
+ if cls:
254
+ try:
255
+ blocks.append(cls.model_validate(item))
256
+ except Exception:
257
+ logger.debug("Failed to deserialize message block type=%s", block_type, exc_info=True)
258
+ if "text" in item:
259
+ blocks.append(TextBlock(text=item["text"]))
260
+ elif "text" in item:
261
+ blocks.append(TextBlock(text=item["text"]))
262
+
263
+ return blocks if blocks else ""