Skip to main content

guides/operation-source-contracts.md

# Operation Source Contracts

`Jidoka.Operation.Source` is the single seam between the runtime and any
external operation surface (Jido actions, Ash resources, browsers, MCP
servers, sub-agents, workflows). Every source compiles to the same two
outputs: a list of `Jidoka.Agent.Spec.Operation` data and one runtime
capability function. This guide documents that contract and shows how the
built-in sources adopt it.

## When To Use This

- Use this guide when authoring a new operation source (custom backend,
  internal tool registry, third-party SDK adapter).
- Use this guide when wiring multiple sources together into one agent.
- Do not use this guide for authoring individual operations or actions; for
  that see [Agent DSL](agent-dsl.md) and `Jidoka.Action`.

## Prerequisites

- You have read [Agent Spec Contract](agent-spec-contract.md) and
  [Turn And Effect Contracts](turn-and-effect-contracts.md).
- You can build and run a Jidoka turn.

## Quick Example

`Source.Local` is the simplest source. It compiles a list of in-process
handlers into the same shape every other source returns.

```elixir
alias Jidoka.Operation.Source

{:ok, source} =
  Source.Local.new(
    operations: [
      %{
        name: "local_time",
        description: "Returns local time for a city.",
        handler: fn args ->
          {:ok, %{city: Map.get(args, "city", "Chicago"), time: "09:30"}}
        end
      }
    ]
  )

{:ok, compiled} = Source.compile(source)

compiled.operations
#=> [%Jidoka.Agent.Spec.Operation{name: "local_time", ...}]

compiled.capability
#=> #Function<...>   (operation_capability/2)
```

`compiled.operations` is what the spec stores. `compiled.capability` is what
the harness invokes when an LLM decides to call an operation.

## Concepts

```diagram
╭───────────────╮     ╭──────────────────╮     ╭─────────────────────╮
│ Source struct │────▶│ Source.compile/1 │────▶│ %{operations,       │
│ (Local,       │     │                  │     │   capability}       │
│  Ash, MCP,    │     ╰──────────────────╯     ╰──────────┬──────────╯
│  Browser, …)  │                                         ▼
╰───────────────╯                              ╭──────────────────────╮
                                               │  Turn.State pending   │
                                               │  Effect.Intent        │
                                               │  └─ capability.(…)    │
                                               ╰──────────────────────╯
```

A source is a struct that implements the `Jidoka.Operation.Source` behaviour.
Two callbacks - `operations/2` and `capability/2` - return the data and the
function the runtime needs. `Source.compile/1` validates name uniqueness
across multiple sources and produces a single routed capability.

## Fields

### `compile/1` Output

`Jidoka.Operation.Source.compile/2` returns `{:ok, compiled()}` where:

| Field | Type | Purpose |
| --- | --- | --- |
| `operations` | `[Jidoka.Agent.Spec.Operation.t()]` | Flat list across all sources, suitable for `Agent.Spec.operations`. |
| `capability` | `t:Jidoka.Runtime.Capabilities.operation_capability/0` | Routed function: looks up the source by operation name and forwards the intent. |

Duplicate operation names across sources fail with
`{:error, {:duplicate_operation_source_name, name}}`.

### `operation_capability/2` Signature

The capability is a two-arity function that mirrors the LLM capability shape:

```elixir
@type operation_capability ::
        (Jidoka.Effect.Intent.t(), Jidoka.Effect.Journal.t() ->
           {:ok, term()} | {:error, term()})
```

| Argument | Purpose |
| --- | --- |
| `Effect.Intent` (kind `:operation`) | Carries the normalized `Effect.OperationRequest` payload, idempotency key, and id. |
| `Effect.Journal` | Read-only view of recorded intents/results, used for replay-safety checks. |

The capability returns the raw operation output on success. The runtime wraps
that output into an `Effect.Result` and an `Effect.OperationResult` for you;
sources should not build those structs themselves.

### `Jidoka.Operation.Source` Behaviour

Two callbacks form the contract:

| Callback | Purpose |
| --- | --- |
| `operations(source, opts) :: {:ok, [Spec.Operation.t()]} \| {:error, term()}` | Return the operation data the spec will store. |
| `capability(source, opts) :: {:ok, operation_capability()} \| {:error, term()}` | Return the executor function. |

Sources are plain structs. The first positional argument to each callback is a
`%__MODULE__{}`; the second is an opts keyword forwarded from
`Source.compile/2`.

### `Source.Local`

In-process operation source for tests and lightweight tools.

| Field | Type | Purpose |
| --- | --- | --- |
| `:operations` | `[operation_def()]` | List of `%{name, handler, description?, idempotency?, kind?, metadata?}` entries. |

Handlers must be 1- or 2-arity functions returning `{:ok, term()}` or
`{:error, term()}`. See [`Jidoka.Operation.Source.Local`](`Jidoka.Operation.Source.Local`).

### Other Built-In Sources

All adopt the same `Source` behaviour:

- [`Jidoka.Operation.Source.MCP`](`Jidoka.Operation.Source.MCP`) - MCP server
  tools.
- [`Jidoka.Operation.Source.Subagent`](`Jidoka.Operation.Source.Subagent`) -
  nested Jidoka agent as a callable operation.
- [`Jidoka.Operation.Source.Handoff`](`Jidoka.Operation.Source.Handoff`) -
  hand-off operations.
- [`Jidoka.Operation.Source.Workflow`](`Jidoka.Operation.Source.Workflow`) -
  workflow-backed operations.

External integrations such as Ash/Jido and the browser source ship in their
own packages but compile to the same `%{operations, capability}` output.

## Common Patterns

- **Compile once, reuse everywhere.** Build the source struct at boot or in a
  module attribute; call `Source.compile/1` only when materializing a spec.
- **Combine sources by listing them.** `Source.compile([local, mcp])`
  returns one routed capability the harness can call directly.
- **Keep capability functions pure-ish.** Capabilities should be deterministic
  given the intent and journal; record any external state through the
  operation's output so the journal stays authoritative.
- **Use `Effect.OperationRequest.from_input/1`** inside capabilities to decode
  the payload safely instead of pattern-matching the raw map.

## Testing

A source test only needs the compile output and an `Effect.Intent`. No harness
is required.

```elixir
test "local source executes its handler" do
  {:ok, source} =
    Jidoka.Operation.Source.Local.new(
      operations: [
        %{name: "echo", handler: fn args -> {:ok, args} end}
      ]
    )

  {:ok, compiled} = Jidoka.Operation.Source.compile(source)

  intent =
    Jidoka.Effect.Intent.new(:operation,
      %{name: "echo", arguments: %{"value" => 42}},
      idempotency: :pure
    )

  assert {:ok, %{"value" => 42}} =
           compiled.capability.(intent, Jidoka.Effect.Journal.new!())
end
```

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `{:error, {:duplicate_operation_source_name, name}}` | Two sources publish the same operation. | Rename one operation or drop the duplicate source. |
| `{:error, {:missing_operation_handler, name}}` | The compiled capability cannot route the operation. | Ensure the operation is published by a source compiled into the same plan. |
| `{:error, {:unsupported_effect_kind, kind}}` | Capability was called with an `:llm` intent. | Operation capabilities only handle `:operation` intents; route LLM intents through `Runtime.Capabilities.llm`. |
| Local source raises `invalid_operation_handler` | Handler is not 1- or 2-arity. | Use `fn args -> ... end` or `fn args, context -> ... end`. |

## Reference

- [`Jidoka.Operation.Source`](`Jidoka.Operation.Source`) - behaviour and
  `compile/2`.
- [`Jidoka.Operation.Source.Local`](`Jidoka.Operation.Source.Local`) -
  in-process source.
- [`Jidoka.Operation.Source.MCP`](`Jidoka.Operation.Source.MCP`),
  [`Jidoka.Operation.Source.Subagent`](`Jidoka.Operation.Source.Subagent`),
  [`Jidoka.Operation.Source.Handoff`](`Jidoka.Operation.Source.Handoff`),
  [`Jidoka.Operation.Source.Workflow`](`Jidoka.Operation.Source.Workflow`).
- [`Jidoka.Runtime.Capabilities`](`Jidoka.Runtime.Capabilities`) - capability
  bundle consumed by the harness.
- [`Jidoka.Effect.OperationRequest`](`Jidoka.Effect.OperationRequest`).

## Related Guides

- [Agent Spec Contract](agent-spec-contract.md) - where compiled operations
  live.
- [Turn And Effect Contracts](turn-and-effect-contracts.md) - the
  `Effect.Intent` shape capabilities consume.
- [Runtime And Harness](runtime-and-harness.md) - how the routed capability
  is invoked at run time.