Skip to content

RockBot.Llm.ClaudeAgent: Node sidecar for MAX-subscription agentic LLM access #408

Description

@rockfordlhotka

Build a RockBot.Llm.ClaudeAgent provider that routes LLM calls through the Claude Agent SDK via a Node sidecar, so RockBot can draw from the MAX subscription's $100/mo Agent SDK credit instead of paying Azure Foundry rates for every call. Mirrors the shape of RockBot.Llm.Copilot and plugs into the existing per-tier configuration.

Why

Anthropic introduced separate "Agent SDK credit" pools for MAX subscriptions (announced 2026-05-14, effective 2026-06-15). MAX 5x includes $100/mo of agentic credit that exists whether used or not and expires monthly. Rockbot's primary LLM spend is ~$170/mo on Azure Foundry (gpt-5.5), so capturing even partial Agent SDK credit is essentially free money once the wrapper exists.

There's no first-party .NET Claude Agent SDK — only TypeScript and Python. A Node sidecar process owns the SDK; .NET talks to it via stdio JSON-lines, the same isolation model rockbot already uses for MCP server subprocesses.

Architecture

+--------------------------+      stdio JSON-lines       +--------------------------+
| .NET agent process       | <-------------------------> | Node sidecar process     |
|                          |                             |                          |
| ClaudeAgentChatClient    | ---> request envelope --->  | index.ts (stdio loop)    |
|   : IChatClient          |                             |    |                     |
|                          | <--- event stream    <----  |    v                     |
| ClaudeAgentSidecarHost   |                             | handler.ts               |
|   (subprocess lifecycle) |                             |    |                     |
|                          |                             |    v                     |
| Per-tier config selects  |                             | @anthropic-ai/           |
| ClaudeAgent for a tier   |                             |   claude-agent-sdk       |
|                          |                             |   query(...)             |
+--------------------------+                             +--------------------------+
        ^                                                          |
        |       tool_invoke / tool_result events                   |
        +----------------------------------------------------------+
            (in-Node MCP server proxies SDK tool calls back to
             .NET, where McpBridgeService executes them)

Projects added

  • src/RockBot.Llm.ClaudeAgent/ — .NET adapter, near-twin of RockBot.Llm.Copilot.
  • src/RockBot.Llm.ClaudeAgent.Sidecar/ — Node TypeScript project owning @anthropic-ai/claude-agent-sdk.

Lifecycle

  • ClaudeAgentSidecarHost is a singleton; spawns node dist/index.js lazily on first use.
  • Heartbeat ping/pong every 30s. If the sidecar dies, host restarts with exponential backoff (mirrors the MCP reconnect sweep pattern in McpBridgeService).
  • Graceful shutdown: host closes stdin, sidecar drains in-flight queries, exits.

Auth

  • Local dev: Agent SDK reads Claude Code credentials from ~/.claude/... automatically — works out of the box.
  • Cluster deployment: mount Claude Code credentials as a Kubernetes Secret into the agent pod at /home/agent/.claude/. Sidecar process inherits. Open question — needs validation that copied credentials work for headless SDK use.

IPC Protocol

Newline-delimited JSON over stdio. Each line is one object. Multiple concurrent queries multiplex over the channel via id.

.NET to Node request

{
  "id": "01HV0EX3MJ9YT4...",
  "type": "query",
  "model": "claude-sonnet-4-6",
  "systemPrompt": "...",
  "messages": [
    { "role": "user", "content": "..." },
    { "role": "assistant", "content": "...", "toolCalls": [] },
    { "role": "tool", "toolCallId": "...", "content": "..." }
  ],
  "tools": [
    { "name": "memory_search", "description": "...", "inputSchema": {} }
  ],
  "maxTurns": 50,
  "permissionMode": "bypassPermissions"
}

Node to .NET event stream (terminated by done or error)

{ "id": "01HV...", "type": "text",        "delta": "..." }
{ "id": "01HV...", "type": "tool_use",    "toolUseId": "...", "name": "...", "input": {} }
{ "id": "01HV...", "type": "tool_invoke", "toolUseId": "...", "name": "...", "input": {} }
{ "id": "01HV...", "type": "tool_result", "toolUseId": "...", "content": "..." }
{ "id": "01HV...", "type": "usage",       "inputTokens": 1234, "outputTokens": 567, "model": "..." }
{ "id": "01HV...", "type": "done",        "stopReason": "end_turn" }
{ "id": "01HV...", "type": "error",       "code": "rate_limit", "message": "..." }

Out-of-band control (no id)

{ "type": "ping" }
{ "type": "pong", "version": "..." }
{ "type": "log",  "level": "info", "message": "..." }

Tool execution path. The SDK owns the tool-calling loop (same compromise we accepted for CopilotChatClient). Tools are exposed to the SDK via an in-Node MCP server that proxies tool calls back over stdio: SDK requests a tool, sidecar emits tool_invoke, .NET executes via existing IToolRegistry / McpBridgeService, .NET replies with tool_result, sidecar feeds the result back to the SDK. Tool execution stays entirely in .NET territory; only the loop boundary lives in Node.

Phases (one PR each)

  • Phase 1 — Node sidecar skeleton. Project scaffold, index.ts stdio loop, ping/pong, structured logging. No SDK integration yet — just prove the IPC channel works end-to-end with a Hello World request/response.
  • Phase 2 — .NET project + sidecar host. RockBot.Llm.ClaudeAgent csproj, ClaudeAgentSidecarHost subprocess lifecycle, JSON-lines protocol reader/writer, ClaudeAgentChatClient : IChatClient calling the sidecar for a no-tools text query. Mirror CopilotChatClient structure.
  • Phase 3 — Agent SDK wiring in the sidecar. Real query() call, text/usage/done events streaming back. Validate end-to-end against a logged-in claude CLI session locally.
  • Phase 4 — Tool path. In-Node MCP server bridging tool calls to .NET; tool_invoke/tool_result handling on both sides; wire through existing McpBridgeService for execution.
  • Phase 5 — Per-tier registration. Add ClaudeAgent as a tier option in the existing tier config (reuse the Copilot integration's pattern). Route one isolable use case through it — candidates: a subagent type, LlmObservationEvaluator, or SessionSummaryService. Validate behavior parity vs gpt-5.5 before expanding.
  • Phase 6 — Cluster deployment. Node runtime added to Dockerfile.agent, Claude Code credentials mounted as a k8s Secret, sidecar process supervised by the .NET host (no extra pod). Surface Agent SDK credit consumption via the existing usage-tracker pattern.

Risks

  • Loop ownership. SDK runs the tool-calling loop, not AgentLoopRunner. Same compromise as Copilot — reasoning scaffolding, completion evaluation, and iteration budget weaken when wrapping the SDK. Mitigation: phase 5 picks a narrow target; expand only after validating behavior.
  • Model swap. gpt-5.5 → Claude Sonnet/Haiku is a real behavior change. Prompts tuned for gpt-5.5 may not transfer cleanly. Mitigation: isolable phase-5 target with low blast radius.
  • Credit ceiling. $100/mo cap; overflow at API list rates if extra-usage is enabled, otherwise requests block until reset. Mitigation: surface credit consumption telemetry so spillover is observable; configure overflow policy explicitly.
  • Headless auth. Agent SDK auth flow assumes interactive claude login. Cluster pods cannot do that. Mitigation: validate copied credentials work for headless SDK use before phase 6. If they don't, MAX-on-cluster is blocked pending an Anthropic non-interactive auth path; the work still benefits local dev.
  • Node runtime in agent image. Adds Node to a previously .NET-only container. Image size grows; supply chain expands. Mitigation: pin Node version, Alpine base for the sidecar layer, audit @anthropic-ai/claude-agent-sdk transitive deps.

Out of scope

  • Replacing AgentLoopRunner. This work routes around it for SDK-wrapped tiers, same as Copilot.
  • Native function calling with .NET-side loop ownership. Would require reviving the dormant text-based tool-calling path or shipping a .NET Agent SDK; neither is in scope here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions