Skip to Content
ProvidersAnthropic Claude

Anthropic Claude

Category: AI / LLM
Adapter: ClaudeAdapter in packages/agent-core/src/adapters/claude.ts
External: Claude Code CLI subprocess (claude binary)


Purpose

Claude is the primary AI backbone of Leadmetrics. All content agents (Blog Writer, Content Brief Writer, Keyword Researcher, etc.) run on Claude by default. Claude is invoked as a subprocess via the Claude Code CLI rather than the REST API directly — this gives access to Claude’s tool use, session resumption, and the --add-dir skill injection mechanism.

Why CLI subprocess, not REST API

FeatureCLI subprocessREST API
Tool use (web_search, web_fetch, rag_search)Native — Claude handles tool routingManual tool loop required
Session resumption--resume <session-id> flagManual history management
Skill injection (--add-dir)Built-inNot available
NDJSON streamingNativeSSE streaming (different format)
Token countingIncluded in streamSeparate API call or estimate

Config Structure

Platform config (env vars)

ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx CLAUDE_MODEL=claude-sonnet-4-6 # Default model for most agents CLAUDE_EXPENSIVE_MODEL=claude-opus-4-6 # For Strategy Writer, Report Writer CLAUDE_CHEAP_MODEL=claude-haiku-4-5-20251001 # For Topic Researcher, Research Note Writer CLAUDE_CLI_PATH=/usr/local/bin/claude # Path to the claude binary

Per-agent config (stored in agent_configs table)

interface ClaudeAgentConfig { model?: string; // Override per agent, e.g. "claude-opus-4-6" maxTokens?: number; // Default: 8192 temperature?: number; // Not supported in Claude Code CLI — omit systemPrompt: string; // Full system prompt for the agent tools?: string[]; // Allowed tools: ["web_search", "web_fetch", "rag_search"] }

Integration Pattern

CLI invocation

The agent execution engine spawns a child process for each activity:

const args = [ '--output-format', 'stream-json', '--model', model, '--max-tokens', String(maxTokens), '--system', systemPrompt, '--resume', sessionId ?? 'new', '--add-dir', skillsDir, // Path to mounted skill files '--print', // Non-interactive mode prompt, // The assembled user prompt ]; const proc = spawn('claude', args, { env: { ...process.env, ANTHROPIC_API_KEY: config.ANTHROPIC_API_KEY }, stdio: ['ignore', 'pipe', 'pipe'], });

NDJSON stream parsing

Each line from stdout is a JSON object. The adapter processes them as they arrive:

for await (const line of readLines(proc.stdout)) { const event = JSON.parse(line); switch (event.type) { case 'assistant': // Content block — text or tool_use yield { type: 'content', text: event.message.content }; break; case 'tool_result': // Tool call result (web_search, rag_search, etc.) yield { type: 'tool_result', name: event.tool_name, result: event.result }; break; case 'result': // Final result — total tokens used totalInputTokens = event.usage.input_tokens; totalOutputTokens = event.usage.output_tokens; break; case 'system': // Session ID — save for resumption if (event.session_id) sessionId = event.session_id; break; } }

Session resumption

Claude Code CLI maintains conversation history via session IDs. On the first run, --resume new (or omit --resume) creates a new session. The session ID is extracted from the system event and stored on the activities row. Subsequent activities in the same pipeline (e.g. ACT-2604-001 → ACT-2604-002) resume the same session to preserve context.

// After first activity completes await db.update(activities) .set({ claudeSessionId: sessionId }) .where(eq(activities.id, activityId)); // Next activity in pipeline const previousSessionId = await db.select({ claudeSessionId: activities.claudeSessionId }) .from(activities) .where(eq(activities.id, previousActivityId)); const args = ['--resume', previousSessionId ?? 'new', ...];

Skill injection (--add-dir)

The --add-dir flag mounts a directory of Markdown files as “skills” — Claude reads them as additional context at the start of each session. Skills are assembled per-tenant from MongoDB and written to a temp directory before the subprocess starts:

const skillsDir = await prepareSkillsDir(tenantId, agentType); // Writes: /tmp/skills-{activityId}/client-context.md // /tmp/skills-{activityId}/activity-planning-sop.md // /tmp/skills-{activityId}/deliverable-types.md

Cost Calculation

Token costs are calculated from the result event at the end of each stream:

const COST_PER_INPUT_MTok = modelCosts[model].inputPerMillionTokens; // USD const COST_PER_OUTPUT_MTok = modelCosts[model].outputPerMillionTokens; // USD const usdCost = (inputTokens / 1_000_000 * COST_PER_INPUT_MTok) + (outputTokens / 1_000_000 * COST_PER_OUTPUT_MTok); // Stored in llm_calls.cost_usd and used for credit consumption const creditsConsumed = Math.ceil(usdCost / CREDIT_EXCHANGE_RATE);

Model cost reference is stored in packages/agent-core/src/costs.ts and updated as Anthropic publishes new pricing.


Test Cases

Unit tests (packages/agent-core/src/adapters/claude.test.ts)

TestApproach
Spawns subprocess with correct argsMock spawn; assert args array
Parses assistant event and yields contentFeed mock NDJSON stream; assert yielded content blocks
Extracts session_id from system eventAssert sessionId set after stream
Calculates cost from result event tokensAssert usdCost matches expected formula
Handles subprocess exit code != 0Mock proc exits with code 1; assert error thrown
Handles malformed NDJSON lineFeed invalid JSON line; assert error logged, stream continues
Uses --resume flag when sessionId providedAssert --resume ${sessionId} in args

Integration tests

TestApproach
Full subprocess with real claude binaryRequires ANTHROPIC_API_KEY in CI; send simple prompt; assert content returned
Session resumption works across two callsRun twice with same --resume ID; assert second response references first
--add-dir skill files are readWrite skill file to temp dir; assert agent references skill content

Cost of CI tests

Integration tests against the real Claude API cost money. Tag them @slow and run only on release branches or nightly, not on every PR.


Model Selection per Agent

AgentModelRationale
Strategy Writerclaude-opus-4-6Highest reasoning — foundational document
Report Writerclaude-opus-4-6Deep analysis across all channel data
Blog Writerclaude-sonnet-4-6Quality + speed balance
Content Brief Writerclaude-sonnet-4-6Research-heavy — Sonnet handles tool use well
Keyword Researcherclaude-sonnet-4-6
Social Post Writerclaude-haiku-4-5-20251001Short output — speed and cost matter
GBP Post Writerclaude-haiku-4-5-20251001
Topic Researcherclaude-haiku-4-5-20251001Runs locally alongside Ollama
Research Note Writerclaude-haiku-4-5-20251001

© 2026 Leadmetrics — Internal use only