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, declaration identity and fingerprints, the binding lifecycle, the repository 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.
Effects stay in the checker. Nothing in this protocol says whether a function is
!tool, 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.
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 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 call to a generic Tool carries the schema of the type the caller expects.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. - A runnable file's own
@tooldeclarations have no repository configuration, so the host must bind them. - 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 declaring
repository's flow-tool.toml. A declaration covered by neither is unbound.
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-tool.toml
A repository that declares Tools may carry a flow-tool.toml at its root,
saying how to run each file's implementations. A runnable file has none: its
configuration is the host's.
[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 repository root 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.
The file is a convention of the reference host and CLI. 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, with the schema the call carries for its success type.
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.