Skip to main content

docs/tool_adapters.md

# Tool Adapters

Squidie exposes a small tool boundary for workflow steps that need to talk
to external systems.

## Contract

Tool adapters implement `Squidie.Tools.Adapter` and are invoked through
`Squidie.Tools.invoke/4`.

```elixir
{:ok, result} =
  Squidie.Tools.invoke(MyApp.Tools.SomeAdapter, request, context)
```

The shared contract is:

- request: a map owned by the adapter
- context: a workflow or step context map
- success: `{:ok, %Squidie.Tools.Result{}}`
- failure: `{:error, %Squidie.Tools.Error{}}`

## Normalized Result

`Squidie.Tools.Result` contains:

- `adapter`: the adapter module
- `payload`: the normalized adapter response
- `metadata`: adapter metadata such as request method or URL

## Normalized Error

`Squidie.Tools.Error` contains:

- `adapter`: the adapter module
- `kind`: normalized error kind
- `message`: stable human-readable message
- `details`: adapter-specific details in a plain map
- `retryable?`: whether the failure is a reasonable candidate for workflow retry

Steps can convert tool errors into plain maps with
`Squidie.Tools.Error.to_map/1` before returning them as workflow step
failures.

## HTTP Adapter

`Squidie.Tools.HTTP` is the first concrete adapter.

Supported request shape:

- `method`
- `url`
- `headers`
- `params`
- `body`
- `json`
- `timeout`

Successful responses are normalized to:

- `status`
- `headers`
- `trailers`
- `body`

HTTP responses with status `>= 400`, transport failures, and timeouts are
normalized into `Squidie.Tools.Error`.

## HTTP Runtime Action

`Squidie.Step.HTTP` wraps the HTTP adapter as a native workflow step for
runtime-authored specs. Hosts expose it through the action registry under a
stable key:

```elixir
registry = %{
  "http.request" => [
    module: Squidie.Step.HTTP,
    category: "HTTP",
    action_opts: [allowed_hosts: ["api.example.test"]],
    credential_requirements: [%{name: "billing_api", required?: true}]
  ]
}
```

The step expects a `request` map with `method` plus either `url` or
`url_template`. Supported request fields are `headers`, `query_params` or
`params`, `body`, `json`, and `timeout`. URLs must not include userinfo or a
query string; use `query_params` for query data. `url_template` placeholders
use `{{ name }}` syntax and are expanded from the `bindings` map.

Use `Squidie.Step.HTTP.validate_request/1` to validate structural request
configuration without policy. Use `validate_request/2` or
`validate_action_input/2` with the same host-owned `action_opts` before
starting a runtime-authored run. The runtime also invokes `validate_action_input/2`
before appending journal facts for planned runtime-spec attempts.

`allowed_hosts` is required in `action_opts` for execution. Credential values
do not belong in the request map; pass host-owned references through
`credential_refs` and let a host wrapper or transport boundary decide how
references become headers. The reusable action rejects common secret-bearing
headers and payload keys rather than persisting them. Raw string bodies require
`allow_body?: true` in `action_opts`.

Successful responses are returned as `%{http_response: response}` with headers
redacted. Response and error bodies are omitted by default; hosts can opt into
bounded body persistence with `persist_response_body?: true` and
`max_body_bytes: ...`. HTTP and transport errors are converted to structured
step errors with redacted details; retryable tool errors return `{:retry, error}`
so normal workflow retry policy remains the only retry scheduler. Redirects are
disabled at the shared HTTP adapter boundary.

## Elixir Runtime Action

`Squidie.Step.Elixir` invokes host-approved Elixir adapters from
runtime-authored specs. Hosts expose the step through the action registry and
provide executable adapter definitions in registry-owned `action_opts`:

```elixir
registry = %{
  "elixir.run" => [
    module: Squidie.Step.Elixir,
    category: "Elixir",
    input_contract: %{
      adapter: %{type: :string, required?: true, enum: ["billing.load_invoice"]},
      params: %{type: :map, required?: true}
    },
    action_opts: [
      adapters: %{
        "billing.load_invoice" => {Billing.Actions, :load_invoice},
        "billing.reprice" => Billing.Actions.Reprice
      }
    ]
  ]
}
```

Runtime input names only an approved `adapter` key and a `params` map:

```elixir
%{
  adapter: "billing.load_invoice",
  params: %{invoice_id: "inv_123"}
}
```

The reusable action never loads modules, creates atoms, or selects functions
from runtime-authored text. Start-time validation uses the registry action
options, but persisted runtime specs store only safe adapter metadata. Workers
that execute Elixir runtime actions should pass the same host-owned
`action_registry:` to `Squidie.execute_next/1` so current adapter policy is
resolved at execution.

Hosts should override `input_contract` when they want editor catalogs to show
the approved adapter choices. Adapter definitions may be a module with `run/2`
or `run/1`, a `{module, function}` tuple, or a keyword/map entry with
`:module`, `:function`, display metadata, and optional boolean `:enabled?`.
Adapter functions return `{:ok, map}`, `{:error, reason}`, or
`{:retry, reason}`.

## Retry Boundary

The HTTP adapter disables Req's built-in retry loop.

That keeps retry policy in one place:

- adapters report the first failure
- workflow steps declare retry policy
- Squidie appends the next journal dispatch attempt with the resolved retry
  visibility time

This keeps transport behavior predictable and avoids stacking HTTP-client
retries underneath workflow retries.