fabric/tool

Typed application tools.

A Definition owns a tool’s name, description, and input and output codecs. Binding it to a handler produces a Tool(context): the typed input, output, and error disappear behind one invocation, so tools with unrelated types share one list. The declaration the model sees and the decoder that checks its arguments come from the same input codec.

The handler receives the run’s context, the Call it answers (its run, its action and the run’s correlation), and the decoded business input. A handler that makes requests of its own tags them with call.correlation (for example http_gun.with_correlation), so its work joins the run’s events.

A tool bound with bind_settling also receives a Settlement: a handle with which its result can be settled after the invocation’s task was stopped (a cancellation, a host fault in another action, or a task that died), for a tool whose effect outlives its task, such as a workflow that compensates after it is cancelled. The run accepts at most one settlement per action, and only while the action awaits one:

Whichever is accepted first, a settlement or a reconciliation, is the only one: every later settlement is refused. A refused settlement changes nothing. settle then says whether that loses anything: AlreadyRecorded when the action has a definite result (its task reported, or it was settled or reconciled), NotAwaited when it does not (the bound passed, or the run ended with the action uncertain), and the refusal needs a person; SettleUnconfirmed when the store did not confirm either way. A settlement offered while the action’s task may still run waits for it to report or be stopped, up to the tool’s bound between commits of the run; one the handler offers from its own task is refused at once. The handle reaches the run through the store its invocation ran with.

With saga

Copy the module saga_tool of consumers/saga_tool into the application; that consumer compiles and tests it. Saga reports its outcome after task cancellation; Fabric accepts settlement up to rollback_within. The workflow and its steps inherit the run’s correlation. saga_tool.tool binds a definition with tool.bind_settling and runs the workflow through saga/reporting.run_owned. A definite failure in the report is tool.Explain and an unknown one is tool.Uncertain; a reporting error whose effect never started is Explain, and one whose effect status is unknown is Uncertain.

Calling Relay tools

Copy the module relay_tools of consumers/relay_tools into the application; that consumer compiles and tests it. relay_tools.tool binds a Relay tool definition as a Fabric tool: it calls the peer with the call’s correlation and idempotency key and requires the typed output. relay_tools.failure maps a failure to Uncertain when the request may have been sent and the tool is not marked read-only, and to Explain otherwise. See the README and the design layer for composition and failure handling.

Discovering Relay tools

Copy the module relay_discovery of consumers/relay_tools into the application; that consumer compiles and tests it. relay_discovery.discovered builds a tool from a discovered declaration. It refuses a name the model cannot use (UnsupportedName) and a schema outside the supported contract (UnsupportedSchema), calls with the discovered declaration, and maps failures with relay_tools.failure. See the README and the design layer for composition and failure handling.

Types

The call a handler answers. Read it by label: Fabric may add fields.

run and action name this call durably (the action survives restarts, so it can key an idempotent request), and correlation is the run’s (see fabric.start): pass it to the packages the handler calls, such as http_gun.with_correlation or Saga’s execution.with_correlation.

pub type Call {
  Call(
    run: run.RunId,
    action: run.ActionId,
    correlation: correlation.Correlation,
  )
}

Constructors

A tool’s name, description, and input and output codecs. Build one with define.

pub type Definition(input, output) =
  @internal Definition(input, output)

What a typed handler error means.

pub type Failure {
  Explain(message: String)
  Uncertain(evidence: String)
}

Constructors

  • Explain(message: String)

    The action definitely failed without its effect. The model sees {"error": message} as the tool’s result and the run continues.

  • Uncertain(evidence: String)

    The effect may or may not have happened. The run stops before its next model turn until the action is reconciled; it is never retried.

Why a settlement was not recorded. Nothing changed in either case.

pub type SettleError {
  AlreadyRecorded
  NotAwaited
  SettleUnconfirmed(reason: String)
}

Constructors

  • AlreadyRecorded

    The action already has a definite result: its task reported, or it was settled or reconciled. Nothing the settlement knows is lost.

  • NotAwaited

    The action does not await a settlement and has no definite result: its task did not stop within the tool’s bound, the bound passed, the run ended with the action uncertain, or an uncertain settlement was offered for an action that is already uncertain. What the settlement knows reaches the run only through a person (fabric.reconcile); the refusal is observed as settlement_refused (fabric/telemetry).

  • SettleUnconfirmed(reason: String)

    The run could not be read or written, or its runner did not take the settlement: whether it was recorded is not confirmed. A write the store reported unavailable has an unknown outcome.

A handle on one invocation of a tool bound with bind_settling, to settle its result after its task was stopped. Whoever holds it may settle; the first settlement the run accepts is the only one.

pub opaque type Settlement(output)

A tool bound to its handler, for an agent’s tool list. Build one with bind or bind_settling.

pub type Tool(context) =
  @internal Tool(context, Call, SettleError)

Values

pub fn action(settlement: Settlement(output)) -> run.ActionRef

The action this settlement settles: its run and its action id, the reference fabric.reconcile takes when the settlement is refused and a person reconciles instead.

pub fn bind(
  definition: Definition(input, output),
  handler: fn(context, Call, input) -> Result(output, error),
  classify: fn(error) -> Failure,
) -> @internal Tool(context, Call, SettleError)

Binds a typed handler. classify decides, for every typed error, what it means: a definite failure the model sees (Explain), or an effect that may have happened (Uncertain), which blocks the run until it is reconciled. There is no default: a timeout after a request was sent must not look like a clean failure the model could simply retry.

The handler runs in its own task with the run’s context, the Call it answers and the decoded input. Its body is bounded by the agent’s timeout (agent.with_tool_timeout, 60 s by default) or with_timeout.

pub fn bind_settling(
  definition: Definition(input, output),
  handler: fn(context, Call, input, Settlement(output)) -> Result(
    output,
    error,
  ),
  classify: fn(error) -> Failure,
  settle_within within: duration.Duration,
) -> @internal Tool(context, Call, SettleError)

Binds a typed handler that also receives its invocation’s Settlement (see the module documentation), and whose stopped run waits up to settle_within for that settlement before recording an uncertain effect. settle_within must be at least 1 ms and at most 2^32 - 1 ms, the longest timer the runtime sets (agent.build checks it). The handler’s own result is used when it returns; the settlement matters only once its task was stopped, also by its body timeout. classify is as for bind.

pub fn define(
  name: String,
  description: String,
  input: codec.Codec(input),
  output: codec.Codec(output),
) -> Definition(input, output)
pub fn description(
  definition: Definition(input, output),
) -> String

The description the model reads.

pub fn idempotency_key(call: Call) -> String

An external idempotency key for this logical action. Stable across replay, independent of correlation, with unambiguous part boundaries.

pub fn input(
  definition: Definition(input, output),
  action: policy.Action,
) -> Result(option.Option(input), String)

The typed input of action when it calls definition: the policy’s typed match on a tool. Ok(None) when the action calls another tool. A graph node whose operation has the tool’s name (built from the same definition, say) matches too.

Error(detail) when the action names this definition’s tool but its arguments do not decode with this definition’s input codec: the definition the policy matches on is not the one the agent’s tool was bound from (another definition shares its name). The runtime decoded the arguments with the bound tool’s codec before the policy ran, so this happens only when the two drifted apart. Pass it on as the policy’s error, which stops the run: an action the policy cannot read is never allowed by a fall-through.

fn policy(member: Member, action: policy.Action) {
  use reservation <- result.try(tool.input(reserve_definition(), action))
  case reservation {
    Some(Reservation(isbn:)) -> check_reservation(member, isbn)
    None -> Ok(policy.Allow)
  }
}
pub fn input_codec(
  definition: Definition(input, output),
) -> codec.Codec(input)

The codec of the tool’s arguments: its JSON Schema is what the model is told, and it decodes what the model sends. Another runtime that serves the same tool (an MCP server, say) can declare and decode with it.

pub fn name(definition: Definition(input, output)) -> String

The name the model calls the tool by.

pub fn output_codec(
  definition: Definition(input, output),
) -> codec.Codec(output)

The codec of the tool’s result, as the model reads it.

pub fn reconciliation(
  definition: Definition(input, output),
  result: Result(output, String),
) -> Result(String, codec.EncodeError)

The content fabric.reconcile and fabric.reconcile_stored take for an action of definition: the output encoded with the tool’s output codec, as the handler’s result would have been, or for Error(message) the definite failure the model sees ({"error": message}, as for Explain). Use it instead of writing the JSON by hand.

let assert Ok(content) =
  tool.reconciliation(refund_definition, Ok(Refund(id: "r-1")))
fabric.reconcile(handle, uncertain.reference, content)
pub fn settle(
  settlement: Settlement(output),
  result: Result(output, Failure),
  summary summary: String,
) -> Result(Nil, SettleError)

Settles the invocation’s result: an output, encoded with the tool’s output codec, or a typed failure, Explain (definite) or Uncertain. Blocks until the run has recorded or refused it. A refusal is observed (settlement_refused) with summary: what a person needs to reconcile the action when the run did not record the settlement. The summary goes to telemetry handlers, so it must not carry secrets.

pub fn unconfirmed_reconciliation(note: String) -> String

The content fabric.reconcile takes when a person cannot yet say what happened: {"unconfirmed": note}, which tells the model the effect may or may not have happened and why, so it does not report the effect as done or as failed.

An uncertain effect waits for a reconciliation, and the run with it. Leave it unreconciled while the answer is near: the run stays Suspended, and fabric.reconcile with reconciliation later records what happened. Reconcile it as unconfirmed only to let the run go on without the answer: the action then ends Reconciled and leaves the run’s uncertain effects, so the application tracks the open question from there, and a policy that must not repeat the effect refuses a second call itself.

let content =
  tool.unconfirmed_reconciliation("finance is checking with the provider")
fabric.reconcile(handle, uncertain.reference, content)
pub fn valid_name(name: String) -> Bool

Whether a tool name fits the model providers’ shared grammar: 1–64 ASCII letters, digits, underscores or hyphens.

pub fn with_replay(
  tool: @internal Tool(context, Call, SettleError),
  max_attempts: Int,
) -> @internal Tool(context, Call, SettleError)

Lets the runtime start this tool’s body again, up to max_attempts starts in all, when an attempt ends without a result of its own: its body crashed, ran past its timeout, or its runner was lost (a restart, a lost node). Without it, such an action becomes an uncertain effect that a person reconciles.

Use it only for a tool whose effect is safe to repeat: a read, or a write the handler makes idempotent (keyed by call.run and call.action, which stay the same across attempts). A typed failure the handler returns (Explain, Uncertain) is never replayed, nor is a tool stopped by a cancellation. An approved action that is replayed after a lost runner asks for its approval again, as any approved action does at recovery. agent.build refuses fewer than 1 or more than 100 attempts (InvalidToolLimit); run.ActionRecord.replays counts the replays.

pub fn with_timeout(
  tool: @internal Tool(context, Call, SettleError),
  timeout: run.Timeout,
) -> @internal Tool(context, Call, SettleError)

Bounds this tool’s body by timeout instead of the agent’s (agent.with_tool_timeout): run.After(duration), or run.Infinity for a body that may run as long as it needs. A body still running at its timeout is stopped and its action becomes an uncertain effect (it may have acted), unless the tool is replayable (with_replay). agent.build refuses a timeout under 1 ms or over 2^32 - 1 ms (InvalidToolLimit). A sub-agent delegation has no body: its child run’s own limits bound it.

✨ Search Document