diff --git a/src/beaver_gateway/agents/claude.py b/src/beaver_gateway/agents/claude.py index b7c6c48..0853297 100644 --- a/src/beaver_gateway/agents/claude.py +++ b/src/beaver_gateway/agents/claude.py @@ -111,6 +111,25 @@ class ClaudeCodeOptions(BaseModel): would take claude out of print mode. Everything else — model, prompts, tools, MCPs, history seeding — behaves identically.""" + warmup_turn: bool | None = None + """Spend a throwaway turn when a session spawns, so MCP tools are + live on the agent's first reply. + + ``None`` (the default) decides per agent: on when it exposes any + MCP, off when it doesn't. Set it explicitly to override. + + It exists because the CLI begins connecting MCP servers when a turn + starts and doesn't wait for them — measured on the pi, turn one saw + 29 tools with both servers ``pending``, turn two saw 90 with both + ``connected``. For an agent whose whole job is those tools, a first + reply without them is worse than a couple of extra seconds. Nothing + cheaper works: sleeping longer changes nothing, and neither a + ``/status`` command nor the SDK's ``initialize`` handshake touches + the MCP client. + + ``stream_json`` only — the PTY transport's readiness wait covers + this incidentally.""" + include_partial_messages: bool = False """Stream token-level deltas to the client instead of one delta per finished block. Requires ``transport="stream_json"``; inert on the diff --git a/src/beaver_gateway/backends/claude_code.py b/src/beaver_gateway/backends/claude_code.py index 54b9494..25d2921 100644 --- a/src/beaver_gateway/backends/claude_code.py +++ b/src/beaver_gateway/backends/claude_code.py @@ -197,6 +197,12 @@ def _build_backend_options( mcp_servers=_build_mcp_servers(agent, mcp_internal_urls), transport=opt.transport, include_partial_messages=opt.include_partial_messages, + # ``None`` means "decide from the agent": an agent that exposes + # MCPs needs them on its first reply, one that doesn't shouldn't + # pay for a warm-up it gains nothing from. + warmup_turn=( + bool(agent.expose_mcps) if opt.warmup_turn is None else opt.warmup_turn + ), disallowed_tools=opt.disallowed_tools, permission_mode=opt.permission_mode, dangerously_skip_permissions=opt.dangerously_skip_permissions, diff --git a/uv.lock b/uv.lock index fdf8d02..abc1ca4 100644 --- a/uv.lock +++ b/uv.lock @@ -287,7 +287,7 @@ local = [ { name = "raycast-api", version = "0.1.0", source = { editable = "../raycast-api" } }, ] prod = [ - { name = "claude-code-api", version = "0.1.0", source = { git = "https://git.kotikot.com/beaver/claude-code-api.git#b50911c3edb62e30597b6b96739b888c8be853fe" } }, + { name = "claude-code-api", version = "0.1.0", source = { git = "https://git.kotikot.com/beaver/claude-code-api.git#621da90623279949fbe47b80bfa57a7c8c6bf74b" } }, { name = "raycast-api", version = "0.1.0", source = { git = "https://git.kotikot.com/beaver/raycast-api.git#e73894c8e435da5c0709f92f69f11bcd0dab9afe" } }, ] @@ -419,7 +419,7 @@ wheels = [ [[package]] name = "claude-code-api" version = "0.1.0" -source = { git = "https://git.kotikot.com/beaver/claude-code-api.git#b50911c3edb62e30597b6b96739b888c8be853fe" } +source = { git = "https://git.kotikot.com/beaver/claude-code-api.git#621da90623279949fbe47b80bfa57a7c8c6bf74b" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version < '3.14'",