ADR-0002: Pluggable executor architecture via behaviours
View SourceStatus
Accepted
Context
Conjure must execute tool calls (bash commands, file operations) in various environments:
- Local development: Fast iteration, no isolation needed
- Production: Strong isolation via containers
- Custom environments: Firecracker microVMs, Kubernetes pods, remote VMs
Note: Anthropic's Skills API provides an alternative hosted execution model, but it is NOT an executor implementation. See ADR-0011 for details on Skills API integration.
A single execution strategy cannot satisfy all use cases. Different deployments have different:
- Security requirements
- Performance characteristics
- Available infrastructure
- Compliance constraints
Elixir provides behaviours as a first-class abstraction for defining pluggable interfaces with compile-time guarantees.
Decision
We will define a Conjure.Executor behaviour that all execution backends must implement.
defmodule Conjure.Executor do
@callback bash(command :: String.t(), context :: ExecutionContext.t()) :: result()
@callback view(path :: Path.t(), context :: ExecutionContext.t(), opts :: keyword()) :: result()
@callback create_file(path :: Path.t(), content :: String.t(), context :: ExecutionContext.t()) :: result()
@callback str_replace(path :: Path.t(), old_str :: String.t(), new_str :: String.t(), context :: ExecutionContext.t()) :: result()
@callback init(context :: ExecutionContext.t()) :: {:ok, ExecutionContext.t()} | {:error, term()}
@callback cleanup(context :: ExecutionContext.t()) :: :ok
@optional_callbacks [init: 1, cleanup: 1]
endExecutor selection is explicit at the call site:
Conjure.execute(tool_call, skills, executor: Conjure.Executor.Docker)We will provide two built-in executors:
Conjure.Executor.Local- Direct execution viaSystem.cmdConjure.Executor.Docker- Container-isolated execution
Note: Anthropic's Skills API (see ADR-0011) provides hosted execution but uses a different integration pattern—it is not an executor implementation.
Consequences
Positive
- Users can implement custom executors for their infrastructure
- Behaviour provides compile-time contract verification
- Clear separation between execution strategy and business logic
- Easy to test with mock executors
- No runtime overhead from abstraction (direct function calls)
Negative
- Users must explicitly choose an executor (no "smart" default)
- Each executor must implement all callbacks, even if some are no-ops
- Executor bugs can be hard to diagnose across abstraction boundary
Neutral
init/1andcleanup/1are optional for stateless executors- Context is threaded through all calls for consistency
Alternatives Considered
Protocol-based dispatch
Using Elixir protocols would allow executor selection based on the execution context type. Rejected because:
- Protocols dispatch on data type, but executor choice is a deployment decision
- Would require wrapper structs for each executor
- Less explicit than module-based selection
GenServer-based executors
Each executor could be a GenServer managing its own state. Rejected because:
- Adds process overhead for simple local execution
- Complicates the API with async patterns
- State management is better handled at the session level
Configuration-only selection
Executor could be set globally via application config. Rejected because:
- Prevents per-request executor selection
- Makes testing harder
- Reduces flexibility for mixed environments