# `Planck.Agent.Sidecar`
[🔗](https://github.com/alexdesousa/planck/blob/v0.2.2/lib/planck/agent/sidecar.ex#L1)

Behaviour and utilities for sidecar applications that extend planck_headless
over distributed Erlang.

## Behaviour

A sidecar entry-point module implements one required callback:

- `tools/0` — returns `[Planck.Agent.Tool.t()]` with full `execute_fn` closures.
  These run **locally on the sidecar node**.

## Module-level utilities

`Planck.Agent.Sidecar` itself provides module-level functions that
planck_headless calls on the sidecar node via `:rpc.call/5`. Because
`planck_agent` is a dependency of both planck_headless and the sidecar, these
are available on both nodes:

- `discover/0` — finds the module implementing this behaviour (cached in
  `:persistent_term` after the first call).
- `list_tools/0` — discovers the entry module and returns its tools as
  `[Planck.AI.Tool.t()]` (no closures, serialisable across nodes).
- `list_tools/1` — same but takes an explicit module; intended for tests.
- `execute_tool/3` — discovers the entry module and executes a named tool.
- `execute_tool/4` — same but takes an explicit module; intended for tests.
- `list_widgets/0` — discovers the entry module and returns the widget
  modules paired with its tools via each tool's `:widget` field. There is no
  separate `widgets/0` callback; see `Planck.Agent.Widget`.
- `list_widgets/1` — same but takes an explicit module; intended for tests.
- `widget_render/2` — discovers the entry module and renders a widget by id.
- `widget_action/3` — discovers the entry module and dispatches an action to
  a widget by id.
- `widget_container/1` — discovers the entry module and returns a widget's
  declared container type by id (`:modal` if unset).
- `set_locale/1` — caches the current UI locale in `:persistent_term`,
  idempotently. Not tied to any i18n library — see below.
- `get_locale/0` — returns the cached locale, `"en"` if never set.

planck_headless calls:

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :list_tools, [])
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :execute_tool,
              [tool_name, agent_id, args], timeout)
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :list_widgets, [])
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_render,
              [widget_id, myself])
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_action,
              [widget_id, action, args])
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_container,
              [widget_id])
    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :set_locale, [locale])

## Minimal example

    defmodule MySidecar.Planck do
      use Planck.Agent.Sidecar

      @impl true
      def tools do
        [
          Planck.Agent.Tool.new(
            name: "run_tests",
            description: "Run the test suite. Pass timeout_ms to override the default.",
            parameters: %{
              "type" => "object",
              "properties" => %{
                "timeout_ms" => %{
                  "type" => "integer",
                  "description" => "Max milliseconds to wait (default 120000)"
                }
              }
            },
            execute_fn: fn _agent_id, _id, args ->
              timeout = Map.get(args, "timeout_ms", 120_000)
              case System.cmd("mix", ["test"], timeout: timeout) do
                {output, 0} -> {:ok, output}
                {output, _} -> {:error, output}
              end
            end
          )
        ]
      end
    end

See `specs/sidecar.md` for the full sidecar design, and `specs/widgets.md`
for the widget mechanism specifically.

# `tools`

```elixir
@callback tools() :: [Planck.Agent.Tool.t()]
```

Return the sidecar's tools as `Planck.Agent.Tool` structs (with `execute_fn`).

This is the only required callback. `execute_fn` closures run **locally on the
sidecar node** — they are never serialised or called on planck_headless.

Each tool should accept an optional `"timeout_ms"` argument in its parameter
schema so the AI can hint at how long to wait for the tool call.

# `__using__`
*macro* 

Convenience macro for implementing the `Planck.Agent.Sidecar` behaviour.

`use Planck.Agent.Sidecar` injects:

- `@behaviour Planck.Agent.Sidecar` — marks the module as a sidecar entry point.
- A default `tools/0` returning `[]` — override this to provide tools.

## Usage

    defmodule MySidecar.Planck do
      use Planck.Agent.Sidecar

      @impl true
      def tools do
        [
          Planck.Agent.Tool.new(
            name: "run_tests",
            description: "Run the test suite.",
            parameters: %{"type" => "object", "properties" => %{}},
            execute_fn: fn _agent_id, _id, _args ->
              {out, 0} = System.cmd("mix", ["test"])
              {:ok, out}
            end
          )
        ]
      end
    end

The `tools/0` function is the only thing you normally need to override.
`list_tools/0`, `discover/0`, `execute_tool/3`, and `execute_tool/4` are
**not** injected here — they are module-level functions on
`Planck.Agent.Sidecar` itself that planck_headless calls on the sidecar node:

    :rpc.call(node, Planck.Agent.Sidecar, :list_tools, [])
    :rpc.call(node, Planck.Agent.Sidecar, :execute_tool,
              [tool_name, agent_id, args], timeout)

This design keeps the dispatch logic in `planck_agent` (available on both
nodes) rather than requiring each sidecar module to implement it. No config
is needed — `list_tools/0` discovers the entry module automatically via
`discover/0`.

# `discover`

```elixir
@spec discover() :: module() | nil
```

Discover the module in the current node that implements `Planck.Agent.Sidecar`.

Scans modules across all loaded OTP applications and returns the first one
whose `@behaviour` attribute includes `Planck.Agent.Sidecar`, or `nil` if
none is found. Only Elixir modules (names starting with `"Elixir."`) are
checked; Erlang modules are skipped.

Successful results are cached in `:persistent_term`. `nil` results are **not**
cached — the next call will retry the scan, which is useful when the sidecar
entry module is loaded after `discover/0` is first called.

Called by planck_headless on the sidecar node via `list_tools/0`. You
normally do not need to call this directly.

# `execute_tool`

```elixir
@spec execute_tool(String.t(), String.t(), String.t(), map()) ::
  {:ok, term()} | {:error, term()}
```

Discover the entry module and execute a named tool.

Called by planck_headless on the sidecar node:

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :execute_tool,
              [tool_name, agent_id, tool_call_id, args], timeout)

The `timeout` is read from `args["timeout_ms"]` by the planck_headless RPC
wrapper, not by this function.

# `execute_tool`

```elixir
@spec execute_tool(module(), String.t(), String.t(), String.t(), map()) ::
  {:ok, term()} | {:error, term()}
```

Execute a named tool via an explicit sidecar module's `tools/0` list.

Intended for tests. Production code should use `execute_tool/3`.

# `get_locale`

```elixir
@spec get_locale() :: String.t()
```

Returns the cached current UI locale, `"en"` if `set_locale/1` was never
called (e.g. before `planck_headless`'s sidecar connects, or in tests).

# `list_tools`

```elixir
@spec list_tools() :: [Planck.AI.Tool.t()]
```

Discover the sidecar entry module and return its tools as `[Planck.AI.Tool.t()]`.

Combines `discover/0` and `list_tools/1`. Returns `[]` if no entry module is
found.

Called by planck_headless on the sidecar node:

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :list_tools, [])

# `list_tools`

```elixir
@spec list_tools(module()) :: [Planck.AI.Tool.t()]
```

Convert `module.tools()` to `[Planck.AI.Tool.t()]` — serialisable, no closures.

# `list_widgets`

```elixir
@spec list_widgets() :: [module()]
```

Discover the sidecar entry module and return the widget modules paired with
its tools via their `:widget` field.

Combines `discover/0` and `list_widgets/1`. Returns `[]` if no entry module
is found. There is no separate `widgets/0` callback — a widget is declared
by setting a `Planck.Agent.Tool.t()`'s `:widget` field, and this just
filters `tools/0` for tools that have one.

Called by planck_headless on the sidecar node:

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :list_widgets, [])

# `list_widgets`

```elixir
@spec list_widgets(module()) :: [module()]
```

Return the widget modules paired with an explicit module's `tools/0`.

Intended for tests. Production code should use `list_widgets/0`.

# `set_locale`

```elixir
@spec set_locale(String.t()) :: :ok
```

Caches the sidecar-wide current UI locale (e.g. `"en"`, `"es"`).

Called by `planck_headless` (via `Planck.Headless.Locale.set/1`) on the
sidecar node whenever `Planck.Web.Locale.Plug` resolves a locale — on
every request, in practice, since the plug has no cheap way to know
whether this call would be a no-op before making it. So this function is
itself idempotent instead: absent, it's set; present and equal, no-op;
present and different, updated. A widget module decides what to do with
the value — this function knows nothing about Gettext or any other i18n
library, only that it's tracking "the current locale" as a plain string.

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :set_locale, [locale])

# `widget_action`

```elixir
@spec widget_action(String.t(), String.t(), map()) :: :ok | {:error, term()}
```

Dispatch an action to a widget by id, via the discovered entry module.

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_action,
              [widget_id, action, args])

# `widget_container`

```elixir
@spec widget_container(String.t()) ::
  {:ok, Planck.Agent.Widget.container()} | {:error, term()}
```

Return a widget's declared container type by id — `:modal`, `:drawer`, or
`:fullscreen`. See `c:Planck.Agent.Widget.container/0`. Defaults to
`:modal` if the widget doesn't implement `container/0`.

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_container, [widget_id])

# `widget_render`

```elixir
@spec widget_render(String.t(), term()) :: {:ok, term()} | {:error, term()}
```

Render a widget by id, via the discovered entry module.

`myself` is passed through opaquely — see `c:Planck.Agent.Widget.render/1`.

    :rpc.call(sidecar_node, Planck.Agent.Sidecar, :widget_render,
              [widget_id, myself])

---

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