README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/holsee/conjure/main/conjure.png" alt="Conjure Logo" width="200">
</p>

<p align="center">
  <a href="https://github.com/holsee/conjure/actions/workflows/ci.yml"><img src="https://github.com/holsee/conjure/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://hex.pm/packages/conjure"><img src="https://img.shields.io/hexpm/v/conjure.svg" alt="Hex.pm"></a>
  <a href="https://hexdocs.pm/conjure"><img src="https://img.shields.io/badge/hex-docs-blue.svg" alt="Documentation"></a>
  <a href="https://github.com/holsee/conjure/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
</p>

# Conjure

An Elixir library for leveraging Anthropic Agent Skills in elixir with a configurable skill execution targets, native elixir skills and artefact storage targets.

Conjure provides a complete implementation of the Agent Skills specification with a **unified Session API** supporting multiple execution backends: local shell, Docker containers, Anthropic's hosted Skills API, and native Elixir modules.

## Documentation

- **[Getting Started](docs/getting-started.md)** - Quick overview and learning path
- **[Tutorials](docs/tutorials/README.md)** - Step-by-step guides:
  - [Hello World](docs/tutorials/hello_world.md) - First steps (10 min)
  - [Local Skills](docs/tutorials/using_local_skills_via_claude_api.md) - Build a log analyzer (30 min)
  - [Anthropic Skills API](docs/tutorials/using_claude_skill_with_elixir_host.md) - Document generation (20 min)
  - [Native Skills](docs/tutorials/using_elixir_native_skill.md) - Pure Elixir skills (25 min)
  - [Unified Backends](docs/tutorials/many_skill_backends_one_agent.md) - Combine all backends (30 min)
- **[Technical Specification](conjure_specification.md)** - Complete API specification
- **[Architecture Decision Records](docs/adr/README.md)** - Design decisions:
  - [ADR-0019: Unified Execution Model](docs/adr/0019-unified-execution-model.md)
  - [ADR-0020: Backend Behaviour Architecture](docs/adr/0020-backend-behaviour.md)

## Features

- **Unified Session API** - Same `chat/3` interface across all execution backends
- **4 Execution Backends** - Local, Docker, Anthropic Skills API, and Native Elixir
- **Skill Loading** - Parse SKILL.md files with YAML frontmatter, load `.skill` packages (ZIP format)
- **Progressive Disclosure** - Efficient token usage with metadata → body → resources loading
- **System Prompt Generation** - Generate XML-formatted skill discovery prompts
- **Tool Definitions** - Claude-compatible tool schemas (view, bash, create_file, str_replace)
- **API-Agnostic** - No HTTP client bundled; you provide the API callback
- **Native Skills** - Implement skills as type-safe Elixir modules
- **OTP Compliant** - GenServer registry, supervision trees, fault tolerance

## Installation

Add `conjure` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:conjure, "~> 0.1.0-alpha"}
  ]
end
```

Then run:

```bash
mix deps.get
```

## Quick Start

### Using the Session API

The Session API provides a unified interface for all execution backends:

```elixir
# Load skills from disk
{:ok, skills} = Conjure.load("/path/to/skills")

# Create a session with local execution
session = Conjure.Session.new_local(skills)

# Chat with Claude (you provide the API callback)
{:ok, response, session} = Conjure.Session.chat(
  session,
  "Create a Python script that calculates fibonacci numbers",
  &my_api_callback/1
)

# Continue the conversation (session tracks state)
{:ok, response, session} = Conjure.Session.chat(
  session,
  "Now add memoization to optimize it",
  &my_api_callback/1
)
```

### API Callback

Conjure is API-agnostic - you provide the callback that makes HTTP calls:

```elixir
defmodule MyApp.Claude do
  def api_callback(messages) do
    body = %{
      model: "claude-sonnet-4-5-20250929",
      max_tokens: 4096,
      system: build_system_prompt(),
      messages: messages,
      tools: Conjure.tool_definitions()
    }

    case Req.post("https://api.anthropic.com/v1/messages", json: body, headers: headers()) do
      {:ok, %{status: 200, body: body}} -> {:ok, body}
      {:ok, %{body: body}} -> {:error, body}
      {:error, reason} -> {:error, reason}
    end
  end
end
```

## Execution Backends

Conjure supports 4 execution backends through the `Conjure.Backend` behaviour:

| Backend | Description | Use Case |
|---------|-------------|----------|
| **Local** | Direct shell execution on host | Development, trusted environments |
| **Docker** | Containerized sandbox execution | Production, untrusted code |
| **Anthropic** | Anthropic's hosted Skills API | Document generation (xlsx, pdf, pptx) |
| **Native** | Elixir modules in BEAM | Type-safe, in-process execution |

### Local Backend

Default backend for development. Skills execute bash commands directly on the host.

```elixir
{:ok, skills} = Conjure.load("/path/to/skills")
session = Conjure.Session.new_local(skills)
```

### Docker Backend

Sandboxed execution in Docker containers. Recommended for production.

```elixir
# Build the sandbox image first
Conjure.Executor.Docker.build_image()

# Create session with Docker executor
session = Conjure.Session.new_local(skills,
  executor: Conjure.Executor.Docker,
  executor_config: %{
    image: "conjure/sandbox:latest",
    memory_limit: "512m",
    cpu_limit: "1.0",
    network: :none
  }
)

{:ok, response, session} = Conjure.Session.chat(session, message, &api_callback/1)
```

### Anthropic Backend

Use Anthropic's hosted Skills API for document generation skills (xlsx, pdf, pptx, docx).

```elixir
# Specify Anthropic-hosted skills
session = Conjure.Session.new_anthropic([
  {:anthropic, "xlsx", "latest"},
  {:anthropic, "pdf", "latest"}
])

# The API callback must include beta headers
{:ok, response, session} = Conjure.Session.chat(
  session,
  "Create a budget spreadsheet with monthly expenses",
  &anthropic_api_callback/1
)

# Access created files
files = Conjure.Session.get_created_files(session)
# => [%{id: "file_01...", source: :anthropic, ...}]
```

See [Anthropic Skills API](#anthropic-skills-api) section for details.

### Native Backend

Execute skills as compiled Elixir modules with direct BEAM access.

```elixir
# Define a native skill module
defmodule MyApp.Skills.Database do
  @behaviour Conjure.NativeSkill

  @impl true
  def __skill_info__ do
    %{
      name: "database",
      description: "Query the application database",
      allowed_tools: [:execute, :read]
    }
  end

  @impl true
  def execute(query, _context) do
    case MyApp.Repo.query(query) do
      {:ok, result} -> {:ok, format_result(result)}
      {:error, err} -> {:error, inspect(err)}
    end
  end

  @impl true
  def read(table, _context, _opts) do
    {:ok, get_table_schema(table)}
  end
end

# Create session with native skills
session = Conjure.Session.new_native([MyApp.Skills.Database])

{:ok, response, session} = Conjure.Session.chat(
  session,
  "What tables do we have?",
  &api_callback/1
)
```

## Native Skills

Native skills are Elixir modules that implement the `Conjure.NativeSkill` behaviour. They execute directly in the BEAM with full access to your application's runtime context.

### Behaviour Callbacks

| Callback | Maps To | Purpose |
|----------|---------|---------|
| `execute/2` | `bash_tool` | Run commands/logic |
| `read/3` | `view` | Read resources |
| `write/3` | `create_file` | Create resources |
| `modify/4` | `str_replace` | Update resources |

### Example: Cache Manager

```elixir
defmodule MyApp.Skills.CacheManager do
  @behaviour Conjure.NativeSkill

  @impl true
  def __skill_info__ do
    %{
      name: "cache-manager",
      description: "Manage application cache (clear, stats, list keys)",
      allowed_tools: [:execute, :read]
    }
  end

  @impl true
  def execute("clear", _context) do
    Cachex.clear(:my_cache)
    {:ok, "Cache cleared successfully"}
  end

  def execute("stats", _context) do
    {:ok, stats} = Cachex.stats(:my_cache)
    {:ok, inspect(stats, pretty: true)}
  end

  @impl true
  def read("keys", _context, _opts) do
    {:ok, keys} = Cachex.keys(:my_cache)
    {:ok, Enum.join(keys, "\n")}
  end
end
```

### Advantages Over Local Backend

- No subprocess/shell overhead
- Type-safe with compile-time checks
- Direct access to application state (Ecto repos, caches, GenServers)
- Better error handling with pattern matching

## Anthropic Skills API

For document generation (xlsx, pdf, pptx, docx), use Anthropic's hosted Skills API.

### Beta Headers

The Skills API requires beta headers:

```elixir
def anthropic_headers do
  [
    {"x-api-key", api_key()},
    {"anthropic-version", "2023-06-01"}
  ] ++ Conjure.API.Anthropic.beta_headers()
end
```

### Container Reuse

Sessions automatically track container IDs for multi-turn conversations:

```elixir
session = Conjure.Session.new_anthropic([{:anthropic, "xlsx", "latest"}])

# First turn - creates container
{:ok, _, session} = Conjure.Session.chat(session, "Create a spreadsheet", &callback/1)

# Subsequent turns reuse the same container
{:ok, _, session} = Conjure.Session.chat(session, "Add a chart", &callback/1)
```

### File Downloads

Files created during Anthropic execution can be downloaded:

```elixir
files = Conjure.Session.get_created_files(session)

for %{id: file_id, source: :anthropic} <- files do
  {:ok, content, filename} = Conjure.Files.Anthropic.download(file_id, &files_api_callback/1)
  File.write!(filename, content)
end
```

### Skill Types

- `:anthropic` - Pre-built skills: `"xlsx"`, `"pptx"`, `"docx"`, `"pdf"`
- `:custom` - User-uploaded skills with generated IDs

## Usage Examples

### Multi-Turn Conversation

```elixir
defmodule MyApp.Agent do
  def run(initial_message) do
    {:ok, skills} = Conjure.load("priv/skills")
    session = Conjure.Session.new_local(skills)

    conversation_loop(session, initial_message)
  end

  defp conversation_loop(session, message) do
    case Conjure.Session.chat(session, message, &api_callback/1) do
      {:ok, response, session} ->
        IO.puts(extract_text(response))

        case IO.gets("You: ") do
          :eof -> :ok
          input -> conversation_loop(session, String.trim(input))
        end

      {:error, error} ->
        IO.puts("Error: #{inspect(error)}")
    end
  end
end
```

### Unified Backend Selection

```elixir
defmodule MyApp.Agent do
  def chat(message, backend_type, skills) do
    session = case backend_type do
      :local -> Conjure.Session.new_local(skills)
      :docker -> Conjure.Session.new_local(skills, executor: Conjure.Executor.Docker)
      :anthropic -> Conjure.Session.new_anthropic(skills)
      :native -> Conjure.Session.new_native(skills)
    end

    # Same API regardless of backend
    Conjure.Session.chat(session, message, &api_callback/1)
  end
end
```

### GenServer Registry

```elixir
# In your application supervisor
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Conjure.Registry, name: MyApp.Skills, paths: ["/path/to/skills"]}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

# Later in your application
skills = Conjure.Registry.list(MyApp.Skills)
pdf_skill = Conjure.Registry.get(MyApp.Skills, "pdf")

# Reload skills at runtime
Conjure.Registry.reload(MyApp.Skills)
```

### Low-Level Conversation API

For more control, use the conversation loop directly:

```elixir
{:ok, skills} = Conjure.load("/path/to/skills")

system_prompt = """
You are a helpful assistant.

#{Conjure.system_prompt(skills)}
"""

tools = Conjure.tool_definitions()
messages = [%{role: "user", content: "Create a Python script"}]

Conjure.Conversation.run_loop(
  messages,
  skills,
  &call_claude(&1, system_prompt, tools),
  max_iterations: 15
)
```

## Progressive Disclosure

Skills are loaded with metadata only by default for token efficiency:

```elixir
# Initial load - metadata only
{:ok, skills} = Conjure.load("/path/to/skills")

# Load full body when needed
{:ok, skill_with_body} = Conjure.load_body(skill)

# Read a specific resource
{:ok, content} = Conjure.read_resource(skill, "scripts/helper.py")
```

## Skill Format

Skills follow the Anthropic Agent Skills specification:

```
my-skill/
├── SKILL.md           # Required - skill definition
├── scripts/           # Optional - executable scripts
│   └── helper.py
├── references/        # Optional - reference documentation
│   └── api_docs.md
└── assets/            # Optional - binary assets
    └── template.xlsx
```

### SKILL.md Format

```yaml
---
name: my-skill
description: A comprehensive description of what this skill does and when to use it.
license: MIT
compatibility:
  products: [claude.ai, claude-code, api]
  packages: [python3, nodejs]
allowed_tools: [bash, view, create_file]
---

# My Skill

Detailed instructions for using this skill...
```

### .skill Package Format

A `.skill` file is a ZIP archive containing the skill directory:

```bash
# Create a .skill package
zip -r my-skill.skill my-skill/

# Conjure can load it directly
{:ok, skill} = Conjure.load_skill_file("my-skill.skill")
```

## Tool Definitions

Conjure provides these tools for Claude:

| Tool | Description |
|------|-------------|
| `view` | Read file contents or directory listings |
| `bash_tool` | Execute bash commands |
| `create_file` | Create new files with content |
| `str_replace` | Replace strings in files |

## Custom Executor

Implement the `Conjure.Executor` behaviour to create custom execution backends:

```elixir
defmodule MyApp.FirecrackerExecutor do
  @behaviour Conjure.Executor

  @impl true
  def init(context), do: {:ok, context}

  @impl true
  def bash(command, context) do
    # Execute in Firecracker microVM
    {:ok, output}
  end

  @impl true
  def view(path, context, opts), do: {:ok, content}

  @impl true
  def create_file(path, content, context), do: {:ok, "File created"}

  @impl true
  def str_replace(path, old_str, new_str, context), do: {:ok, "File updated"}

  @impl true
  def cleanup(context), do: :ok
end

# Use your custom executor
session = Conjure.Session.new_local(skills, executor: MyApp.FirecrackerExecutor)
```

## Security

### Recommendations

1. **Use Docker executor in production** - Local executor provides no sandboxing
2. **Audit skills before loading** - Review SKILL.md and bundled scripts
3. **Restrict network access** - Default to `:none`, use allowlists for `:limited`
4. **Set resource limits** - Configure memory, CPU, and timeout limits
5. **Use read-only skill mounts** - Skills directory mounted as read-only in Docker
6. **Separate working directories** - Use per-session working directories

### Path Validation

```elixir
# Conjure validates paths are within allowed boundaries
context = Conjure.create_context(skills,
  allowed_paths: ["/tmp/conjure", "/home/user/projects"]
)
```

## Requirements

- Elixir 1.14+
- Erlang/OTP 25+
- Docker 20.10+ (for Docker executor)

## License

Apache License 2.0 - See [LICENSE](LICENSE) for details.

## Links

- [Anthropic Agent Skills Specification](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview)
- [Claude API Documentation](https://docs.anthropic.com/en/api)
- [Anthropic Skills API Guide](https://platform.claude.com/docs/en/build-with-claude/skills-guide)