Skip to content

terryso/open-agent-sdk-swift

Repository files navigation

Open Agent SDK (Swift)

Product Hunt

Swift Platform CI Coverage BMAD License: MIT DeepWiki

中文文档

Open-source Agent SDK for Swift — run the full agent loop in-process with native Swift concurrency. Build AI-powered applications with streaming responses, 34 built-in tools, sub-agent orchestration, MCP integration, session persistence, and multi-provider LLM support.

Inspired by open-agent-sdk-typescript — bringing the same agentic architecture to the Swift ecosystem.

Also available in TypeScript: open-agent-sdk-typescript | Go: open-agent-sdk-go

Highlights

  • Full Agent Loop — Prompt, tool execution, and response in a single await call or streaming AsyncStream
  • 34 Built-in Tools — Core file/search/web tools, advanced task/team management, specialist cron/plan/worktree tools
  • Multi-Provider LLM — Anthropic (Claude) and OpenAI-compatible APIs (GLM, Ollama, OpenRouter, etc.)
  • MCP Integration — Connect external tools via stdio, SSE, HTTP, or in-process MCP servers
  • Session Persistence — Save, load, fork, and manage conversation transcripts as JSON
  • Hook System — 20+ lifecycle events with function and shell hook handlers
  • Permission Control — 6 permission modes plus custom authorization callbacks with policy composition
  • Sub-Agent Orchestration — Spawn child agents, manage teams, tasks, and inter-agent messaging
  • Auto-Compaction — Automatically compresses long conversations to stay within context limits
  • Skills System — 5 built-in skills (Commit, Review, Simplify, Debug, Test) with custom skill registration
  • File Cache & Context — LRU file cache, Git status auto-injection, project document discovery (CLAUDE.md/AGENT.md)
  • Runtime Controls — Dynamic model switching, query abort with partial results, session memory
  • Sandbox & Logging — Configurable sandbox for command/path restrictions, structured JSON logging
  • HTTP API Server — Expose any Agent as a REST + SSE service with run tracking, concurrency limits, and auth
  • Cost & Trace — Built-in CostTracker for token/cost budgeting, TraceRecorder for JSONL execution observability
  • Enhanced Memory — Fact-based memory with candidate→active→retired lifecycle, evidence-driven confidence, and import/export
  • Self-Evolution — ExperienceExtractor for memory auto-extraction, SkillEvolver for skill adaptation, ReviewAgent background pipeline, and IntelligentCurator for LLM-driven skill library curation with Markdown/YAML reports
  • Output Formatting — SDKMessageOutputHandler protocol with Terminal and JSON output formatters
  • Runtime Event Layer — 18 typed events via EventBus, SSE bridge for HTTP API, optional token streaming

Quick Start (15 minutes)

Installation

Add the dependency in your Package.swift:

dependencies: [
    .package(url: "https://github.com/terryso/open-agent-sdk-swift.git", from: "0.1.0")
],
targets: [
    .target(name: "YourApp", dependencies: ["OpenAgentSDK"])
]

Or in Xcode: File > Add Package Dependencies and enter the repository URL.

Configuration

Set your API key via environment variable:

export CODEANY_API_KEY=sk-...

Your First Agent

import OpenAgentSDK

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    model: "claude-sonnet-4-6",
    systemPrompt: "You are a helpful assistant.",
    maxTurns: 10,
    permissionMode: .bypassPermissions
))

let result = await agent.prompt("Explain Swift concurrency in one paragraph.")
print(result.text)
print("Used \(result.usage.inputTokens) input + \(result.usage.outputTokens) output tokens")

Streaming Response

// Using the agent created above:
for await message in agent.stream("Read Package.swift and summarize it.") {
    switch message {
    case .partialMessage(let data):
        print(data.text, terminator: "")
    case .toolUse(let data):
        print("Using tool: \(data.toolName)")
    case .result(let data):
        print("\nDone (\(data.numTurns) turns, $\(String(format: "%.4f", data.totalCostUsd)))")
    default:
        break
    }
}

Custom Tools

struct WeatherInput: Codable {
    let city: String
}

let weatherTool = defineTool(
    name: "get_weather",
    description: "Get current weather for a city",
    inputSchema: [
        "type": "object",
        "properties": [
            "city": ["type": "string", "description": "City name"]
        ],
        "required": ["city"]
    ]
) { (input: WeatherInput, context: ToolContext) in
    return "Weather in \(input.city): 22C, sunny"
}

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    tools: [weatherTool]
))

Built With This SDK

  • SwiftWork — A native macOS app built with Swift, powered by this SDK for AI-driven workflows.
  • axion — A Swift CLI tool that uses this SDK as its agentic engine.

Advanced Features

Multi-Provider Support

Use OpenAI-compatible APIs (GLM, Ollama, OpenRouter, etc.):

let agent = createAgent(options: AgentOptions(
    provider: .openai,
    apiKey: "sk-...",
    model: "gpt-4o",
    baseURL: "https://api.openai.com/v1",
    systemPrompt: "You are a helpful assistant."
))

Or via environment variables:

export CODEANY_API_KEY=sk-...
export CODEANY_BASE_URL=https://api.openai.com/v1
export CODEANY_MODEL=gpt-4o

Session Persistence

Save and restore conversation history:

let sessionStore = SessionStore()

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    sessionStore: sessionStore,
    sessionId: "my-session"
))

// First conversation is auto-saved after prompt/stream
let result = await agent.prompt("Remember: my favorite color is blue.")

// Resume in a new process — history is auto-loaded
let agent2 = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    sessionStore: sessionStore,
    sessionId: "my-session"
))
let result2 = await agent2.prompt("What is my favorite color?")

Hook System

Register lifecycle event handlers:

let hookRegistry = HookRegistry()

await hookRegistry.register(.postToolUse, definition: HookDefinition(
    handler: { input in
        if let toolName = input.toolName {
            print("Tool completed: \(toolName)")
        }
        return nil
    }
))

await hookRegistry.register(.preToolUse, definition: HookDefinition(
    matcher: "Bash",
    handler: { input in
        return HookOutput(message: "Bash command blocked", block: true)
    }
))

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    hookRegistry: hookRegistry
))

Permission Control

Choose from 6 permission modes or define custom policies:

// Built-in modes
let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    permissionMode: .acceptEdits
))

// Custom authorization callback
agent.setCanUseTool { tool, input, context in
    if tool.name == "Bash" { return .deny("Bash is disabled") }
    return .allow()
}

// Policy composition
let policy = CompositePolicy(policies: [
    ReadOnlyPolicy(),
    ToolNameDenylistPolicy(deniedToolNames: ["WebFetch"])
])
agent.setCanUseTool(canUseTool(policy: policy))

MCP Integration

Connect external tool servers via MCP (Model Context Protocol):

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    mcpServers: [
        "filesystem": .stdio(McpStdioConfig(
            command: "npx",
            args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
        )),
        "remote": .sse(McpSseConfig(
            url: "http://localhost:3001/sse"
        ))
    ]
))
// MCP tools are auto-discovered and merged into the agent's tool pool

Budget Control

Set cost limits to cap LLM spending:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    maxBudgetUsd: 0.10  // Stop when cost exceeds $0.10
))

Skills System

Register built-in or custom skills that encapsulate prompt templates and tool restrictions:

import OpenAgentSDK

// Built-in skills are auto-registered
let registry = SkillRegistry()
registry.register(BuiltInSkills.commit)
registry.register(BuiltInSkills.review)

// Register a custom skill
let explainSkill = Skill(
    name: "explain",
    description: "Explain code in detail",
    promptTemplate: "Read the specified files and explain the code line by line...",
    toolRestrictions: [.bash, .read, .glob, .grep]
)
registry.register(explainSkill)

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    tools: getAllBaseTools(tier: .core) + [createSkillTool(registry: registry)]
))

Runtime Model Switching

Switch LLM models mid-conversation with per-model cost tracking:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    model: "claude-sonnet-4-6"
))

// Use fast model for simple queries
let result1 = await agent.prompt("Quick question...")

// Switch to powerful model for complex tasks
try agent.switchModel("claude-opus-4-6")
let result2 = await agent.prompt("Analyze this complex codebase...")
// result2.usage.costBreakdown contains separate entries per model

Query Abort

Cancel running queries and retrieve partial results:

let task = Task {
    for await message in agent.stream("Long-running analysis...") {
        // process events
    }
}

// Cancel after timeout
task.cancel()
// The agent returns a QueryResult with isCancelled=true and partial results

Context Injection

Automatic Git status and project document discovery:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    projectRoot: "/path/to/project"  // auto-discovers CLAUDE.md, AGENT.md
))
// System prompt now includes <git-context> and <project-instructions> blocks

Sandbox & Logging

Restrict agent operations and capture structured logs:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    sandbox: SandboxSettings(
        allowedReadPaths: ["/project/"],
        allowedWritePaths: ["/project/src/"],
        deniedCommands: ["rm", "sudo"]
    ),
    logLevel: .debug,
    logOutput: .custom { jsonLine in
        // Integrate with ELK, Datadog, etc.
        print(jsonLine)
    }
))

HTTP API Server

Expose any Agent as a REST + SSE HTTP service:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    tools: getAllBaseTools(tier: .core)
))

let server = AgentHTTPServer(
    agent: agent,
    host: "127.0.0.1",
    port: 4242,
    authKey: "my-secret-key",
    maxConcurrentRuns: 5
)

try await server.start()
// POST /v1/runs        — submit tasks (202 + run_id)
// GET  /v1/runs        — list runs
// GET  /v1/runs/{id}   — get run status
// GET  /v1/runs/{id}/events — SSE stream
// GET  /v1/health      — health check

Cost Tracking & Tracing

Monitor agent spending and trace execution:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    maxBudgetUsd: 0.50,
    traceEnabled: true,
    traceBaseURL: "/tmp/traces"
))

for await message in agent.stream("Analyze the codebase") {
    // CostTracker accumulates per-model token/cost automatically
    // TraceRecorder writes JSONL events to /tmp/traces/{runId}/trace.jsonl
}

Output Formatting

Format SDK message streams for terminal or JSON output:

// Terminal output with step counting
let terminal = TerminalOutputHandler()
for await message in agent.stream("Summarize this project") {
    terminal.handle(message)
}

// JSON output for programmatic consumption
let json = JSONOutputHandler(write: { print($0) })
terminal.displayRunStart(runId: "run-1", task: "Summarize")
for await message in agent.stream("Summarize this project") {
    json.handle(message)
}
let result = json.finalize()  // [String: Any] dictionary

Runtime Event Layer

Subscribe to typed runtime events via EventBus — session lifecycle, agent progress, tool execution, LLM cost tracking, and token streaming.

let eventBus = EventBus()

// Subscribe to all events
let (_, stream) = await eventBus.subscribe()
for await event in stream {
    switch event {
    case let e as AgentStartedEvent:
        print("Agent started: \(e.task)")
    case let e as ToolCompletedEvent:
        print("Tool done: \(e.toolName) in \(e.durationMs)ms")
    case let e as LLMCostEvent:
        print("Cost: $\(String(format: "%.4f", e.estimatedCostUsd))")
    default: break
    }
}

// Or subscribe to a specific event type only
let costStream = await eventBus.subscribe(LLMCostEvent.self)

Pass the EventBus to an agent and all events are emitted automatically:

let agent = createAgent(options: AgentOptions(
    apiKey: "sk-...",
    eventBus: eventBus,            // opt-in, nil = zero overhead
    emitTokenStream: true          // enable LLMTokenStreamEvent for TUI rendering
))

18 event types across 4 categories:

Category Events
Session SessionCreatedEvent, SessionRestoredEvent, SessionClosedEvent, SessionAutoSavedEvent
Agent AgentStartedEvent, AgentCompletedEvent, AgentFailedEvent, AgentInterruptedEvent, AgentResumedEvent
Tool ToolStartedEvent, ToolStreamingEvent, ToolCompletedEvent, ToolFailedEvent
LLM LLMRequestStartedEvent, LLMResponseReceivedEvent, LLMCostEvent, LLMTokenStreamEvent

Bridge EventBus to SSE for HTTP API:

let broadcaster = EventBroadcaster()
let bridge = EventBusBridge(eventBus: eventBus, broadcaster: broadcaster, runId: "run-1")
await bridge.start()
// SSE clients now receive typed events via broadcaster

Built-in Tools

Core Tools (10)

Tool Description
Bash Execute shell commands with timeout
Read Read file contents
Write Create or overwrite files
Edit Find and replace in files
Glob Search files by pattern
Grep Search file contents with regex
WebFetch Fetch and read web pages
WebSearch Search the web
AskUser Ask user for input during execution
ToolSearch Search available tools

Advanced Tools (11)

Tool Description
Agent Spawn sub-agents (Explore, Plan types)
SendMessage Send messages between agents
TaskCreate Create tasks with descriptions
TaskList List all tasks with status filtering
TaskUpdate Update task status and owner
TaskGet Get task details by ID
TaskStop Stop a running task
TaskOutput Get output from a completed task
TeamCreate Create a team for multi-agent coordination
TeamDelete Delete a team and clean up resources
NotebookEdit Edit Jupyter notebook cells

Specialist Tools (13)

Tool Description
WorktreeEnter Enter an isolated worktree workspace
WorktreeExit Exit and optionally remove a worktree
PlanEnter Enter plan mode for structured planning
PlanExit Exit plan mode and return to execution
CronCreate Schedule a recurring task
CronDelete Delete a scheduled task
CronList List all scheduled tasks
RemoteTrigger Trigger a remote webhook or event
LSP Language Server Protocol integration
Config Read and write SDK configuration values
TodoWrite Manage todo lists with priorities
ListMcpResources List available MCP server resources
ReadMcpResource Read a specific MCP resource

Self-Evolution

The SDK can automatically learn from conversations and evolve its behavior over time:

Memory Extraction — Extract structured experience signals from conversations:

let extractor = LLMExperienceExtractor(client: myLLMClient)
let result = try await extractor.extract(
    from: messages,
    config: ExtractionConfig()
)
for signal in result.signals {
    // Save to FactStore for cross-session knowledge
    try await factStore.save(signal: signal)
}

Skill Evolution — Adapt skills based on usage signals:

let evolver = LLMSkillEvolver(client: myLLMClient)
let evolved = try await evolver.evolve(
    skill: mySkill,
    signals: usageSignals,
    config: SkillEvolutionConfig()
)

Background Review Agent — Fork a review agent after each session:

let orchestrator = ReviewOrchestrator(
    scheduleConfig: ReviewScheduleConfig(),
    factStore: factStore,
    skillRegistry: skillRegistry,
    skillEvolver: evolver,
    usageStore: usageStore
)

// Check if review is due and execute
let (doMemory, doSkills) = orchestrator.shouldReview(
    sessionId: session.id,
    messageCount: messages.count,
    config: ReviewAgentConfig()
)
if doMemory || doSkills {
    let reviewResult = await orchestrator.executeReview(
        parentAgent: agent,
        messages: messages,
        config: ReviewAgentConfig()
    )
}

Intelligent Curation — LLM-driven skill library maintenance:

let curator = IntelligentCurator(
    skillCurator: SkillCurator(usageStore: usageStore, curatorStore: curatorStore),
    factStore: factStore,
    skillRegistry: skillRegistry,
    skillEvolver: evolver,
    usageStore: usageStore,
    curatorStore: curatorStore
)

// Run two-phase curation: mechanical transitions + LLM consolidation
let result = try await curator.execute(parentAgent: agent)

// Generate reports
let report = CuratorRunReport(from: result)
print(report.renderMarkdown())  // Human-readable
print(report.renderYAML())      // Machine-readable

Architecture

graph TD
    A["<b>Your Application</b><br/><i>import OpenAgentSDK</i>"] --> B
    B["<b>Agent</b><br/>prompt() / stream()<br/><i>Session, Hooks, Permissions</i>"] --> C
    C["<b>Agentic Loop</b><br/><i>API call &rarr; tools &rarr; repeat</i>"] --> D
    C --> E
    C --> F
    C --> G
    C --> H
    C --> I
    C --> J
    C --> K
    C --> L
    D["<b>LLMClient Protocol</b><br/>AnthropicClient &middot; OpenAIClient"]
    E["<b>34 Built-in Tools</b><br/>Core 10 &middot; Advanced 11 &middot; Specialist 13"]
    F["<b>MCP Servers</b><br/>stdio &middot; SSE &middot; HTTP &middot; In-Process"]
    G["<b>Session Store</b><br/>JSON Persistence &middot; Fork &middot; Restore"]
    H["<b>Hook Registry</b><br/>20+ Lifecycle Events"]
    I["<b>HTTP API Server</b><br/>REST + SSE &middot; Run Tracking"]
    J["<b>Cost &amp; Trace</b><br/>Budget Control &middot; JSONL Traces"]
    K["<b>Self-Evolution</b><br/>ExperienceExtractor &middot; SkillEvolver<br/>ReviewOrchestrator &middot; IntelligentCurator"]
    L["<b>Runtime Event Layer</b><br/>EventBus &middot; 18 Event Types<br/>SSE Bridge &middot; Token Streaming"]

    style A fill:#0277bd,stroke:#01579b,color:#fff,stroke-width:2px
    style B fill:#ef6c00,stroke:#e65100,color:#fff,stroke-width:2px
    style C fill:#6a1b9a,stroke:#4a148c,color:#fff,stroke-width:2px
    style D fill:#2e7d32,stroke:#1b5e20,color:#fff,stroke-width:2px
    style E fill:#b71c1c,stroke:#7f0000,color:#fff,stroke-width:2px
    style F fill:#00695c,stroke:#004d40,color:#fff,stroke-width:2px
    style G fill:#4a148c,stroke:#4a148c,color:#fff,stroke-width:2px
    style H fill:#e65100,stroke:#bf360c,color:#fff,stroke-width:2px
    style I fill:#1565c0,stroke:#0d47a1,color:#fff,stroke-width:2px
    style J fill:#880e4f,stroke:#560027,color:#fff,stroke-width:2px
    style K fill:#00695c,stroke:#004d40,color:#fff,stroke-width:2px
    style L fill:#283593,stroke:#1a237e,color:#fff,stroke-width:2px
Loading

Environment Variables

Variable Description
CODEANY_API_KEY API key (required)
CODEANY_MODEL Default model (default: claude-sonnet-4-6)
CODEANY_BASE_URL Custom API endpoint for third-party providers

Documentation

API documentation and guides are available via Swift-DocC:

  • Getting Started — 15-minute walkthrough
  • Tool System — Tool protocol, custom tools, tiers
  • Multi-Agent Orchestration — Sub-agents, teams, tasks
  • MCP, Sessions & Hooks — MCP integration, persistence, hook system
  • Cookbook — 18 real-world scenarios with runnable code (structured output, sandbox, multi-agent, self-evolution, runtime events, etc.)
  • Runnable Examples — 47 runnable example targets with 38 tutorial sections, including Claude Code compatibility, EventBus, MCP, skills, and SDK parity demos

Requirements

  • Swift 6.1+
  • macOS 13+

Development

# Build
swift build

# Run tests
swift test

# Open in Xcode
open Package.swift

Acknowledgments

This project is inspired by open-agent-sdk-typescript, which provides the same agentic architecture for the TypeScript/Node.js ecosystem.

Star History

Star History Chart

License

MIT

About

Agent-SDK without CLI dependencies, as an alternative to claude-agent-sdk, completely open source

Topics

Resources

License

Stars

23 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages