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,765 @@
|
|
|
1
|
+
"""gRPC server implementation for CrabCode Gateway.
|
|
2
|
+
|
|
3
|
+
Uses the generated stubs checked in alongside ``crabcode.proto``. Keeping
|
|
4
|
+
the generated modules in the package makes the gRPC endpoint available in
|
|
5
|
+
normal installations without requiring ``grpcio-tools`` at runtime.
|
|
6
|
+
|
|
7
|
+
In production, run::
|
|
8
|
+
|
|
9
|
+
python -m grpc_tools.protoc \\
|
|
10
|
+
-I packages/gateway/crabcode_gateway/grpc/proto \\
|
|
11
|
+
--python_out=packages/gateway/crabcode_gateway/grpc \\
|
|
12
|
+
--grpc_python_out=packages/gateway/crabcode_gateway/grpc \\
|
|
13
|
+
packages/gateway/crabcode_gateway/grpc/proto/crabcode.proto
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import binascii
|
|
20
|
+
import hmac
|
|
21
|
+
import json
|
|
22
|
+
from concurrent import futures
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import grpc
|
|
26
|
+
|
|
27
|
+
from crabcode_core import VERSION
|
|
28
|
+
from crabcode_core.logging_utils import get_logger
|
|
29
|
+
from crabcode_gateway.adapter import ProtocolAdapter
|
|
30
|
+
from crabcode_gateway.auth import decode_jwt, verify_password
|
|
31
|
+
from crabcode_gateway.schemas import core_event_to_payload
|
|
32
|
+
from crabcode_gateway.task_registry import (
|
|
33
|
+
SessionOperationRejected,
|
|
34
|
+
run_session_operation,
|
|
35
|
+
session_lease,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
_PROTO_INT64_MIN = -(1 << 63)
|
|
41
|
+
_PROTO_INT64_MAX = (1 << 63) - 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _call_sync(fn: Any) -> Any:
|
|
45
|
+
"""Adapt a synchronous CoreSession accessor to the owned-task helper."""
|
|
46
|
+
return fn()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _session_from_app(
|
|
50
|
+
app_state: Any,
|
|
51
|
+
session_id: str | None = None,
|
|
52
|
+
*,
|
|
53
|
+
agent_id: str | None = None,
|
|
54
|
+
agent_ids: list[str] | None = None,
|
|
55
|
+
) -> Any | None:
|
|
56
|
+
"""Resolve a session without silently crossing an explicit boundary.
|
|
57
|
+
|
|
58
|
+
Agent RPCs may omit ``session_id`` for backwards compatibility. In that
|
|
59
|
+
case an agent selector is resolved by ownership; requests that explicitly
|
|
60
|
+
name a session must stay in that session and are never redirected to the
|
|
61
|
+
process default.
|
|
62
|
+
"""
|
|
63
|
+
sessions: dict = getattr(app_state, "sessions", {})
|
|
64
|
+
if getattr(app_state, "gateway_closing", False):
|
|
65
|
+
return None
|
|
66
|
+
closing = getattr(app_state, "closing_sessions", set())
|
|
67
|
+
# A missing selector may use the legacy default; an explicitly supplied
|
|
68
|
+
# value is authoritative, including an empty/invalid value.
|
|
69
|
+
# Protobuf scalar strings use ``""`` for an omitted optional selector;
|
|
70
|
+
# normalize that wire representation before applying ownership rules.
|
|
71
|
+
requested = session_id if session_id not in (None, "") else None
|
|
72
|
+
selected = sessions.get(requested) if requested else None
|
|
73
|
+
|
|
74
|
+
ids = [item for item in (agent_ids or []) if item]
|
|
75
|
+
if agent_id:
|
|
76
|
+
ids.append(agent_id)
|
|
77
|
+
|
|
78
|
+
def owns(candidate: Any) -> bool:
|
|
79
|
+
if candidate is None:
|
|
80
|
+
return False
|
|
81
|
+
if getattr(candidate, "session_id", None) in closing:
|
|
82
|
+
return False
|
|
83
|
+
if not ids:
|
|
84
|
+
return True
|
|
85
|
+
try:
|
|
86
|
+
return all(candidate.get_agent(item) is not None for item in ids)
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
if requested:
|
|
91
|
+
return selected if owns(selected) else None
|
|
92
|
+
|
|
93
|
+
if ids:
|
|
94
|
+
for candidate in sessions.values():
|
|
95
|
+
if owns(candidate):
|
|
96
|
+
return candidate
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
default_id = getattr(app_state, "default_session_id", None)
|
|
100
|
+
if default_id and default_id in sessions:
|
|
101
|
+
return sessions[default_id]
|
|
102
|
+
if sessions:
|
|
103
|
+
return next(iter(sessions.values()))
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _event_to_proto(event: Any) -> dict:
|
|
108
|
+
"""Convert a CoreEvent to a gRPC-friendly dict."""
|
|
109
|
+
payload = core_event_to_payload(event)
|
|
110
|
+
return {
|
|
111
|
+
"type": payload.type,
|
|
112
|
+
"payload_json": payload.model_dump_json(),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _snapshot_to_proto(snapshot: Any) -> dict[str, Any]:
|
|
117
|
+
"""Map the complete observable agent lifecycle to the gRPC shape."""
|
|
118
|
+
usage: dict[str, int] = {}
|
|
119
|
+
for key, value in (snapshot.usage or {}).items():
|
|
120
|
+
try:
|
|
121
|
+
numeric = int(value)
|
|
122
|
+
except (TypeError, ValueError, OverflowError):
|
|
123
|
+
# The protobuf contract uses int64 values. A malformed provider
|
|
124
|
+
# usage field should not make an otherwise valid status unreadable.
|
|
125
|
+
logger.debug("Ignoring non-integer usage value for %s: %r", key, value)
|
|
126
|
+
continue
|
|
127
|
+
if not _PROTO_INT64_MIN <= numeric <= _PROTO_INT64_MAX:
|
|
128
|
+
logger.debug("Ignoring out-of-range usage value for %s: %r", key, value)
|
|
129
|
+
continue
|
|
130
|
+
usage[str(key)] = numeric
|
|
131
|
+
payload: dict[str, Any] = {
|
|
132
|
+
"agent_id": snapshot.agent_id,
|
|
133
|
+
"session_id": snapshot.session_id,
|
|
134
|
+
"parent_agent_id": snapshot.parent_agent_id or "",
|
|
135
|
+
"title": snapshot.title,
|
|
136
|
+
"subagent_type": snapshot.subagent_type,
|
|
137
|
+
"status": snapshot.status,
|
|
138
|
+
"model": snapshot.model,
|
|
139
|
+
"created_at": snapshot.created_at,
|
|
140
|
+
"usage": usage,
|
|
141
|
+
"final_result": snapshot.final_result,
|
|
142
|
+
"error": snapshot.error,
|
|
143
|
+
"callback_enabled": snapshot.callback_enabled,
|
|
144
|
+
"callback_state": snapshot.callback_state,
|
|
145
|
+
"callback_epoch": snapshot.callback_epoch,
|
|
146
|
+
}
|
|
147
|
+
if snapshot.finished_at:
|
|
148
|
+
payload["finished_at"] = snapshot.finished_at
|
|
149
|
+
if snapshot.transcript_path:
|
|
150
|
+
payload["transcript_path"] = snapshot.transcript_path
|
|
151
|
+
if snapshot.callback_message_id:
|
|
152
|
+
payload["callback_message_id"] = snapshot.callback_message_id
|
|
153
|
+
return payload
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class _CrabCodeServicer:
|
|
157
|
+
"""Hand-written servicer that delegates to CoreSession.
|
|
158
|
+
|
|
159
|
+
The generated protobuf service wrapper below adapts wire messages to this
|
|
160
|
+
dictionary-based implementation, keeping the core session API protocol
|
|
161
|
+
agnostic.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
app_state: Any,
|
|
167
|
+
*,
|
|
168
|
+
username: str = "crabcode",
|
|
169
|
+
password: str | None = None,
|
|
170
|
+
password_hash: str | None = None,
|
|
171
|
+
security_mode: str | None = None,
|
|
172
|
+
jwt_secret: str | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self._app_state = app_state
|
|
175
|
+
self._event_bus = getattr(app_state, "event_bus", None)
|
|
176
|
+
self._auth_username = username
|
|
177
|
+
self._auth_password = password
|
|
178
|
+
self._auth_password_hash = password_hash
|
|
179
|
+
self._security_mode = security_mode or (
|
|
180
|
+
"password" if password or password_hash else "none"
|
|
181
|
+
)
|
|
182
|
+
self._jwt_secret = jwt_secret
|
|
183
|
+
|
|
184
|
+
async def _require_auth(self, context: Any) -> None:
|
|
185
|
+
"""Apply the same credentials as HTTP/WS when gRPC is protected."""
|
|
186
|
+
if self._security_mode == "none":
|
|
187
|
+
return
|
|
188
|
+
metadata = {}
|
|
189
|
+
try:
|
|
190
|
+
metadata = {
|
|
191
|
+
str(key).lower(): str(value)
|
|
192
|
+
for key, value in context.invocation_metadata()
|
|
193
|
+
}
|
|
194
|
+
except Exception:
|
|
195
|
+
metadata = {}
|
|
196
|
+
header = metadata.get("authorization", "")
|
|
197
|
+
scheme, separator, credentials = header.partition(" ")
|
|
198
|
+
authorized = False
|
|
199
|
+
if separator and scheme.lower() == "bearer":
|
|
200
|
+
authorized = bool(self._jwt_secret and decode_jwt(credentials, self._jwt_secret))
|
|
201
|
+
if not authorized and self._security_mode in ("password", "mixed"):
|
|
202
|
+
authorized = bool(
|
|
203
|
+
(self._auth_password and hmac.compare_digest(credentials, self._auth_password))
|
|
204
|
+
or (self._auth_password_hash and verify_password(credentials, self._auth_password_hash))
|
|
205
|
+
)
|
|
206
|
+
elif separator and scheme.lower() == "basic" and self._security_mode in ("password", "mixed"):
|
|
207
|
+
try:
|
|
208
|
+
decoded = base64.b64decode(credentials, validate=True).decode("utf-8")
|
|
209
|
+
user, supplied = decoded.split(":", 1)
|
|
210
|
+
authorized = hmac.compare_digest(user, self._auth_username) and bool(
|
|
211
|
+
(self._auth_password and hmac.compare_digest(supplied, self._auth_password))
|
|
212
|
+
or (self._auth_password_hash and verify_password(supplied, self._auth_password_hash))
|
|
213
|
+
)
|
|
214
|
+
except (ValueError, UnicodeDecodeError, binascii.Error):
|
|
215
|
+
authorized = False
|
|
216
|
+
if not authorized:
|
|
217
|
+
await context.abort(
|
|
218
|
+
code=grpc.StatusCode.UNAUTHENTICATED,
|
|
219
|
+
details="Unauthenticated",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _owned(self, session: Any, operation: Any, context: Any) -> Any:
|
|
223
|
+
"""Run a unary RPC under the session task/lifecycle fence."""
|
|
224
|
+
try:
|
|
225
|
+
return await run_session_operation(self._app_state, session, operation)
|
|
226
|
+
except SessionOperationRejected as exc:
|
|
227
|
+
await context.abort(
|
|
228
|
+
code=grpc.StatusCode.UNAVAILABLE,
|
|
229
|
+
details=str(exc),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# ── Unary RPCs ──────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async def SpawnAgent(self, request: dict, context: Any) -> dict:
|
|
235
|
+
await self._require_auth(context)
|
|
236
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
237
|
+
if not session:
|
|
238
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
239
|
+
agent_id = await self._owned(
|
|
240
|
+
session,
|
|
241
|
+
lambda: session.spawn_agent(
|
|
242
|
+
prompt=request.get("prompt", ""),
|
|
243
|
+
subagent_type=request.get("subagent_type", "generalPurpose"),
|
|
244
|
+
name=request.get("name"),
|
|
245
|
+
model_profile=request.get("model_profile"),
|
|
246
|
+
callback=bool(request.get("callback", False)),
|
|
247
|
+
),
|
|
248
|
+
context,
|
|
249
|
+
)
|
|
250
|
+
return {"agent_id": agent_id}
|
|
251
|
+
|
|
252
|
+
async def GetAgent(self, request: dict, context: Any) -> dict:
|
|
253
|
+
await self._require_auth(context)
|
|
254
|
+
session = _session_from_app(
|
|
255
|
+
self._app_state,
|
|
256
|
+
request.get("session_id"),
|
|
257
|
+
agent_id=request.get("agent_id"),
|
|
258
|
+
)
|
|
259
|
+
if not session:
|
|
260
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
261
|
+
snapshot = await self._owned(
|
|
262
|
+
session,
|
|
263
|
+
lambda: _call_sync(lambda: session.get_agent(request.get("agent_id", ""))),
|
|
264
|
+
context,
|
|
265
|
+
)
|
|
266
|
+
if not snapshot:
|
|
267
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="Agent not found")
|
|
268
|
+
return _snapshot_to_proto(snapshot)
|
|
269
|
+
|
|
270
|
+
async def ListAgents(self, request: dict, context: Any) -> dict:
|
|
271
|
+
await self._require_auth(context)
|
|
272
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
273
|
+
if not session:
|
|
274
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
275
|
+
agents = await self._owned(
|
|
276
|
+
session,
|
|
277
|
+
lambda: _call_sync(lambda: [_snapshot_to_proto(snapshot) for snapshot in session.list_agents()]),
|
|
278
|
+
context,
|
|
279
|
+
)
|
|
280
|
+
return {"agents": agents}
|
|
281
|
+
|
|
282
|
+
async def CancelAgent(self, request: dict, context: Any) -> dict:
|
|
283
|
+
await self._require_auth(context)
|
|
284
|
+
session = _session_from_app(
|
|
285
|
+
self._app_state,
|
|
286
|
+
request.get("session_id"),
|
|
287
|
+
agent_id=request.get("agent_id"),
|
|
288
|
+
)
|
|
289
|
+
if not session:
|
|
290
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
291
|
+
ok = await self._owned(
|
|
292
|
+
session,
|
|
293
|
+
lambda: session.cancel_agent(request.get("agent_id", "")),
|
|
294
|
+
context,
|
|
295
|
+
)
|
|
296
|
+
return {"ok": ok}
|
|
297
|
+
|
|
298
|
+
async def WaitAgent(self, request: dict, context: Any) -> dict:
|
|
299
|
+
await self._require_auth(context)
|
|
300
|
+
agent_ids = [item for item in request.get("agent_ids", []) if item]
|
|
301
|
+
if not agent_ids:
|
|
302
|
+
await context.abort(
|
|
303
|
+
code=grpc.StatusCode.INVALID_ARGUMENT,
|
|
304
|
+
details="At least one agent id is required",
|
|
305
|
+
)
|
|
306
|
+
session = _session_from_app(
|
|
307
|
+
self._app_state,
|
|
308
|
+
request.get("session_id"),
|
|
309
|
+
agent_ids=agent_ids,
|
|
310
|
+
)
|
|
311
|
+
if not session:
|
|
312
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
313
|
+
timeout_ms = request.get("timeout_ms")
|
|
314
|
+
if len(agent_ids) == 1:
|
|
315
|
+
snapshot = await self._owned(
|
|
316
|
+
session,
|
|
317
|
+
lambda: session.wait_agent(agent_ids[0], timeout_ms=timeout_ms),
|
|
318
|
+
context,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
snapshot = await self._owned(
|
|
322
|
+
session,
|
|
323
|
+
lambda: session.wait_agent(agent_ids, timeout_ms=timeout_ms),
|
|
324
|
+
context,
|
|
325
|
+
)
|
|
326
|
+
if not snapshot:
|
|
327
|
+
await context.abort(code=grpc.StatusCode.DEADLINE_EXCEEDED, details="Wait timed out")
|
|
328
|
+
return _snapshot_to_proto(snapshot)
|
|
329
|
+
|
|
330
|
+
async def RespondPermission(self, request: dict, context: Any) -> dict:
|
|
331
|
+
from crabcode_core.types.event import PermissionResponseEvent
|
|
332
|
+
await self._require_auth(context)
|
|
333
|
+
session = _session_from_app(
|
|
334
|
+
self._app_state,
|
|
335
|
+
request.get("session_id"),
|
|
336
|
+
agent_id=request.get("agent_id"),
|
|
337
|
+
)
|
|
338
|
+
if not session:
|
|
339
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
340
|
+
event = PermissionResponseEvent(
|
|
341
|
+
tool_use_id=request.get("tool_use_id", ""),
|
|
342
|
+
allowed=request.get("allowed", False),
|
|
343
|
+
always_allow=request.get("always_allow", False),
|
|
344
|
+
agent_id=request.get("agent_id"),
|
|
345
|
+
)
|
|
346
|
+
await self._owned(
|
|
347
|
+
session,
|
|
348
|
+
lambda: session.respond_permission(event),
|
|
349
|
+
context,
|
|
350
|
+
)
|
|
351
|
+
return {}
|
|
352
|
+
|
|
353
|
+
async def RespondChoice(self, request: dict, context: Any) -> dict:
|
|
354
|
+
from crabcode_core.types.event import ChoiceResponseEvent
|
|
355
|
+
await self._require_auth(context)
|
|
356
|
+
session = _session_from_app(
|
|
357
|
+
self._app_state,
|
|
358
|
+
request.get("session_id"),
|
|
359
|
+
agent_id=request.get("agent_id"),
|
|
360
|
+
)
|
|
361
|
+
if not session:
|
|
362
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
363
|
+
event = ChoiceResponseEvent(
|
|
364
|
+
tool_use_id=request.get("tool_use_id", ""),
|
|
365
|
+
selected=request.get("selected", []),
|
|
366
|
+
cancelled=request.get("cancelled", False),
|
|
367
|
+
agent_id=request.get("agent_id"),
|
|
368
|
+
)
|
|
369
|
+
await self._owned(
|
|
370
|
+
session,
|
|
371
|
+
lambda: session.respond_choice(event),
|
|
372
|
+
context,
|
|
373
|
+
)
|
|
374
|
+
return {}
|
|
375
|
+
|
|
376
|
+
async def ListModels(self, request: dict, context: Any) -> dict:
|
|
377
|
+
await self._require_auth(context)
|
|
378
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
379
|
+
if not session:
|
|
380
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
381
|
+
models = await self._owned(
|
|
382
|
+
session,
|
|
383
|
+
lambda: _call_sync(lambda: list(session.list_models().items())),
|
|
384
|
+
context,
|
|
385
|
+
)
|
|
386
|
+
result_models = []
|
|
387
|
+
for name, desc in models:
|
|
388
|
+
result_models.append({"name": name, "description": desc})
|
|
389
|
+
return {"models": result_models}
|
|
390
|
+
|
|
391
|
+
async def SwitchModel(self, request: dict, context: Any) -> dict:
|
|
392
|
+
await self._require_auth(context)
|
|
393
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
394
|
+
if not session:
|
|
395
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
396
|
+
ok = await self._owned(
|
|
397
|
+
session,
|
|
398
|
+
lambda: _call_sync(lambda: session.switch_model(request.get("name", ""))),
|
|
399
|
+
context,
|
|
400
|
+
)
|
|
401
|
+
if not ok:
|
|
402
|
+
await context.abort(code=grpc.StatusCode.INVALID_ARGUMENT, details="Model not found")
|
|
403
|
+
return {}
|
|
404
|
+
|
|
405
|
+
async def SwitchMode(self, request: dict, context: Any) -> dict:
|
|
406
|
+
await self._require_auth(context)
|
|
407
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
408
|
+
if not session:
|
|
409
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
410
|
+
ok = await self._owned(
|
|
411
|
+
session,
|
|
412
|
+
lambda: _call_sync(lambda: session.switch_mode(request.get("mode", "agent"))),
|
|
413
|
+
context,
|
|
414
|
+
)
|
|
415
|
+
if not ok:
|
|
416
|
+
await context.abort(code=grpc.StatusCode.INVALID_ARGUMENT, details="Invalid mode")
|
|
417
|
+
return {}
|
|
418
|
+
|
|
419
|
+
async def HealthCheck(self, request: dict, context: Any) -> dict:
|
|
420
|
+
# Keep health probes public, matching the HTTP /health endpoint.
|
|
421
|
+
return {"status": "ok", "version": VERSION}
|
|
422
|
+
|
|
423
|
+
# ── Server-streaming RPCs ────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
async def SendMessage(self, request: dict, context: Any) -> Any:
|
|
426
|
+
"""Stream CoreEvents as the query loop runs."""
|
|
427
|
+
await self._require_auth(context)
|
|
428
|
+
from crabcode_gateway.session_registry import get_session_lock
|
|
429
|
+
|
|
430
|
+
async with get_session_lock(self._app_state):
|
|
431
|
+
session = _session_from_app(self._app_state, request.get("session_id"))
|
|
432
|
+
if not session:
|
|
433
|
+
await context.abort(code=grpc.StatusCode.NOT_FOUND, details="No active session")
|
|
434
|
+
|
|
435
|
+
text = request.get("text", "")
|
|
436
|
+
max_turns = request.get("max_turns", 0)
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
async with session_lease(self._app_state, session):
|
|
440
|
+
async for event in session.send_message(text, max_turns=max_turns):
|
|
441
|
+
yield _event_to_proto(event)
|
|
442
|
+
except SessionOperationRejected as exc:
|
|
443
|
+
await context.abort(code=grpc.StatusCode.UNAVAILABLE, details=str(exc))
|
|
444
|
+
|
|
445
|
+
async def SubscribeEvents(self, request: dict, context: Any) -> Any:
|
|
446
|
+
"""Subscribe to the event bus SSE stream via gRPC."""
|
|
447
|
+
await self._require_auth(context)
|
|
448
|
+
if not self._event_bus:
|
|
449
|
+
await context.abort(code=grpc.StatusCode.INTERNAL, details="Event bus not available")
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
session_id = request.get("session_id") or None
|
|
453
|
+
from crabcode_gateway.session_registry import get_session_lock
|
|
454
|
+
|
|
455
|
+
subscriber = None
|
|
456
|
+
async with get_session_lock(self._app_state):
|
|
457
|
+
closing = getattr(self._app_state, "gateway_closing", False)
|
|
458
|
+
selected = (
|
|
459
|
+
_session_from_app(self._app_state, session_id)
|
|
460
|
+
if session_id is not None
|
|
461
|
+
else None
|
|
462
|
+
)
|
|
463
|
+
known = session_id is None or selected is not None
|
|
464
|
+
session_closing = bool(
|
|
465
|
+
session_id is not None
|
|
466
|
+
and session_id in getattr(self._app_state, "closing_sessions", set())
|
|
467
|
+
)
|
|
468
|
+
if not closing and known and not session_closing:
|
|
469
|
+
# Async generators are lazy: constructing ``sse_stream`` alone
|
|
470
|
+
# does not register its subscriber until iteration begins.
|
|
471
|
+
# Create it under the same lock used by gateway stop so
|
|
472
|
+
# close_all() cannot run before this queue becomes visible.
|
|
473
|
+
subscriber = self._event_bus.subscribe(session_id)
|
|
474
|
+
if closing:
|
|
475
|
+
await context.abort(
|
|
476
|
+
code=grpc.StatusCode.UNAVAILABLE,
|
|
477
|
+
details="Gateway is shutting down",
|
|
478
|
+
)
|
|
479
|
+
if not known:
|
|
480
|
+
await context.abort(
|
|
481
|
+
code=grpc.StatusCode.NOT_FOUND,
|
|
482
|
+
details="Session not found",
|
|
483
|
+
)
|
|
484
|
+
if session_closing:
|
|
485
|
+
await context.abort(
|
|
486
|
+
code=grpc.StatusCode.UNAVAILABLE,
|
|
487
|
+
details="Session is shutting down",
|
|
488
|
+
)
|
|
489
|
+
assert subscriber is not None
|
|
490
|
+
try:
|
|
491
|
+
if session_id is None:
|
|
492
|
+
# A global subscription has no session lease; it is bounded by
|
|
493
|
+
# the transport itself and is not tied to one CoreSession.
|
|
494
|
+
stream = self._event_bus.sse_stream(
|
|
495
|
+
session_id,
|
|
496
|
+
subscriber=subscriber,
|
|
497
|
+
)
|
|
498
|
+
async for data in stream:
|
|
499
|
+
try:
|
|
500
|
+
parsed = json.loads(data)
|
|
501
|
+
yield {"type": parsed.get("type", "unknown"), "payload_json": data}
|
|
502
|
+
except json.JSONDecodeError:
|
|
503
|
+
yield {"type": "unknown", "payload_json": data}
|
|
504
|
+
else:
|
|
505
|
+
async with session_lease(self._app_state, selected):
|
|
506
|
+
async for data in self._event_bus.sse_stream(
|
|
507
|
+
session_id,
|
|
508
|
+
subscriber=subscriber,
|
|
509
|
+
):
|
|
510
|
+
try:
|
|
511
|
+
parsed = json.loads(data)
|
|
512
|
+
yield {"type": parsed.get("type", "unknown"), "payload_json": data}
|
|
513
|
+
except json.JSONDecodeError:
|
|
514
|
+
yield {"type": "unknown", "payload_json": data}
|
|
515
|
+
except SessionOperationRejected as exc:
|
|
516
|
+
await context.abort(code=grpc.StatusCode.UNAVAILABLE, details=str(exc))
|
|
517
|
+
finally:
|
|
518
|
+
# ``sse_stream`` normally removes it in its own finally block;
|
|
519
|
+
# this idempotent call also covers rejection before iteration.
|
|
520
|
+
self._event_bus.unsubscribe(subscriber)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class GrpcAdapter(ProtocolAdapter):
|
|
524
|
+
"""gRPC protocol adapter.
|
|
525
|
+
|
|
526
|
+
Starts a grpcio async server on the configured port.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
def __init__(
|
|
530
|
+
self,
|
|
531
|
+
app_state: Any,
|
|
532
|
+
password: str | None = None,
|
|
533
|
+
password_hash: str | None = None,
|
|
534
|
+
security_mode: str | None = None,
|
|
535
|
+
jwt_secret: str | None = None,
|
|
536
|
+
) -> None:
|
|
537
|
+
self._app_state = app_state
|
|
538
|
+
self._password = password
|
|
539
|
+
self._password_hash = password_hash
|
|
540
|
+
self._security_mode = security_mode or (
|
|
541
|
+
"password" if password or password_hash else "none"
|
|
542
|
+
)
|
|
543
|
+
self._jwt_secret = jwt_secret
|
|
544
|
+
self._server: Any = None
|
|
545
|
+
self._running = False
|
|
546
|
+
self._bound_port: int | None = None
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def is_running(self) -> bool:
|
|
550
|
+
return self._running
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def bound_port(self) -> int | None:
|
|
554
|
+
"""Return the port selected by gRPC (useful when binding port 0)."""
|
|
555
|
+
return self._bound_port
|
|
556
|
+
|
|
557
|
+
async def start(self, host: str, port: int) -> None:
|
|
558
|
+
"""Start the gRPC server using the bundled generated protobuf stubs."""
|
|
559
|
+
if self._running or self._server is not None:
|
|
560
|
+
raise RuntimeError("gRPC server is already started")
|
|
561
|
+
try:
|
|
562
|
+
from grpc import aio as grpc_aio
|
|
563
|
+
except ImportError:
|
|
564
|
+
logger.warning("grpcio not installed, skipping gRPC server")
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
servicer = _CrabCodeServicer(
|
|
568
|
+
self._app_state,
|
|
569
|
+
username=getattr(self._app_state, "gateway_username", "crabcode"),
|
|
570
|
+
password=(
|
|
571
|
+
self._password
|
|
572
|
+
if self._password is not None
|
|
573
|
+
else getattr(self._app_state, "gateway_password", None)
|
|
574
|
+
),
|
|
575
|
+
password_hash=self._password_hash,
|
|
576
|
+
security_mode=self._security_mode,
|
|
577
|
+
jwt_secret=self._jwt_secret,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
self._server = grpc_aio.server(futures.ThreadPoolExecutor(max_workers=4))
|
|
581
|
+
|
|
582
|
+
# Register the generated stubs shipped with this package.
|
|
583
|
+
try:
|
|
584
|
+
from crabcode_gateway.grpc import crabcode_pb2_grpc # noqa: F401
|
|
585
|
+
|
|
586
|
+
crabcode_pb2_grpc.add_CrabCodeServiceServicer_to_server(
|
|
587
|
+
_GeneratedStubServicer(servicer), self._server
|
|
588
|
+
)
|
|
589
|
+
logger.info("Using generated gRPC stubs")
|
|
590
|
+
except ImportError as exc:
|
|
591
|
+
# Stubs are shipped in this package. Keep the error explicit if a
|
|
592
|
+
# broken/partial installation omits them instead of silently
|
|
593
|
+
# reporting a running-but-unusable gRPC endpoint.
|
|
594
|
+
await self._cleanup_failed_start()
|
|
595
|
+
raise RuntimeError(
|
|
596
|
+
"Bundled gRPC protobuf stubs are unavailable; reinstall crabcode-gateway"
|
|
597
|
+
) from exc
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
|
|
601
|
+
except ImportError as exc:
|
|
602
|
+
await self._cleanup_failed_start()
|
|
603
|
+
raise RuntimeError(
|
|
604
|
+
"grpcio-health-checking is required for the gRPC gateway"
|
|
605
|
+
) from exc
|
|
606
|
+
|
|
607
|
+
health_servicer = health.HealthServicer()
|
|
608
|
+
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._server)
|
|
609
|
+
health_servicer.set(
|
|
610
|
+
"", health_pb2.HealthCheckResponse.SERVING
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
self._bound_port = self._server.add_insecure_port(f"{host}:{port}")
|
|
615
|
+
except Exception:
|
|
616
|
+
await self._cleanup_failed_start()
|
|
617
|
+
raise
|
|
618
|
+
if not self._bound_port:
|
|
619
|
+
await self._cleanup_failed_start()
|
|
620
|
+
raise OSError(f"Unable to bind gRPC listener on {host}:{port}")
|
|
621
|
+
try:
|
|
622
|
+
await self._server.start()
|
|
623
|
+
except Exception:
|
|
624
|
+
await self._cleanup_failed_start()
|
|
625
|
+
raise
|
|
626
|
+
self._running = True
|
|
627
|
+
logger.info("gRPC server listening on %s:%d", host, self._bound_port)
|
|
628
|
+
|
|
629
|
+
async def _cleanup_failed_start(self) -> None:
|
|
630
|
+
server = self._server
|
|
631
|
+
self._server = None
|
|
632
|
+
self._bound_port = None
|
|
633
|
+
self._running = False
|
|
634
|
+
if server is not None:
|
|
635
|
+
await server.stop(grace=0)
|
|
636
|
+
|
|
637
|
+
async def stop(self) -> None:
|
|
638
|
+
server = self._server
|
|
639
|
+
self._server = None
|
|
640
|
+
self._running = False
|
|
641
|
+
self._bound_port = None
|
|
642
|
+
if server:
|
|
643
|
+
await server.stop(grace=5)
|
|
644
|
+
logger.info("gRPC server stopped")
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class _GeneratedStubServicer:
|
|
648
|
+
"""Wrapper that delegates to _CrabCodeServicer using generated stub interfaces."""
|
|
649
|
+
|
|
650
|
+
def __init__(self, servicer: _CrabCodeServicer) -> None:
|
|
651
|
+
self._servicer = servicer
|
|
652
|
+
|
|
653
|
+
async def SendMessage(self, request, context):
|
|
654
|
+
req_dict = {
|
|
655
|
+
"text": request.text,
|
|
656
|
+
"max_turns": request.max_turns,
|
|
657
|
+
"session_id": request.session_id,
|
|
658
|
+
}
|
|
659
|
+
async for event in self._servicer.SendMessage(req_dict, context):
|
|
660
|
+
yield self._proto_event(event)
|
|
661
|
+
|
|
662
|
+
async def SubscribeEvents(self, request, context):
|
|
663
|
+
req_dict = {"session_id": request.session_id}
|
|
664
|
+
async for event in self._servicer.SubscribeEvents(req_dict, context):
|
|
665
|
+
yield self._proto_event(event)
|
|
666
|
+
|
|
667
|
+
async def SpawnAgent(self, request, context):
|
|
668
|
+
req_dict = {
|
|
669
|
+
"prompt": request.prompt,
|
|
670
|
+
"subagent_type": request.subagent_type or "generalPurpose",
|
|
671
|
+
"callback": request.callback,
|
|
672
|
+
"session_id": request.session_id,
|
|
673
|
+
}
|
|
674
|
+
if request.HasField("name"):
|
|
675
|
+
req_dict["name"] = request.name
|
|
676
|
+
if request.HasField("model_profile"):
|
|
677
|
+
req_dict["model_profile"] = request.model_profile
|
|
678
|
+
result = await self._servicer.SpawnAgent(req_dict, context)
|
|
679
|
+
from crabcode_gateway.grpc import crabcode_pb2 as _pb2
|
|
680
|
+
return _pb2.SpawnAgentResponse(**result)
|
|
681
|
+
|
|
682
|
+
async def GetAgent(self, request, context):
|
|
683
|
+
result = await self._servicer.GetAgent({
|
|
684
|
+
"agent_id": request.agent_id,
|
|
685
|
+
"session_id": request.session_id,
|
|
686
|
+
}, context)
|
|
687
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
688
|
+
return crabcode_pb2.AgentSnapshotProto(**result)
|
|
689
|
+
|
|
690
|
+
async def ListAgents(self, request, context):
|
|
691
|
+
result = await self._servicer.ListAgents({"session_id": request.session_id}, context)
|
|
692
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
693
|
+
return crabcode_pb2.ListAgentsResponse(**result)
|
|
694
|
+
|
|
695
|
+
async def CancelAgent(self, request, context):
|
|
696
|
+
result = await self._servicer.CancelAgent({
|
|
697
|
+
"agent_id": request.agent_id,
|
|
698
|
+
"session_id": request.session_id,
|
|
699
|
+
}, context)
|
|
700
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
701
|
+
return crabcode_pb2.CancelAgentResponse(**result)
|
|
702
|
+
|
|
703
|
+
async def WaitAgent(self, request, context):
|
|
704
|
+
req_dict = {
|
|
705
|
+
"agent_ids": list(request.agent_ids),
|
|
706
|
+
"session_id": request.session_id,
|
|
707
|
+
}
|
|
708
|
+
if request.HasField("timeout_ms"):
|
|
709
|
+
req_dict["timeout_ms"] = request.timeout_ms
|
|
710
|
+
result = await self._servicer.WaitAgent(req_dict, context)
|
|
711
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
712
|
+
return crabcode_pb2.AgentSnapshotProto(**result)
|
|
713
|
+
|
|
714
|
+
async def RespondPermission(self, request, context):
|
|
715
|
+
await self._servicer.RespondPermission({
|
|
716
|
+
"tool_use_id": request.tool_use_id,
|
|
717
|
+
"allowed": request.allowed,
|
|
718
|
+
"always_allow": request.always_allow,
|
|
719
|
+
"agent_id": request.agent_id if request.HasField("agent_id") else None,
|
|
720
|
+
"session_id": request.session_id,
|
|
721
|
+
}, context)
|
|
722
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
723
|
+
return crabcode_pb2.Empty()
|
|
724
|
+
|
|
725
|
+
async def RespondChoice(self, request, context):
|
|
726
|
+
await self._servicer.RespondChoice({
|
|
727
|
+
"tool_use_id": request.tool_use_id,
|
|
728
|
+
"selected": list(request.selected),
|
|
729
|
+
"cancelled": request.cancelled,
|
|
730
|
+
"agent_id": request.agent_id if request.HasField("agent_id") else None,
|
|
731
|
+
"session_id": request.session_id,
|
|
732
|
+
}, context)
|
|
733
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
734
|
+
return crabcode_pb2.Empty()
|
|
735
|
+
|
|
736
|
+
async def ListModels(self, request, context):
|
|
737
|
+
result = await self._servicer.ListModels({"session_id": request.session_id}, context)
|
|
738
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
739
|
+
return crabcode_pb2.ListModelsResponse(**result)
|
|
740
|
+
|
|
741
|
+
async def SwitchModel(self, request, context):
|
|
742
|
+
await self._servicer.SwitchModel({
|
|
743
|
+
"name": request.name,
|
|
744
|
+
"session_id": request.session_id,
|
|
745
|
+
}, context)
|
|
746
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
747
|
+
return crabcode_pb2.Empty()
|
|
748
|
+
|
|
749
|
+
async def SwitchMode(self, request, context):
|
|
750
|
+
await self._servicer.SwitchMode({
|
|
751
|
+
"mode": request.mode,
|
|
752
|
+
"session_id": request.session_id,
|
|
753
|
+
}, context)
|
|
754
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
755
|
+
return crabcode_pb2.Empty()
|
|
756
|
+
|
|
757
|
+
async def HealthCheck(self, request, context):
|
|
758
|
+
result = await self._servicer.HealthCheck({}, context)
|
|
759
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
760
|
+
return crabcode_pb2.HealthCheckResponse(**result)
|
|
761
|
+
|
|
762
|
+
@staticmethod
|
|
763
|
+
def _proto_event(event_dict: dict):
|
|
764
|
+
from crabcode_gateway.grpc import crabcode_pb2
|
|
765
|
+
return crabcode_pb2.CoreEventProto(**event_dict)
|