fabric/agent

Pure agent configuration.

A Spec describes an agent: its name, model, tools, policy, its final answer, and the bounds of its runs, set with the with_* functions. build checks it once and reports every problem at once; only build makes the Agent that fabric.start, fabric.open and fabric.recover take, so a run never starts under an invalid agent. Building starts nothing.

let assert Ok(desk) =
  agent.new("desk", model, [refund_tool], policy)
  |> agent.with_answer(resolution_codec)
  |> agent.with_max_turns(6)
  |> agent.with_approval_expiry(run.After(duration.hours(24)))
  |> agent.with_approvers(desk_approvers)  // who may answer approvals
  |> agent.build

Every wait is bounded by default:

BoundDefaultSetter
model attempts per run8with_max_turns
tool bodies at once4with_max_concurrency
tokens per runnone (opt-in)with_token_budget
sub-agent runs per run4with_max_children
sub-agent nesting1 levelwith_max_depth
policy decision5 swith_policy_timeout
first model retry delay200 ms, doubling up to 64 timeswith_model_retry_delay
command waiting for the runner5 swith_command_timeout
model call600 swith_model_timeout
tool body60 swith_tool_timeout, tool.with_timeout
tool result1 MiBwith_max_result_bytes
approval request7 dayswith_approval_expiry
final answers a typed answer may take2 (one corrective turn)with_answer_attempts
family budgetnone (opt-in)with_family_budget

A timeout that may be unbounded is a run.Timeout: run.Infinity must be asked for.

An agent’s final answer is its model’s text (Spec(context, String)) until with_answer gives it a codec: the model is then asked for that schema, and a run completes with the decoded value (run.Completed(answer)). An answer that does not decode gets one corrective turn by default (with_answer_attempts); the run ends with run.AnswerInvalid when that answer does not decode either.

Types

A checked agent whose runs end with an answer. Only build makes one.

pub type Agent(context, answer) =
  @internal Agent(context, answer)

Why build refused a spec. This union may grow: match the variants you handle and keep a catch-all, or use describe_config_error.

pub type ConfigError {
  DuplicateToolName(String)
  InvalidToolName(String)
  ToolSchemaUnavailable(String)
  AnswerSchemaUnavailable
  InvalidIdentity(name: String, version: Int)
  InvalidLimit(
    limit: Limit,
    value: Int,
    minimum: Int,
    maximum: Int,
  )
  InvalidToolLimit(
    tool: String,
    limit: Limit,
    value: Int,
    minimum: Int,
    maximum: Int,
  )
}

Constructors

  • DuplicateToolName(String)
  • InvalidToolName(String)

    Providers accept tool names matching ^[a-zA-Z0-9_-]{1,64}$.

  • ToolSchemaUnavailable(String)

    The tool’s input codec has no JSON Schema to declare to the model.

  • AnswerSchemaUnavailable

    The answer codec (with_answer) has no JSON Schema to give the model.

  • InvalidIdentity(name: String, version: Int)

    The name is empty or the version is not positive.

  • InvalidLimit(
      limit: Limit,
      value: Int,
      minimum: Int,
      maximum: Int,
    )

    A bound is outside minimum..maximum (both included). Durations are in milliseconds.

  • InvalidToolLimit(
      tool: String,
      limit: Limit,
      value: Int,
      minimum: Int,
      maximum: Int,
    )

    A bound one tool sets for itself (tool.bind_settling, tool.with_timeout, tool.with_replay) is outside minimum..maximum.

A bound build checks, named after its setter. This union may grow.

pub type Limit {
  MaxTurns
  MaxConcurrency
  TokenBudget
  MaxChildren
  MaxDepth
  PolicyTimeout
  ModelRetryDelay
  CommandTimeout
  ModelTimeout
  ToolTimeout
  MaxResultBytes
  ApprovalExpiry
  FamilyWork
  FamilyChildren
  FamilyDepth
  SettleWithin
  ReplayAttempts
  AnswerAttempts
}

Constructors

  • MaxTurns
  • MaxConcurrency
  • TokenBudget
  • MaxChildren

    At most 999: with the depth limit, every child run id (the parent’s id, -, and a sequence number) fits the 128 characters of a run id.

  • MaxDepth

    At most 16.

  • PolicyTimeout

    At most 2^32 - 1 ms, the longest timer the runtime can set, like every timeout below.

  • ModelRetryDelay

    At most (2^32 - 1) / 64 ms, so that the longest delay still fits a timer.

  • CommandTimeout
  • ModelTimeout
  • ToolTimeout
  • MaxResultBytes
  • ApprovalExpiry
  • FamilyWork

    budget.limits(work:).

  • FamilyChildren

    budget.with_children.

  • FamilyDepth

    budget.with_depth, at most 63.

  • SettleWithin

    tool.bind_settling’s settle_within.

  • ReplayAttempts

    tool.with_replay’s attempts, at most 100.

  • AnswerAttempts

    with_answer_attempts, at most 100.

An agent’s description, checked by build. answer is the type of its final answer: String until with_answer.

pub opaque type Spec(context, answer)

Values

pub fn build(
  spec: Spec(context, answer),
) -> Result(Agent(context, answer), List(ConfigError))

Checks spec and reports every problem at once. A sub-agent was checked by its own build.

pub fn describe_config_error(error: ConfigError) -> String

One line naming the problem and the setter that changes it.

pub fn describe_config_errors(
  errors: List(ConfigError),
) -> String

One line for every problem build reported, in its order, joined with "; ":

case agent.build(spec) {
  Ok(desk) -> desk
  Error(errors) -> panic as agent.describe_config_errors(errors)
}
pub fn new(
  name: String,
  model: @internal Port(
    model.Request,
    model.Reply,
    model.ModelError,
  ),
  tools: List(@internal Tool(context, tool.Call, tool.SettleError)),
  policy: fn(context, policy.Action) -> Result(
    policy.Decision,
    String,
  ),
) -> Spec(context, String)

An agent named name, with the given model, tools, and policy, version 1 and the default bounds (see the module documentation). A stored run records the name and version it started with and continues only under the same pair. The policy is required: there is no implicit allow (policy.always_allow() is the explicit one).

pub fn with_answer(
  spec: Spec(context, String),
  answer: codec.Codec(answer),
) -> Spec(context, answer)

Gives the agent a typed final answer. The model is asked for answer’s JSON Schema (model.Request.answer, which fabric/llm sends as the provider’s structured output format), and Fabric decodes the model’s final text with answer before it commits the run’s end: a run completes with the decoded value (run.Completed(value)). A text that does not decode gets a corrective turn (with_answer_attempts, one by default), and the run ends with run.AnswerInvalid(raw:, reason:) when the last attempt does not decode either. The run stores the text, so the stored record is the same as for a plain agent.

Any codec with a schema works, a record, a codec.union, a list or a scalar: fabric/llm wraps an answer whose schema has no object root as {"answer": ..} for the provider and unwraps the reply, so the run stores the answer’s own JSON. llm_wire’s structured output does not take a codec.union yet, so through fabric/llm such an answer fails the turn as model.InvalidRequest; a model built with model.new answers it directly. build refuses a codec without a schema (AnswerSchemaUnavailable).

pub fn with_answer_attempts(
  spec: Spec(context, answer),
  attempts: Int,
) -> Spec(context, answer)

How many final answers a run asks the model for, in all, before it gives up on a typed answer (with_answer). When the model’s final text does not decode, the run does not end at once: the text stays in the transcript, and the model gets one more turn with a message that says why the answer was refused and repeats the answer’s JSON Schema. That turn is a model attempt like any other: it counts against with_max_turns and with_token_budget, and it is stored, so a recovered run goes on with it. The run ends with run.AnswerInvalid(raw:, reason:), for the last answer, when the attempts or the budget run out.

Default 2 (one corrective turn); 1 ends the run on the first refused answer. build refuses fewer than 1 or more than 100 (InvalidLimit(AnswerAttempts, ..)). A plain agent’s answer is its text and is never refused.

pub fn with_approval_expiry(
  spec: Spec(context, answer),
  expiry: run.Timeout,
) -> Spec(context, answer)

How long an approval request this agent issues waits for an answer. Its deadline is stored with the request (run.PendingApproval.expires). After it the request expires: the action is rejected as run.Expired, the model sees that its approval expired, and the run goes on. A late fabric.approve or fabric.reject is refused with fabric.ApprovalExpired.

An expired request is rejected by whoever touches the run next: fabric.await, an answer, fabric.recover, or on a leased store the sweeper (fabric/sweeper), which finds it when it is due. Deadlines are set and judged by the store’s clock (store.now), so every node judges them alike. Default 7 days; run.Infinity never expires. Requests stored without a deadline never expire.

pub fn with_approvers(
  spec: Spec(context, answer),
  approvers: @internal Approvers(credential, approvers.Denial),
) -> Spec(context, answer)

Who may answer this agent’s approval requests: fabric.approve and fabric.reject take a proof from approvers.check with these approvers, checked for the request’s requirement (see fabric/approvers), and refuse any other. An agent without approvers refuses every answer (approvers.NoApprovers): its policy may never require an approval, or its requests wait until they expire. A sub-agent without approvers of its own takes its parent’s.

The approvers are not stored: a run opened or recovered with another agent value is answered with that value’s approvers.

pub fn with_command_timeout(
  spec: Spec(context, answer),
  timeout: duration.Duration,
) -> Spec(context, answer)

How long a command (approve, reject, cancel, reconcile) waits for the run’s live runner to take it. A runner busy for longer (for example held by a synchronous telemetry handler) refuses the command with fabric.RunnerBusy, and never applies it later. Default 5 s.

pub fn with_family_budget(
  spec: Spec(context, answer),
  limits: budget.Limits,
) -> Spec(context, answer)

One budget shared by a root run of this agent and all the runs it delegates to (see fabric/budget), stored with the root. Failed or uncertain reservations keep their charge. It complements the agent’s own turn, token and delegation limits, and applies only when this agent starts a root run: a sub-agent shares its root’s. The store must write agent records of version 7 or later (fabric.FamilyBudgetUnsupported).

pub fn with_max_children(
  spec: Spec(context, answer),
  children: Int,
) -> Spec(context, answer)

Sub-agent runs one run starts, at most 999. A delegation beyond it is refused before the policy, and the model sees why. Default 4.

pub fn with_max_concurrency(
  spec: Spec(context, answer),
  tools: Int,
) -> Spec(context, answer)

Tool bodies of one run that execute at the same time. Default 4.

pub fn with_max_depth(
  spec: Spec(context, answer),
  levels: Int,
) -> Spec(context, answer)

Levels of sub-agents below a run of this agent, at most 16 (1: its children may not delegate in turn). A child is bounded by its own setting and by what its parent has left. Default 1.

pub fn with_max_result_bytes(
  spec: Spec(context, answer),
  bytes: Int,
) -> Spec(context, answer)

The largest tool result, in bytes of its encoded content, that a run keeps: a result is stored and sent to the model on every later turn. A larger result stops the run with run.OutputEncodingFailed, naming this limit; the tool’s effect has happened. Default 1 MiB.

pub fn with_max_turns(
  spec: Spec(context, answer),
  turns: Int,
) -> Spec(context, answer)

Model attempts per run, counting the first request and every retry. Default 8.

pub fn with_model_retry_delay(
  spec: Spec(context, answer),
  delay: duration.Duration,
) -> Spec(context, answer)

The wait before the first retry of a retryable model failure. The delay doubles with each consecutive retryable failure, up to 64 times this value; a provider’s own delay (model.retry_after) is waited when it is longer, up to 10 minutes. Every attempt still counts against the turn limit, and a cancelled run does not wait. Default 200 ms.

pub fn with_model_timeout(
  spec: Spec(context, answer),
  timeout: run.Timeout,
) -> Spec(context, answer)

How long one model call may take, from the moment it is issued (a retry’s delay is not counted). A call still running then is stopped and is a model.TimedOut error, retried like any other and spending a turn. The default, 600 s, matches llm_wire’s whole-call deadline, so fabric/llm is never cut short by it. run.Infinity leaves a model call unbounded.

pub fn with_policy_timeout(
  spec: Spec(context, answer),
  timeout: duration.Duration,
) -> Spec(context, answer)

How long one policy decision may take. A policy that gives no decision in time has failed: the run stops closed. The policy runs in its own process. Default 5 s.

pub fn with_sub_agent(
  spec: Spec(context, answer),
  definition: tool.Definition(input, output),
  to child: Agent(context, output),
  prompt prompt: fn(input) -> String,
) -> Spec(context, answer)

Lets the model delegate to a sub-agent: a call to definition (declared to the model like any tool) starts a run of child in the same store, with prompt(input) as its prompt, once the policy allows it. The policy sees policy.StartAgent as the action’s target, and may require an approval like for any tool; no child run exists before it is allowed. The child is its own run with its own limits and policy, and shares this agent’s context type and store. It starts with the context its start was allowed with: this run’s, or for an approved start, the context the answer was checked with. Its approvals are this run’s pending approvals (their references name the child run), and cancelling this run cancels it.

The child’s answer is the call’s output: definition’s output type is the child’s answer type (with_answer, or String for a plain child), and its output codec encodes the result the model sees. A child that ended otherwise (its answer invalid, refused, cancelled, out of budget, failed) is a definite failure that names how it ended. A child that ended with effects of unknown status (for example cancelled while a tool ran) makes the call an uncertain effect however it ended.

pub fn with_system_prompt(
  spec: Spec(context, answer),
  text: String,
) -> Spec(context, answer)
pub fn with_token_budget(
  spec: Spec(context, answer),
  tokens: Int,
) -> Spec(context, answer)

Input plus output tokens per run, as the provider reports them. A reply without usage then stops the run with run.BudgetUnverifiable, which is why there is no default.

pub fn with_tool_timeout(
  spec: Spec(context, answer),
  timeout: run.Timeout,
) -> Spec(context, answer)

How long one tool body may run once it has started. A body still running then is stopped and its action becomes an uncertain effect (it may have acted), which the run waits to have reconciled, unless the tool is replayable (tool.with_replay); a tool bound with tool.bind_settling may still settle it. tool.with_timeout overrides it for one tool. Sub-agent runs are bounded by their own limits instead. Default 60 s; run.Infinity leaves tool bodies unbounded.

pub fn with_version(
  spec: Spec(context, answer),
  version: Int,
) -> Spec(context, answer)

Changes the version a run records. Change it when a change to the agent must not continue older runs.

✨ Search Document