# `WardrikChannel`

Lets people talk to Wardrik's agents from chat apps, such as Slack.

A *channel* is one chat app Wardrik runs, under a name you choose. A *room* is a place in it,
such as a Slack channel.

    config :my_app, MyApp.AgentRuntime,
      channels: [
        slack: {WardrikSlack, token: "xoxb-...", signing_secret: "...", owner: "acme", approvers: ["@maria"]}
      ]

An agent answers in a channel when its definition lists it:

    Wardrik.Agent.define(scope, "acme/triage", model: "anthropic:claude-haiku-4-5", channels: ["slack"])

Mention the bot in a room to start a thread. Reply in the thread to go on. Each thread is its own
conversation, as `acme/triage/slack.T0123.C024BE91L.1696.1234`. Calls the agent waits on are
posted in the thread for someone to decide. See `WardrikChannel.Rooms` and
`WardrikChannel.Replies`.

## Options

Every channel takes these. Its adapter reads the rest.

  * `:token` - the token the channel acts with, in the one workspace it reaches. Needs `:owner`.
  * `:owner` - whose agents answer, as `"acme"`.
  * `:installations` - instead of `:token`, to add one app to many workspaces. Either `:stored`,
    to keep them in Wardrik (needs Wardrik's `:keys`), or `{module, config}`, a
    `Wardrik.Channel.Installations`.
  * `:approvers` - who may decide what agents wait on, as the chat app names people, or
    `:anyone`. `[]` unless set. Whoever asked for a turn may always decide it.
  * `:offering` - what a room with no agent is told.
  * `:deciding` - what someone who may not decide is told.

Raises `ArgumentError` on start if neither or both of `:token` and `:installations` are given,
or if the channel's name isn't lower-case letters, digits and `-`, starting with a letter.

## Writing an adapter

An adapter is a package, such as `wardrik_slack`, that does `use WardrikChannel`. It implements
`c:deliver/2` and `c:include?/4`, and `c:Wardrik.Channel.request/2` to take what the chat app
sends:

    defmodule MyChat do
      use WardrikChannel

      @impl Wardrik.Channel
      def request(opts, %{path: "events", body: body}) do
        channel = WardrikChannel.channel(__MODULE__, opts)
        event = JSON.decode!(body)

        with {:ok, workspace} <- WardrikChannel.workspace(channel, event["team"]) do
          WardrikChannel.received(workspace, %WardrikChannel.Inbound{
            room: event["room"],
            id: event["id"],
            sender: %WardrikChannel.Member{id: event["user"]},
            text: event["text"],
            mentioned?: event["mentioned"]
          })
        end

        %{status: 200, headers: %{}, body: ""}
      end

      @impl WardrikChannel
      def deliver(channel, %WardrikChannel.Outbound{} = outbound) do
        # Post outbound.body in outbound.room, in outbound.thread
        :ok
      end

      @impl WardrikChannel
      def include?(_channel, _installation, names, member), do: member.id in names
    end

Hand each button press to `decide/4`. `use WardrikChannel` defines `child_spec/1`,
`c:children/1` and `c:Wardrik.Channel.notify/4`. Each can be overridden.

# `children`

```elixir
@callback children(WardrikChannel.Channel.t()) :: [
  Supervisor.child_spec() | {module(), term()}
]
```

Returns extra children to run, such as a connection to the chat app. `[]` by default.

# `deliver`

```elixir
@callback deliver(WardrikChannel.Channel.t(), WardrikChannel.Outbound.t()) ::
  :ok | {:error, term()}
```

Posts `outbound` in its thread. Returning an error has it tried again later.

# `include?`

```elixir
@callback include?(
  WardrikChannel.Channel.t(),
  Wardrik.Channel.Installation.t(),
  names :: [String.t()],
  WardrikChannel.Member.t()
) :: boolean()
```

Returns whether `member` is one of `names`, as the chat app names people, such as `@maria`.

# `install_url`
*optional* 

```elixir
@callback install_url(WardrikChannel.Channel.t(), owner :: String.t(), keyword()) ::
  {:ok, String.t()} | {:error, String.t()}
```

Returns the URL where someone acting for `owner` adds the chat app to their workspace.

## Options

  * `:return_to` - where they're sent once it's added.

# `installed`
*optional* 

```elixir
@callback installed(WardrikChannel.Channel.t(), owner :: String.t(), query :: map()) ::
  {:ok, Wardrik.Channel.Installation.t(), String.t() | nil}
  | {:error, String.t()}
```

Finishes adding the chat app to a workspace, for `owner`.

`query` is the query string the chat app redirected back with. Returns the installation and
where to send the person next.

# `uninstall`
*optional* 

```elixir
@callback uninstall(WardrikChannel.Channel.t(), workspace :: String.t()) ::
  :ok | {:error, String.t()}
```

Removes the chat app from `workspace`, where the chat app allows it, and forgets the workspace.

# `channel`

```elixir
@spec channel(module(), keyword()) :: WardrikChannel.Channel.t()
```

Returns `adapter`'s channel, built from the options Wardrik passes it.

# `decide`

```elixir
@spec decide(
  WardrikChannel.Workspace.t(),
  WardrikChannel.Member.t(),
  map(),
  {:approve | :deny, String.t() | nil}
) :: {:ok, Wardrik.Decision.t()} | {:error, String.t()}
```

Approves or denies a call the agent waits on, as `member`.

    waiting = %{conversation: conversation, interrupt: "int_1", asked_by: nil}
    WardrikChannel.decide(workspace, member, waiting, {:approve, "Looks fine."})
    #=> {:ok, %Wardrik.Decision{}}

`waiting` is what the adapter put on the button, from a `WardrikChannel.Waiting`:

  * `:conversation` - the conversation's ID.
  * `:interrupt` - the interrupt's ID.
  * `:asked_by` - who asked for the turn. Optional.

`answer` is `{:approve, note}` or `{:deny, reason}`. Either may be `nil`.

Approvers may decide, and so may whoever asked. Anyone else gets `{:error, deciding}`, the
channel's `:deciding` message.

# `notify`

```elixir
@spec notify(module(), keyword(), String.t(), Wardrik.Notice.t(), [String.t()]) ::
  :ok | {:skip, String.t()} | {:error, term()}
```

Posts `notice` in `room`, asking `approvers`. See `c:Wardrik.Channel.notify/4`.

Posts in the workspace of the agent's owner. That is the owning user's workspace if the channel
is added to one, else the org's. Returns `{:skip, why}` if it's added to neither.

# `received`

```elixir
@spec received(WardrikChannel.Workspace.t(), WardrikChannel.Inbound.t()) ::
  {:asked, String.t()} | :ignored | {:error, String.t()}
```

Sends `inbound` to the room's agent, as its sender, and has the reply posted in its thread.

The agent is asked when:

  * the message mentions the bot
  * the room's agent answers every message
  * the message replies in a thread the agent already answers in

Returns `{:asked, conversation}`, `:ignored` if no agent answers, or `{:error, message}`.

If the agent is waiting on calls, the message queues behind them and the calls are posted again.

# `workspace`

```elixir
@spec workspace(WardrikChannel.Channel.t(), String.t()) ::
  {:ok, WardrikChannel.Workspace.t()} | :error
```

Returns the channel in workspace `id`, or `:error` if it isn't added there.

---

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