docs/adr/0011-anthropic-executor.md

# ADR-0011: Anthropic Skills API Integration

## Status

Proposed

## Context

> **Note:** This ADR replaces an earlier incorrect design that assumed a "pass-through executor" pattern. The original design was based on a misunderstanding of how Anthropic's code execution capabilities work. This revision accurately describes the actual Anthropic Skills API.

Anthropic provides a Skills API (beta) that allows:

1. **Uploading custom skills** to Anthropic's infrastructure
2. **Using pre-built Anthropic skills** (xlsx, pptx, docx, pdf)
3. **Executing skills in Anthropic-managed containers** with code execution

This provides an alternative to local/Docker execution for users who:

- Cannot run Docker in their environment
- Want Anthropic to manage sandbox security
- Need access to Anthropic's pre-built document skills
- Prefer hosted execution without local infrastructure

### How the Skills API Actually Works

```
1. Upload skill files → POST /v1/skills → receive skill_id
2. Include skill_id in container.skills parameter
3. Enable code_execution_20250825 tool in the request
4. Anthropic's container loads skills at /skills/{directory}/
5. Claude uses code execution to run code with skill files available
6. Download created files via Files API
```

### Key Differences from Original Design

| Original (Incorrect) | Actual API |
|---------------------|------------|
| Pass-through for bash commands | Skills uploaded first, get skill_id |
| `{:passthrough, config}` return | Uses `container.skills` parameter |
| Executor behaviour implementation | Not an executor—API integration helpers |
| Local skill files | Skills must be uploaded to Anthropic |

### Beta Requirements

The Skills API requires beta headers:

- `code-execution-2025-08-25` - Enables code execution
- `skills-2025-10-02` - Enables Skills API
- `files-api-2025-04-14` - For uploading/downloading files

### Skill Types

| Type | Description | ID Format |
|------|-------------|-----------|
| `anthropic` | Pre-built by Anthropic | Short names: `xlsx`, `pptx`, `docx`, `pdf` |
| `custom` | User-uploaded | Generated: `skill_01AbCdEfGhIjKlMnOpQrStUv` |

## Decision

We will provide **optional** Anthropic Skills API integration as helper modules, NOT as an executor implementation. This is fundamentally different from local/Docker execution because:

1. Conjure does NOT execute tools—Anthropic's container does
2. Skills must be uploaded to Anthropic first
3. The integration is at the API request level, not execution level

> **Note:** While this ADR describes the Anthropic-specific modules, [ADR-0019: Unified Execution Model](0019-unified-execution-model.md) describes how these modules are surfaced through a unified `Conjure.Session` API that provides identical interaction patterns for both local/Docker and Anthropic execution.

### Module Design

```elixir
defmodule Conjure.Skills.Anthropic do
  @moduledoc """
  Upload and manage skills via Anthropic Skills API.

  This module provides helpers for interacting with Anthropic's
  Skills API to upload custom skills and manage versions.

  Note: This is NOT an executor. Skills uploaded here are executed
  by Anthropic's infrastructure, not by Conjure.
  """

  @doc """
  Upload a skill directory to Anthropic.

  Returns the skill_id for use in API requests.

  ## Example

      {:ok, skill_id} = Conjure.Skills.Anthropic.upload(
        "priv/skills/csv-helper",
        display_title: "CSV Helper",
        api_key: System.get_env("ANTHROPIC_API_KEY")
      )

  """
  @spec upload(Path.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
  def upload(skill_path, opts \\ [])

  @doc """
  List skills available in your Anthropic workspace.

  ## Options

    * `:source` - Filter by "anthropic" or "custom"
    * `:api_key` - Anthropic API key

  """
  @spec list(keyword()) :: {:ok, [map()]} | {:error, term()}
  def list(opts \\ [])

  @doc """
  Delete a custom skill from Anthropic.

  Note: All versions must be deleted first.
  """
  @spec delete(String.t(), keyword()) :: :ok | {:error, term()}
  def delete(skill_id, opts \\ [])

  @doc """
  Create a new version of an existing skill.
  """
  @spec create_version(String.t(), Path.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
  def create_version(skill_id, skill_path, opts \\ [])
end
```

### API Request Helpers

```elixir
defmodule Conjure.API.Anthropic do
  @moduledoc """
  Helpers for building Anthropic API requests with Skills.
  """

  @doc """
  Build the container parameter for skills.

  ## Example

      container = Conjure.API.Anthropic.container_config([
        {:anthropic, "xlsx", "latest"},
        {:anthropic, "pptx", "latest"},
        {:custom, "skill_01AbCdEfGhIjKlMnOpQrStUv", "latest"}
      ])

      # Returns:
      # %{
      #   "skills" => [
      #     %{"type" => "anthropic", "skill_id" => "xlsx", "version" => "latest"},
      #     %{"type" => "anthropic", "skill_id" => "pptx", "version" => "latest"},
      #     %{"type" => "custom", "skill_id" => "skill_01...", "version" => "latest"}
      #   ]
      # }

  """
  @spec container_config([skill_spec()]) :: map()
  def container_config(skills)

  @type skill_spec ::
    {:anthropic, String.t(), String.t()} |
    {:custom, String.t(), String.t()}

  @doc """
  Get the required beta headers for Skills API.
  """
  @spec beta_headers() :: [{String.t(), String.t()}]
  def beta_headers do
    [
      {"anthropic-beta", "code-execution-2025-08-25,skills-2025-10-02,files-api-2025-04-14"}
    ]
  end

  @doc """
  Get the code execution tool definition.
  """
  @spec code_execution_tool() :: map()
  def code_execution_tool do
    %{
      "type" => "code_execution_20250825",
      "name" => "code_execution"
    }
  end
end
```

### Multi-Skill Support

The Skills API supports **up to 8 skills per request**. Skills can be combined for complex workflows (e.g., analyze data with Excel skill, create presentation with PowerPoint skill):

```elixir
container = Conjure.API.Anthropic.container_config([
  {:anthropic, "xlsx", "latest"},
  {:anthropic, "pptx", "latest"},
  {:anthropic, "pdf", "latest"},
  {:custom, "skill_01AbCdEfGhIjKlMnOpQrStUv", "latest"}
])
```

### Long-Running Operations

Skills may perform operations that require multiple turns. The API returns a `pause_turn` stop reason when an operation is paused, requiring the client to continue the conversation:

```elixir
defmodule Conjure.Conversation.Anthropic do
  @moduledoc """
  Conversation loop for Anthropic Skills API with pause_turn handling.
  """

  @doc """
  Run a conversation with Anthropic-hosted skills, handling pause_turn.

  Unlike local/Docker execution where Conjure manages tool execution,
  here Anthropic executes in their container. However, long-running
  operations still require a conversation loop to handle pause_turn.

  ## Options

    * `:max_retries` - Maximum pause_turn iterations (default: 10)
    * `:on_pause` - Callback when pause_turn received

  """
  @spec run(list(), map(), keyword()) :: {:ok, map()} | {:error, term()}
  def run(messages, container_config, opts \\ []) do
    max_retries = Keyword.get(opts, :max_retries, 10)
    do_run(messages, container_config, opts, 0, max_retries)
  end

  defp do_run(messages, container_config, opts, attempt, max_retries)
       when attempt < max_retries do
    case call_api(messages, container_config, opts) do
      {:ok, %{"stop_reason" => "pause_turn", "content" => content} = response} ->
        # Long-running operation paused - continue with same container
        container_id = get_in(response, ["container", "id"])
        updated_messages = messages ++ [%{role: "assistant", content: content}]
        updated_container = Map.put(container_config, "id", container_id)

        if callback = opts[:on_pause] do
          callback.(response, attempt + 1)
        end

        do_run(updated_messages, updated_container, opts, attempt + 1, max_retries)

      {:ok, response} ->
        {:ok, response}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp do_run(_messages, _container, _opts, attempt, max_retries) do
    {:error, {:max_retries_exceeded, attempt, max_retries}}
  end
end
```

### Multi-Turn Conversations

Reuse the same container across multiple user messages by preserving the container ID:

```elixir
defmodule Conjure.Session.Anthropic do
  @moduledoc """
  Manage multi-turn sessions with Anthropic Skills API.
  """

  defstruct [:container_id, :skills, :messages]

  @doc """
  Start a new session with specified skills.
  """
  def new(skills) do
    %__MODULE__{
      container_id: nil,
      skills: skills,
      messages: []
    }
  end

  @doc """
  Send a message and get response, preserving container state.
  """
  def chat(session, user_message, opts \\ []) do
    messages = session.messages ++ [%{role: "user", content: user_message}]

    container_config = build_container(session)

    case Conjure.Conversation.Anthropic.run(messages, container_config, opts) do
      {:ok, response} ->
        updated_session = %{session |
          container_id: get_in(response, ["container", "id"]),
          messages: messages ++ [%{role: "assistant", content: response["content"]}]
        }
        {:ok, response, updated_session}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp build_container(%{container_id: nil, skills: skills}) do
    Conjure.API.Anthropic.container_config(skills)
  end

  defp build_container(%{container_id: id, skills: skills}) do
    Conjure.API.Anthropic.container_config(skills)
    |> Map.put("id", id)
  end
end
```

### File Handling

Skills that create documents return `file_id` values. Use the Files API to download:

```elixir
defmodule Conjure.Files.Anthropic do
  @moduledoc """
  Download files created by Anthropic Skills.
  """

  @doc """
  Extract file IDs from a response.
  """
  @spec extract_file_ids(map()) :: [String.t()]
  def extract_file_ids(response)

  @doc """
  Download a file by ID.
  """
  @spec download(String.t(), keyword()) :: {:ok, binary(), String.t()} | {:error, term()}
  def download(file_id, opts \\ [])

  @doc """
  Get file metadata.
  """
  @spec metadata(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
  def metadata(file_id, opts \\ [])
end
```

### Usage Example

```elixir
defmodule MyApp.AnthropicSkillChat do
  @moduledoc """
  Example: Using Anthropic's hosted skills with full conversation support.
  """

  alias Conjure.Session.Anthropic, as: Session
  alias Conjure.Conversation.Anthropic, as: Conversation

  def chat_with_hosted_skills(user_message) do
    # Configure skills (up to 8)
    skills = [
      {:anthropic, "xlsx", "latest"},
      {:anthropic, "pptx", "latest"}
    ]

    # Start session
    session = Session.new(skills)

    # Chat with pause_turn handling for long-running operations
    case Session.chat(session, user_message, on_pause: &log_pause/2) do
      {:ok, response, updated_session} ->
        # Download any created files
        file_ids = Conjure.Files.Anthropic.extract_file_ids(response)
        files = Enum.map(file_ids, &download_file/1)

        {:ok, response, files, updated_session}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp log_pause(response, attempt) do
    IO.puts("Operation paused (attempt #{attempt}), continuing...")
  end

  defp download_file(file_id) do
    {:ok, content, filename} = Conjure.Files.Anthropic.download(file_id)
    File.write!(filename, content)
    filename
  end
end
```

### Comparison: Local/Docker vs Anthropic Hosted

| Aspect | Local/Docker | Anthropic Hosted |
|--------|--------------|------------------|
| Who executes | Your application | Anthropic's container |
| Skill location | Local filesystem | Uploaded to Anthropic |
| Conversation loop | Tool call/result loop | pause_turn handling loop |
| Multi-turn state | Message history | Container ID + messages |
| Skills per request | Unlimited | Up to 8 |
| Network from sandbox | Configurable | Never (isolated) |
| Pre-built skills | N/A | xlsx, pptx, docx, pdf |
| File output | Local filesystem | Files API download |
| Cost | Your infrastructure | Anthropic API pricing |

## Consequences

### Positive

- **No local Docker required** - Anthropic manages containers
- **Pre-built document skills** - Access xlsx, pptx, docx, pdf without custom implementation
- **Multi-skill workflows** - Combine up to 8 skills per request
- **Long-running support** - pause_turn handling for complex operations
- **Persistent sessions** - Container ID reuse for multi-turn conversations
- **Anthropic-managed security** - Sandbox isolation handled by Anthropic
- **Consistent environment** - Same execution environment for all users
- **File output support** - Files API for downloading created documents

### Negative

- **Beta feature** - API may change, requires beta headers
- **Skills must be uploaded** - Cannot use local skill files directly
- **Network dependency** - Requires connectivity to Anthropic API
- **No network from container** - Skills cannot make external API calls
- **No runtime package installation** - Only pre-installed packages available
- **Upload size limit** - 8MB maximum per skill
- **Skills limit** - Maximum 8 skills per request
- **Conversation loop required** - Must handle pause_turn for long operations

### Neutral

- **Different architecture** - Not an executor, but API integration
- **Complementary to local** - Can use both approaches in same application
- **Version management** - Skills have versions, can pin for stability
- **Two loop types** - Local uses tool call/result loop, hosted uses pause_turn loop

## Alternatives Considered

### Executor Behaviour Implementation (Original Design)

The original ADR proposed implementing `Conjure.Executor.Anthropic` as a behaviour that returns `{:passthrough, config}`. This was rejected because:

- Based on incorrect understanding of the API
- Anthropic doesn't accept arbitrary bash commands
- Skills must be uploaded first, not passed through
- The conversation loop model doesn't apply—it's a single request

### Transparent Upload on Execute

Automatically upload skills when first used. Rejected because:

- Requires API key in executor context
- Upload is a separate concern from execution
- Better to make upload explicit for version control
- Skills have IDs that should be managed explicitly

### Skip Anthropic Integration Entirely

Only support local/Docker execution. Rejected because:

- Users may want hosted execution without Docker
- Pre-built document skills are valuable
- Anthropic's security expertise for sandboxing
- Useful for environments where Docker isn't available

## References

- [Anthropic Skills API Guide](https://platform.claude.com/docs/en/build-with-claude/skills-guide)
- [Code Execution Tool Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool)
- [Files API Documentation](https://docs.anthropic.com/en/api/files-content)
- [ADR-0002: Pluggable Executor Architecture](0002-pluggable-executor-architecture.md)
- [ADR-0004: API-Client Agnostic Design](0004-api-client-agnostic-design.md)
- [ADR-0019: Unified Execution Model](0019-unified-execution-model.md)