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:
- The run is stopping. Once the executor has confirmed that the
action’s task no longer runs, the run waits, up to the tool’s bound
(
settle_within), for the settlement before it ends, and records it as the action’s result, definite or uncertain. A settlement offered before that confirmation waits for it (the task may still be acting); one offered after the bound is refused, and the action stays an uncertain effect. - The run continues. An action that became an uncertain effect while the run was not stopping (its task died, or its runner was lost and the run recovered) accepts a definite settlement at any later time, recorded like a reconciliation of exactly that action; the run then continues. An uncertain settlement adds nothing and is refused.
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
-
Call( run: run.RunId, action: run.ActionId, correlation: correlation.Correlation, )
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
-
AlreadyRecordedThe action already has a definite result: its task reported, or it was settled or reconciled. Nothing the settlement knows is lost.
-
NotAwaitedThe 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 assettlement_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.