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

Implementing Tools with a toolkit

This guide is non-normative. It shows practice; it creates no semantics and states no guarantee. Tools owns @tool declarations; the runtime ABI owns the value encoding, the Tool process protocol and what a toolkit does; execution owns flow tools.

A @tool declaration says what a call takes and returns. Something outside Flow answers the call: a process that speaks the Tool process protocol on its standard input and output. A toolkit writes everything in that process except the function bodies. The repository has three, under toolkits/: Python, TypeScript and Rust.

The steps

  1. List the declarations. flow tools tools.flow -o json prints every @tool declaration of the program, with its identity, fingerprint, signature and types. That document is all a toolkit reads; none parses Flow.
  2. Generate a module from it, in the implementation's language. By default every declaration outside std is chosen; --module ./tools (or a repository file such as search) narrows it to one file's declarations.
  3. Implement the generated interface: one method per declaration, typed by its parameters and its success type.
  4. Bind the process to the declarations: --tool './tools.*=python3 serve.py' on the command line, or the repository's flow-tool.toml (abi).

Generate again whenever the declarations change. A stale module answers describe with the old fingerprints, and the host refuses the binding before the run starts rather than letting a call go to a mismatched implementation.

Language Generate Serve
Python python -m flow_toolkit.generate tools.json -o tools_gen.py subclass Implementation, call serve(...)
TypeScript node toolkits/typescript/src/generate.ts tools.json -o tools_gen.ts implement Implementation, call serve(...)
Rust flow-toolkit-gen tools.json -o src/tools.rs implement Tools, call tools::serve(...)

Each toolkit's own module documentation, or the header of what it generates, gives its exact names.

What the author returns

A method returns the declared success value. For the other outcomes it says which one happened (errors):

  • Failed(error) — the call happened and failed with the declared error.
  • NotRun(reason) — the call definitely did not happen: the request was refused before anything was done.
  • Unknown(reason) — the call may or may not have happened: a timeout after the request was sent, a connection lost mid-write.

Python and TypeScript raise or throw these; Rust returns them as a Problem. Anything else that escapes a method — an exception, a panic — is answered Unknown, because the toolkit cannot know what the method did before it stopped. Arguments that do not decode never reach the method; the call is answered NotRun. The runtime alone decides BadReply.

Choosing honestly between NotRun and Unknown is the author's main job. A program can retry a NotRun freely; it has to reconcile an Unknown first.

Values

Every toolkit keeps the encoding's exactness: an Int is an arbitrary-size integer (Python int, JavaScript bigint, Rust flow_toolkit::Int), and a Decimal is an exact decimal (Python decimal.Decimal, the toolkits' own Decimal elsewhere), never a binary float. Instant and Duration are exact seconds, with conversions to the language's date types that refuse rather than round. A @tool type is a distinct type holding the host's token, and a generic Tool's type parameter reaches the method as raw JSON, with the schema of the type the caller expects in the call's context.

Narrowing a big integer to a machine integer is the author's checked conversion: an Int from Flow may not fit.

Cancellation and concurrency

Calls arrive multiplexed and each runs concurrently. A cancel from the host reaches the method through its call's context — an event, an AbortSignal, a flag — and proves nothing: the method may stop early, finish anyway, or have already finished. Whatever it returns after a cancel is harmless.

Conformance

toolkits/conformance/sample.flow declares a Tool for every row of the encoding table and every outcome. Each toolkit's end-to-end test generates from it, implements it, has the real flow run call that implementation through --tool, and compares the result with expected.json. A toolkit for another language can be held to the same program.