effect-claude-agent-sdk
v0.4.0
Published
Effect-native bindings for the Claude Agent SDK - build type-safe AI agents with Effect
Maintainers
Readme
effect-claude-agent-sdk
Effect-native bindings for the Claude Agent SDK. Build type-safe AI agents with Effect's powerful service composition, streaming, and error handling.
Features
- Effect Services -
AgentSdkandAgentRuntimeas composable Effect services - Type-Safe Tools - Define MCP tools with Effect Schema, automatic Zod conversion
- Stream-Based I/O - Query results as Effect Streams with backpressure
- Lifecycle Hooks - Effect-based hook handlers for SDK events
- Query Supervision - Concurrency limits, timeouts, and automatic cleanup
- Layered Configuration - Environment-aware config with sensible defaults
Installation
bun add effect-claude-agent-sdk effect @effect/platform @effect/platform-bunRequirements
- Bun 1.0+ (peer dependency)
ANTHROPIC_API_KEYenvironment variable
Quick Start
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"
import { AgentSdk } from "effect-claude-agent-sdk"
const program = Effect.scoped(
Effect.gen(function* () {
const sdk = yield* AgentSdk
const handle = yield* sdk.query("What is 2 + 2?")
yield* handle.stream.pipe(
Stream.tap((message) => Console.log(message)),
Stream.runDrain
)
}).pipe(Effect.provide(AgentSdk.layerDefault))
)
Effect.runPromise(program)Core Concepts
AgentSdk
The low-level service wrapping the Claude Agent SDK. Use this when you need direct control over queries.
import { AgentSdk, AgentSdkConfig } from "effect-claude-agent-sdk"
// Default layer (uses environment variables)
AgentSdk.layerDefault
// Custom configuration
AgentSdk.layer.pipe(
Layer.provide(
AgentSdkConfig.layerFromEnv("MY_PREFIX") // reads MY_PREFIX_API_KEY, etc.
)
)AgentRuntime
A higher-level service that adds supervision, retries, and timeouts on top of AgentSdk.
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"
import { AgentRuntime } from "effect-claude-agent-sdk"
const program = Effect.gen(function* () {
const runtime = yield* AgentRuntime
// Get stats about active queries
const stats = yield* runtime.stats
console.log(`Active queries: ${stats.active}`)
// Stream responses directly
yield* runtime
.stream("Explain quantum computing")
.pipe(Stream.runForEach((msg) => Effect.log(msg)))
// Interrupt all active queries
yield* runtime.interruptAll
}).pipe(Effect.provide(AgentRuntime.layerDefault))QueryHandle
Both services return a QueryHandle for controlling the query:
interface QueryHandle {
readonly stream: Stream.Stream<SDKMessage, AgentSdkError>
readonly send: (message: SDKUserMessage) => Effect.Effect<void, AgentSdkError>
readonly sendAll: (messages: Iterable<SDKUserMessage>) => Effect.Effect<void, AgentSdkError>
readonly sendForked: (message: SDKUserMessage) => Effect.Effect<void, AgentSdkError, Scope.Scope>
readonly closeInput: Effect.Effect<void, AgentSdkError>
readonly interrupt: Effect.Effect<void, AgentSdkError>
}Defining Tools
Define type-safe MCP tools using Effect Schema:
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import { Tools } from "effect-claude-agent-sdk"
// Define a tool with typed parameters and return value
const Calculator = Tools.Tool.make("calculator", {
description: "Perform arithmetic operations",
parameters: {
operation: Schema.Literal("add", "subtract", "multiply", "divide"),
a: Schema.Number,
b: Schema.Number
},
success: Schema.Number,
failure: Schema.String
})
// Create a toolkit from multiple tools
const toolkit = Tools.Toolkit.make(Calculator)
// Implement handlers
const handlers = {
calculator: ({ operation, a, b }) =>
Effect.gen(function* () {
switch (operation) {
case "add":
return a + b
case "subtract":
return a - b
case "multiply":
return a * b
case "divide":
if (b === 0) return yield* Effect.fail("Division by zero")
return a / b
}
})
}Tool Annotations
Add metadata to tools for better documentation:
const ReadFile = Tools.Tool.make("read_file", {
description: "Read contents of a file",
parameters: { path: Schema.String },
success: Schema.String
})
.annotate(Tools.Tool.Readonly, true)
.annotate(Tools.Tool.OpenWorld, true)MCP Servers
Create in-process MCP servers with your tools:
import * as Effect from "effect/Effect"
import { AgentSdk, Mcp, Tools } from "effect-claude-agent-sdk"
const program = Effect.scoped(
Effect.gen(function* () {
const sdk = yield* AgentSdk
const toolkit = Tools.Toolkit.make(Calculator)
const tools = yield* Mcp.toolsFromToolkit(toolkit, handlers)
const server = yield* sdk.createSdkMcpServer({
name: "my-tools",
version: "1.0.0",
tools
})
const handle = yield* sdk.query("Calculate 15 * 7", {
tools: ["calculator"],
mcpServers: { "my-tools": server }
})
// ...
})
)Hooks
Handle SDK lifecycle events with Effect:
import * as Effect from "effect/Effect"
import { Hooks } from "effect-claude-agent-sdk"
const myHook = Hooks.Hook.callback((input, context) =>
Effect.gen(function* () {
if (input.type === "PreToolUse") {
yield* Effect.log(`Tool ${input.tool_name} about to be called`)
}
return {} // Hook output
})
)
// Create a matcher for specific events
const matcher = Hooks.Hook.matcher({
matcher: "PreToolUse",
timeout: "30 seconds",
hooks: [yield* myHook]
})Logging & Observability
Match-based logging helpers with Effect-native loggers:
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"
import { AgentRuntime, Logging } from "effect-claude-agent-sdk"
const program = Effect.gen(function* () {
const runtime = yield* AgentRuntime
yield* runtime
.stream("Explain quantum computing")
.pipe(Logging.tapSdkLogs, Stream.runDrain)
yield* Logging.logQueryEventStream(runtime.events)
}).pipe(
Effect.provide(AgentRuntime.layerDefault),
Effect.provide(Logging.layerDefault)
)Configuration
Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| ANTHROPIC_API_KEY | API key for Claude | Required |
| AGENTSDK_MODEL | Model to use | claude-sonnet-4-20250514 |
| AGENTSDK_MAX_TURNS | Maximum conversation turns | 100 |
| AGENTSDK_SYSTEM_PROMPT | System prompt | None |
| AGENTSDK_LOG_FORMAT | Logger format (pretty, structured, json, logfmt, string) | pretty |
| AGENTSDK_LOG_LEVEL | Minimum log level (trace, debug, info, warn, error, fatal, none) | info |
| AGENTSDK_LOG_SPANS | Include span annotations in logs | false |
| AGENTSDK_LOG_MESSAGES | Enable SDK message logging | true |
| AGENTSDK_LOG_QUERY_EVENTS | Enable query event logging | true |
| AGENTSDK_LOG_HOOKS | Enable hook input logging | true |
Runtime Configuration
import { AgentRuntimeConfig } from "effect-claude-agent-sdk"
const config = AgentRuntimeConfig.layer({
queryTimeout: "5 minutes",
firstMessageTimeout: "30 seconds",
retryMaxRetries: 3,
retryBaseDelay: "1 second",
maxConcurrentQueries: 10
})Experimental Features
Rate-Limited Tool Handlers
import { Experimental, Tools } from "effect-claude-agent-sdk"
const handlers = {
echo: ({ text }) => Effect.succeed(text)
}
const limited = Experimental.RateLimiter.rateLimitHandlers(
handlers,
{ limit: 10, window: "1 minute" },
{ keyPrefix: "tools" }
)
// Provide the rate limiter layer
Effect.provide(Experimental.RateLimiter.layerMemory)Persisted Input Queue
import { Experimental, Schema } from "effect-claude-agent-sdk"
const program = Effect.gen(function* () {
const queue = yield* Experimental.PersistedQueue.makeUserMessageQueue()
const adapter = yield* Experimental.PersistedQueue.makeInputAdapter(queue)
const message: Schema.SDKUserMessage = {
type: "user",
session_id: "",
message: { role: "user", content: [{ type: "text", text: "hello" }] },
parent_tool_use_id: null
}
yield* adapter.send(message)
}).pipe(Effect.provide(Experimental.PersistedQueue.layerMemory))Audit Event Log
import { Experimental } from "effect-claude-agent-sdk"
const program = Effect.scoped(
Effect.gen(function* () {
const log = yield* Experimental.EventLog.EventLog
yield* log.write({
schema: Experimental.EventLog.AuditEventSchema,
event: "hook_event",
payload: {
sessionId: "session-1",
hook: "SessionStart",
outcome: "success"
}
})
}).pipe(
Effect.provide([
Experimental.EventLog.layerMemory,
Experimental.EventLog.layerAuditHandlers
])
)
)Sessions (v2)
import { SessionManager, SessionService } from "effect-claude-agent-sdk"
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"
const program = Effect.scoped(
Effect.gen(function* () {
const session = yield* SessionService
yield* session.send("hello")
return yield* Stream.runCollect(session.stream)
}).pipe(
Effect.provide(SessionService.layerDefault({
model: "claude-sonnet-4-5-20250929"
}))
)
)Session configuration (via SessionManager.layerDefaultFromEnv) supports:
EXECUTABLE, PATH_TO_CLAUDE_CODE_EXECUTABLE, EXECUTABLE_ARGS,
PERMISSION_MODE, ALLOWED_TOOLS, DISALLOWED_TOOLS,
ANTHROPIC_API_KEY/API_KEY, CLAUDE_CODE_SESSION_ACCESS_TOKEN.
Guidance:
- Use
SessionServiceorSessionManagerfor normal app usage (applies SessionConfig defaults and validates requiredmodel). - Use the low-level
Sessionmodule when you want to manage every option explicitly; it does not read SessionConfig defaults.
API Reference
Services
| Service | Description |
|---------|-------------|
| AgentSdk | Low-level SDK wrapper with query() and createSdkMcpServer() |
| AgentRuntime | High-level runtime with supervision, retries, timeouts |
| QuerySupervisor | Manages concurrent queries and cleanup |
| SessionManager | Session factory that applies SessionConfig defaults |
| SessionService | Scoped Session wrapper for single-session usage |
Modules
| Module | Description |
|--------|-------------|
| Schema | Effect Schema definitions for SDK types |
| Tools | Tool and Toolkit definitions |
| Hooks | Hook handlers and matchers |
| Logging | Logging config, matchers, and stream helpers |
| Mcp | MCP server creation utilities |
| Storage | Chat history, artifacts, and audit log persistence |
| Experimental | Rate limiting, persisted queues, event logging |
Layers
| Layer | Description |
|-------|-------------|
| AgentSdk.layerDefault | Default SDK configuration |
| AgentSdk.layerDefaultFromEnv(prefix) | SDK from environment with custom prefix |
| AgentRuntime.layerDefault | Default runtime with supervision |
| AgentRuntime.layerDefaultFromEnv(prefix) | Runtime from environment |
| SessionManager.layerDefault | Session manager with default SessionConfig |
| SessionManager.layerDefaultFromEnv(prefix) | Session manager from environment |
| SessionService.layerDefault(options) | Scoped session service with defaults |
| SessionService.layerDefaultFromEnv(options, prefix) | Scoped session service from environment |
Examples
See the examples/ directory:
agent-sdk-mcp-rate-limit.ts- Rate-limited MCP toolsagent-sdk-audit-log.ts- Event logging integrationagent-sdk-persisted-input.ts- Persisted message queueagent-sdk-metadata-cache.ts- Result cachingagent-service-http-server.ts/agent-service-http-client.ts- HTTP APIagent-service-rpc-server.ts/agent-service-rpc-client.ts- RPC API
License
MIT
