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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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 ""
|