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,392 @@
1
+ """Agent-team lifecycle, messaging, and task-board routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from inspect import isawaitable
7
+ from typing import Any, TypeVar
8
+
9
+ from fastapi import APIRouter, HTTPException, Request
10
+
11
+ from crabcode_core.team.models import BridgePolicy, TeammateRole
12
+ from crabcode_gateway.schemas import (
13
+ CrossTeamMessageInfo,
14
+ TeamBroadcastRequest,
15
+ TeamBridgeInfo,
16
+ TeamBridgeRequest,
17
+ TeamCreateRequest,
18
+ TeamCrossMessageRequest,
19
+ TeamMessageInfo,
20
+ TeamMessageRequest,
21
+ TeamMessagesReadRequest,
22
+ TeamRemoveRequest,
23
+ TeamShutdownRequest,
24
+ TeamSpawnRequest,
25
+ TeamStatusInfo,
26
+ TeamTaskAddRequest,
27
+ TeamTaskClaimRequest,
28
+ TeamTaskCompleteRequest,
29
+ TeamTaskFailRequest,
30
+ TeamTaskInfo,
31
+ )
32
+ from crabcode_gateway.task_registry import SessionOperationRejected, run_session_operation
33
+
34
+
35
+ router = APIRouter(prefix="/team", tags=["team"])
36
+ T = TypeVar("T")
37
+
38
+
39
+ def _get_session(request: Request, session_id: str | None = None):
40
+ sessions: dict[str, Any] = request.app.state.sessions
41
+ sid = request.app.state.default_session_id if session_id is None else session_id
42
+ session = sessions.get(sid) if sid else None
43
+ if session is None or getattr(session, "session_id", None) in getattr(request.app.state, "closing_sessions", set()):
44
+ return None
45
+ return session
46
+
47
+
48
+ async def _run_team_operation(
49
+ request: Request,
50
+ session_id: str | None,
51
+ operation: Callable[[Any], T | Awaitable[T]],
52
+ ) -> T:
53
+ session = _get_session(request, session_id)
54
+ if not session:
55
+ raise HTTPException(status_code=404, detail="Session not found")
56
+
57
+ async def _owned() -> T:
58
+ initializer = getattr(session, "initialize", None)
59
+ if callable(initializer):
60
+ await initializer()
61
+ manager = getattr(session, "_team_manager", None)
62
+ if manager is None:
63
+ raise HTTPException(status_code=503, detail="Team manager is unavailable")
64
+ result = operation(manager)
65
+ if isawaitable(result):
66
+ return await result
67
+ return result
68
+
69
+ try:
70
+ return await run_session_operation(
71
+ request.app.state,
72
+ session,
73
+ _owned,
74
+ )
75
+ except SessionOperationRejected as exc:
76
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
77
+
78
+
79
+ def _message_info(message: Any) -> TeamMessageInfo:
80
+ data = message.model_dump() if hasattr(message, "model_dump") else dict(message)
81
+ return TeamMessageInfo.model_validate(data)
82
+
83
+
84
+ def _task_info(task: Any) -> TeamTaskInfo:
85
+ data = task.model_dump() if hasattr(task, "model_dump") else dict(task)
86
+ return TeamTaskInfo.model_validate(data)
87
+
88
+
89
+ def _cross_message_info(message: Any) -> CrossTeamMessageInfo:
90
+ data = message.model_dump() if hasattr(message, "model_dump") else dict(message)
91
+ return CrossTeamMessageInfo.model_validate(data)
92
+
93
+
94
+ @router.get("/list")
95
+ async def list_teams(request: Request, session_id: str | None = None) -> list[str]:
96
+ return await _run_team_operation(request, session_id, lambda manager: list(manager.list_teams()))
97
+
98
+
99
+ @router.get("/status/{team_id}", response_model=TeamStatusInfo)
100
+ @router.get("/{team_id}/status", response_model=TeamStatusInfo)
101
+ async def team_status(team_id: str, request: Request, session_id: str | None = None) -> TeamStatusInfo:
102
+ status = await _run_team_operation(request, session_id, lambda manager: manager.get_team_status(team_id))
103
+ if not status:
104
+ raise HTTPException(status_code=404, detail=f"Team '{team_id}' not found")
105
+ return TeamStatusInfo.model_validate(status)
106
+
107
+
108
+ @router.get("/{team_id}/messages", response_model=list[TeamMessageInfo])
109
+ async def team_messages(
110
+ team_id: str,
111
+ request: Request,
112
+ session_id: str | None = None,
113
+ agent_id: str | None = None,
114
+ unread: bool = False,
115
+ ) -> list[TeamMessageInfo]:
116
+ def _messages(manager: Any) -> list[TeamMessageInfo] | None:
117
+ status = manager.get_team_status(team_id)
118
+ if not status:
119
+ return None
120
+ teammate_ids = {
121
+ str(teammate.get("agent_id") or "")
122
+ for teammate in status.get("teammates", [])
123
+ }
124
+ if agent_id is not None:
125
+ if agent_id not in teammate_ids:
126
+ return None
127
+ source = (
128
+ manager.get_unread_messages(team_id, agent_id)
129
+ if unread
130
+ else manager.get_all_messages(team_id, agent_id)
131
+ )
132
+ return [_message_info(message) for message in source]
133
+ seen: set[str] = set()
134
+ result: list[TeamMessageInfo] = []
135
+ for teammate in status.get("teammates", []):
136
+ teammate_id = str(teammate.get("agent_id") or "")
137
+ source = (
138
+ manager.get_unread_messages(team_id, teammate_id)
139
+ if unread
140
+ else manager.get_all_messages(team_id, teammate_id)
141
+ )
142
+ for message in source:
143
+ info = _message_info(message)
144
+ if info.id in seen:
145
+ continue
146
+ seen.add(info.id)
147
+ result.append(info)
148
+ result.sort(key=lambda item: item.timestamp)
149
+ return result
150
+
151
+ result = await _run_team_operation(request, session_id, _messages)
152
+ if result is None:
153
+ raise HTTPException(status_code=404, detail=f"Team '{team_id}' not found")
154
+ return result
155
+
156
+
157
+ @router.get("/{team_id}/tasks", response_model=list[TeamTaskInfo])
158
+ async def team_tasks(team_id: str, request: Request, session_id: str | None = None) -> list[TeamTaskInfo]:
159
+ def _tasks(manager: Any) -> list[TeamTaskInfo] | None:
160
+ if not manager.get_team_status(team_id):
161
+ return None
162
+ return [_task_info(task) for task in manager.list_tasks(team_id)]
163
+
164
+ result = await _run_team_operation(request, session_id, _tasks)
165
+ if result is None:
166
+ raise HTTPException(status_code=404, detail=f"Team '{team_id}' not found")
167
+ return result
168
+
169
+
170
+ @router.post("/create")
171
+ async def create_team(req: TeamCreateRequest, request: Request) -> dict[str, Any]:
172
+ try:
173
+ team_id = await _run_team_operation(
174
+ request,
175
+ req.session_id,
176
+ lambda manager: manager.create_team(req.name, max_teammates=req.max_teammates),
177
+ )
178
+ except (RuntimeError, ValueError) as exc:
179
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
180
+ return {"team_id": team_id}
181
+
182
+
183
+ @router.post("/spawn")
184
+ async def spawn_teammate(req: TeamSpawnRequest, request: Request) -> dict[str, Any]:
185
+ try:
186
+ role = TeammateRole(req.role)
187
+ agent_id = await _run_team_operation(
188
+ request,
189
+ req.session_id,
190
+ lambda manager: manager.add_teammate(
191
+ req.team_id,
192
+ role=role,
193
+ prompt=req.prompt,
194
+ name=req.name,
195
+ model_profile=req.model_profile,
196
+ ),
197
+ )
198
+ except (RuntimeError, ValueError) as exc:
199
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
200
+ return {"team_id": req.team_id, "agent_id": agent_id}
201
+
202
+
203
+ @router.post("/remove")
204
+ @router.post("/teammate/remove")
205
+ async def remove_teammate(req: TeamRemoveRequest, request: Request) -> dict[str, Any]:
206
+ removed = await _run_team_operation(
207
+ request,
208
+ req.session_id,
209
+ lambda manager: manager.remove_teammate(req.team_id, req.agent_id),
210
+ )
211
+ if not removed:
212
+ raise HTTPException(status_code=404, detail="Team or teammate not found")
213
+ return {"team_id": req.team_id, "agent_id": req.agent_id, "removed": True}
214
+
215
+
216
+ @router.post("/message")
217
+ async def send_team_message(req: TeamMessageRequest, request: Request) -> dict[str, Any]:
218
+ message = await _run_team_operation(
219
+ request,
220
+ req.session_id,
221
+ lambda manager: manager.send_message(req.team_id, req.from_agent, req.to, req.text),
222
+ )
223
+ if message is None:
224
+ raise HTTPException(status_code=400, detail="Team message delivery failed")
225
+ return {"message_id": message.id, "delivered": True}
226
+
227
+
228
+ @router.post("/broadcast")
229
+ async def broadcast_team_message(req: TeamBroadcastRequest, request: Request) -> dict[str, Any]:
230
+ messages = await _run_team_operation(
231
+ request,
232
+ req.session_id,
233
+ lambda manager: manager.broadcast(req.team_id, req.from_agent, req.text),
234
+ )
235
+ return {"recipient_count": len(messages)}
236
+
237
+
238
+ @router.post("/messages/read")
239
+ async def mark_team_messages_read(
240
+ req: TeamMessagesReadRequest,
241
+ request: Request,
242
+ ) -> dict[str, Any]:
243
+ def _mark(manager: Any):
244
+ if manager.get_teammate(req.team_id, req.agent_id) is None:
245
+ return None
246
+ return manager.mark_read(req.team_id, req.agent_id, req.message_ids)
247
+
248
+ count = await _run_team_operation(request, req.session_id, _mark)
249
+ if count is None:
250
+ raise HTTPException(status_code=404, detail="Team or teammate not found")
251
+ return {
252
+ "team_id": req.team_id,
253
+ "agent_id": req.agent_id,
254
+ "marked_read": count,
255
+ }
256
+
257
+
258
+ @router.post("/task/add")
259
+ async def add_team_task(req: TeamTaskAddRequest, request: Request) -> dict[str, Any]:
260
+ try:
261
+ task_id = await _run_team_operation(
262
+ request,
263
+ req.session_id,
264
+ lambda manager: manager.add_task(req.team_id, req.description),
265
+ )
266
+ except (RuntimeError, ValueError) as exc:
267
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
268
+ return {"task_id": task_id}
269
+
270
+
271
+ @router.post("/task/claim")
272
+ async def claim_team_task(req: TeamTaskClaimRequest, request: Request) -> dict[str, Any]:
273
+ claimed = await _run_team_operation(
274
+ request,
275
+ req.session_id,
276
+ lambda manager: manager.claim_task(req.team_id, req.task_id, req.agent_id),
277
+ )
278
+ if not claimed:
279
+ raise HTTPException(status_code=400, detail="Task not found or already claimed")
280
+ return {"task_id": req.task_id, "claimed": True}
281
+
282
+
283
+ @router.post("/task/complete")
284
+ async def complete_team_task(req: TeamTaskCompleteRequest, request: Request) -> dict[str, Any]:
285
+ completed = await _run_team_operation(
286
+ request,
287
+ req.session_id,
288
+ lambda manager: manager.complete_task(req.team_id, req.task_id, req.result, req.agent_id),
289
+ )
290
+ if not completed:
291
+ raise HTTPException(status_code=400, detail="Task not found or not in claimed state")
292
+ return {"task_id": req.task_id, "completed": True}
293
+
294
+
295
+ @router.post("/task/fail")
296
+ async def fail_team_task(req: TeamTaskFailRequest, request: Request) -> dict[str, Any]:
297
+ failed = await _run_team_operation(
298
+ request,
299
+ req.session_id,
300
+ lambda manager: manager.fail_task(
301
+ req.team_id,
302
+ req.task_id,
303
+ req.reason,
304
+ req.agent_id,
305
+ ),
306
+ )
307
+ if not failed:
308
+ raise HTTPException(status_code=400, detail="Task not found or not in claimed state")
309
+ return {"task_id": req.task_id, "failed": True}
310
+
311
+
312
+ @router.get("/{team_a}/bridge/{team_b}", response_model=TeamBridgeInfo)
313
+ async def get_team_bridge(
314
+ team_a: str,
315
+ team_b: str,
316
+ request: Request,
317
+ session_id: str | None = None,
318
+ ) -> TeamBridgeInfo:
319
+ def _get(manager: Any) -> TeamBridgeInfo | None:
320
+ if not manager.get_team_status(team_a) or not manager.get_team_status(team_b):
321
+ return None
322
+ policy = manager.get_bridge_policy(team_a, team_b)
323
+ return TeamBridgeInfo(
324
+ team_a=team_a,
325
+ team_b=team_b,
326
+ policy=getattr(policy, "value", policy),
327
+ )
328
+
329
+ result = await _run_team_operation(request, session_id, _get)
330
+ if result is None:
331
+ raise HTTPException(status_code=404, detail="One or both teams were not found")
332
+ return result
333
+
334
+
335
+ @router.post("/bridge", response_model=TeamBridgeInfo)
336
+ async def register_team_bridge(
337
+ req: TeamBridgeRequest,
338
+ request: Request,
339
+ ) -> TeamBridgeInfo:
340
+ def _register(manager: Any) -> TeamBridgeInfo | None:
341
+ if not manager.get_team_status(req.team_a) or not manager.get_team_status(req.team_b):
342
+ return None
343
+ manager.register_bridge(req.team_a, req.team_b, BridgePolicy(req.policy))
344
+ return TeamBridgeInfo(
345
+ team_a=req.team_a,
346
+ team_b=req.team_b,
347
+ policy=req.policy,
348
+ )
349
+
350
+ try:
351
+ result = await _run_team_operation(request, req.session_id, _register)
352
+ except ValueError as exc:
353
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
354
+ if result is None:
355
+ raise HTTPException(status_code=404, detail="One or both teams were not found")
356
+ return result
357
+
358
+
359
+ @router.post("/cross-message", response_model=CrossTeamMessageInfo)
360
+ async def send_cross_team_message(
361
+ req: TeamCrossMessageRequest,
362
+ request: Request,
363
+ ) -> CrossTeamMessageInfo:
364
+ message = await _run_team_operation(
365
+ request,
366
+ req.session_id,
367
+ lambda manager: manager.send_cross_team(
368
+ req.from_team,
369
+ req.from_agent,
370
+ req.to_team,
371
+ req.to_agent,
372
+ req.text,
373
+ ),
374
+ )
375
+ if message is None:
376
+ raise HTTPException(
377
+ status_code=400,
378
+ detail="Cross-team message was refused or could not be delivered",
379
+ )
380
+ return _cross_message_info(message)
381
+
382
+
383
+ @router.post("/shutdown")
384
+ async def shutdown_team(req: TeamShutdownRequest, request: Request) -> dict[str, Any]:
385
+ shutdown = await _run_team_operation(
386
+ request,
387
+ req.session_id,
388
+ lambda manager: manager.shutdown_team(req.team_id),
389
+ )
390
+ if not shutdown:
391
+ raise HTTPException(status_code=404, detail=f"Team '{req.team_id}' not found")
392
+ return {"team_id": req.team_id, "shutdown": True}
@@ -0,0 +1,49 @@
1
+ """Provider-reported token usage, scoped to this Gateway's local ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sqlite3
7
+ from datetime import date, datetime
8
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
9
+
10
+ from fastapi import APIRouter, HTTPException, Query, Request
11
+
12
+ from crabcode_core import usage as usage_module
13
+ from crabcode_core.usage import UsageStore
14
+ from crabcode_gateway.routes.workspace import _resolve_directory, _workspace_roots
15
+ from crabcode_gateway.schemas import UsageDailyResponse
16
+
17
+ router = APIRouter(prefix="/usage", tags=["usage"])
18
+
19
+
20
+ @router.get("/daily", response_model=UsageDailyResponse)
21
+ async def daily_usage(request: Request, start_date: date, end_date: date,
22
+ timezone: str = Query(..., max_length=100),
23
+ cwd: str | None = None) -> dict:
24
+ if (end_date - start_date).days < 0 or (end_date - start_date).days >= 366:
25
+ raise HTTPException(status_code=422, detail="日期范围必须为 1–366 天")
26
+ try:
27
+ zone = ZoneInfo(timezone)
28
+ except ZoneInfoNotFoundError:
29
+ try:
30
+ ZoneInfo("Etc/UTC")
31
+ except ZoneInfoNotFoundError:
32
+ raise HTTPException(status_code=503, detail=(
33
+ "Gateway 缺少 IANA 时区数据;请在 Gateway 使用的 Python 环境安装 tzdata 后重启 Gateway"
34
+ )) from None
35
+ raise HTTPException(status_code=422, detail="无效的 IANA 时区") from None
36
+ except ValueError:
37
+ raise HTTPException(status_code=422, detail="无效的 IANA 时区") from None
38
+ if end_date > datetime.now(zone).date():
39
+ raise HTTPException(status_code=422, detail="结束日期不能晚于今天")
40
+ store = UsageStore()
41
+ if usage_module.recording_error or store.error_marker.is_file():
42
+ raise HTTPException(status_code=503, detail=(
43
+ f"使用记录曾写入失败,统计可能不完整;请检查 Gateway/CLI 日志及 {store.error_marker}"
44
+ ))
45
+ project = str(_resolve_directory(cwd, _workspace_roots(request))) if cwd else None
46
+ try:
47
+ return await asyncio.to_thread(store.daily, start_date, end_date, zone, project)
48
+ except (OSError, sqlite3.Error) as exc:
49
+ raise HTTPException(status_code=503, detail="使用记录暂时无法读取") from exc