The host boundary
This specification fixes what crosses between a Flow runtime and the implementations behind a program's Tools: the value encoding, the Tool process protocol, schemas, type descriptions, declaration identity and fingerprints, the binding lifecycle, the Tool configuration file, toolkits, and adapters. Two implementations that agree on this file and its data version interoperate.
It realizes contracts owned above and never redefines them.
tools.md owns what a Tool is, which types may
cross, and which types decoding may build;
errors.md owns ToolProblem and its four
cases; data.md owns what each value means;
types.md owns the data-type category;
history.md owns call ids.
Within the runtime track, execution.md owns how a run is started, paused, resumed and ended across this boundary; trace.md owns how history stores the values encoded here; diagnostics.md owns the diagnostic shape.
One interface, no topology
The protocol is an interface, not a deployment. The same messages and the same rules hold whether an implementation is a function registered in process, a child process, a WebAssembly module, a shared service, or an adapter in front of something else. Flow prescribes none of these and prefers none.
An encoding is required only where an exchange leaves the runtime's address space. An in-process implementation satisfies this file by exchanging the same fields as values.
Nothing in this protocol says what kind of operation a Tool performs or what authority its implementation holds.
Serves foundations: Tools are declarations, not implementations; A general core, bounded in scope.
The value encoding
Every Flow data value crossing this boundary, and every value history stores, is JSON under the rules below. Both sides know the declared type, so the encoding carries no type tags of its own.
| Flow | JSON |
|---|---|
Int |
a number, exact, any size, no fraction or exponent |
Decimal |
a number, exact, normalized |
Bool |
true or false |
Text |
a string |
Bytes |
a string: standard base64 with padding |
Duration, Instant |
a string: ISO 8601 |
List<T> |
an array in list order |
Set<T> |
an array in ascending order, no duplicates |
Dict<Text, V> |
an object |
Dict<K, V>, other keys |
an array of [key, value] pairs in ascending key order |
| tuple | an array of its elements; Unit is [] |
| record | an object keyed by field name, private fields included |
| enum constructor, no payload | a string: "NotRun" |
| enum constructor, one positional value | {"Failed": value} |
| enum constructor, several positional values | {"Pair": [a, b]} |
| enum constructor, named payload | {"Arrived": {"index": 3, "reply": ...}} |
@tool type value |
the host's token, a string |
Optionin a record field is the one shortcut: a field holdingNoneis left out, and a field holdingSome(v)holdsv. Anywhere elseOptionis an ordinary enum,"None"or{"Some": v}.json.Value, an enum std marks for raw JSON encoding (stdlib.md), encodes as the JSON value it stands for, not as a tagged enum, so a program can pass arbitrary JSON through untouched. It is the only such type.- A host token is chosen by the host that made it. Flow stores it, compares nothing about it, and hands it back unchanged.
- Functions and
Taskvalues have no encoding. Nothing that holds one can cross (tools.md). Neverhas no values, so it needs no encoding, and a type that holds it, such asResult<T, Never>, crosses like any other.
The canonical form
One canonical text exists for every value. History stores it, and every hash in this file and in trace.md is taken over it.
- No whitespace outside strings.
- Object keys in ascending code-point order; no duplicate keys.
- Strings escape only
",\and the control characters U+0000 to U+001F, using\b,\f,\n,\r,\twhere one exists and\u00XXotherwise. Every other code point is written as itself in UTF-8. - Numbers in plain decimal notation: no exponent, no
+, no leading zeros, no trailing zeros after a decimal point, no decimal point when the value is whole, and0for zero.Decimalvalues are already normalized (data.md), so1.50and1.5share the text1.5. Instantin UTC with theZsuffix and the fewest fractional-second digits that are exact, on the proleptic Gregorian calendar with no leap seconds, a year outside0000to9999written with a sign and at least four digits (+10000-01-01T00:00:00Z);DurationasPTsecondsS, the seconds a number written as above, with the same rule for the fraction (PT90S,PT0.25S,PT-5S).
Equal values have one canonical text and different values have different ones.
Decoding
Decoding is directed by the declared type. These rules apply to every decoded
value: a Tool reply, json.decode, a run's input, and supplied fork arguments.
Which types may be decoded at all is
tools.md's rule.
| Case | Rule |
|---|---|
| A field the type declares is missing | A decoding failure, unless the field is an Option |
null |
Only for an Option field, where it means None |
| A field the type does not declare | Ignored in a reply and in json.decode; refused in a run's input |
| Numbers | Any JSON number syntax, read exactly; Int needs a whole value |
| Text | Must be valid JSON; an escape naming a lone surrogate is refused |
| Duplicate object keys | Refused |
A Dict with two equal keys |
Refused |
A Set with repeated elements |
The repeats collapse |
| An unknown constructor name | Refused |
Decoding never coerces between types, guesses a constructor, fills a default,
or repairs a value. A value outside any limit execution.md
sets is refused like any other failure to decode: in a Tool reply, including a
reply larger than the host's size limit, that is BadReply, a value the
program can handle, never a fault. A number is held to the limits by its value,
not its spelling: 1e5000 is an Int past the size limit however few
characters it takes, and is refused without being written out.
A decoding failure of a Tool reply is BadReply, with the reason in its text
(errors.md). A decoding failure of a run's
input refuses the run.
Rebuilding a checkpoint's arguments on resume or fork is not decoding: the program made those values, so every type is rebuilt, opaque types and host tokens included (trace.md).
Serves foundations: Typed, composable crossings; Stable meaning.
Schemas
Every data type has one JSON Schema (draft 2020-12), generated from its declaration. Nothing is written by hand.
- A record is an
objectwith its fields asproperties; every field that is not anOptionisrequired, andadditionalPropertiesis not restricted, since replies may carry extra fields. - An enum is a
oneOf, one branch per constructor: aconststring for a constructor without payload, otherwise anobjectwith that single property. Intisinteger,Decimalisnumber,Textisstring,Boolisboolean,Bytesis astringwithcontentEncoding: "base64",Instantis astringwithformat: "date-time", andDurationastringwithformat: "duration".- Lists are
arraywithitems; sets adduniqueItems; tuples useprefixItems;Dict<Text, V>is anobjectwithadditionalProperties, and other dicts anarrayof two-elementprefixItemsarrays. - Named types go into
$defsand are referenced with$ref, so recursive types have finite schemas. Each$defskey is the type's identity, writtenrepository@version/file.Name. ///doc comments becomedescription: a type's on its$defsentry, a field's on its property's schema, a constructor's on itsoneOfbranch, and a named payload field's on its property's schema inside that branch.json.Valueis the empty schema, which accepts any JSON.
A schema describes the encoding; it adds no rule. Where JSON Schema cannot express a rule of the encoding, such as exact number size or sorted set order, the encoding decides.
Serves foundations: Typed, composable crossings.
Type descriptions
A type description retains a type's structure and nominal arguments independently of its value schema. Schemas describe encoding; they do not define equality, comparison or declaration identity. Those remain owned by types.md.
A generated TypeSpec has these JSON members:
{ "generic": [type parameter names],
"params": [{"name": name, "type": term}, ...],
"ok": term,
"err": term,
"declarations": {local declaration id: template, ...} }generic and params retain declaration order. Each template holds name,
identity, lexical_ordinal, generic, opaque, kind and doc. generic
lists its declared parameter names. The ordinal is the source-derived type-name
ordinal defined by trace.md, not its position in the
reachable graph. kind is record, enum, host or nondata. A record holds
an ordered fields array of {"name", "type", "doc"}. An enum holds ordered
constructors, each with name, doc and payload: null for no payload,
{"positional": [term, ...]} for positional parts, or
{"named": [{"name", "type", "doc"}, ...]} for named parts. Host and nondata
templates have no member body. Templates are unsubstituted; one declaration is
not repeated with a different instantiated body.
These metadata object shapes are exact: required members have the types stated
here, and additional members or unknown tags are invalid. Names, identities,
local ids and owners are strings; generic is an array of names, opaque is a
boolean, lexical_ordinal is a positive integer, and doc is a string or null.
The TypeSpec and concrete call-view objects have exactly their five listed
members. A template has exactly the common members above plus fields for a
record or constructors for an enum; host and nondata templates have only the
common members. Fields, constructors, Named, Param and Function objects have
exactly their listed members. A payload object has exactly one positional or
named member, whose value is an array. Generic names are unique within each
owner; parameter and binding names are unique within their arrays. Field names
are unique within a record or one named payload, and constructor names within
one enum. Equal field names in different payloads are not duplicates.
Primitive terms are Int, Decimal, Bool, Text, Bytes, Duration,
Instant, Unit, Never and json.Value, as strings. Compound terms are
one-member objects: List, Set, Option and Task hold one term; Dict
holds an array of exactly two terms; Tuple holds an ordered array of at least
two terms, realizing types.md's
(A, B, ...) form. Unit retains its primitive term. Function holds
{"params": [term, ...], "result": term}. The remaining terms are:
{"Named": {"declaration": local id, "args": [term, ...]}}
{"Param": {"owner": owner, "index": index}}A Named term names one template and supplies its arguments in declaration order.
A Param has a nonnegative integral index within its owner's declared parameters:
a declaration template uses its own local id, and a TypeSpec's Tool roots use
tool. Owners and indices are checked before substitution. Concrete roots have
no free Param. Every reference resolves within the graph, arities agree, and
recursive templates obey types.md.
Validation uses the finite graph rather than copying recursive bodies.
Local ids are references, not identities outside the description, and need not be consecutive. Same-spelling private declarations remain distinct templates; neither a display name nor an identical schema merges them. Member arrays preserve declaration order. Canonical object-key sorting changes none of those arrays. Function, Task and nondata terms are descriptions, not additional value encodings. Whether a position may carry a value is tools.md's rule; an unused nominal argument is not a substituted value field.
flow tools supplies each Tool's generic, params, ok and err terms and
one program types declaration map. That map contains exactly the declarations
reachable from all selected Tool roots, following Named arguments, Function
parameters/results, Task items and declaration members. An individual generated
TypeSpec retains exactly its Tool roots' reachable closure, and a concrete call
view its bindings and value roots' closure; disconnected extra declarations are
invalid. Another selected Tool's reachable declarations are not extras in the
program map. A toolkit joins the selected roots and templates into its TypeSpec,
which is generated internal metadata, not an additional describe response
member. It validates every consumed type part before generating
source; missing parts are invalid, not silently omitted. A validation failure
emits no partial generated source. It does not parse Flow or infer missing type
parts from schemas, values or caller paths.
A generic call's types view has the same terms and templates, with these
members:
{ "bindings": [{"name": type parameter name, "type": concrete term}, ...],
"params": [{"name": parameter name, "type": concrete term}, ...],
"ok": concrete term,
"err": concrete term,
"declarations": {local declaration id: template, ...} }Bindings and parameter names/counts/order must match the served TypeSpec. Its
signature instantiated with the bindings must agree with the concrete roots.
The runtime describes the actual caller's checked types, including caller-private
declarations; the served TypeSpec supplies comparison, not missing caller types.
Known served top-level declaration templates are matched by their exported
identity, remapping local ids; lexical ordinal, generic arity and ordered
semantic members must agree, ignoring doc comments. Caller-private declarations
receive structural validation only and are not merged by display identity.
The schema view must agree with the success type's projection under Schemas,
with its own local reference root.
Unknown terms or extra object members, missing consumed type parts, duplicate
names in the scopes above, disconnected extra declarations, invalid
owners/indices/arity, unresolved references, conflicting templates, invalid
recursive substitution or a mismatched view make the call NotRun before the
author's code runs.
Arguments are decoded only at the completely validated roots. No partial graph
is admitted and no ordering is inferred from JSON insertion order or a schema.
The type requirements and crossing judgments are those of
tools.md, not new rules attached to this graph.
Structural consistency does not authenticate a declaration or establish the
truth of its claimed private source. Supplied type names do not cause source
lookup or select code to import.
Identity and fingerprints
A @tool declaration's identity is its repository, the repository's
resolved version, the file within it, and its name. A declaration in a file
outside any repository has the file's path in place of repository and version.
Its fingerprint is the SHA-256 hash of its canonical signature, written
sha256: followed by lowercase hex. The canonical signature is the canonical
JSON of:
{ "params": [[name, schema], ...], in declaration order
"ok": schema of T,
"err": schema of E,
"generic": [type parameter names] } in declaration orderfor a declaration returning Result<T, E>, with every schema stripped of
description, and every $defs key written without its @version, so the
signature names each type by repository, file and name only. A generic type
parameter is written in a schema as {"$param": "T"}.
The fingerprint moves when a parameter is added, removed, renamed, retyped or reordered, when the success or error type changes, or when a type those reach changes its shape. It does not move for a doc comment, for formatting, or for a new version of the repository whose signature is unchanged.
An implementation fits a declaration when their fingerprints match. A fingerprint is an equality check over a signature. It does not show that an implementation is authentic, truthful or the one an author intended.
Serves foundations: Typed, composable crossings; Bounded trust and disclosure.
The Tool process protocol
A process implementation speaks JSON-RPC 2.0 over its standard input and output, one message per line, as MCP and LSP do. There are four messages.
| Message | Direction | Kind | Carries |
|---|---|---|---|
describe |
host → implementation | request | the host's data version |
| implementation → host | response | its data version and the declarations it implements | |
call |
host → implementation | notification | run, call, tool, args, and schema plus types for a generic Tool |
reply |
implementation → host | notification | run, call, outcome |
cancel |
host → implementation | notification | run, call |
describecomes first. Its response lists each declaration the implementation serves by identity and fingerprint, with its schemas: the result is{"data", "tools"}, each entry{"tool", "fingerprint", "signature"}, wheresignaturehas the canonical signature's members with each schema written as Schemas says, descriptions and versions included. Binding goes by the fingerprint; the signature is there to be read. A data version whose major differs from the host's refuses the binding, as a version refusal distinct from any shape error.callnames the call by its history id (history.md) and the host's run id.toolis the declaration's identity, andargsan object keyed by parameter name holding canonical values. A generic call requires bothschemaandtypes, as Type descriptions defines; neither may be null or missing. A nongeneric call carries neither. The recorded representation of these views is trace.md's, including the reference retained by a parked checkpoint call and its resend rule.replyanswers one call.outcomeis one of:{"Ok": value} the declared success value {"Failed": error} the declared error value {"NotRun": "reason"} the call definitely did not happen {"Unknown": "reason"} the call may or may not have happenedThe runtime decodes
valueorerroragainst the declared types; a reply that does not decode, one larger than the host's size limit, or a malformedoutcome, is admitted asBadReply.BadReplyis never sent; only the runtime decides it.cancelis a request to stop work on a call. It proves nothing: it does not establish that work stopped, that nothing happened, or that anything was rolled back. A call cancelled with no reply is closed as the language rule for stopping says (concurrency.md).
Calls are multiplexed. Several may be outstanding, and replies may arrive in any order; arrival order carries no meaning. The pair of run id and call id is the only correlation key. A call a host sends again after a crash (history.md) carries the same pair, so an implementation can recognize a call it has already seen.
A process that ends is started again for the next call sent to it, and
answers describe again before that call is sent; a declaration it no longer
serves, or no longer fits, makes that call NotRun. A call that was out to it
when it ended is Unknown if it reached the process and NotRun if it did
not (errors.md).
The host accepts at most one reply per call. A reply for a call that is not open, or a second reply for one call, is dropped and changes nothing in history; the host may log it.
Retries inside one call are the host's and the implementation's business, and invisible to history (history.md).
The protocol carries no callback into the run, no access to its history, and no effect class, authority, credential, approval, budget or retry policy. A host may enforce any of these around a call; none becomes a field here.
Serves foundations: Visible outside influence; Honest uncertainty; Applications remain applications.
Binding
A binding connects a @tool declaration to the implementation that answers
its calls.
- Every Tool a run can reach is bound before it starts. The compiler lists
every
@toolfunction a program can reach. A declaration with no binding, or a binding whose fingerprint does not match, refuses the run before anything is recorded (execution.md). Nothing is bound lazily at a first call. - Local declarations with physical source paths use the nearest
flow.tomlin the declaring file's directory or an ancestor. Its table key is the declaring file's path relative to that manifest's directory, and commands run in that directory. The nearest manifest is the boundary: an absent table does not cause a search in more distant manifests. - Submitted source has no inferred filesystem binding. Without physical source paths supplied by the host, declarations need explicit host bindings.
- The host can override any binding, keyed by the declaration's identity. Tests bind fakes; a deployment routes a Tool to a proxy; an embedding application registers implementations in code. There is no namespace of binding name strings.
- Replay binds nothing and has nothing to dispatch to.
- Resume may bind different implementations than the run started with, provided each fits. Which implementation served a call is the host's record, not history.
- A nested run uses the outer run's bindings unless the host overrides them.
Which binding wins, in order: the host's override, then the declaration's
flow.toml — the nearest local manifest for a physical local source, or the
declaring repository's manifest for a repository source. A declaration covered
by neither is unbound. Resume and fork retain the original local source base
in the host's records; they do not search from the caller's working directory.
Binding authenticates nothing. Whether the host runs an implementation automatically, asks first, or isolates it is host policy; execution.md states the reference CLI's.
Serves foundations: Tools are declarations, not implementations; Applications remain applications.
flow.toml
A Tool repository carries its flow.toml at its root. A local application
places the same file above its Tool declarations as Binding describes.
It says how to run each file's implementations, not which Flow modules to
import or which versions to resolve.
[tools."openai.flow"]
command = "acme-tools openai" # a process speaking the protocol above
[tools."search.flow"]
mcp = "npx @acme/search-mcp" # an MCP server, through the MCP adapterEach table is keyed by a file path relative to the manifest directory and names
exactly one of command or mcp. An unknown key, or a table naming
none or several of them, is a configuration error that refuses the run.
This is the reference host and CLI's only configuration file for Tool bindings. It carries no credential, isolation, approval or authority setting, and the language reads nothing from it.
Toolkits
A toolkit is a library for one implementation language. It generates that
language's types from the @tool declarations and converts values under the
encoding above, so an implementation author never writes decoding by hand.
Intmaps to the language's exact big integer: Pythonint, JavaScriptBigInt, a Rust big-integer type. Narrowing to a fixed width is a checked conversion that the author writes and handles.Decimalmaps to an exact decimal type, never to binary floating point.- A toolkit answers
describefrom the declarations it was generated from, so its fingerprints are the declarations' own. It generates from whatflow toolslists, not from Flow source. - A call whose
argsdo not decode at the declared types is answeredNotRun, since the author's code never ran, and a call that names no declaration the toolkit serves likewise. An author's code that stops with an error other than the declared one is answeredUnknown, since what it did before it stopped is not known. - A generic Tool's values of a type parameter reach the author as JSON. Its generated context retains the validated type view and success schema, and supplies codecs for the concrete roots. Collections containing those values use contextual equality and comparison from the type description, following types.md, rather than generic JSON equality, object insertion order or schema order. Rust generic JSON carriers gain no invented total order; TypeScript collections remain readonly, and Python contextual collections do not infer a caller's record order from a dictionary.
- Nested generated codecs use the same call context. Record and named-payload
field handling follows the substituted type, including The value encoding's
Option-field rule. Descriptive bindings remain available even when they have
no usable standalone boundary codec; an unavailable lookup does not refuse a
call whose actual value roots do not require it. Required root codecs must
exist before the author's code runs. Invalid result or declared-error
encoding after the author ran is
Unknown, not a retroactiveNotRun.
Adapters
An adapter lets an implementation that does not speak this protocol answer Flow calls.
MCP. The adapter starts the named MCP server, offers each declaration as the MCP tool of the same name, passes
argsas the tool's arguments, and decodes the structured result against the declared types. A result the adapter cannot map isBadReply.The server speaks MCP over its standard input and output. The adapter completes MCP's
initializehandshake and lists the server's tools before the run starts, and answersdescribefrom the declarations, as a toolkit does; a declaration the server offers no tool of its name for is unbound. A call is atools/callrequest whose id is the call's history id. The structured result is the result'sstructuredContentor, when it has none, the JSON its one text content block holds; it is decoded against the success type, or against the error type when the result is markedisError. An error response is a result the adapter cannot map. Acancelis MCP'snotifications/cancelled, and a server that ends is a process that ends.
An adapter is an implementation like any other: it is bound, fits by fingerprint, and answers with the four outcomes above. It is a host component; the language knows nothing of MCP. Any other service, such as a plain HTTP API, is put behind a process written with a toolkit.
Versioning
The value encoding, this protocol and the history format share one data
version. It is exchanged in describe and recorded in every history's
Start (trace.md). Within a major version, minors only add; a
change of meaning takes a new major.
No encoding here is stable before the first public release (runtime architecture).
Serves foundations: Stable meaning.