Skip to content

feat: claude_agent_sdk runtime — Claude via subscription OAuth (Agent SDK) - #1

Open
snagnever wants to merge 23 commits into
mainfrom
feat/claude-agent-sdk-runtime
Open

feat: claude_agent_sdk runtime — Claude via subscription OAuth (Agent SDK)#1
snagnever wants to merge 23 commits into
mainfrom
feat/claude-agent-sdk-runtime

Conversation

@snagnever

Copy link
Copy Markdown
Owner

Summary

Adds a claude-agent provider + claude_agent_sdk runtime so Claude models run through the Claude Agent SDK, authenticated by a Claude subscription (Claude Code OAuth) instead of a pay-per-token ANTHROPIC_API_KEY. Mirrors the existing codex_app_server runtime 1:1. Implements the intent of NousResearch#25267.

Claude Code owns the tool loop (terminal/file-ops run in its runtime); Hermes' own tool surface is injected via the existing hermes_tools_mcp_server stdio MCP; events/usage project back into Hermes' accounting. Opt-in and additive — default paths are unchanged.

How to use

# config.yaml — as your default
model:
  default: claude-opus-4-8
  provider: claude-agent

…or switch to it at runtime: /model opus --provider claude-agent (shows as "Claude (subscription)" in the picker). Requires npm i -g @anthropic-ai/claude-code (logged in) and pip install hermes-agent[claude-agent].

Architecture

  • Provider profile (plugins/model-providers/claude-agent/) — registers claude-agent (aliases claude-sdk, claude-subscription), api_mode=claude_agent_sdk, auth_type=oauth_external, no REST catalog.
  • ClaudeAgentSession (agent/transports/claude_agent_session.py) — drives the async ClaudeSDKClient from Hermes' sync loop via one event loop on a dedicated thread. Lifecycle, run_turn (text + tool-use projection + usage), can_use_tool → Hermes approval bridge, interrupt. ANTHROPIC_API_KEY is stripped from the child env so subscription auth is never silently bypassed.
  • run_claude_agent_sdk_turn (agent/claude_runtime.py) — turn orchestrator, projects messages + usage exactly like codex_runtime.py.
  • Dispatchrun_conversation short-circuits to the SDK runtime when api_mode == "claude_agent_sdk"; agent_init sets up the keyless no-HTTP-client path.
  • Picker/switch — bridged into hermes_cli (HermesOverlay + claude_agent_sdk transport, credential resolver short-circuit, keyless validation). Selecting the provider always routes through the SDK.
  • Auxiliary tasks — title generation / compression run through the SDK on Haiku when no HTTP aux provider is configured but the claude CLI is present (subscription-only setups get them for free).

Auth hygiene / ToS

Uses the Agent SDK + Claude Code subscription login — the sanctioned subscription path. ANTHROPIC_API_KEY is deliberately blanked in the SDK subprocess so a stray key can't flip billing to pay-per-token.

Testing

  • ~230 unit tests (fake claude_agent_sdk fixture — no package/binary/network needed), plus codex-runtime regression suites green.
  • Verified against the real claude-agent-sdk 0.2.110 + claude CLI 2.1.193: ClaudeAgentOptions fields, message/permission classes, ResultMessage.usage shape.
  • Live E2E on a real subscription: plain turn, multi-turn context, Bash tool execution, hermes-tools MCP injection, usage accounting, interactive /model switch + turn in hermes chat, and aux title generation on Haiku (~6.8s).

Notes / limitations

  • No temperature/top_p (SDK doesn't expose sampling params). delegate_task/memory/todo agent-loop tools unavailable (same as codex_app_server).
  • hermes-tools MCP has a ~15-20s cold-start (async load); Claude Code's own tools are immediate.
  • Endpoint-less provider: base_url must be null for claude-agent — the switch now clears it so no probe path (context-length, vision) hangs on a stale URL.

Commits

  • chore: start claude-agent-sdk runtime branch (refs [Feature]: Claude Agent SDK model provider with subscription OAuth (Codex-style) NousResearch/hermes-agent#25267)
  • feat(providers): claude-agent profile plugin (Agent SDK, subscription OAuth)
  • feat(runtime): claude_agent_sdk api_mode + anthropic_runtime config gate
  • test: fake claude_agent_sdk fixture for session tests
  • feat(transports): ClaudeAgentSession lifecycle (sync bridge over Agent SDK)
  • feat(transports): ClaudeAgentSession.run_turn with tool projection + usage
  • feat(transports): bridge Agent SDK can_use_tool to Hermes approval flow
  • feat(transports): interrupt support for claude_agent_sdk sessions
  • feat(runtime): run_claude_agent_sdk_turn orchestrator + usage accounting
  • feat(agent): dispatch claude_agent_sdk turns from run_conversation
  • feat(doctor,docs): claude CLI check + claude-agent provider docs
  • build: register claude-agent-sdk as optional extra + lazy dep
  • feat(providers): claude-agent as a first-class /model picker option
  • fix(providers): resolve claude-agent as keyless SDK runtime on /model switch
  • fix(transports): surface SDK result text on is_error turns
  • docs(claude-agent): note hermes-tools MCP cold-start behavior
  • fix(models): skip /models probe when validating claude-agent on switch
  • fix(cli): allow empty base_url for the claude_agent_sdk runtime
  • fix(claude-agent): eliminate startup/switch freeze from context-length probe
  • feat(picker): list claude-agent in /model when the claude CLI is installed
  • docs(claude-agent): document auxiliary-task limitation + workaround
  • feat(aux): route auxiliary tasks through Claude Agent SDK on Haiku
  • fix(image-routing): don't probe base_url for claude-agent at startup

🤖 Generated with Claude Code

snagnever added 23 commits July 4, 2026 09:49
Verified the real Hermes approval-callback signature
(command, description, allow_permanent=False) -> once|session|always|deny
against tools/terminal_tool.py + codex_app_server_session.py, correcting
the plan's assumed (tool_name, preview, args) shape.
Also registers claude_agent_sdk in agent_init's explicit-api_mode set so a
passed api_mode='claude_agent_sdk' is honored instead of falling through to
anthropic_messages (parity with codex_app_server).
Adds [claude-agent] optional-dependency (claude-agent-sdk==0.2.110, the
version the runtime is verified against) and a provider.claude_agent
LAZY_DEPS entry, mirroring how provider.anthropic is handled. Keeps the
runtime a lazy import; the extra just pins the floor and lets hermes update
refresh it.
Bridges claude-agent into the picker registry (HermesOverlay + a
claude_agent_sdk transport → api_mode, 'Claude (subscription)' label,
claude-sdk/claude-subscription aliases) so the Claude-subscription models
are selectable via /model instead of only as a config default. Selecting
the provider always routes through the SDK (provider is self-describing),
independent of the anthropic_runtime flag, which stays as the make-it-default
reroute for the plain anthropic provider. agent_init sets up the keyless
no-client path for the claude_agent_sdk api_mode.
… switch

resolve_runtime_provider short-circuits claude-agent (and aliases) to a
no-credentials runtime dict (api_mode=claude_agent_sdk, no base_url) before
the credential pool, which previously rejected it as 'Unknown provider'.
The Claude Code subprocess owns auth, so Hermes needs no in-process key.
Real E2E showed the SDK reports auth failures (not-logged-in) as
is_error=True with subtype='success' and the message in result — so the
error string was the useless 'claude-agent-sdk: success'. Now append the
result text (e.g. 'Not logged in · Please run /login').
The claude-agent runtime has no REST /models endpoint (the SDK validates
the model at turn time), so the switch validator's live probe always failed
and printed a misleading 'could not reach the claude-agent API ... may not
be valid' warning (and cost a slow timeout). Early-accept claude-agent (and
its aliases) with no probe — instant switch, no false alarm.
The per-turn credential setup rejected an empty base URL, but the
claude_agent_sdk runtime has no HTTP endpoint by design (the Claude Code
subprocess owns the connection). Exempt api_mode=claude_agent_sdk from the
base-URL guard; every other runtime still requires one. run_conversation
short-circuits to the SDK runtime before any client/base_url machinery, so
nothing downstream needs it.
…h probe

Root cause of the ~10-45s freeze on 'hermes chat' open and /model: the
claude-agent runtime has no HTTP endpoint, but context-length detection
probed the resolved base_url — which, after switching from another provider,
was a stale leftover URL (e.g. an LM Studio LAN address). The probe hung on
that host's TCP connect timeout (measured 44s).

- model_metadata.get_model_context_length: early-return Claude's 200K window
  for claude-agent (+aliases) before any probe.
- model_switch: force base_url='' + api_mode=claude_agent_sdk when switching
  to claude-agent so it never inherits the previous provider's endpoint.
…alled

The /model picker only surfaces providers with detectable credentials, but
claude-agent's auth lives in the Claude Code CLI (subscription login /
Keychain), not an env var or Hermes auth store — so it was silently dropped.
Detect availability via check_claude_binary and populate its model list from
the provider profile's fallback_models. Hidden when the CLI isn't installed
so it doesn't clutter the picker for users who haven't set it up.
Auxiliary helpers (title generation, auto-compression, vision, memory) need
a separate API-key-backed provider; the subscription runtime has no HTTP
client for them, so they're skipped non-fatally. Document configuring an
auxiliary provider to enable them.
Auxiliary helpers (title generation, compression, memory) have no HTTP
client on the subscription runtime. Add a one-shot SDK path in call_llm:
route when the aux task resolves to claude-agent (config/inherited), and as
a last resort when no HTTP provider is configured but the claude CLI is
present. Runs a stateless ClaudeAgentSession on Haiku (no hermes tools, no
MCP boot) and returns an OpenAI-ChatCompletion-shaped response so callers
are unchanged. Verified live: title generated in ~6.8s. Streaming/tool aux
tasks fall through to the normal HTTP path.
Startup hung ~33s in show_banner: the vision-capability check probes the
model base_url to detect a local Ollama server, and a stale leftover URL
(a previous provider's LM Studio LAN address, still in config because
claude-agent has no endpoint of its own) hung on the TCP connect timeout.
_resolve_inference_base_url now returns '' for claude-agent and
_should_probe_ollama_vision skips it — no probe path sees a base_url for the
endpoint-less runtime. (Same disease as the earlier context-length freeze.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant