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,1820 @@
1
+ """Pydantic request/response schemas for the gateway API.
2
+
3
+ Maps crabcode_core CoreEvent dataclasses to serializable Pydantic models
4
+ for HTTP JSON and SSE transport.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import binascii
11
+ from typing import Any, Literal, Union
12
+
13
+ from pydantic import BaseModel, Field, field_validator, model_validator
14
+
15
+ from crabcode_core import VERSION
16
+ from crabcode_gateway.protocol import (
17
+ GATEWAY_MAX_PROTOCOL_VERSION,
18
+ GATEWAY_MIN_PROTOCOL_VERSION,
19
+ GATEWAY_PROTOCOL_VERSION,
20
+ )
21
+ from crabcode_core.types.config import DEFAULT_COMPACT_BUFFER_TOKENS, PermissionRule, ReasoningEffort
22
+
23
+
24
+ class UsageDayResponse(BaseModel):
25
+ date: str
26
+ input_tokens: int
27
+ output_tokens: int
28
+ total_tokens: int | None
29
+ request_count: int
30
+ unknown_requests: int
31
+ missing_requests: int
32
+ partial_requests: int
33
+ coverage: Literal["complete", "partial", "unavailable"]
34
+
35
+
36
+ class UsagePointResponse(BaseModel):
37
+ date: str
38
+ total_tokens: int | None
39
+
40
+
41
+ class UsageModelResponse(BaseModel):
42
+ key: str
43
+ provider: str
44
+ model_id: str
45
+ model: str
46
+ total_tokens: int
47
+ recorded_request_count: int
48
+ points: list[UsagePointResponse]
49
+
50
+
51
+ class UsageSummaryResponse(BaseModel):
52
+ input_tokens: int
53
+ output_tokens: int
54
+ total_tokens: int
55
+ request_count: int
56
+ unknown_requests: int
57
+ missing_requests: int
58
+ partial_requests: int
59
+
60
+
61
+ class UsageDailyResponse(BaseModel):
62
+ start: str
63
+ end: str
64
+ timezone: str
65
+ scope: Literal["global", "project"]
66
+ generated_at: str
67
+ tracking_started_at: str | None
68
+ first_recorded_at: str | None
69
+ last_recorded_at: str | None
70
+ days: list[UsageDayResponse]
71
+ models: list[UsageModelResponse]
72
+ summary: UsageSummaryResponse
73
+
74
+
75
+ # ── Request schemas ──────────────────────────────────────────────
76
+
77
+
78
+ MAX_IMAGE_ATTACHMENT_BYTES = 20 * 1024 * 1024
79
+
80
+
81
+ class ImageAttachment(BaseModel):
82
+ media_type: str
83
+ data: str
84
+ description: str = ""
85
+
86
+ @field_validator("media_type")
87
+ @classmethod
88
+ def validate_media_type(cls, value: str) -> str:
89
+ media_type = value.strip()
90
+ if not media_type.lower().startswith("image/"):
91
+ raise ValueError("media_type must be an image MIME type")
92
+ return media_type
93
+
94
+ @field_validator("data")
95
+ @classmethod
96
+ def validate_data(cls, value: str) -> str:
97
+ # Reject obviously oversized input before decoding it. A base64
98
+ # string for N bytes is at most 4 * ceil(N / 3) characters.
99
+ max_encoded_chars = 4 * ((MAX_IMAGE_ATTACHMENT_BYTES + 2) // 3)
100
+ if len(value) > max_encoded_chars:
101
+ raise ValueError("image data exceeds the 20MB limit")
102
+ try:
103
+ decoded = base64.b64decode(value, validate=True)
104
+ except (binascii.Error, ValueError) as exc:
105
+ raise ValueError("data must be valid base64") from exc
106
+ if len(decoded) > MAX_IMAGE_ATTACHMENT_BYTES:
107
+ raise ValueError("image data exceeds the 20MB limit")
108
+ return value
109
+
110
+
111
+ class SendMessageRequest(BaseModel):
112
+ text: str
113
+ max_turns: int = Field(default=0, ge=0, strict=True)
114
+ session_id: str | None = None
115
+ operation_id: str | None = Field(default=None, min_length=1)
116
+ images: list[ImageAttachment] = Field(default_factory=list)
117
+ computer_use_enabled: bool | None = None
118
+ computer_use_mode: Literal["background_app", "foreground_desktop"] | None = None
119
+ computer_use_target_scope: Literal["app_window", "desktop"] | None = None
120
+ computer_use_delivery_policy: Literal["strict_background", "allow_foreground"] | None = None
121
+
122
+ @model_validator(mode="after")
123
+ def validate_content(self) -> "SendMessageRequest":
124
+ if not self.text.strip() and not self.images:
125
+ raise ValueError("text or at least one image is required")
126
+ return self
127
+
128
+
129
+ class NewSessionRequest(BaseModel):
130
+ cwd: str | None = None
131
+ additional_directories: list[str] = Field(default_factory=list)
132
+ model: str | None = None
133
+ provider: str | None = None
134
+ base_url: str | None = None
135
+ api_format: str | None = None
136
+ model_profile: str | None = None
137
+ reasoning_effort: ReasoningEffort | None = None
138
+ ultra_mode: bool | None = None
139
+ mode: Literal["agent", "plan"] | None = None
140
+ permission_mode: Literal[
141
+ "default",
142
+ "ask",
143
+ "run_everything",
144
+ "bypassPermissions",
145
+ "ai_review",
146
+ "aiReview",
147
+ ] | None = None
148
+ computer_use_host_id: str | None = Field(default=None, min_length=1, max_length=200)
149
+ computer_use_enabled: bool | None = None
150
+ computer_use_mode: Literal["background_app", "foreground_desktop"] | None = None
151
+ computer_use_target_scope: Literal["app_window", "desktop"] | None = None
152
+ computer_use_delivery_policy: Literal["strict_background", "allow_foreground"] | None = None
153
+
154
+
155
+ class ResumeSessionRequest(BaseModel):
156
+ session_id: str
157
+ additional_directories: list[str] = Field(default_factory=list)
158
+ model: str | None = None
159
+ provider: str | None = None
160
+ base_url: str | None = None
161
+ api_format: str | None = None
162
+ model_profile: str | None = None
163
+ computer_use_host_id: str | None = Field(default=None, min_length=1, max_length=200)
164
+ computer_use_enabled: bool | None = None
165
+ computer_use_mode: Literal["background_app", "foreground_desktop"] | None = None
166
+ computer_use_target_scope: Literal["app_window", "desktop"] | None = None
167
+ computer_use_delivery_policy: Literal["strict_background", "allow_foreground"] | None = None
168
+
169
+
170
+ class ForkSessionRequest(BaseModel):
171
+ session_id: str
172
+ message_uuid: str | None = Field(default=None, min_length=1)
173
+ title: str | None = Field(default=None, max_length=200)
174
+
175
+
176
+ class CompactRequest(BaseModel):
177
+ session_id: str
178
+ custom_instructions: str | None = None
179
+
180
+
181
+ class ClearSessionRequest(BaseModel):
182
+ session_id: str
183
+
184
+
185
+ class InterruptRequest(BaseModel):
186
+ session_id: str
187
+ operation_id: str | None = Field(default=None, min_length=1)
188
+
189
+
190
+ class PermissionResponseRequest(BaseModel):
191
+ tool_use_id: str
192
+ allowed: bool
193
+ always_allow: bool = False
194
+ agent_id: str | None = None
195
+ feedback: str | None = None
196
+ # Optional for multi-session clients; omitted keeps default-session behavior.
197
+ session_id: str | None = None
198
+
199
+
200
+ class ChoiceResponseRequest(BaseModel):
201
+ tool_use_id: str
202
+ selected: list[str]
203
+ cancelled: bool = False
204
+ agent_id: str | None = None
205
+ # Optional for multi-session clients; omitted keeps default-session behavior.
206
+ session_id: str | None = None
207
+
208
+
209
+ class SpawnAgentRequest(BaseModel):
210
+ prompt: str = Field(min_length=1)
211
+ subagent_type: str = Field(default="generalPurpose", min_length=1)
212
+ name: str | None = None
213
+ model_profile: str | None = None
214
+ callback: bool = False
215
+ session_id: str | None = None
216
+
217
+ @field_validator("prompt", "subagent_type")
218
+ @classmethod
219
+ def validate_required_text(cls, value: str) -> str:
220
+ if not value.strip():
221
+ raise ValueError("value must not be blank")
222
+ return value
223
+
224
+
225
+ class AgentInputRequest(BaseModel):
226
+ prompt: str = Field(min_length=1)
227
+ interrupt: bool = False
228
+ session_id: str | None = None
229
+
230
+ @field_validator("prompt")
231
+ @classmethod
232
+ def validate_prompt(cls, value: str) -> str:
233
+ if not value.strip():
234
+ raise ValueError("prompt must not be blank")
235
+ return value
236
+
237
+
238
+ class WaitAgentRequest(BaseModel):
239
+ agent_id: str | list[str]
240
+ timeout_ms: int | None = Field(default=None, ge=0, strict=True)
241
+ session_id: str | None = None
242
+
243
+ @field_validator("agent_id")
244
+ @classmethod
245
+ def validate_agent_ids(cls, value: str | list[str]) -> str | list[str]:
246
+ values = [value] if isinstance(value, str) else value
247
+ if not values or any(not isinstance(item, str) or not item.strip() for item in values):
248
+ raise ValueError("agent_id must contain at least one non-blank ID")
249
+ return value
250
+
251
+
252
+ class SwitchModelRequest(BaseModel):
253
+ name: str
254
+ session_id: str | None = None
255
+
256
+
257
+ class SwitchModeRequest(BaseModel):
258
+ mode: Literal["agent", "plan"]
259
+ session_id: str | None = None
260
+
261
+
262
+ class SetReasoningEffortRequest(BaseModel):
263
+ effort: ReasoningEffort | Literal["auto"]
264
+ session_id: str | None = None
265
+
266
+
267
+ class SetUltraModeRequest(BaseModel):
268
+ # Omitted/null means toggle; an explicit boolean is idempotent.
269
+ enabled: bool | None = None
270
+ session_id: str | None = None
271
+
272
+
273
+ class SetPermissionModeRequest(BaseModel):
274
+ """Client-side tool permission override used by chat surfaces."""
275
+
276
+ mode: Literal["default", "ask", "run_everything", "ai_review"]
277
+ session_id: str | None = None
278
+
279
+
280
+ class ModelSettingsMutationRequest(BaseModel):
281
+ """A focused mutation of the named model configuration catalog."""
282
+
283
+ action: Literal[
284
+ "upsert_model",
285
+ "delete_model",
286
+ "upsert_group",
287
+ "delete_group",
288
+ "set_default_model",
289
+ "clear_default_model",
290
+ ]
291
+ source: Literal["userSettings", "projectSettings", "localSettings"] = "projectSettings"
292
+ cwd: str | None = None
293
+ name: str | None = None
294
+ previous_name: str | None = None
295
+ config: dict[str, Any] | None = None
296
+ remove_fields: list[str] = Field(default_factory=list)
297
+
298
+ @model_validator(mode="after")
299
+ def validate_mutation(self) -> "ModelSettingsMutationRequest":
300
+ needs_name = self.action not in {"clear_default_model"}
301
+ if needs_name and (self.name is None or not self.name.strip()):
302
+ raise ValueError("name is required for this model settings action")
303
+ if self.action in {"upsert_model", "upsert_group"} and self.config is None:
304
+ raise ValueError("config is required when creating or updating settings")
305
+ if self.name is not None:
306
+ self.name = self.name.strip()
307
+ return self
308
+
309
+
310
+ class RuntimeSettingsMutationRequest(BaseModel):
311
+ """A focused mutation for runtime, Computer Use, and extra-tool settings."""
312
+
313
+ action: Literal[
314
+ "set_snapshot",
315
+ "set_compaction",
316
+ "set_computer_use_mode",
317
+ "set_computer_use_options",
318
+ "add_extra_tool",
319
+ "remove_extra_tool",
320
+ ]
321
+ source: Literal["userSettings", "projectSettings", "localSettings"] = "projectSettings"
322
+ cwd: str | None = None
323
+ snapshot_enabled: bool | None = None
324
+ snapshot_max_size_mb: int | None = Field(default=None, ge=1, le=1_048_576)
325
+ auto_compact_enabled: bool | None = None
326
+ compact_buffer_tokens: int | None = Field(default=None, ge=0, strict=True)
327
+ max_context_length: int | None = Field(default=None, ge=1, strict=True)
328
+ computer_use_mode: Literal["background_app", "foreground_desktop"] | None = None
329
+ computer_use_target_scope: Literal["app_window", "desktop"] | None = None
330
+ computer_use_delivery_policy: Literal["strict_background", "allow_foreground"] | None = None
331
+ tool_path: str | None = None
332
+
333
+ @model_validator(mode="after")
334
+ def validate_mutation(self) -> "RuntimeSettingsMutationRequest":
335
+ if self.action == "set_snapshot":
336
+ if self.snapshot_enabled is None and self.snapshot_max_size_mb is None:
337
+ raise ValueError("snapshot_enabled or snapshot_max_size_mb is required")
338
+ elif self.action == "set_compaction":
339
+ if (
340
+ self.auto_compact_enabled is None
341
+ and self.compact_buffer_tokens is None
342
+ and "max_context_length" not in self.model_fields_set
343
+ ):
344
+ raise ValueError("At least one compaction setting is required")
345
+ elif self.action in {"set_computer_use_mode", "set_computer_use_options"}:
346
+ if self.action == "set_computer_use_mode" and self.computer_use_mode is None:
347
+ raise ValueError("computer_use_mode is required")
348
+ if self.action == "set_computer_use_options" and (
349
+ self.computer_use_target_scope is None and self.computer_use_delivery_policy is None
350
+ ):
351
+ raise ValueError("computer_use_target_scope or computer_use_delivery_policy is required")
352
+ else:
353
+ if self.tool_path is None or not self.tool_path.strip():
354
+ raise ValueError("tool_path is required for extra tool mutations")
355
+ self.tool_path = self.tool_path.strip()
356
+ if len(self.tool_path) > 240:
357
+ raise ValueError("tool_path is too long")
358
+ return self
359
+
360
+
361
+ class PromptSettingsMutationRequest(BaseModel):
362
+ """Create templates and choose which saved prompts are appended to user input."""
363
+
364
+ action: Literal[
365
+ "save_template",
366
+ "delete_template",
367
+ "set_active_template",
368
+ "add_user_prompt",
369
+ "set_user_prompt_enabled",
370
+ "delete_user_prompt",
371
+ ]
372
+ source: Literal["userSettings", "projectSettings", "localSettings"] = "projectSettings"
373
+ cwd: str | None = None
374
+ template_id: str | None = None
375
+ template_name: str | None = None
376
+ sections: dict[str, str] | None = None
377
+ prompt_id: str | None = None
378
+ prompt_text: str | None = None
379
+ enabled: bool | None = None
380
+
381
+ @model_validator(mode="after")
382
+ def validate_mutation(self) -> "PromptSettingsMutationRequest":
383
+ if self.action == "save_template":
384
+ if self.template_name is None or not self.template_name.strip():
385
+ raise ValueError("template_name is required")
386
+ self.template_name = self.template_name.strip()
387
+ elif self.action in {"delete_template", "set_active_template"}:
388
+ if self.template_id is not None:
389
+ self.template_id = self.template_id.strip() or None
390
+ if self.action == "delete_template" and not self.template_id:
391
+ raise ValueError("template_id is required")
392
+ elif self.action == "add_user_prompt":
393
+ if self.prompt_text is None or not self.prompt_text.strip():
394
+ raise ValueError("prompt_text is required")
395
+ self.prompt_text = self.prompt_text.strip()
396
+ else:
397
+ if self.prompt_id is None or not self.prompt_id.strip():
398
+ raise ValueError("prompt_id is required")
399
+ self.prompt_id = self.prompt_id.strip()
400
+ if self.action == "set_user_prompt_enabled" and self.enabled is None:
401
+ raise ValueError("enabled is required")
402
+ return self
403
+
404
+
405
+ class GoalRequest(BaseModel):
406
+ action: Literal[
407
+ "set", "edit", "pause", "resume", "complete", "blocked", "clear"
408
+ ] = "set"
409
+ objective: str | None = None
410
+ token_budget: int | None = Field(default=None, gt=0)
411
+ session_id: str | None = None
412
+
413
+
414
+ class SkillExpandRequest(BaseModel):
415
+ name: str
416
+ user_input: str = ""
417
+ session_id: str | None = None
418
+
419
+
420
+ class TaskStopRequest(BaseModel):
421
+ task_id: str
422
+ session_id: str | None = None
423
+
424
+
425
+ class ScheduleCreateRequest(BaseModel):
426
+ name: str = Field(min_length=1)
427
+ prompt: str = Field(min_length=1)
428
+ schedule: str = Field(min_length=1)
429
+ schedule_type: Literal["cron", "interval", "once"]
430
+ cwd: str | None = None
431
+ enabled: bool = True
432
+ max_runs: int | None = Field(default=None, gt=0)
433
+ next_run: str | None = None
434
+ description: str = ""
435
+ tags: list[str] = Field(default_factory=list)
436
+ timeout: int | None = Field(default=None, gt=0)
437
+ model_profile: str | None = None
438
+ # The Gateway session that owns this request is ``session_id``. A job can
439
+ # independently reuse a session for its executions via ``job_session_id``.
440
+ job_session_id: str | None = Field(default=None, min_length=1)
441
+ extra: dict[str, Any] = Field(default_factory=dict)
442
+ session_id: str | None = None
443
+
444
+ @field_validator("name", "prompt", "schedule")
445
+ @classmethod
446
+ def validate_required_text(cls, value: str) -> str:
447
+ if not value.strip():
448
+ raise ValueError("value must not be blank")
449
+ return value
450
+
451
+ @field_validator("job_session_id")
452
+ @classmethod
453
+ def validate_job_session_id(cls, value: str | None) -> str | None:
454
+ if value is not None and not value.strip():
455
+ raise ValueError("job_session_id must not be blank")
456
+ return value
457
+
458
+
459
+ class ScheduleJobRequest(BaseModel):
460
+ job_id: str = Field(min_length=1)
461
+ session_id: str | None = None
462
+ scope: Literal["global"] | None = None
463
+
464
+
465
+ class PeerSendRequest(BaseModel):
466
+ to: str = Field(min_length=1)
467
+ text: str = Field(min_length=1)
468
+ session_id: str | None = None
469
+
470
+ @field_validator("to", "text")
471
+ @classmethod
472
+ def validate_required_text(cls, value: str) -> str:
473
+ if not value.strip():
474
+ raise ValueError("value must not be blank")
475
+ return value
476
+
477
+
478
+ class TeamCreateRequest(BaseModel):
479
+ name: str = Field(min_length=1)
480
+ max_teammates: int | None = Field(default=None, gt=0)
481
+ session_id: str | None = None
482
+
483
+ @field_validator("name")
484
+ @classmethod
485
+ def validate_name(cls, value: str) -> str:
486
+ if not value.strip():
487
+ raise ValueError("name must not be blank")
488
+ return value
489
+
490
+
491
+ class TeamSpawnRequest(BaseModel):
492
+ team_id: str = Field(min_length=1)
493
+ prompt: str = Field(min_length=1)
494
+ role: Literal["lead", "worker", "researcher", "reviewer"] = "worker"
495
+ name: str | None = None
496
+ model_profile: str | None = None
497
+ session_id: str | None = None
498
+
499
+ @field_validator("team_id", "prompt")
500
+ @classmethod
501
+ def validate_required_text(cls, value: str) -> str:
502
+ if not value.strip():
503
+ raise ValueError("value must not be blank")
504
+ return value
505
+
506
+
507
+ class TeamMessageRequest(BaseModel):
508
+ team_id: str = Field(min_length=1)
509
+ to: str = Field(min_length=1)
510
+ text: str = Field(min_length=1)
511
+ from_agent: str = ""
512
+ session_id: str | None = None
513
+
514
+ @field_validator("team_id", "to", "text")
515
+ @classmethod
516
+ def validate_required_text(cls, value: str) -> str:
517
+ if not value.strip():
518
+ raise ValueError("value must not be blank")
519
+ return value
520
+
521
+
522
+ class TeamBroadcastRequest(BaseModel):
523
+ team_id: str = Field(min_length=1)
524
+ text: str = Field(min_length=1)
525
+ from_agent: str = ""
526
+ session_id: str | None = None
527
+
528
+ @field_validator("team_id", "text")
529
+ @classmethod
530
+ def validate_required_text(cls, value: str) -> str:
531
+ if not value.strip():
532
+ raise ValueError("value must not be blank")
533
+ return value
534
+
535
+
536
+ class TeamTaskAddRequest(BaseModel):
537
+ team_id: str = Field(min_length=1)
538
+ description: str = Field(min_length=1)
539
+ session_id: str | None = None
540
+
541
+ @field_validator("team_id", "description")
542
+ @classmethod
543
+ def validate_required_text(cls, value: str) -> str:
544
+ if not value.strip():
545
+ raise ValueError("value must not be blank")
546
+ return value
547
+
548
+
549
+ class TeamTaskClaimRequest(BaseModel):
550
+ team_id: str = Field(min_length=1)
551
+ task_id: str = Field(min_length=1)
552
+ agent_id: str = ""
553
+ session_id: str | None = None
554
+
555
+ @field_validator("team_id", "task_id")
556
+ @classmethod
557
+ def validate_required_text(cls, value: str) -> str:
558
+ if not value.strip():
559
+ raise ValueError("value must not be blank")
560
+ return value
561
+
562
+
563
+ class TeamTaskCompleteRequest(BaseModel):
564
+ team_id: str = Field(min_length=1)
565
+ task_id: str = Field(min_length=1)
566
+ result: str = ""
567
+ agent_id: str = ""
568
+ session_id: str | None = None
569
+
570
+ @field_validator("team_id", "task_id")
571
+ @classmethod
572
+ def validate_required_text(cls, value: str) -> str:
573
+ if not value.strip():
574
+ raise ValueError("value must not be blank")
575
+ return value
576
+
577
+
578
+ class TeamTaskFailRequest(BaseModel):
579
+ team_id: str = Field(min_length=1)
580
+ task_id: str = Field(min_length=1)
581
+ reason: str = ""
582
+ agent_id: str = ""
583
+ session_id: str | None = None
584
+
585
+ @field_validator("team_id", "task_id")
586
+ @classmethod
587
+ def validate_required_text(cls, value: str) -> str:
588
+ if not value.strip():
589
+ raise ValueError("value must not be blank")
590
+ return value
591
+
592
+
593
+ class TeamRemoveRequest(BaseModel):
594
+ team_id: str = Field(min_length=1)
595
+ agent_id: str = Field(min_length=1)
596
+ session_id: str | None = None
597
+
598
+ @field_validator("team_id", "agent_id")
599
+ @classmethod
600
+ def validate_required_text(cls, value: str) -> str:
601
+ if not value.strip():
602
+ raise ValueError("value must not be blank")
603
+ return value
604
+
605
+
606
+ class TeamMessagesReadRequest(BaseModel):
607
+ team_id: str = Field(min_length=1)
608
+ agent_id: str = Field(min_length=1)
609
+ message_ids: list[str] | None = None
610
+ session_id: str | None = None
611
+
612
+ @field_validator("team_id", "agent_id")
613
+ @classmethod
614
+ def validate_required_text(cls, value: str) -> str:
615
+ if not value.strip():
616
+ raise ValueError("value must not be blank")
617
+ return value
618
+
619
+ @field_validator("message_ids")
620
+ @classmethod
621
+ def validate_message_ids(cls, value: list[str] | None) -> list[str] | None:
622
+ if value is not None and any(not item.strip() for item in value):
623
+ raise ValueError("message_ids must not contain blank IDs")
624
+ return value
625
+
626
+
627
+ class TeamBridgeRequest(BaseModel):
628
+ team_a: str = Field(min_length=1)
629
+ team_b: str = Field(min_length=1)
630
+ policy: Literal["allow_all", "allow_tagged", "deny"] = "allow_tagged"
631
+ session_id: str | None = None
632
+
633
+ @field_validator("team_a", "team_b")
634
+ @classmethod
635
+ def validate_team_ids(cls, value: str) -> str:
636
+ if not value.strip():
637
+ raise ValueError("team ID must not be blank")
638
+ return value
639
+
640
+ @model_validator(mode="after")
641
+ def validate_distinct_teams(self) -> "TeamBridgeRequest":
642
+ if self.team_a == self.team_b:
643
+ raise ValueError("a bridge requires two distinct teams")
644
+ return self
645
+
646
+
647
+ class TeamCrossMessageRequest(BaseModel):
648
+ from_team: str = Field(min_length=1)
649
+ to_team: str = Field(min_length=1)
650
+ text: str = Field(min_length=1)
651
+ from_agent: str = ""
652
+ to_agent: str = ""
653
+ session_id: str | None = None
654
+
655
+ @field_validator("from_team", "to_team", "text")
656
+ @classmethod
657
+ def validate_required_text(cls, value: str) -> str:
658
+ if not value.strip():
659
+ raise ValueError("value must not be blank")
660
+ return value
661
+
662
+ @model_validator(mode="after")
663
+ def validate_distinct_teams(self) -> "TeamCrossMessageRequest":
664
+ if self.from_team == self.to_team:
665
+ raise ValueError("cross-team messages require two distinct teams")
666
+ return self
667
+
668
+
669
+ class TeamShutdownRequest(BaseModel):
670
+ team_id: str = Field(min_length=1)
671
+ session_id: str | None = None
672
+
673
+ @field_validator("team_id")
674
+ @classmethod
675
+ def validate_team_id(cls, value: str) -> str:
676
+ if not value.strip():
677
+ raise ValueError("team_id must not be blank")
678
+ return value
679
+
680
+
681
+ class ContextPushRequest(BaseModel):
682
+ """Client pushes workspace context to the server.
683
+
684
+ Used by VSCode extension to inform the gateway about the current
685
+ editor state (active file, selection, cursor position, etc.).
686
+ """
687
+
688
+ session_id: str
689
+ active_file: str | None = None
690
+ selected_text: str | None = None
691
+ cursor_line: int | None = None
692
+ cursor_column: int | None = None
693
+ open_files: list[str] = Field(default_factory=list)
694
+ language_id: str | None = None
695
+
696
+
697
+ class CheckpointRequest(BaseModel):
698
+ """Create a checkpoint with file snapshot."""
699
+ session_id: str
700
+ label: str = ""
701
+
702
+
703
+ class RevertRequest(BaseModel):
704
+ """Revert files + conversation to a checkpoint."""
705
+ session_id: str
706
+ checkpoint_id: str
707
+
708
+
709
+ class SearchSessionsRequest(BaseModel):
710
+ query: str
711
+ limit: int = 20
712
+
713
+
714
+ class ArchiveSessionRequest(BaseModel):
715
+ session_id: str
716
+
717
+
718
+ class PruneSessionsRequest(BaseModel):
719
+ days: int = Field(default=30, ge=0, le=36500)
720
+ delete_files: bool = False
721
+
722
+
723
+ class ExportSessionRequest(BaseModel):
724
+ session_id: str
725
+ format: Literal["md", "json"] = "md"
726
+
727
+
728
+ class WorkspaceDirectoryCreateRequest(BaseModel):
729
+ path: str = Field(min_length=1)
730
+
731
+
732
+ # ── Response / event schemas ─────────────────────────────────────
733
+
734
+
735
+ class SessionInfo(BaseModel):
736
+ session_id: str
737
+ message_count: int = 0
738
+ model: str = ""
739
+ provider: str = ""
740
+ created_at: str = ""
741
+ title: str = ""
742
+ cwd: str = ""
743
+ tokens_used: int = 0
744
+ preview: str = ""
745
+ forked_from_session_id: str | None = None
746
+ forked_from_message_uuid: str | None = None
747
+ forked_from_title: str | None = None
748
+
749
+
750
+ class GatewayRuntimeInfo(BaseModel):
751
+ gateway_version: str
752
+ gateway_path: str
753
+ python_version: str
754
+ python_executable: str
755
+ python_prefix: str
756
+ environment_kind: Literal["venv", "conda", "system"]
757
+ platform: str
758
+
759
+
760
+ class WorkspaceInfo(BaseModel):
761
+ startup_cwd: str
762
+ runtime: GatewayRuntimeInfo | None = None
763
+ home: str
764
+ browse_roots: list[str] = Field(default_factory=list)
765
+ documents_dir: str = ""
766
+
767
+
768
+ class WorkspaceDirectoryEntry(BaseModel):
769
+ name: str
770
+ path: str
771
+ hidden: bool = False
772
+ is_symlink: bool = False
773
+
774
+
775
+ class WorkspaceFileEntry(BaseModel):
776
+ name: str
777
+ path: str
778
+ size: int = 0
779
+ hidden: bool = False
780
+ is_symlink: bool = False
781
+
782
+
783
+ class WorkspaceDirectoryListing(BaseModel):
784
+ path: str
785
+ parent: str | None = None
786
+ directories: list[WorkspaceDirectoryEntry] = Field(default_factory=list)
787
+ files: list[WorkspaceFileEntry] = Field(default_factory=list)
788
+
789
+
790
+ class SearchIndexStatus(BaseModel):
791
+ """Observable state for the optional semantic-search indexer."""
792
+
793
+ state: str
794
+ chunks: int | None = None
795
+ files: int | None = None
796
+ done: int | None = None
797
+ total: int | None = None
798
+
799
+
800
+ class PromptBudget(BaseModel):
801
+ source: Literal["estimated"] = "estimated"
802
+ mode: Literal["eager", "discovery"]
803
+ system_tokens: int
804
+ tool_tokens: int
805
+ directory_tokens: int
806
+ loaded_tools: int
807
+ available_tools: int
808
+ loaded_names: list[str] = Field(default_factory=list)
809
+
810
+
811
+ class PermissionPolicyInfo(BaseModel):
812
+ """Loaded permission policy, independent of the client's inheritance choice."""
813
+
814
+ configured_mode: str
815
+ effective_mode: str
816
+ allow: list[PermissionRule] = Field(default_factory=list)
817
+ ask: list[PermissionRule] = Field(default_factory=list)
818
+ deny: list[PermissionRule] = Field(default_factory=list)
819
+ runtime_allow_count: int = 0
820
+
821
+
822
+ class SessionRuntimeStatus(BaseModel):
823
+ """Complete, non-secret runtime status shared by CLI-style clients."""
824
+
825
+ version: str = VERSION
826
+ session_id: str
827
+ cwd: str = ""
828
+ initialized: bool = False
829
+ message_count: int = 0
830
+ model: str = ""
831
+ model_profile: str | None = None
832
+ provider: str = ""
833
+ mode: Literal["agent", "plan"] = "agent"
834
+ reasoning_effort: ReasoningEffort | None = None
835
+ ultra_mode: bool = False
836
+ permission_mode: str = "default"
837
+ permission_policy: PermissionPolicyInfo | None = None
838
+ context_used_tokens: int = 0
839
+ context_window_tokens: int = 0
840
+ context_remaining_tokens: int = 0
841
+ context_used_percent: float = 0.0
842
+ context_token_source: Literal["server", "calibrated", "estimated"] = "estimated"
843
+ compact_count: int = 0
844
+ auto_compact_enabled: bool = True
845
+ compact_buffer_tokens: int = DEFAULT_COMPACT_BUFFER_TOKENS
846
+ max_context_length: int | None = None
847
+ compact_input_limit: int | None = None
848
+ thinking_enabled: bool = False
849
+ max_tokens: int = 0
850
+ tool_count: int | None = None
851
+ prompt_budget: PromptBudget | None = None
852
+ agent_total: int = 0
853
+ agent_active: int = 0
854
+ agent_failed: int = 0
855
+ agent_pending_callbacks: int = 0
856
+ agent_max_concurrency: int = 0
857
+ monitor_total: int = 0
858
+ monitor_active: int = 0
859
+ monitor_failed: int = 0
860
+ search_index: SearchIndexStatus | None = None
861
+
862
+
863
+ class GoalInfo(BaseModel):
864
+ objective: str
865
+ status: Literal["active", "paused", "complete", "blocked"]
866
+ token_budget: int | None = None
867
+ tokens_used: int = 0
868
+ created_at: str
869
+ updated_at: str
870
+ completed_at: str | None = None
871
+
872
+
873
+ class GoalState(BaseModel):
874
+ goal: GoalInfo | None = None
875
+
876
+
877
+ class AgentInfo(BaseModel):
878
+ agent_id: str
879
+ session_id: str = ""
880
+ parent_agent_id: str | None = None
881
+ parent_tool_use_id: str | None = None
882
+ title: str
883
+ subagent_type: str
884
+ status: str
885
+ model: str
886
+ model_profile: str | None = None
887
+ created_at: str
888
+ started_at: str | None = None
889
+ finished_at: str | None = None
890
+ updated_at: str = ""
891
+ usage: dict[str, Any] = Field(default_factory=dict)
892
+ final_result: str = ""
893
+ error: str = ""
894
+ depth: int = 0
895
+ transcript_path: str | None = None
896
+ callback_enabled: bool = False
897
+ callback_state: str = "disabled"
898
+ callback_message_id: str | None = None
899
+ callback_epoch: int = 0
900
+
901
+
902
+ class ToolInfo(BaseModel):
903
+ name: str
904
+ description: str = ""
905
+ is_read_only: bool = False
906
+ is_enabled: bool = True
907
+ is_loaded: bool | None = None
908
+
909
+
910
+ class SkillInfo(BaseModel):
911
+ name: str
912
+ description: str = ""
913
+
914
+
915
+ class SkillExpansion(BaseModel):
916
+ name: str
917
+ prompt: str
918
+
919
+
920
+ class MonitorInfo(BaseModel):
921
+ task_id: str
922
+ session_id: str = ""
923
+ description: str = ""
924
+ task_type: str = "local_bash"
925
+ source: str = ""
926
+ status: str = ""
927
+ output_file: str | None = None
928
+ timeout_ms: int = 0
929
+ persistent: bool = False
930
+ tool_use_id: str | None = None
931
+ created_at: str = ""
932
+ started_at: str | None = None
933
+ finished_at: str | None = None
934
+ updated_at: str = ""
935
+ sequence: int = 0
936
+ error: str = ""
937
+ exit_code: int | None = None
938
+
939
+
940
+ class BackgroundTaskInfo(BaseModel):
941
+ task_id: str
942
+ session_id: str = ""
943
+ cwd: str = ""
944
+ description: str = ""
945
+ task_type: str = ""
946
+ source: str = ""
947
+ status: str = ""
948
+ output_file: str | None = None
949
+ created_at: str = ""
950
+ started_at: str | None = None
951
+ finished_at: str | None = None
952
+ updated_at: str = ""
953
+ error: str = ""
954
+ exit_code: int | None = None
955
+ agent_id: str | None = None
956
+
957
+
958
+ class ScheduleJobInfo(BaseModel):
959
+ id: str
960
+ name: str
961
+ prompt: str
962
+ schedule: str
963
+ schedule_type: Literal["cron", "interval", "once"]
964
+ cwd: str | None = None
965
+ enabled: bool = True
966
+ status: str
967
+ last_run: str | None = None
968
+ next_run: str | None = None
969
+ run_count: int = 0
970
+ max_runs: int | None = None
971
+ created_at: str
972
+ session_id: str | None = None
973
+ description: str = ""
974
+ tags: list[str] = Field(default_factory=list)
975
+ timeout: int | None = None
976
+ model_profile: str | None = None
977
+ extra: dict[str, Any] = Field(default_factory=dict)
978
+ running: bool = False
979
+
980
+
981
+ class ScheduleRunInfo(BaseModel):
982
+ id: str
983
+ job_id: str
984
+ status: str
985
+ started_at: str | None = None
986
+ finished_at: str | None = None
987
+ duration_seconds: float | None = None
988
+ session_id: str | None = None
989
+ exit_code: int | None = None
990
+ error_message: str | None = None
991
+ result_summary: str = ""
992
+ tokens_used: int = 0
993
+ created_at: str
994
+
995
+
996
+ class PeerInfo(BaseModel):
997
+ version: int = 1
998
+ session_id: str
999
+ name: str
1000
+ cwd: str
1001
+ pid: int
1002
+ socket_path: str = ""
1003
+ permission_class: Literal["prompting", "bypass"] = "prompting"
1004
+ started_at: str = ""
1005
+
1006
+
1007
+ class PeerDeliveryInfo(BaseModel):
1008
+ message_id: str = ""
1009
+ status: Literal["delivered", "held", "refused", "failed"]
1010
+ detail: str = ""
1011
+
1012
+
1013
+ class TeamTeammateInfo(BaseModel):
1014
+ agent_id: str
1015
+ name: str | None = None
1016
+ role: str
1017
+ state: str
1018
+ model_profile: str | None = None
1019
+
1020
+
1021
+ class TeamTaskInfo(BaseModel):
1022
+ id: str
1023
+ description: str = ""
1024
+ assignee: str | None = None
1025
+ status: str
1026
+ result: str = ""
1027
+ created_at: str = ""
1028
+ updated_at: str = ""
1029
+
1030
+
1031
+ class TeamTaskSummary(BaseModel):
1032
+ total: int = 0
1033
+ pending: int = 0
1034
+ claimed: int = 0
1035
+ completed: int = 0
1036
+ failed: int = 0
1037
+
1038
+
1039
+ class TeamStatusInfo(BaseModel):
1040
+ team_id: str
1041
+ state: str
1042
+ teammates: list[TeamTeammateInfo] = Field(default_factory=list)
1043
+ teammate_count: int = 0
1044
+ max_teammates: int = 0
1045
+ tasks: TeamTaskSummary = Field(default_factory=TeamTaskSummary)
1046
+
1047
+
1048
+ class TeamMessageInfo(BaseModel):
1049
+ id: str
1050
+ from_agent: str = ""
1051
+ to_agent: str = ""
1052
+ text: str = ""
1053
+ timestamp: str = ""
1054
+ read: bool = False
1055
+ msg_type: str = "text"
1056
+
1057
+
1058
+ class TeamBridgeInfo(BaseModel):
1059
+ team_a: str
1060
+ team_b: str
1061
+ policy: Literal["allow_all", "allow_tagged", "deny"]
1062
+
1063
+
1064
+ class CrossTeamMessageInfo(BaseModel):
1065
+ id: str
1066
+ from_team: str
1067
+ from_agent: str = ""
1068
+ to_team: str
1069
+ to_agent: str = ""
1070
+ text: str
1071
+ timestamp: str = ""
1072
+ bridge_policy: Literal["allow_all", "allow_tagged", "deny"]
1073
+
1074
+
1075
+ class LogInfo(BaseModel):
1076
+ name: str
1077
+ path: str
1078
+ updated_at: str | None = None
1079
+ state: str | None = None
1080
+
1081
+
1082
+ class LogsResponse(BaseModel):
1083
+ logs: list[LogInfo] = Field(default_factory=list)
1084
+ lines: list[str] = Field(default_factory=list)
1085
+ name: str | None = None
1086
+ path: str | None = None
1087
+ truncated: bool = False
1088
+ note: str | None = None
1089
+
1090
+
1091
+ class AgentTranscriptResponse(BaseModel):
1092
+ agent_id: str
1093
+ path: str | None = None
1094
+ lines: list[str] = Field(default_factory=list)
1095
+ truncated: bool = False
1096
+
1097
+
1098
+ class ModelInfo(BaseModel):
1099
+ name: str
1100
+ description: str = ""
1101
+ group: str = "default"
1102
+
1103
+
1104
+ class ModelSettingsEntry(BaseModel):
1105
+ """One named model as configured and after group inheritance."""
1106
+
1107
+ name: str
1108
+ group: str | None = None
1109
+ is_default: bool = False
1110
+ configured: dict[str, Any] = Field(default_factory=dict)
1111
+ effective: dict[str, Any] = Field(default_factory=dict)
1112
+ overridden_fields: list[str] = Field(default_factory=list)
1113
+ sources: list[str] = Field(default_factory=list)
1114
+
1115
+
1116
+ class ModelSettingsSource(BaseModel):
1117
+ """A settings layer that can be selected as a mutation target."""
1118
+
1119
+ id: Literal["userSettings", "projectSettings", "localSettings"]
1120
+ label: str
1121
+ path: str
1122
+ exists: bool = False
1123
+ writable: bool = True
1124
+
1125
+
1126
+ class ModelSettingsResponse(BaseModel):
1127
+ """Model configuration visible from one working directory."""
1128
+
1129
+ cwd: str
1130
+ default_model: str | None = None
1131
+ sources: list[str] = Field(default_factory=list)
1132
+ groups: dict[str, dict[str, Any]] = Field(default_factory=dict)
1133
+ group_sources: dict[str, list[str]] = Field(default_factory=dict)
1134
+ models: list[ModelSettingsEntry] = Field(default_factory=list)
1135
+ warnings: list[str] = Field(default_factory=list)
1136
+ editable_sources: list[ModelSettingsSource] = Field(default_factory=list)
1137
+
1138
+
1139
+ class RuntimeSettingsResponse(BaseModel):
1140
+ """Effective runtime and extra-tool settings visible from a workspace."""
1141
+
1142
+ cwd: str
1143
+ permission_policy: PermissionPolicyInfo | None = None
1144
+ snapshot_enabled: bool = True
1145
+ snapshot_max_size_mb: int = 1024
1146
+ auto_compact_enabled: bool = True
1147
+ compact_buffer_tokens: int = DEFAULT_COMPACT_BUFFER_TOKENS
1148
+ max_context_length: int | None = None
1149
+ computer_use_mode: Literal["background_app", "foreground_desktop"] = "background_app"
1150
+ computer_use_target_scope: Literal["app_window", "desktop"] = "app_window"
1151
+ computer_use_delivery_policy: Literal["strict_background", "allow_foreground"] = "allow_foreground"
1152
+ extra_tools: list[str] = Field(default_factory=list)
1153
+ extra_tools_by_source: dict[str, list[str]] = Field(default_factory=dict)
1154
+ sources: list[str] = Field(default_factory=list)
1155
+ warnings: list[str] = Field(default_factory=list)
1156
+ editable_sources: list[ModelSettingsSource] = Field(default_factory=list)
1157
+
1158
+
1159
+ class PromptSectionInfo(BaseModel):
1160
+ key: str
1161
+ label: str
1162
+ description: str | None = None
1163
+ default_text: str | None = None
1164
+
1165
+
1166
+ class PromptTemplateView(BaseModel):
1167
+ id: str
1168
+ name: str
1169
+ sections: dict[str, str] = Field(default_factory=dict)
1170
+ source: str
1171
+
1172
+
1173
+ class UserAppendPromptView(BaseModel):
1174
+ id: str
1175
+ text: str
1176
+ enabled: bool = False
1177
+ source: str
1178
+
1179
+
1180
+ class PromptSettingsResponse(BaseModel):
1181
+ """Prompt templates and user-input prompts visible from one workspace."""
1182
+
1183
+ cwd: str
1184
+ active_template_id: str | None = None
1185
+ templates: list[PromptTemplateView] = Field(default_factory=list)
1186
+ user_prompts: list[UserAppendPromptView] = Field(default_factory=list)
1187
+ sections: list[PromptSectionInfo] = Field(default_factory=list)
1188
+ warnings: list[str] = Field(default_factory=list)
1189
+ editable_sources: list[ModelSettingsSource] = Field(default_factory=list)
1190
+
1191
+
1192
+ class HealthResponse(BaseModel):
1193
+ status: str = "ok"
1194
+ version: str = VERSION
1195
+ protocol_version: int = GATEWAY_PROTOCOL_VERSION
1196
+ min_protocol_version: int = GATEWAY_MIN_PROTOCOL_VERSION
1197
+ max_protocol_version: int = GATEWAY_MAX_PROTOCOL_VERSION
1198
+
1199
+
1200
+ # ── CoreEvent serialization ──────────────────────────────────────
1201
+ # These models represent CoreEvent variants on the wire. They carry
1202
+ # a ``type`` discriminator so the client can dispatch correctly.
1203
+
1204
+
1205
+ class StreamTextPayload(BaseModel):
1206
+ type: Literal["stream_text"] = "stream_text"
1207
+ text: str
1208
+
1209
+
1210
+ class ThinkingPayload(BaseModel):
1211
+ type: Literal["thinking"] = "thinking"
1212
+ text: str
1213
+
1214
+
1215
+ class ToolUsePayload(BaseModel):
1216
+ type: Literal["tool_use"] = "tool_use"
1217
+ tool_name: str
1218
+ tool_input: dict[str, Any]
1219
+ tool_use_id: str
1220
+ agent_id: str | None = None
1221
+
1222
+
1223
+ class ToolResultPayload(BaseModel):
1224
+ type: Literal["tool_result"] = "tool_result"
1225
+ tool_use_id: str
1226
+ tool_name: str
1227
+ result: str
1228
+ is_error: bool = False
1229
+ result_for_display: str | None = None
1230
+ tool_input: dict[str, Any] = Field(default_factory=dict)
1231
+ agent_id: str | None = None
1232
+ images: list[ImageAttachment] = Field(default_factory=list)
1233
+
1234
+
1235
+ class PermissionRequestPayload(BaseModel):
1236
+ type: Literal["permission_request"] = "permission_request"
1237
+ tool_name: str
1238
+ tool_input: dict[str, Any]
1239
+ tool_use_id: str
1240
+ reason: str | None = None
1241
+ permission_key: str | None = None
1242
+ agent_id: str | None = None
1243
+ request_kind: Literal["tool", "peer_message"] = "tool"
1244
+
1245
+
1246
+ class PermissionResponsePayload(BaseModel):
1247
+ type: Literal["permission_response"] = "permission_response"
1248
+ tool_use_id: str
1249
+ allowed: bool
1250
+ always_allow: bool = False
1251
+ agent_id: str | None = None
1252
+ feedback: str | None = None
1253
+
1254
+
1255
+ class ChoiceRequestPayload(BaseModel):
1256
+ type: Literal["choice_request"] = "choice_request"
1257
+ tool_use_id: str
1258
+ question: str
1259
+ options: list[str]
1260
+ multiple: bool = False
1261
+ agent_id: str | None = None
1262
+
1263
+
1264
+ class ChoiceResponsePayload(BaseModel):
1265
+ type: Literal["choice_response"] = "choice_response"
1266
+ tool_use_id: str
1267
+ selected: list[str]
1268
+ cancelled: bool = False
1269
+ agent_id: str | None = None
1270
+
1271
+
1272
+ class CompactPayload(BaseModel):
1273
+ type: Literal["compact"] = "compact"
1274
+ summary: str
1275
+ messages_before: int = 0
1276
+ messages_after: int = 0
1277
+ trigger: str = "auto"
1278
+ agent_id: str | None = None
1279
+
1280
+
1281
+ class ErrorPayload(BaseModel):
1282
+ type: Literal["error"] = "error"
1283
+ message: str
1284
+ recoverable: bool = True
1285
+ error_type: str = ""
1286
+ agent_id: str | None = None
1287
+ # Errors generated while admitting/validating a transport command are
1288
+ # deliberately distinct from Core turn errors. They may carry the
1289
+ # command and target operation without claiming foreground terminal
1290
+ # ownership.
1291
+ command: str | None = None
1292
+ command_error: bool = False
1293
+ request_id: str | None = None
1294
+
1295
+
1296
+ class TurnDetails(BaseModel):
1297
+ """Statistics for one input, stored on its final assistant message."""
1298
+
1299
+ session_id: str
1300
+ started_at: str | None = None
1301
+ ended_at: str | None = None
1302
+ duration_ms: int | None = None
1303
+ tool_call_count: int = 0
1304
+ thinking_count: int = 0
1305
+ request_count: int = 0
1306
+ retry_count: int | None = None
1307
+ compact_count: int | None = None
1308
+ model: str = ""
1309
+ provider: str = ""
1310
+ reason: str | None = None
1311
+ usage: dict[str, Any] = Field(default_factory=dict)
1312
+ source: Literal["recorded", "history"] = "recorded"
1313
+
1314
+
1315
+ class TurnCompletePayload(BaseModel):
1316
+ type: Literal["turn_complete"] = "turn_complete"
1317
+ reason: str = "end_turn"
1318
+ turn_count: int = 0
1319
+ usage: dict[str, Any] = Field(default_factory=dict)
1320
+ context_used_tokens: int = 0
1321
+ context_window_tokens: int = 0
1322
+ context_remaining_tokens: int = 0
1323
+ context_used_percent: float = 0.0
1324
+ context_token_source: Literal["server", "calibrated", "estimated"] = "estimated"
1325
+ assistant_message_uuid: str | None = None
1326
+ turn_details: TurnDetails | None = None
1327
+ prompt_budget: PromptBudget | None = None
1328
+
1329
+
1330
+ class StreamModePayload(BaseModel):
1331
+ type: Literal["stream_mode"] = "stream_mode"
1332
+ mode: str
1333
+ agent_id: str | None = None
1334
+
1335
+
1336
+ class StreamRetryPayload(BaseModel):
1337
+ type: Literal["stream_retry"] = "stream_retry"
1338
+ message: str
1339
+ error: str
1340
+ retry_count: int
1341
+ max_retries: int
1342
+ delay_seconds: float
1343
+ unbounded: bool = False
1344
+ transport_fallback: bool = False
1345
+ discarded_text_chars: int = 0
1346
+ agent_id: str | None = None
1347
+
1348
+
1349
+ class SteeringAppliedPayload(BaseModel):
1350
+ type: Literal["steering_applied"] = "steering_applied"
1351
+ count: int = 1
1352
+
1353
+
1354
+ class QueuedMessageStartedPayload(BaseModel):
1355
+ type: Literal["queued_message_started"] = "queued_message_started"
1356
+ text: str
1357
+ images: list[ImageAttachment] = Field(default_factory=list)
1358
+ request_id: str | None = None
1359
+
1360
+
1361
+ class QueuedMessageUpdatedPayload(BaseModel):
1362
+ type: Literal["queued_message_updated"] = "queued_message_updated"
1363
+ request_id: str
1364
+ action: Literal["remove", "edit", "steer"]
1365
+
1366
+
1367
+ class DocumentJobPayload(BaseModel):
1368
+ type: Literal["document_job"] = "document_job"
1369
+ action: str
1370
+ status: str
1371
+ locale: str = ""
1372
+ language: str = ""
1373
+ source: str = ""
1374
+ current: int = 0
1375
+ total: int = 0
1376
+ message: str = ""
1377
+
1378
+
1379
+ class DocumentSelectionTranslationPayload(BaseModel):
1380
+ type: Literal["document_selection_translation"] = "document_selection_translation"
1381
+ operation_id: str
1382
+ locale: str
1383
+ source_text: str
1384
+ translated_text: str
1385
+ status: str = "completed"
1386
+ message: str = ""
1387
+
1388
+
1389
+ class AgentStatePayload(BaseModel):
1390
+ type: Literal["agent_state"] = "agent_state"
1391
+ agent_id: str
1392
+ parent_agent_id: str | None = None
1393
+ status: str
1394
+ subagent_type: str
1395
+ title: str
1396
+ message: str = ""
1397
+ usage: dict[str, Any] = Field(default_factory=dict)
1398
+
1399
+
1400
+ class AgentOutputPayload(BaseModel):
1401
+ type: Literal["agent_output"] = "agent_output"
1402
+ agent_id: str
1403
+ stream: str
1404
+ text: str
1405
+ tool_name: str | None = None
1406
+
1407
+
1408
+ class ModeChangePayload(BaseModel):
1409
+ type: Literal["mode_change"] = "mode_change"
1410
+ mode: str
1411
+ reason: str = ""
1412
+
1413
+
1414
+ class ModelChangePayload(BaseModel):
1415
+ type: Literal["model_change"] = "model_change"
1416
+ session_id: str
1417
+ model_profile: str
1418
+
1419
+
1420
+ class PermissionModeChangePayload(BaseModel):
1421
+ type: Literal["permission_mode_change"] = "permission_mode_change"
1422
+ session_id: str
1423
+ permission_mode: Literal["default", "ask", "ai_review", "run_everything"]
1424
+
1425
+
1426
+ class PlanReadyPayload(BaseModel):
1427
+ type: Literal["plan_ready"] = "plan_ready"
1428
+ plan: dict[str, Any]
1429
+
1430
+
1431
+ class TeamMessagePayload(BaseModel):
1432
+ type: Literal["team_message"] = "team_message"
1433
+ team_id: str
1434
+ from_agent: str
1435
+ to_agent: str
1436
+ text: str
1437
+ msg_type: str = "text"
1438
+ message_id: str = ""
1439
+
1440
+
1441
+ class PeerMessagePayload(BaseModel):
1442
+ type: Literal["peer_message"] = "peer_message"
1443
+ message_id: str
1444
+ from_session_id: str
1445
+ from_name: str
1446
+ from_cwd: str
1447
+ text: str
1448
+
1449
+
1450
+ class TeamStatePayload(BaseModel):
1451
+ type: Literal["team_state"] = "team_state"
1452
+ team_id: str
1453
+ agent_id: str
1454
+ old_state: str
1455
+ new_state: str
1456
+ role: str = ""
1457
+
1458
+
1459
+ class TaskUpdatePayload(BaseModel):
1460
+ type: Literal["task_update"] = "task_update"
1461
+ team_id: str
1462
+ task_id: str
1463
+ status: str
1464
+ assignee: str | None = None
1465
+ description: str = ""
1466
+
1467
+
1468
+ class ScheduleRunPayload(BaseModel):
1469
+ """A scheduled job execution result delivered to connected clients."""
1470
+
1471
+ type: Literal["schedule_run"] = "schedule_run"
1472
+ job_id: str
1473
+ run_id: str
1474
+ status: str
1475
+ duration_seconds: float = 0.0
1476
+ error_message: str = ""
1477
+ result_summary: str = ""
1478
+ next_run: str | None = None
1479
+
1480
+
1481
+ class FileChangePayload(BaseModel):
1482
+ """File change notification for frontends (VSCode, etc.).
1483
+
1484
+ Emitted when a tool modifies the filesystem so the client can
1485
+ refresh its editor view, show diffs, etc.
1486
+ """
1487
+
1488
+ type: Literal["file_change"] = "file_change"
1489
+ path: str
1490
+ action: Literal["create", "modify", "delete"]
1491
+ diff: str | None = None
1492
+
1493
+
1494
+ class SnapshotPayload(BaseModel):
1495
+ type: Literal["snapshot"] = "snapshot"
1496
+ snapshot_id: str
1497
+ tool_name: str
1498
+ files: list[str] = Field(default_factory=list)
1499
+
1500
+
1501
+ class RevertPayload(BaseModel):
1502
+ type: Literal["revert"] = "revert"
1503
+ snapshot_id: str
1504
+ files_restored: list[str] = Field(default_factory=list)
1505
+
1506
+
1507
+ class ServerConnectedPayload(BaseModel):
1508
+ type: Literal["server.connected"] = "server.connected"
1509
+ properties: dict[str, Any] = Field(default_factory=dict)
1510
+
1511
+
1512
+ class ServerHeartbeatPayload(BaseModel):
1513
+ type: Literal["server.heartbeat"] = "server.heartbeat"
1514
+ properties: dict[str, Any] = Field(default_factory=dict)
1515
+
1516
+
1517
+ class SessionMessagePayload(BaseModel):
1518
+ """Complete persisted message shape used when replaying a session."""
1519
+
1520
+ uuid: str
1521
+ role: Literal["user", "assistant", "system"]
1522
+ content: str | list[dict[str, Any]]
1523
+ timestamp: str
1524
+ parent_uuid: str | None = None
1525
+ is_compact_summary: bool = False
1526
+ origin: str | None = None
1527
+ usage: dict[str, Any] | None = None
1528
+ tool_use_result: str | None = None
1529
+ source_tool_assistant_uuid: str | None = None
1530
+ reply_to_uuid: str | None = None
1531
+ api_error: str | None = None
1532
+ request_id: str | None = None
1533
+ turn_details: TurnDetails | None = None
1534
+
1535
+
1536
+ class SessionHistoryPayload(BaseModel):
1537
+ type: Literal["session_history"] = "session_history"
1538
+ session_id: str
1539
+ messages: list[SessionMessagePayload] = Field(default_factory=list)
1540
+
1541
+
1542
+ EventPayload = Union[
1543
+ StreamTextPayload,
1544
+ ThinkingPayload,
1545
+ ToolUsePayload,
1546
+ ToolResultPayload,
1547
+ PermissionRequestPayload,
1548
+ PermissionResponsePayload,
1549
+ ChoiceRequestPayload,
1550
+ ChoiceResponsePayload,
1551
+ CompactPayload,
1552
+ ErrorPayload,
1553
+ TurnCompletePayload,
1554
+ StreamModePayload,
1555
+ StreamRetryPayload,
1556
+ SteeringAppliedPayload,
1557
+ QueuedMessageStartedPayload,
1558
+ QueuedMessageUpdatedPayload,
1559
+ DocumentJobPayload,
1560
+ DocumentSelectionTranslationPayload,
1561
+ AgentStatePayload,
1562
+ AgentOutputPayload,
1563
+ ModeChangePayload,
1564
+ ModelChangePayload,
1565
+ PermissionModeChangePayload,
1566
+ PlanReadyPayload,
1567
+ PeerMessagePayload,
1568
+ TeamMessagePayload,
1569
+ TeamStatePayload,
1570
+ TaskUpdatePayload,
1571
+ ScheduleRunPayload,
1572
+ FileChangePayload,
1573
+ SnapshotPayload,
1574
+ RevertPayload,
1575
+ ServerConnectedPayload,
1576
+ ServerHeartbeatPayload,
1577
+ SessionHistoryPayload,
1578
+ ]
1579
+
1580
+
1581
+ # ── CoreEvent → Pydantic conversion ──────────────────────────────
1582
+
1583
+ def core_event_to_payload(event: Any) -> EventPayload:
1584
+ """Convert a crabcode_core CoreEvent dataclass to a Pydantic payload."""
1585
+ from crabcode_core.types.event import (
1586
+ AgentOutputEvent,
1587
+ AgentStateEvent,
1588
+ ChoiceRequestEvent,
1589
+ ChoiceResponseEvent,
1590
+ CompactEvent,
1591
+ DocumentJobEvent,
1592
+ DocumentSelectionTranslationEvent,
1593
+ ErrorEvent,
1594
+ ModeChangeEvent,
1595
+ PermissionRequestEvent,
1596
+ PermissionResponseEvent,
1597
+ PeerMessageEvent,
1598
+ PlanReadyEvent,
1599
+ ScheduleRunEvent,
1600
+ StreamModeEvent,
1601
+ StreamRetryEvent,
1602
+ SteeringAppliedEvent,
1603
+ QueuedMessageStartedEvent,
1604
+ QueuedMessageUpdatedEvent,
1605
+ StreamTextEvent,
1606
+ TaskUpdateEvent,
1607
+ TeamMessageEvent,
1608
+ TeamStateEvent,
1609
+ ThinkingEvent,
1610
+ ToolResultEvent,
1611
+ ToolUseEvent,
1612
+ TurnCompleteEvent,
1613
+ )
1614
+
1615
+ if isinstance(event, StreamTextEvent):
1616
+ return StreamTextPayload(text=event.text)
1617
+ if isinstance(event, ThinkingEvent):
1618
+ return ThinkingPayload(text=event.text)
1619
+ if isinstance(event, ToolUseEvent):
1620
+ return ToolUsePayload(
1621
+ tool_name=event.tool_name,
1622
+ tool_input=event.tool_input,
1623
+ tool_use_id=event.tool_use_id,
1624
+ agent_id=event.agent_id,
1625
+ )
1626
+ if isinstance(event, ToolResultEvent):
1627
+ return ToolResultPayload(
1628
+ tool_use_id=event.tool_use_id,
1629
+ tool_name=event.tool_name,
1630
+ result=event.result,
1631
+ is_error=event.is_error,
1632
+ result_for_display=event.result_for_display,
1633
+ tool_input=event.tool_input,
1634
+ agent_id=event.agent_id,
1635
+ images=event.images,
1636
+ )
1637
+ if isinstance(event, PermissionRequestEvent):
1638
+ return PermissionRequestPayload(
1639
+ tool_name=event.tool_name,
1640
+ tool_input=event.tool_input,
1641
+ tool_use_id=event.tool_use_id,
1642
+ reason=event.reason,
1643
+ permission_key=event.permission_key,
1644
+ agent_id=event.agent_id,
1645
+ request_kind=event.request_kind,
1646
+ )
1647
+ if isinstance(event, PermissionResponseEvent):
1648
+ return PermissionResponsePayload(
1649
+ tool_use_id=event.tool_use_id,
1650
+ allowed=event.allowed,
1651
+ always_allow=event.always_allow,
1652
+ agent_id=event.agent_id,
1653
+ feedback=event.feedback,
1654
+ )
1655
+ if isinstance(event, ChoiceRequestEvent):
1656
+ return ChoiceRequestPayload(
1657
+ tool_use_id=event.tool_use_id,
1658
+ question=event.question,
1659
+ options=event.options,
1660
+ multiple=event.multiple,
1661
+ agent_id=event.agent_id,
1662
+ )
1663
+ if isinstance(event, ChoiceResponseEvent):
1664
+ return ChoiceResponsePayload(
1665
+ tool_use_id=event.tool_use_id,
1666
+ selected=event.selected,
1667
+ cancelled=event.cancelled,
1668
+ agent_id=event.agent_id,
1669
+ )
1670
+ if isinstance(event, CompactEvent):
1671
+ return CompactPayload(
1672
+ summary=event.summary,
1673
+ messages_before=event.messages_before,
1674
+ messages_after=event.messages_after,
1675
+ trigger=event.trigger,
1676
+ agent_id=event.agent_id,
1677
+ )
1678
+ if isinstance(event, ErrorEvent):
1679
+ return ErrorPayload(
1680
+ message=event.message,
1681
+ recoverable=event.recoverable,
1682
+ error_type=event.error_type,
1683
+ agent_id=event.agent_id,
1684
+ )
1685
+ if isinstance(event, TurnCompleteEvent):
1686
+ return TurnCompletePayload(
1687
+ reason=event.reason,
1688
+ turn_count=event.turn_count,
1689
+ usage=event.usage,
1690
+ context_used_tokens=event.context_used_tokens,
1691
+ context_token_source=event.context_token_source,
1692
+ context_window_tokens=event.context_window_tokens,
1693
+ context_remaining_tokens=event.context_remaining_tokens,
1694
+ context_used_percent=event.context_used_percent,
1695
+ assistant_message_uuid=event.assistant_message_uuid,
1696
+ turn_details=event.turn_details,
1697
+ prompt_budget=event.prompt_budget or None,
1698
+ )
1699
+ if isinstance(event, StreamModeEvent):
1700
+ return StreamModePayload(mode=event.mode, agent_id=event.agent_id)
1701
+ if isinstance(event, StreamRetryEvent):
1702
+ return StreamRetryPayload(
1703
+ message=event.message,
1704
+ error=event.error,
1705
+ retry_count=event.retry_count,
1706
+ max_retries=event.max_retries,
1707
+ delay_seconds=event.delay_seconds,
1708
+ unbounded=event.unbounded,
1709
+ transport_fallback=event.transport_fallback,
1710
+ discarded_text_chars=event.discarded_text_chars,
1711
+ agent_id=event.agent_id,
1712
+ )
1713
+ if isinstance(event, SteeringAppliedEvent):
1714
+ return SteeringAppliedPayload(count=event.count)
1715
+ if isinstance(event, QueuedMessageStartedEvent):
1716
+ return QueuedMessageStartedPayload(text=event.text, images=event.images, request_id=event.request_id)
1717
+ if isinstance(event, QueuedMessageUpdatedEvent):
1718
+ return QueuedMessageUpdatedPayload(request_id=event.request_id, action=event.action)
1719
+ if isinstance(event, DocumentJobEvent):
1720
+ return DocumentJobPayload(
1721
+ action=event.action,
1722
+ status=event.status,
1723
+ locale=event.locale,
1724
+ language=event.language,
1725
+ source=event.source,
1726
+ current=event.current,
1727
+ total=event.total,
1728
+ message=event.message,
1729
+ )
1730
+ if isinstance(event, DocumentSelectionTranslationEvent):
1731
+ return DocumentSelectionTranslationPayload(
1732
+ operation_id=event.operation_id,
1733
+ locale=event.locale,
1734
+ source_text=event.source_text,
1735
+ translated_text=event.translated_text,
1736
+ status=event.status,
1737
+ message=event.message,
1738
+ )
1739
+ if isinstance(event, AgentStateEvent):
1740
+ return AgentStatePayload(
1741
+ agent_id=event.agent_id,
1742
+ parent_agent_id=event.parent_agent_id,
1743
+ status=event.status,
1744
+ subagent_type=event.subagent_type,
1745
+ title=event.title,
1746
+ message=event.message,
1747
+ usage=event.usage,
1748
+ )
1749
+ if isinstance(event, AgentOutputEvent):
1750
+ return AgentOutputPayload(
1751
+ agent_id=event.agent_id,
1752
+ stream=event.stream,
1753
+ text=event.text,
1754
+ tool_name=event.tool_name,
1755
+ )
1756
+ if isinstance(event, ModeChangeEvent):
1757
+ return ModeChangePayload(mode=event.mode, reason=event.reason)
1758
+ if isinstance(event, PlanReadyEvent):
1759
+ return PlanReadyPayload(plan=event.plan)
1760
+ if isinstance(event, PeerMessageEvent):
1761
+ return PeerMessagePayload(
1762
+ message_id=event.message_id,
1763
+ from_session_id=event.from_session_id,
1764
+ from_name=event.from_name,
1765
+ from_cwd=event.from_cwd,
1766
+ text=event.text,
1767
+ )
1768
+ if isinstance(event, TeamMessageEvent):
1769
+ return TeamMessagePayload(
1770
+ team_id=event.team_id,
1771
+ from_agent=event.from_agent,
1772
+ to_agent=event.to_agent,
1773
+ text=event.text,
1774
+ msg_type=event.msg_type,
1775
+ message_id=event.message_id,
1776
+ )
1777
+ if isinstance(event, TeamStateEvent):
1778
+ return TeamStatePayload(
1779
+ team_id=event.team_id,
1780
+ agent_id=event.agent_id,
1781
+ old_state=event.old_state,
1782
+ new_state=event.new_state,
1783
+ role=event.role,
1784
+ )
1785
+ if isinstance(event, TaskUpdateEvent):
1786
+ return TaskUpdatePayload(
1787
+ team_id=event.team_id,
1788
+ task_id=event.task_id,
1789
+ status=event.status,
1790
+ assignee=event.assignee,
1791
+ description=event.description,
1792
+ )
1793
+ if isinstance(event, ScheduleRunEvent):
1794
+ return ScheduleRunPayload(
1795
+ job_id=event.job_id,
1796
+ run_id=event.run_id,
1797
+ status=event.status,
1798
+ duration_seconds=event.duration_seconds,
1799
+ error_message=event.error_message,
1800
+ result_summary=event.result_summary,
1801
+ next_run=event.next_run,
1802
+ )
1803
+ from crabcode_core.types.event import SnapshotEvent, RevertEvent
1804
+ if isinstance(event, SnapshotEvent):
1805
+ return SnapshotPayload(
1806
+ snapshot_id=event.snapshot_id,
1807
+ tool_name=event.tool_name,
1808
+ files=event.files,
1809
+ )
1810
+ if isinstance(event, RevertEvent):
1811
+ return RevertPayload(
1812
+ snapshot_id=event.snapshot_id,
1813
+ files_restored=event.files_restored,
1814
+ )
1815
+ # Fallback: wrap as error
1816
+ return ErrorPayload(
1817
+ message=f"Unknown event type: {type(event).__name__}",
1818
+ recoverable=True,
1819
+ error_type="unknown",
1820
+ )