Operation Source Contracts

Copy Markdown View Source

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 and Jidoka.Action.

Prerequisites

Quick Example

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

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

╭───────────────╮     ╭──────────────────╮     ╭─────────────────────╮
│ 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:

FieldTypePurpose
operations[Jidoka.Agent.Spec.Operation.t()]Flat list across all sources, suitable for Agent.Spec.operations.
capabilityJidoka.Runtime.Capabilities.operation_capability/0Routed 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:

@type operation_capability ::
        (Jidoka.Effect.Intent.t(), Jidoka.Effect.Journal.t() ->
           {:ok, term()} | {:error, term()})
ArgumentPurpose
Effect.Intent (kind :operation)Carries the normalized Effect.OperationRequest payload, idempotency key, and id.
Effect.JournalRead-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:

CallbackPurpose
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.

FieldTypePurpose
: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.

Other Built-In Sources

All adopt the same Source behaviour:

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.

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

SymptomLikely CauseFix
{: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_handlerHandler is not 1- or 2-arity.Use fn args -> ... end or fn args, context -> ... end.

Reference