# `Pixir.Tool`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/tool.ex#L1)

The Tool behaviour (CONTEXT.md; the Kimojo-shaped contract minus `risk_level`).

A Tool declares itself via `__tool__/0` and runs via `execute/2`. Per ADR 0005 every
Tool is also **dry-runnable**: `use Pixir.Tool` injects a default `dry_run/2` that
reports the planned action without side effects; side-effecting Tools override it
with a richer (still effect-free) plan.

## `__tool__/0`

    %{
      name: "read",
      description: "Read a file from the workspace",
      parameters: %{                       # JSON Schema (string keys)
        "type" => "object",
        "properties" => %{"path" => %{"type" => "string"}},
        "required" => ["path"]
      }
    }

## `execute/2` and `dry_run/2`

Receive `args` (a string-keyed map, validated by the Executor) and a `context`
`%{session_id, workspace, call_id, dry_run}`. They return:

  * `{:ok, result}` — `result` is a string-keyed map; `"output"` (a string) is the
    model-facing payload (token-bounded via `truncate/2`, ADR 0005).
  * `{:error, structured}` — the standard `%{ok: false, error: %{kind, message,
    details}}` envelope (use `error/3`).

# `context`

```elixir
@type context() :: %{
  :session_id =&gt; String.t(),
  :workspace =&gt; String.t(),
  :call_id =&gt; String.t(),
  optional(:dry_run) =&gt; boolean()
}
```

# `kind`

```elixir
@type kind() ::
  :invalid_args
  | :unknown_tool
  | :outside_workspace
  | :protected_path
  | :not_found
  | :resource_missing
  | :no_match
  | :not_unique
  | :read_failed
  | :write_failed
  | :command_failed
  | :timeout
  | :permission_denied
  | :write_policy_denied
  | :bash_disabled
  | :detached
  | :backpressure
  | :iteration_cap
  | :tool_result_record_failed
  | :session_record_unavailable
  | :unsafe_state_path
  | :corrupt_log_line
  | :ephemeral_not_loggable
  | :log_encode_failed
  | :log_read_failed
  | :log_write_failed
  | :session_writer_active
  | :session_writer_stale
  | :session_writer_ambiguous
  | :session_writer_lost
  | :provider_http_error
  | :model_not_supported
  | :usage_limit_reached
  | :rate_limited
  | :network
  | :provider_refusal
  | :unsupported_transport
  | :unsupported_backend_capability
  | :context_overflow
  | :stream_idle_timeout
  | :not_authenticated
  | :insecure_auth_transport
  | :token_request_failed
  | :no_account_id
  | :invalid_response
  | :corrupt_auth
  | :auth_read_failed
  | :auth_write_failed
  | :device_auth_failed
  | :device_code_unsupported
  | :session_start_failed
  | :no_prompt
  | :stdin_error
```

The curated, stable error-`kind` vocabulary (ADR 0005, rule 3). Callers and the model
branch on these, so the set is documented here as the single source of truth — adding a
kind is a deliberate change, not an ad-hoc string. Grouped by origin:

  * **tools / executor** — `:invalid_args`, `:unknown_tool`, `:outside_workspace`,
    `:protected_path`, `:not_found`, `:resource_missing`, `:no_match`, `:not_unique`,
    `:read_failed`, `:write_failed`, `:command_failed`, `:timeout`,
    `:permission_denied`, `:write_policy_denied`, `:bash_disabled`, `:detached`,
    `:backpressure`
  * **turn loop** — `:iteration_cap`, `:tool_result_record_failed`,
    `:session_record_unavailable`
  * **log / writer lease** — `:unsafe_state_path`, `:corrupt_log_line`, `:ephemeral_not_loggable`,
    `:log_encode_failed`, `:log_read_failed`, `:log_write_failed`,
    `:session_writer_active`, `:session_writer_stale`,
    `:session_writer_ambiguous`, `:session_writer_lost`
  * **provider** — `:provider_http_error`, `:model_not_supported`, `:usage_limit_reached`,
    `:rate_limited`, `:network`, `:provider_refusal`, `:unsupported_transport`,
    `:unsupported_backend_capability`, `:context_overflow`, `:stream_idle_timeout`
  * **auth** — `:not_authenticated`, `:insecure_auth_transport`,
    `:token_request_failed`, `:no_account_id`, `:invalid_response`, `:corrupt_auth`,
    `:auth_read_failed`, `:auth_write_failed`, `:device_auth_failed`,
    `:device_code_unsupported`, `:session_start_failed`
  * **cli / stdin** — `:no_prompt`, `:stdin_error`

Note: a `bash` command that runs but exits nonzero is **not** an error — it returns a
successful result `%{"output", "exit_code", "ok" => false}` so the model can read the
output and decide (a no-match `grep` exiting 1 is normal). See ADR 0005.

# `result`

```elixir
@type result() :: {:ok, map()} | {:error, map()}
```

# `spec`

```elixir
@type spec() :: %{name: String.t(), description: String.t(), parameters: map()}
```

# `dry_run`

```elixir
@callback dry_run(args :: map(), context()) :: result()
```

# `execute`

```elixir
@callback execute(args :: map(), context()) :: result()
```

# `error`

```elixir
@spec error(kind() | atom(), String.t(), map()) :: map()
```

Build the standard structured error envelope (ADR 0005). `kind` should be a member of
the documented `t:kind/0` vocabulary so callers can branch on it.

# `truncate`

```elixir
@spec truncate(binary(), pos_integer()) :: binary()
```

Token-bound a model-facing string (ADR 0005). Truncates to `max` bytes with an
explicit marker so the model knows output was cut.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
