Installation

npm install @openharness/adapter-anthropic-agent

Quick Start

import { AnthropicAgentAdapter } from "@openharness/adapter-anthropic-agent";

const adapter = new AnthropicAgentAdapter({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

// Simple execution
const result = await adapter.execute({
  message: "What is 2 + 2?",
});

console.log(result.output);

Capabilities

This adapter implements a subset of the Open Harness API focused on core execution capabilities.

Supported

DomainCapabilityNotes
Executionexecute()Sync execution with agentic loop
ExecutionexecuteStream()Async generator streaming
ExecutionCancelVia AbortSignal
ExecutionExtended ThinkingenableThinking config
ToolsregisterTool()Register custom handlers
ToolslistTools()List registered tools
ToolsinvokeTool()Direct invocation
SessionscreateConversation()In-memory state
SessionssendMessage()With history
SessionssendMessageStream()Streaming with history
ModelsModel switchingPer-request via options

Not Supported

DomainReasonWorkaround
AgentsSDK is statelessExternal state management
SkillsNot in SDK scopeRegister tools directly
MCPRequires separate packageAdd MCP tools manually
MemorySDK is statelessExternal database
SubagentsRequires orchestrationBuild custom orchestrator
FilesNot in SDK scopeRegister file tools
HooksRequires orchestrationWrap adapter methods
PlanningNot in SDK scopeBuild custom layer

Tool Registration

adapter.registerTool({
  name: "get_weather",
  description: "Get the current weather for a location",
  inputSchema: {
    type: "object",
    properties: {
      location: { type: "string", description: "City name" },
    },
    required: ["location"],
  },
  handler: async (input) => {
    const location = input.location as string;
    // Your weather API logic here
    return {
      success: true,
      output: { temp: 72, condition: "sunny", location },
    };
  },
});

// Execute with tools (agentic loop handles tool calls automatically)
const result = await adapter.execute({
  message: "What's the weather in San Francisco?",
});

Streaming

for await (const event of adapter.executeStream({
  message: "Write a haiku about programming",
})) {
  switch (event.type) {
    case "text":
      process.stdout.write(event.content);
      break;
    case "tool_call_start":
      console.log(`\nUsing tool: ${event.name}`);
      break;
    case "tool_result":
      console.log(`Tool result:`, event.output);
      break;
    case "done":
      console.log(`\n\nTokens: ${event.usage.total_tokens}`);
      break;
  }
}

Event Types

EventDescription
textText content chunk
tool_call_startTool invocation beginning
tool_call_deltaPartial tool input (JSON streaming)
tool_call_endTool invocation complete
tool_resultTool execution result
progressIteration progress
errorError occurred
doneExecution complete with usage stats

Conversation Management

In-Memory Only

Conversations are stored in-memory and lost on process restart. For persistence, serialize conversation.messages to your database.

// Create a conversation
const conversation = adapter.createConversation({
  systemPrompt: "You are a helpful coding assistant.",
});

// Send messages in the conversation
const response1 = await adapter.sendMessage(
  conversation.id,
  "What's a good way to handle errors in TypeScript?"
);

const response2 = await adapter.sendMessage(
  conversation.id,
  "Can you show me an example?"
);

// Stream a message in the conversation
for await (const event of adapter.sendMessageStream(
  conversation.id,
  "Now explain async/await"
)) {
  if (event.type === "text") {
    process.stdout.write(event.content);
  }
}

Configuration

interface AnthropicAgentConfig {
  // Anthropic API key (or use ANTHROPIC_API_KEY env var)
  apiKey?: string;

  // Model (default: "claude-sonnet-4-20250514")
  model?: string;

  // Max tokens (default: 4096)
  maxTokens?: number;

  // Base URL for API (useful for proxies)
  baseUrl?: string;

  // Default system prompt
  systemPrompt?: string;

  // Custom Anthropic client
  client?: Anthropic;

  // Enable extended thinking (default: false)
  enableThinking?: boolean;

  // Thinking budget (default: 10000)
  thinkingBudget?: number;
}

Execution Options

Per-request options can override adapter defaults:

const result = await adapter.execute({
  message: "Explain quantum computing",
}, {
  model: "claude-opus-4-20250514",
  maxTokens: 8192,
  temperature: 0.7,
  maxIterations: 5,  // Max tool use iterations (default: 10)
  abortSignal: controller.signal,
});

API Behavior

Agentic Loop

The adapter implements an automatic agentic loop for tool use:

  1. User message is sent to Claude
  2. If Claude requests tool use, the adapter automatically invokes the registered handler
  3. Tool results are sent back to Claude
  4. Loop continues until Claude produces a final text response or maxIterations is reached

This differs from raw SDK usage where you must manually handle tool calls.

Token Usage

// Sync execution
const result = await adapter.execute({ message: "Hello" });
console.log(result.usage);
// { inputTokens: 10, outputTokens: 50, totalTokens: 60, durationMs: 1234 }

// Streaming - usage is in the final "done" event
for await (const event of adapter.executeStream({ message: "Hello" })) {
  if (event.type === "done") {
    console.log(event.usage);
  }
}

Extending the Adapter

Adding MCP Support

import { Client } from "@anthropic-ai/mcp";

// Connect to MCP server
const mcp = new Client();
await mcp.connect("npx -y @anthropic-ai/mcp-server-filesystem /tmp");

// Get tools from MCP server
const mcpTools = await mcp.listTools();

// Register MCP tools with the adapter
for (const tool of mcpTools) {
  adapter.registerTool({
    name: tool.name,
    description: tool.description,
    inputSchema: tool.inputSchema,
    handler: async (input) => {
      const result = await mcp.callTool(tool.name, input);
      return { success: true, output: result };
    },
  });
}

Adding File Operations

import * as fs from "fs/promises";

adapter.registerTool({
  name: "read_file",
  description: "Read a file from the filesystem",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string", description: "File path" },
    },
    required: ["path"],
  },
  handler: async (input) => {
    try {
      const content = await fs.readFile(input.path as string, "utf-8");
      return { success: true, output: { content } };
    } catch (error) {
      return { success: false, error: error.message };
    }
  },
});

Adding Hooks

const originalExecute = adapter.execute.bind(adapter);

adapter.execute = async (request, options) => {
  // Pre-hook
  console.log("Starting execution:", request.message);

  const result = await originalExecute(request, options);

  // Post-hook
  console.log("Completed:", result.usage.totalTokens, "tokens");

  return result;
};