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
@toolsignature returnsResult<T, E>, because any Tool call can fail. A Tool that declares no errors of its own writesNeverforE:
Open in playground →@tool pub flow sleep(d: duration.Duration) -> Result<Unit, Never>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
@toolfunction 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
Pagecan't be passed where aSessionis 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
Modeland 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 !toolOpen 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
opaquepart (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 decodedOpen 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
@toolfunction with a body, or a@tool typewith one; - a
@toolfunction whose result type is notResult<T, E>; - a Tool parameter whose type is neither data nor a host type, and a
declared
TorEthat decoding cannot build, such as one containing a function, a task or anopaquetype; - a generic Tool call whose type argument is not data or contains an
opaquetype; json.decodeor 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
AnyorJsontype. - 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.