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