Browse docs

Start here

examplesGetting started with Flowdocumentation

Design

FoundationsLanguage architecturePhilosophy

Language specification

Program checkingConcurrencyDataEffectsResults, Tool problems, and faultsGrammarHistoryModules and importsLanguage specificationEvaluationStandard libraryToolsTypes

Runtime

Runtime architectureThe host boundaryDiagnosticsRunning a programThe history format

Guides

Writing programs that reach checkpointsImplementing Tools with a toolkitLoops that never returnRecursive delegationSharing types between Tool modulesHarnesses over tool registries

Tools

This specification owns Flow's one way to reach the outside: the @tool declaration of a function whose body is outside Flow, the @tool type declaration of a value only the host can make, calling a Tool, Tools as function values, generic Tools, what may cross the boundary in each direction, which types decoding may build, and the language's view of a Flow run used as a Tool.

It does not own the rest of the boundary. errors.md owns ToolProblem and what each of its cases claims. effects.md owns !tool. types.md owns the data-type category, opaque, and visibility. history.md owns what a call records and how replay serves it. The runtime ABI owns the value encoding and the decoding table, the Tool process protocol, schemas, declaration identity and fingerprints, binding, flow-tool.toml, toolkits and adapters. execution.md owns starting, pausing and resuming runs, nested runs included.

A Tool is a function whose body is outside Flow

A function has one of three kinds of body:

Body Declared as Implemented by
Flow source flow f(...) -> T = ... the program
the compiler @lang(name) flow f(...) -> T the compiler, in std only
the outside world @tool flow f(...) -> Result<T, E> the host

A @tool declaration is a top-level function declaration with no body. It is imported, made pub, documented and named like any other function, and its signature is written in full:

// github.com/acme/tools/openai.flow
@tool pub flow chat(prompt: Text) -> Result<Text, ChatError>

// github.com/acme/tools/browser.flow
@tool pub type Page
@tool pub flow open(url: Text) -> Result<Page, BrowseError>
@tool pub flow click(page: Page, selector: Text) -> Result<Unit, PageError>
@tool pub flow close(page: Page) -> Result<Unit, PageError>
Open in playground →
  • A @tool signature returns Result<T, E>, because any Tool call can fail. A Tool that declares no errors of its own writes Never for E:

    @tool pub flow sleep(d: duration.Duration) -> Result<Unit, Never>
    Open in playground →
  • The declaration carries no effect mark. Calling it is what has an effect; effects.md owns that rule.

  • Any module may declare Tools, a runnable file included. A Tool a program can reach must be bound by the host before the run starts, or the run is refused (execution.md).

  • The compiler lists every @tool function a program can reach, so a run's whole outside surface is known before it starts and tooling can print it.

Serves foundations: Tools are declarations, not implementations.

Calling a Tool

Calling a @tool function is a Tool call. For a Tool declared to return Result<T, E>, the call's type is Result<T, ToolProblem<E>>, and the calling function is !tool:

use "github.com/acme/tools@v1/openai"
use "github.com/acme/tools@v1/browser"

flow sign_in(url: Text) -> SignIn !tool = match browser.open(url) {
    Ok(page) => match page.click("#login") {
        Ok(()) => SignedIn(page),
        Err(problem) => ClickFailed(problem),
    },
    Err(problem) => OpenFailed(problem),
}
Open in playground →

page.click("#login") is browser.click(page, "#login") by method syntax, because browser declares Page (semantics.md).

A Tool call evaluates its arguments left to right, as any call does (semantics.md). The call and its reply are recorded in history, each before its consequence (history.md), and the reply is checked against the declared type before the program sees it.

A reply that does not match the declared type is BadReply, recorded like any other outcome. The other ways a call can end without a value of the declared type are ToolProblem's remaining cases (errors.md). An Err is ordinary data: it does not leave the function or stop a traversal.

A Tool call is the only way a program reaches anything outside its closed evaluation. There is no ambient clock, randomness, filesystem, network, process, environment or person: each is a Tool, and receives exactly these rules. Std's clock module is @tool declarations the host implements (stdlib.md). Flow does not classify Tools as reads or writes, safe or unsafe, repeatable or not, and infers nothing from a Tool's name, module or provider.

A host may retry inside one call, route it, sandbox it, ask for approval or refuse it. None of that is a Flow form (history.md).

Serves foundations: visible outside influence, recorded before its consequence and honest uncertainty.

Host types

@tool type declares a type only the host can make:

@tool pub type Page
@tool pub type Model
@tool pub flow complete(model: Model, prompt: Text) -> Result<Text, ModelError>
@tool pub flow primary() -> Result<Model, Never>
@tool pub flow backup() -> Result<Model, Never>
Open in playground →
  • A host value is an opaque token. A program can hold one, store it in a record, list or task result, pass it, return it and send it back to its host as a Tool argument. It cannot build one, look inside one, compare one, order one or turn one into text.
  • A host type is never data (types.md), and neither is any type that contains one. A run's input and output therefore never carry one.
  • Host types are nominal. A Page can't be passed where a Session is expected.
  • Host values come only from Tool calls. History records the host's token in the reply (history.md). If the host no longer recognizes a token, every call that passes it returns NotRun.
  • Several implementations of one operation are instances. A program that needs a primary and a backup model declares Model and takes each instance from a Tool call, as above; which implementation backs each one is the host's choice.

A test fake may make values of a host type for its test; no other Flow code can (execution.md).

This is how stateful things work (a browser page, a sandbox session, a configured model) while staying typed.

Tools are function values

A @tool function is an ordinary function value. Its type is its signature with the result wrapped as a call's result is, and with !tool:

let fetch: (Request) -> Result<Reply, ToolProblem<FetchError>> !tool = workbench.run
let replies = list.map(requests, workbench.run)          // this call is !tool
Open in playground →

A program never builds a Tool, never receives one from a Tool call, and never looks a Tool up by name. There is no Tool value type beside function types.

Serves foundations: typed, composable crossings.

Generic Tools

A @tool function may be generic. The caller picks the type through the result it expects, inferred like any type argument (types.md):

@tool pub flow extract<T>(prompt: Text) -> Result<T, ModelError>

let plan: Plan = match model.extract("Plan the migration: ${goal}") {
    Ok(p) => p,
    Err(problem) => return Failed(describe(problem)),
}
Open in playground →
  • Each call sends the schema of its concrete type with the request, and the reply is checked against that type like any other. History records the concrete type with the call.
  • The type argument must be a data type that decoding can build: one with no opaque part (Decoding below). Inside a generic function the requirement is inferred and passed to its callers, and is checked at the call where the type becomes concrete.

One declaration serves every structured output.

What crosses

What Allowed
Tool arguments Any data type, private fields included, and host values going back to their host
Tool replies, and anything else decoded Data types with no opaque part, plus host values anywhere inside them
A run's input and output Data types
Functions and tasks Never, in either direction

Private fields protect how a value is built, not who may see it. Sending one to a Tool forges nothing, and hiding a value's fields from the host would be a confidentiality label, which Flow does not have.

Effects never cross: !tool and !pure are checked by the compiler and are not part of the Tool protocol. How values are encoded, and the exact decoding rules for missing fields, null, extra fields and numbers, belong to the runtime ABI.

Decoding

Decoding builds a value of a declared type from outside data. A Tool reply, json.decode (stdlib.md) and a run's input are decoded.

Decoding can't build an opaque type. A decoded type may not be, or contain, an opaque type from any module. A module uses opaque for a value it hands out only after a check, and no decoder may forge one:

pub opaque type Approved { Approved(Plan) }

let fake: review.Approved = match model.extract("...") { ... }
// error: review.Approved is opaque, so it can't be decoded
Open in playground →
  • Private fields don't block decoding. They hide fields from other modules; a type that must not be forged is declared opaque. A module that needs an opaque value decodes a plain one and converts it through its own function.
  • "Not opaque" belongs to the type alone, like "is data", and is inferred for type variables the same way.
  • Host values may appear anywhere in a reply. The host makes them; decoding does not build them.
  • Rebuilding checkpoint arguments is not decoding. The program made those values itself, so resuming rebuilds them whatever their type, opaque types and host values included (history.md).

A value that decodes proves only that Flow can interpret it. It does not make the content true, the provider authentic, or the world consistent with it.

Serves foundations: typed, composable crossings and bounded trust and disclosure.

Runtime-shaped data

There is no dynamic type. Data whose shape is known only at runtime is typed through a generic Tool or kept as Text. Std's json.Value is an enum std marks for raw JSON encoding (runtime/spec/abi.md), for passing arbitrary JSON through untouched:

pub type Value {
    Null,
    Bool(Bool),
    Number(Decimal),
    Text(Text),
    List(List<Value>),
    Object(Dict<Text, Value>),
}
Open in playground →

Option<json.Value> cannot appear in a type that crosses a boundary: an absent value and Some(Null) would encode the same way. json.Value already holds Null, so a field that may be absent is a plain json.Value.

A program over an open tool registry, where the host offers tools the program does not know in advance, passes json.Value arguments to one generic call the host provides:

@tool pub flow invoke<T>(name: Text, arguments: json.Value) -> Result<T, InvokeError>
Open in playground →

That is an ordinary Tool. Its name argument is data like any other, and the host decides what it means.

A model choosing a Tool

A program that asks a model what to do next receives a reply like any other, so the reply has a type: an enum of the actions the program allows.

type Action {
    Read(ReadArgs),
    Search(SearchArgs),
    Finish(Text),
}

@tool pub flow next(context: List<Turn>) -> Result<Action, ModelError>

match planner.next(context) {
    Ok(Read(args)) => step(context, files.read(args.reference)),
    Ok(Search(args)) => step(context, web.search(args.text)),
    Ok(Finish(text)) => Done(text),
    Err(problem) => Failed(problem),
}
Open in playground →

The implementation shows the model the Action schema and decodes its answer into it. Every action the model can request is visible in one enum, and a malformed answer is a ToolProblem, not a crash. Flow has no reflection over Tools and no call that names a Tool by text.

Where implementations come from

The language prescribes no process, transport or topology for a Tool's implementation, and a file that is run has no configuration beside it. The host binds each reachable @tool declaration to an implementation, keyed by the declaration's identity, before the run starts; it may override any binding, for a test, a proxy or an implementation registered in code. A module repository may say how to run its own Tools. Replay binds nothing. The runtime ABI owns identity, fingerprints, binding and repository configuration, and execution.md owns whether a host asks before running a repository's implementation.

Serves foundations: applications remain applications.

A Flow run as a Tool

Checking, starting and resuming other Flow programs is a Tool the host provides, declared with @tool like any other in std's runner module (stdlib.md); the language adds nothing for it. execution.md owns its operations and declarations, what a nested run returns, and how its values cross.

Refusals

The compiler rejects:

  • a @tool function with a body, or a @tool type with one;
  • a @tool function whose result type is not Result<T, E>;
  • a Tool parameter whose type is neither data nor a host type, and a declared T or E that decoding cannot build, such as one containing a function, a task or an opaque type;
  • a generic Tool call whose type argument is not data or contains an opaque type;
  • json.decode or a run's input at a type decoding cannot build;
  • building, comparing, ordering or interpolating a host value;
  • a function or a task as a Tool argument, a Tool reply, or part of a run's input or output.

The runtime turns a reply that does not match its declared type into BadReply and refuses to start a run with an unbound Tool.

Not in the language

  • A Tool<P> value type, Tools as entry parameters or top-level slots, and Tools returned from Tool calls. Host types cover instances without them.
  • Binding by name strings, which would be a second global namespace.
  • A configuration file beside a runnable program.
  • A dynamic Any or Json type.
  • Classifying Tools by kind (filesystem, network, process, person, clock) or by safety.
  • Hidden re-entry: an implementation cannot call back into the run that called it. An application that wants a provider to start more work starts or resumes a run through the host.