Reference

Every exported name, and the few used qualified (FunctAI.save, …), by task. ?name in the REPL shows the same text.

FunctAI.FunctAI — Module
FunctAI

Typed Julia functions whose body a language model writes. Write the signature, a model writes the body; run it over a column, measure how often it is right, have people rate its calls, improve it, and save it for any FunctAI language to load.

using FunctAI

@enum Mood happy unhappy mixed

@ai function mood(review::String)::Mood
    "How does the customer feel about what they bought?"
end

mood("Broke after a day.")          # unhappy
df.mood = mood.(df.review)          # the column, 8 calls at a time
evaluate(mood, labelled)            # how often it is right, with a 95% range

It follows the FunctAI contract, so a function has the same version, the same call log and the same saved form as the same function in Python, TypeScript and R. It stands on lmcc (how values are written into a prompt and read back) and lm15 (every provider, one wire).

Writing AI functions

FunctAI.@ai — Macro
@ai [settings...] function name(inputs...)::Type
    "What it does."
    [output::Type = ai"words" ...]
    [code of your own ...]
end

An AI function: typed inputs, typed outputs, and a body a language model writes. The first string of the body is what the function does (a docstring's # Arguments list describes the inputs); name::T = ai"words" declares outputs (the last is the answer); without them the one output, result, has the return type (String when none is written). Types are Julia's: String, numbers, Bool, an @enum, OneOf, Vector, Dict, NamedTuples and structs, Union{T,Nothing}.

Settings go before function: @ai lm = "claude-haiku-4-5" temperature = 0 function …, and so do tools = [f, g] (Julia functions the model may call), demos (worked examples), instructions (an instruction that replaces the written one) and module_name (the call log's module).

@enum Mood happy unhappy mixed

@ai function mood(review::String)::Mood
    "How does the customer feel about what they bought?"
end

mood("Broke after a day.")        # unhappy
df.mood = mood.(df.review)        # the column, 8 calls at a time

An input with a default may be left out: tone::String = "kind". The default is written in the function's interface and sent to the model whenever the input is left out. A literal ("kind", 3, String[], (a = 1,)) or a constant whose value can never change (const TONE = "kind", an @enum value) is data: it counts in the function's version by its value, and each call gets its own copy. A computed default (day::String = string(today())) is run at each call that leaves the input out, as Julia runs a default; the interface holds the value it gave when the function was defined, and the version counts its code (string(today())), so the version is the same every day and changes when the code does. A default that uses another input, or a constant that can change (const TAGS = ["a"]: a Vector can be pushed to), is refused when the function is defined.

With several outputs, calling returns them all as a NamedTuple ((; summary, minutes) = triage(ticket)). With code after the outputs, that code runs on them, and its value is what calling returns:

@ai function price(item::String)::Float64
    "Estimate the price in US dollars."
    usd::Float64 = ai"the price"
    round(usd; digits = 2)
end

Each input is bound to its type (contract/programs.md, "Binding a call's inputs"): 3 for a String is "3", "5" for an Int is 5, a record keeps only its fields; one that does not bind is refused (InterfaceError), recorded, before any request. missing or nothing for an input whose type takes null is null, sent; for an optional one whose type does not, the input is left out (its default); for a required one, missing out, the refused call recorded, and no request.

FunctAI.AIFunction — Type
AIFunction

A function whose body a language model writes. Call it like any function; broadcast it over a column (f.(xs), map(f, xs), ByRow(f)), and the calls run concurrency at a time. Made by @ai, by the AIFunction(name; inputs, output, …) constructor, or by FunctAI.load.

Its parts: instructions, demos, version, signature_id, predict, render, stream, configure (a copy with other settings).

FunctAI.OneOf — Type
OneOf(values...)

An answer from a fixed list, without declaring an @enum: ::OneOf(:happy, :unhappy, :mixed) answers a Symbol, ::OneOf("yes", "no") a String. Its shape is the choice {"enum": [...], "type": "string"}, as Python's Literal[...].

Examples

julia> species = ["mallard", "blue jay", "other"];

julia> print(FunctAI.LMCC.json_text(FunctAI.shape_of(OneOf(species))))
{"enum":["mallard","blue jay","other"],"type":"string"}
FunctAI.@ai_str — Macro
ai"words"

In the body of an @ai function, an output the model writes, with words about it: summary::String = ai"one sentence, no names". It is read by @ai from your code before anything runs, so it needs no import, and FunctAI doesn't export it (PromptingTools.jl exports its own ai"…"): outside @ai it is FunctAI.@ai_str.

LMCC.tool — Function
tool(f; name, description, parameters, effects)

A tool from a Julia function. Its arguments (names and types) come from the function's one method, its description from its docstring; give parameters (a JSON Schema) and description to say them yourself. effects says what it does to the world: :reads (it only looks: a search, reading a file) or :changes (it writes, sends or pays). Left out, it is unknown, which every approval rule treats as :changes: forgetting to declare is safe. An AI function or a program is a tool too: its inputs are the tool's.

"Look up an order by its number."
lookup(order::String) = orders[order]
"Refund an order."
refund(order::String) = payments.refund(order)
@ai tools = [tool(lookup; effects = :reads), tool(refund; effects = :changes)] function helper(question::String)::String
    "Help with the order."
end
tool(f::AIFunction; name, description, effects)
tool(p::AIProgram; name, description, effects)

An AI function or a program as another AI function's tool: its inputs are the tool's (as its interface states them), its description the tool's. An AI function used as a tool reads, unless one of its own tools changes things or says nothing; a program's code may do anything, so it counts as changing things unless effects = :reads says otherwise.

FunctAI.AITool — Type
AITool

A tool the model may call: name, description, parameters (JSON Schema), the Julia function that runs it, and its effects: "reads" (it only looks), "changes" (it writes, sends or pays), or nothing (it says nothing, which every approval rule treats as "changes"). Made by tool, or from a function given in tools = [...].

FunctAI.@program — Macro
@program [outputs = (name = T, …)] function name(args...; kw...)::T
    "What it does."
    … code that calls AI functions …
end

A program: Julia code that calls AI functions, followed as one call. In the call log it is the parent of every AI call it makes, and a stream of it shows them all. Broadcasting it (support.(tickets)) runs the rows concurrency at a time.

Its interface is read from the declaration (contract/programs.md): each argument is an input, typed by its Julia type (an untyped argument, Any, or a type with no JSON form is opaque: never checked); an argument with a default may be left out (a default that is data, a literal or a constant whose value can never change, is written in the interface; any other, a constant Vector included, is Julia's: the code runs it on each call, as Julia does, and the record has no value for it); the return type is its one output, result (none: opaque), or outputs = (summary = String, minutes = Int) declares several, returned as a NamedTuple or Dict and converted to the declared types. Arguments are given by position, as declared, or any of them by name. Every call checks its inputs before the code runs and its outputs when it returns (InterfaceError, naming the field): a refused call is still a call (its events, its record), and its code does not run. The first string of the body is its description.

A call made inside it, even on a task it starts, is a step of it: its call ends once they all have. Start work meant to outlive it with FunctAI.detached.

FunctAI.AIProgram — Type
AIProgram

Julia code that calls AI functions, made by @program (or from an interface as data: AIProgram(name, code; interface)). Calling it checks its inputs against its interface, runs the code, and checks what the code returned; the AI functions it calls are its children in the call log and in a stream. Its version changes when its code or its interface changes, or when an AI function it names is improved.

FunctAI.interface — Function
interface(f)

A program's interface as data (contract/programs.md): its description, its inputs (an input a caller may leave out is optional, its default in its shape) and its outputs, without the fields FunctAI adds to an AI function. The same JSON in every language.

FunctAI.interface(mood)["inputs"]       # [{"name": "review", "shape": {"type": "string"}, "type": "String"}]
FunctAI.interface_signature — Function
interface_signature(interface)

The interface's signature (programs.md): lmcc's fingerprint of its fields, each plain and untyped, each shape without its own default. The call log's program.interface: calls with one signature record the same data.

interface_signature(f)

The call log's program.interface: the signature of f's interface (what its recorded data looks like). Equal to signature_id for an AI function with neither reasoning nor tools.

FunctAI.InterfaceError — Type
InterfaceError

A program given, or returning, what its interface does not take or give (code "interface-input" / "interface-output"), or an interface that is refused when its program is defined or read ("interface-malformed"). field names the field at fault (nothing when the fault is not a field's). contract/programs.md.

Calling, and what a call returns

StatsAPI.predict — Method
predict(f, args...; kw...) -> Prediction

Call f and keep everything: the value, every output (typed), the call's id (to rate it), the turn and the replies. missing in, missing out.

FunctAI.Prediction — Type
Prediction

Everything one call produced: value (what calling the function returns), answer (the answer output's value), outputs (every output, typed, as a NamedTuple), call (the call log's id: rate it with rate(p, :right)), turn (lmcc's record of the exchange) and responses (lm15's replies).

FunctAI.problems — Function
problems()

The rows whose calls failed in the last run over a column (f.(xs), map(f, xs)): their index and error. Their answers are missing.

LMCC.render — Method
render(f, args...; kw...) -> LM15.Request

The exact request the next call would send, without sending it (nothing is paid): the instruction, the worked examples, the layout, the model.

FunctAI.prompt — Function
FunctAI.prompt(f, args...; kw...)

The request the next call would send, shown as a conversation (the system message, the worked examples, the question, the settings); nothing is sent. prompt(f, …).request is the lm15 request itself (render).

What a function is

FunctAI.instructions — Function
instructions(f)

The instruction the model gets: the improved one when there is one, else the one written from the name, the description and the guidance.

FunctAI.demos — Function
demos(f)

The worked examples, as Dict("inputs" => …, "outputs" => …) (or recorded turns).

FunctAI.version — Function
version(f)

A fingerprint of everything that decides what f sends besides its inputs (contract/calls.md, "Versions"): the same function has the same version in every FunctAI language, and an improved one a new version.

version(p::AIProgram)

{"code": {key: C}, "ai": {key: version}, "interface": I} hashed (contract/calls.md, "Versions"): the program's code, and that of the programs it names, the version of every AI function they name, and its interface. Plain Julia functions it calls are not followed: a change inside one does not change the version.

FunctAI.signature_id — Function

A call's program.signature (contract/calls.md): lmcc's fingerprint with every type name empty, so the shapes decide, not how a language spells types.

signature_id(f)

The call log's program.signature: the fields' names and shapes. Calls with the same signature id share data even when the instruction changed.

FunctAI.signature — Function
signature(f)

The lmcc signature the model gets: the instruction and the fields.

FunctAI.shape_of — Function
shape_of(T)

The JSON Schema shape of a Julia type, as FunctAI writes it in every language: String → {"type": "string"}, Int → integer, Float64 → number, Bool → boolean, an @enum or OneOf → a choice, Union{T,Missing} or Union{T,Nothing} → optional (the model may leave it empty: missing or nothing back), Vector{T} → a list, Dict{String,T} → a map, a NamedTuple or a struct → a record (every field required, in declaration order), Union{T,Nothing} → optional, Any → any JSON. A JSON Schema Dict is used as it is.

Examples

julia> print(FunctAI.LMCC.json_text(FunctAI.shape_of(Union{Int,Missing})))
{"anyOf":[{"type":"integer"},{"type":"null"}]}

julia> print(FunctAI.LMCC.json_text(FunctAI.shape_of(Vector{@NamedTuple{name::String, age::Int}})))
{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}}
FunctAI.julia_type — Function
julia_type(shape)

The Julia type values of a shape are read as, for a function loaded from a saved folder (its Julia types are gone; its shapes remain): a record is a NamedTuple, a choice a String, a list a Vector, a map a Dict.

Settings and models

FunctAI.configure! — Function
configure!(; settings...)

Settings for every AI function in this process (their own settings still win). A setting given as nothing goes back to its default. Returns the settings now in force.

FunctAI.configure!(lm = "gpt-4.1-mini", temperature = 0)

Settings: lm: the model: "gpt-4.1-mini", "claude-haiku-4-5", "groq:openai/gpt-oss-120b", …; router: the lm15 router calls go through (keys, base URLs; a fake in tests); temperature: sampling temperature; max_tokens: the token budget of a reply; top_p: nucleus sampling; stop: stop sequences; seed: the provider's seed; config: other lm15 Config fields, as keywords: (reasoning = LM15.Reasoning(effort="low"),); adapter: the layout: :xml (default), :chat, :json, or an lmcc adapter or artifact; template: a chat template: [:system => "…", :turns, :user => "{review}"]; reasoning: true: the model writes its reasoning before the answer (chain of thought); include_name: false: leave the function's name out of the instruction; capabilities: facts about the model that replace the table's: Dict("nativereasoning" => false); retries: re-asks after an unreadable reply (default 1); `apiretries: re-sends after a transient provider error (default 3);maxsteps: model requests per tool loop (default 8);toolerrors: :report (the model sees a tool's error, default) or :raise;logcalls: the call log: a folder, true (the default folder) or false; unset: the environment variableFUNCTAILOGCALLSdecides;logcontent: what the call log keeps: false (sizes, times and tokens, never values or messages), or a map of fields: (transcript = false,), Dict("*" => false, "question" => true); layers only remove;observers: functions (or Channels) given the kept form of every event of the calls in scope: [e -> println(e)]; layers add up;journal: where each call tree's kept log is kept while it is written: a store (best effort), Journal(store; required = true), or false (none);programobservers: false (a host's block or configure!): the observers a program sets for itself are given no event; the host's still are. Only removes;caller: who is calling, added to the environment variableFUNCTAICALLER: Dict("kind" => "notebook");concurrency: calls in flight at once over a column (broadcasting, map, evaluate; default 8);progress: a progress line on stderr while a column runs (rows done, failures, tokens, time left): true, false, or unset (on when stderr is a terminal);cachereplies: the reply cache: false (default), true (this process's memory), :disk (one SQLite file every process shares, so a long run started again sends only what has no kept reply), a folder or .sqlite path, or a store (a Dict, a FunctAI.ReplyStore);replicate: the n-th independent answer to the same request (default 0): part of the reply cache's key, nothing else;approve: ask before tools run: a function (asked at once; answers true, false or a reason), :changes (tools that change things or say nothing), :all, or tool names and approval paths ("support/answer/refund"); a person answers later;plugins: plugins around calls: [Plugin(...), "plugins/modes.jl"]; the program's own run first, the host's last;programplugins: false (a host's block or configure!): the plugins a program sets for itself do not run. Only removes;escalateto`: when the model is less sure of its answer than escalatebelow, another answers instead: a model name, a baked model, or an AI function (needs a model that measures its confidence); escalate_below: the confidence under which escalate_to answers (default 0.9).

FunctAI.with_settings — Function
with_settings(f; settings...)
with_settings(settings...) do ... end

Run f() with these settings over configure!'s, in this task and every task it starts (a function's own settings still win).

with_settings(lm = "claude-haiku-4-5", log_calls = true) do
    mood.(reviews)
end
LM15.configure — Method
configure(f; settings...)

A copy of f with its own settings changed (a setting given as nothing goes back to what configure! and the defaults say): configure(mood; lm = "claude-haiku-4-5", temperature = 0).

FunctAI.settings — Function
settings(f)

The settings f sets itself (what configure! and with_settings add is not shown).

FunctAI.model_capabilities — Function
model_capabilities(provider, model) -> NamedTuple

What FunctAI declares model served by provider can do, from the contract's table (never guessed): native tool calls, reasoning, stop sequences, enforced JSON, a prefilled reply. These facts decide how a function's layout is written for the model; a function's capabilities setting replaces any of them.

Examples

julia> model_capabilities("anthropic", "claude-haiku-4-5")
(assistant_prefill = false, instruct = true, native_function_calling = true, native_reasoning = true, native_structured_output = true, stop_sequences = true)
FunctAI.login — Function
FunctAI.login(provider; key)

Sign in to a provider once; every later session (in any FunctAI language) uses it. Subscriptions ("claude", "chatgpt", "copilot", "grok", "kimi") open a browser, or print a link or code over SSH; for an API provider ("openai", "anthropic", "groq", …) the key is asked for, or given as key, and saved.

FunctAI.login("claude")
@ai lm = "claude:claude-sonnet-4-5" function …
FunctAI.logins — Function
FunctAI.logins()

The providers this machine is signed in to (lm15's saved connections; no secrets are shown).

FunctAI.logout — Function
FunctAI.logout(provider)

Forget the saved sign-in of a provider.

Streaming

LM15.stream — Method
stream(f, args...; kw...) -> AIStream
stream(on_piece, f, args...; kw...) -> the value

Call f and watch its answer being written. Iterate the stream for the answer's text piece by piece; fetch(s) gives the typed value, the same as calling f. The do form hands each piece to on_piece and returns the value:

answer = stream(haiku, "the first snow") do piece
    print(piece)
end
FunctAI.AIStream — Type
AIStream

A call being made and watched: iterate it for the answer's text as it is written; eachevent for everything (format 2 events: the calls inside it, requests, reasoning, tool calls, retries); fetch for its value, typed (the same as calling); close to cancel it; FunctAI.text(s) for the answer so far. It shows the whole log of its call's tree from that call down, with the tree's numbers (law 7: a stream opened on a call inside a tree starts at that call, its first after is nothing).

FunctAI.eachevent — Function
eachevent(s::AIStream)

An iterator over every event of the watched call, in order, as they happen: the calls inside it, text, thinking, tool calls and results, retries, and the end (:done or :failed). A new iteration starts from the first. Each is a format 2 Event: FunctAI.event_json(e) is what a page or another process reads.

s = stream(support, "Where is order A-1042?")
for e in eachevent(s)
    println(e.seq, " ", e.kind, " ", e.function)
end
eachevent(turn; after = nothing, view = :kept, timeout = nothing)

The turn's events from its store (the kept form, or a view made from it: view = :outside), after the event after names (a Position), those kept so far and then each as it is kept, until its last: what another process, a page after a reload, reads.

FunctAI.Event — Type
Event

One thing that happened in a call tree (contract/streaming.md, format 2): its kind (:started, :request, :text, :thinking, :tool_call, :tool_result, :retry, :done, :failed, or a kind a later stage adds), where it is in its log (tree, writer, seq, after: a Position or nothing), when it was numbered (at), the call it is about and that call's program (e.function), and the keys of its kind (e.text, e.field, e.answer, e.value, e.error, e.request, …). FunctAI.event_json(e) is its JSON form; FunctAI.Event(json) reads one.

An event is a value: nothing changes it once made. Its keys are read as copies (e.inputs["x"] = … changes a copy, never the event, a store's log or what another reader was given), and it holds nothing but its JSON form.

FunctAI.event_json — Function

An event as the contract's JSON (schema/event.schema.json): a new object, which the event does not share.

FunctAI.Position — Type
Position(writer, seq)

An event of a log, named by the writer that numbered it and its seq (contract/streaming.md). Two positions are the same only when both numbers are; nothing stands for "before the first event". Position(e) is an event's own.

LM15.text — Method

The answer so far (restarts after a retry). Provisional: fetch has the typed value.

Measuring

FunctAI.evaluate — Function
evaluate(f, data; expected, metric, concurrency) -> Evaluation

Run f on every row of data (a DataFrame, any Tables.jl table, or a vector of NamedTuples or Dicts) and score it. A row's inputs are its columns named like f's inputs; the right answers are the columns named like its outputs, or expected (a column name for the answer, or output => column pairs). The default metric is exact_match; metric is a function (row, outputs) -> score (0 to 1, or any number), or several by name. A failed row scores 0 and keeps its error. Calls run concurrency at a time and carry caller.evaluation in the call log.

e = evaluate(mood, reviews)      # reviews has columns review and result
e.score, e.low, e.high
DataFrame(e)                     # every row, its answer, its score
evaluate(m::AIModelFit, data)

How often the fitted model is right on rows with known answers (the outcome's column).

FunctAI.Evaluation — Type
Evaluation

What evaluate measured: score, low and high (the first metric's mean and its 95% range), summary (every metric), run (its id in the call log), and every row, as a table (DataFrame(e) works: the row's columns, pred_<output>, each metric, error, call).

FunctAI.compare — Function
compare(before, after)

Two evaluations of the same rows, row by row: for each metric both have, before and after (the means), diff with its 95% range low to high (a paired t interval), and how many rows got better, worse or stayed the same. When the range includes 0, the change could be luck.

FunctAI.exact_match — Function
exact_match(answers, prediction) -> Dict

The default metric: 1 when every output the answers have a value for equals the prediction's (text compared ignoring case and repeated white space; numbers by value), else 0. With several, each also gets <name>_match.

Examples

julia> exact_match(Dict("summary" => "Charged twice", "result" => "billing"),
                   Dict("summary" => "charged  twice", "result" => "shipping"))
OrderedCollections.OrderedDict{String, Float64} with 3 entries:
  "exact_match"   => 0.0
  "summary_match" => 1.0
  "result_match"  => 0.0

See also evaluate.

FunctAI.score_interval — Function
score_interval(values) -> (; mean, low, high)

The mean and its 95% range: Wilson's interval when every value is 0 or 1 (right or wrong), Student's t otherwise. With fewer than two values there is no range (nothing).

Examples

julia> score_interval(vcat(ones(72), zeros(8)))  # right on 72 of 80 rows
(mean = 0.9, low = 0.8148931111226091, high = 0.9484523846972116)
FunctAI.scores — Function

Each row's value for a metric (the first by default); a failed row counts 0.

FunctAI.normalize_text — Function
normalize_text(text)

Text as exact_match compares it: white space collapsed to one space and trimmed, then case folded: what exact_match compares.

Examples

julia> normalize_text("  New   York ")
"new york"
FunctAI.casefold — Function
casefold(text)

Unicode full case folding, code point by code point (CaseFolding.txt, statuses C and F; Python's str.casefold), from the contract's table.

Examples

julia> casefold("Straße")
"strasse"

See also normalize_text.

Improving

FunctAI.with_demos — Function
with_demos(f, examples)

A copy of f with these worked examples. Each is input => answer ("Broke in a day." => unhappy), a row (a NamedTuple or Dict with the inputs and outputs under their names), Dict("inputs" => …, "outputs" => …), or a recorded turn. Rows of a table work too: with_demos(f, Tables.rowtable(df)).

FunctAI.with_instructions — Function
with_instructions(f, text)

A copy of f whose instruction is text (nothing: the written one again).

FunctAI.labeled_few_shot — Function
labeled_few_shot(f, data; k = 16, seed = 0, sample = true, expected)

A copy of f whose worked examples are up to k rows of data with known answers (a seeded random sample; sample = false: the first k). Nothing is called.

FunctAI.bootstrap_few_shot — Function
bootstrap_few_shot(f, data; metric, threshold, max_bootstrapped = 4, max_labeled = 16,
                   teacher, seed = 0, concurrency = 4, expected)

Run f (or a teacher: a stronger model's name, or another AI function of the same signature) on rows with known answers; the runs metric accepts become worked examples, whole turns included (reasoning, tool calls). Labeled rows fill the rest, up to max_labeled. Returns an improved copy.

FunctAI.random_search — Function
random_search(f, data; valset, candidates = 8, metric, max_bootstrapped = 4,
              max_labeled = 16, teacher, seed = 0, stop_at, expected) -> (f, trials)

Try several sets of worked examples (none, labeled only, bootstrapped, bootstrapped from shuffled rows of random sizes), score each on valset (default: data), and return the best copy with every trial as rows.

FunctAI.instruction_search — Function
instruction_search(f, data; candidates = 6, trials = 12, minibatch = 20, valset,
                   prompt_lm, metric, max_bootstrapped = 4, max_labeled = 4,
                   seed = 0, finalists = 3, expected) -> (f, trials)

Search instructions a model writes, with sets of worked examples, and keep the best: instruction candidates (the current one, plus proposals written by prompt_lm from the signature and a few examples) × example sets (bootstrapped), tried on minibatches of valset; the top finalists are then scored on all of valset. MIPRO-style: random search with greedy refinement, not Bayesian.

FunctAI.gepa — Function
gepa(f, data; selection, teacher, budget = 300, minibatch = 4, expected,
     metric, feedback, seed = 0, concurrency) -> (f, trials)

Rewrite the instruction from the function's mistakes. A teacher model reads the function's answers on a few rows with feedback in words ("wrong: the right answer is billing") and writes a better instruction. The instructions it writes are kept in a pool, each scored on rows the teacher is never shown (selection, or half of data at random). Candidates that are best on at least one of those rows stay in the pool, so ideas that fix different mistakes both survive, and every fourth step two of them are combined. The teacher sees what it tried that failed; a proposal that copies an input is dropped; ties go to the shorter instruction; no row runs twice for one instruction. It stops at budget calls of f (the teacher's calls are counted apart, and logged, marked as part of the optimization).

Returns an improved copy (f itself when nothing beat the written instruction) and every instruction tried, as rows: candidate (its number in the pool; 1 is the written one; missing when it did not join), kind (:written, :reflect, :combine), parents, minibatch_parent and minibatch (how many of the same minibatch rows its parent and it got right), score (its mean on the choosing rows), length, note, calls (of f so far), instruction, chosen.

The chosen score flatters: it is the best of many on the choosing rows (the winner's curse). Measure the result with evaluate on rows it never saw.

  • data, selection: tables (or vectors of rows) with the right answers, in columns named like the outputs, or expected (as for evaluate).
  • metric: (row, outputs) -> score, 1 meaning right (default: exact match).
  • feedback: (row, outputs, error) -> words (outputs is nothing when the call failed): what the teacher reads about each answer. Default: "right", "wrong: the right answer is …", or the call's error.
  • teacher: the model that writes instructions ("gpt-6-sol"); default the function's own.
better, trials = gepa(configure(refund; lm = "gpt-5.4-nano"), examples;
                      selection = dev, teacher = "gpt-6-sol", budget = 300)
DataFrame(trials)
evaluate(better, test)              # the honest number: rows the search never saw

It improves one AI function's instruction and leaves its worked examples as they are; add examples after it with labeled_few_shot.

Models: fit, formulas, MLJ

FunctAI.AIModel — Type
AIModel(description = ""; examples = 16, seed = 0, name = "", levels = nothing,
        lm = nothing, reasoning = false, settings = (;), method = :labeled,
        teacher = nothing, budget = 300)

A model whose predictions a language model makes, for rows of a table. fit(AIModel(…), X, y) (or with a formula: fit(AIModel(…), @formula(y ~ a + b), data)) builds an AI function whose inputs are the columns of X, whose answer has y's type (a categorical y: a choice of its levels; levels: a choice of these), and whose worked examples are examples rows of the training data (a seeded sample). Nothing is called until predict.

It is also an MLJ model: machine(AIModel("…"), X, y), with examples a hyperparameter to tune. Predictions are deterministic: providers give an answer, not a probability for each class.

name is the function's name (the instruction starts "Function: <name>"); empty, it is the outcome's column name. Other settings (temperature, adapter, concurrency, …) go in settings.

Fitting that learns. method says what fitting does with the rows:

  • :labeled (the default): examples rows become worked examples; nothing is called.
  • :bootstrap: the function (or a teacher model) runs on the rows, and up to examples runs it got right become worked examples, reasoning included (bootstrap_few_shot).
  • :gepa: a teacher model rewrites the instruction from the function's mistakes on the rows, within budget calls (gepa); then examples rows become worked examples (examples = 0 for none).

With :bootstrap and :gepa, fitting calls the model and costs money. Inside MLJ's evaluate!, each fold is fitted on its own rows, so cross-validation measures the whole procedure, search included, on rows it never saw: the honest number for a searched instruction.

FunctAI.AIModelFit — Type
AIModelFit

A fitted AIModel: predict(m, newdata) answers each row; m.fn is the AI function it built (its instruction, examples, version); evaluate(m, data) measures it on rows with known answers.

StatsAPI.fit — Method
fit(m::AIModel, X, y) -> AIModelFit

X is a table (its columns are the inputs), y the outcome. Nothing is called: the rows become the function's worked examples.

StatsAPI.predict — Method
predict(m::AIModelFit, newdata)

The model's answer for each row of newdata (its columns named like the inputs), concurrency calls at a time: as for a column, a row whose call fails (or that has a missing input) is missing, with one warning.

The call log

FunctAI.rate — Function
rate(call, verdict; answer, outputs, note, reasons, by, origin, sample, folder)

Say whether a call's answer is right, and if not what it should have been (contract/calls.md, "A rating record"). call is a Prediction or its id; verdict is :right, :wrong (or true, false), or nothing to withdraw your rating. A correction alone is :wrong:

p = predict(mood, "Arrived broken, but support was great.")
rate(p, :right)
rate(p; answer = mixed, note = "broken item, happy with the help")
rate.(predictions, :right)          # many at once

The rating is written to the call log folder (the log_calls setting, or folder). by defaults to the caller's user, else your account's name.

FunctAI.calls — Function
calls(f = nothing; folder, since)

The logged calls (of f, when given: an AI function, a program, or a name), oldest first, as rows (a Tables.jl table: DataFrame(calls(mood))): id, started, name, module, version, model, seconds, inputs, outputs, error, tokens, parent, caller (who called: an evaluation's calls have caller["evaluation"] == e.run), language.

FunctAI.rated — Function
rated(f; by, folder, since, any_file = false)

Rows with known answers from people's ratings of f's calls (contract/calls.md, "Rows with known answers"): the inputs, the right answer under the output's name (typed), then call, version, rating, rated_by, origin, sample and disputed. Ready for evaluate and the optimizers. Calls of format 1 and 2 are read; a call is used when its interface is f's (or its signature, for records written before interfaces), so turning reasoning on still pools its ratings. Calls rated under another interface, whose inputs were not all logged, or rated wrong with no correction are left out (and counted in an @info).

FunctAI.LogContentError — Type
LogContentError

A log_content map refused (code "log-content-field", contract/calls.md, "Content"): in a program's own settings, a name that is not one of its fields (a misspelling would write the value it was meant to keep out); anywhere, a key that is neither a field name nor "*".

FunctAI.written_record — Function
written_record(record, keep::Keep) -> record

The record a call writes (calls.md, "Content", "What the record keeps"): the whole record when every field is kept; otherwise content: false, omitted naming the fields not kept, only the kept values, no exchange request, reply or request hash, and of each error only its type and code.

FunctAI.saw — Function
saw(records, call) -> Vector

The calls a call was given as context, in order, each entry as its record has it (call, and steps, without, slot when present), every saw_of replaced by the entries of the call it names. records are call records (FunctAI.read_log(folder)[1]). Throws SawUnknown when they cannot be known: not-recorded, missing-call, unknown-key, saw-cycle.

FunctAI.keeps_saw — Function
keeps_saw(records, call) -> nothing

Whether the log keeps what showing a call its context again needs (contract/calls.md, "Knowing is not replaying"): every call it saw is in the log, not truncated, with the values it was shown as data (and with steps, each exchange's request hash and reply). Throws SawUnknown naming the first call that fails: turn-invalid (an entry no call can have been shown: steps without the tool calls), missing-call, not-kept, or why what it saw is not known. It shows nothing: turning a record's steps into a turn is stage 5's.

FunctAI.SawUnknown — Type
SawUnknown

What a call saw cannot be known, or shown again (contract/calls.md, "Saw"): code is not-recorded, missing-call, unknown-key, saw-cycle, not-kept or turn-invalid, and call the call whose record says so.

Saving and loading

FunctAI.save — Function
FunctAI.save(path, f) -> the manifest's path

Write an AI function to a folder (its functai.json), for any language's loader: the signature, the instruction, the worked examples, the layout and the settings, with the fingerprints a loader checks. A function with code of its own or tools is refused: they are Julia code, which the folder cannot carry yet.

FunctAI.load — Function
FunctAI.load(path; node, types) -> AIFunction

An AI function saved in any language (Python, TypeScript, R, Julia), from its folder (or its functai.json). It is checked to send exactly the bytes it sent where it was saved; what it cannot run (code of its own, tools, a baked model) is refused with a LoadRefused that says why.

A saved folder keeps shapes, not Julia types: answers come back as String, numbers, Vectors, Dicts and NamedTuples. types gives fields their Julia types back when the shapes agree:

mood = FunctAI.load("saved/mood"; types = (result = Mood,))
mood("Arrived broken.")          # unhappy::Mood
FunctAI.to_manifest — Function
to_manifest(f) -> Dict

The manifest of an AI function defined here: the part every language reads (contract/saved.md). See save.

FunctAI.from_manifest — Function
from_manifest(manifest; node, types, saved_id) -> AIFunction

An AI function from a saved manifest (the parsed functai.json); node names one by key (module:name), the entry by default. See load.

FunctAI.describe — Function

A plugin's manifest, as data: name, version, api, description, the hooks it uses.

FunctAI.describe(path_or_manifest; node) -> Dict

What a saved program takes and gives, without loading or running anything (contract/saved.md, "Describing without loading"): the node's interface (the entry's by default), checked. A module saved in any language is described too. An AI node written before nodes had an interface is described by its signature (its instruction as the description: do not show it to outside callers as it is). Throws LoadRefused.

The program, described (GET /interface): its interface with a format number, as it is served outside a saved manifest.

FunctAI.LoadRefused — Type
LoadRefused

A saved program this loader will not run; code says why (saved-malformed, saved-format, saved-not-ai, saved-code, saved-tools, saved-model, saved-differs, saved-no-interface, interface-malformed: contract/saved.md).

Conversations

FunctAI.conversation — Function
conversation(program, id = nothing; store, context, earlier_without, remembers, sends, settings...)

A conversation with an AI function or a program: called like it, each call is a turn that sees the earlier ones. Memory belongs to the conversation; the program is unchanged.

chat = conversation(tutor, "alex"; store = "tutoring/")
chat("Hi, I'm Alex.")
chat("What is 1/2 + 1/3?")            # sees the first turn
predict(chat, "Is it 5/6?").outputs   # one turn, everything it produced
s = stream(chat, "Why?")              # watched while it is made; s.turn is known at once
  • id: letters, digits, ., _, -. The same id in the same store opens the same conversation. Default: a new one.
  • store: nothing (this process's memory), a folder (FolderStore), true (the default folder), or a FunctAI.ConversationStore.
  • context: all_turns() (default) or last_turns(10), each with without = [...].
  • earlier_without: outputs the program now writes that earlier turns lack (reasoning turned on, a first tool): earlier turns are shown without them.
  • remembers: a program's helpers' memory: Dict(answer => :conversation), :turn, remember(:conversation; steps = true), or :own for a conversation used inside it. Helpers remember nothing otherwise.
  • sends: two sends at once: :queue (default: the second waits and continues from the first), :refuse (conversation-busy), or :branch.
  • other keywords are settings for every turn (approve, lm, plugins, …).
FunctAI.Turn — Type
Turn

One turn of a conversation, as its records say now: id (also call: its call's id in the call log), parent, inputs, outputs, result (the answer, typed), state ("running", "waiting", "interrupted", "done", "failed", "stopped", "abandoned"), saw (the earlier turns it was shown), model, error, waiting (the Approvals it waits for), unfinished (tools that may have run when it stopped), usage (tokens over every call inside it), request_id, reads and made_by (a merge). Another output is a property too: turn.reasoning.

FunctAI.turns — Function

The done turns of the ended turn's branch, this one last when it is done.

turns(chat; all = false) -> Vector{Turn}

The turns from the first to the head, in order; all = true: every turn of every branch, in the order they were made.

FunctAI.turn — Function

One turn, by its id, its index in turns(chat), or a Turn.

FunctAI.head — Function

The turn this conversation continues from next (nothing: none yet).

FunctAI.head! — Function
head!(chat, turn)

Make a turn the conversation's head, for everyone who opens it.

FunctAI.continue_from — Function
continue_from(chat, turn) -> Conversation

This conversation, continuing after turn (a Turn, its id, or its index in turns(chat)): the next turn is a new branch. Nothing is deleted.

FunctAI.all_turns — Function
all_turns(; without = String[])

Show the model every earlier turn of the conversation (the default). Running out of the model's context, and being told, is better than a model that silently misses what was said. without: inputs or outputs left out of every earlier turn (a long document the model already answered about); the current turn is always whole.

FunctAI.last_turns — Function
last_turns(n; without = String[])

Show the model only the last n earlier turns. The turns left out are still kept, and each turn's record says which earlier turns it was shown (turn.saw), so an answer can be asked again exactly as it was.

FunctAI.remember — Function
remember(mode = :conversation; steps = false)

What an AI function called inside a module's conversation remembers. In a module's conversation, the module's turns remember each other, but the AI functions it calls (its helpers) start fresh at every call unless the conversation says otherwise: conversation(support; remembers = Dict(answer => remember(:conversation; steps = true))). A plain :conversation or :turn there is the same as remember(:conversation).

FunctAI.earlier — Function
earlier() -> Vector

The conversation so far, as data: inside a module's turn, one row per earlier turn it is shown (its inputs and outputs by name, without the fields the conversation leaves out); [] outside a conversation. For a helper that declares an input for it:

@ai function handoff(conversation::Vector{Dict{String,String}})::String
    "Summarize this support conversation for the person who takes it over."
end
@program function support(message::String)::String
    topic(message) == "other" && notify_staff(handoff(FunctAI.earlier()))
    …
end
LMCC.render — Method
render(chat, inputs...; call = nothing) -> LM15.Request

The exact request the next turn would send (nothing is sent or recorded). call = helper: the request that helper would get, with the memory the conversation gives it (inputs: the helper's).

StatsAPI.predict — Method
predict(chat, inputs...) -> Prediction

One turn of an AI function's conversation: the whole call (p.call is the turn's id).

LM15.stream — Method
stream(chat, inputs...) -> AIStream

One turn, watched while it is made; s.turn is known at once (the turn is saved before the model is asked).

Base.merge! — Method
merge!(chat, branches, fn; inputs...) -> Turn

A turn after the conversation's head made from several branches by another AI function (fn): its answer becomes this program's answer, recorded as a turn (made_by fn, reads the branches), so the next turn sees it. Its rating belongs to fn's call. When fn has one input and none is given, it is given the branches' answers ([(model, inputs…, outputs…)]).

FunctAI.wait_turn — Function
wait_turn(turn; timeout = nothing) -> Turn

Wait until the turn is no longer running; returns it as it is then.

FunctAI.stop! — Function
stop!(chat, turn)
stop!(turn)

Stop a running turn, wherever it runs: in this process or another one that opened the same store. It ends stopped within about a second (its stream throws Cancelled). A turn that waits for an approval, or was interrupted, has nothing running it: stopping it ends it abandoned. A turn that already ended is left as it is.

FunctAI.calls_in — Function
FunctAI.calls_in(turn) -> Vector

The calls inside a turn, as its kept log says: (call, parent, function, invocation, ended) each, in the order they started. rate takes their ids.

FunctAI.call_tree — Function
FunctAI.call_tree(turn) -> String

The calls inside a turn, as an indented tree (from its store's kept log): the program, its helpers, the calls their tools made, and how each ended.

FunctAI.MemoryConversations — Type
MemoryConversations()

Conversations kept in this process's memory (lost when it ends): the store a conversation uses when none is named. One per process by default, so opening the same id again opens the same conversation.

FunctAI.FolderStore — Type
FolderStore(folder)

Conversations kept in a folder, shared by every process that opens it (and by Python's, TypeScript's and R's FolderStore on the same folder):

<folder>/conversations/<id>.jsonl   the records, one per line
<folder>/conversations/<id>.lock    held while appending
<folder>/trees/<tree>.jsonl         each turn's call tree log (kept form)

Each append is one step across processes (a lock on the conversation), written and flushed to disk before it returns (durability "disk"). Files are readable by their owner only. store = "folder/" names one.

FunctAI.append_records! — Function
FunctAI.append_records!(store, conversation, records; expect = nothing) -> Int

Add records at the end of a conversation, all or none, giving each its seq; with expect, only when the conversation holds exactly expect records (else ConversationError("store-conflict")). Returns how many it holds after.

FunctAI.read_records — Function
FunctAI.read_records(store, conversation, after = 0) -> Vector

The records of a conversation after position after, in order (copies).

Tools that ask first

FunctAI.Approval — Type
Approval

One tool call a person is asked about: call (the id of the AI function's call that asked), invocation (the tool call's number in that call), id (the id the model gave it), name and input (the tool and what it would be given), effects, path (where it is, by names: support/answer/refund, for rules written before any call exists), site (the asking call's place in its tree), plugin (which plugin asks) and question (why, when it says).

FunctAI.approve! — Function
approve!(turn, approval = nothing; by = nothing, resume = true)
approve!(stream, approval = nothing; by = nothing)

Say yes to an approval a turn waits for (turn.waiting[1], its invocation number, or nothing for the only one). When nothing else waits, the turn goes on here (resume = false: later, resume!) and its answer is returned. On a stream without a conversation, the call waiting in this process goes on.

FunctAI.deny! — Function
deny!(turn, approval = nothing; reason = nothing, by = nothing, resume = true)
deny!(stream, approval = nothing; reason = nothing, by = nothing)

Say no: the model is told the person did not allow the call (and why), and may try something else.

FunctAI.resume! — Function
resume!(turn; results = Dict(), rerun = []) -> the turn's answer

Go on with a turn that waits (every approval answered) or was interrupted (its process stopped), in this process: its program runs again with the same inputs and earlier turns, each model reply it had and each tool result it kept are reused (nothing is paid for or run twice), and it goes on from where it stopped. A tool that started and has no result may have run: results = Dict(invocation => output) says what it returned, rerun = [invocation] runs it again. Throws Waiting when it waits again.

FunctAI.abandon! — Function
abandon!(turn)

End a turn that waits or was interrupted, without going on: it ends abandoned.

FunctAI.Waiting — Type
Waiting

A turn stopped to wait for a person's answer (code turn-waiting): a tool call its approve rule (or a plugin) asks about, with no function to ask. turn is the Turn and approvals what waits; approve! or deny!, from any process that opens the conversation, answers and resumes it.

FunctAI.ApprovalError — Type
ApprovalError

A tool call needs a person's answer and nobody can be asked (code approval-required): a plain call, with a rule and no function to ask, no stream to answer on and no conversation to wait in. The tool did not run. approval is what would have been asked.

Plugins

FunctAI.Plugin — Type
Plugin(name; version = "0.0.0", api = 1, description = "", hooks...)

A named, versioned set of hooks: functions FunctAI calls at fixed points of a call or a conversation's turn (contract/plugins.md). Each is given an event and returns a Change (or nothing: no opinion). The hooks:

hookwhena Change of
turn_starta conversation's turn is sent, before it is recordedinputs
contextits earlier turns are chosenkeep, without, sections
before_callan AI function is about to be asked (helpers too)instruction, sections, lm, settings, tools
requesta provider request is about to be sentreturns another LM15.Request
tool_calla tool is about to runinputs, block; or ask(event) a person
tool_resulta tool ranoutput
turn_enda turn ended (hears only; may remember! entries)

Register them as keywords, or with on!:

guard = Plugin("no-deletes"; version = "1.0.0",
               tool_call = t -> t.name == "delete_file" ? Change(block = "deleting files is not allowed here") : nothing)
configure!(plugins = [guard])

name is lower-case letters, digits, _ and - (its changes and entries are named by it); api is the plugin API it was written for (1; another refuses plugin-api).

FunctAI.on! — Function
on!(f, plugin, hook)

Register f (a function of one event) for hook (:turn_start, :context, :before_call, :request, :tool_call, :tool_result, :turn_end). Within a plugin, handlers run in the order they were registered. A hook that does not exist refuses plugin-hook: a misspelt hook would otherwise never run.

on!(modes, :before_call) do call
    call.function == "answer" ? Change(sections = ["Cite your sources."]) : nothing
end
FunctAI.Change — Type
Change(; instruction, sections, lm, settings, tools, keep, without, inputs, block, output)

What a hook changes, as data (contract/plugins.md, "The fields"). Each hook accepts some fields; another refuses plugin-change.

  • instruction: the instruction itself, for this call; sections: text added after it, in order (before_call, context);
  • lm: the model for this call; settings: lm15 settings for it (temperature, max_tokens, reasoning, …) (before_call);
  • tools: the names of the tools offered, from the function's own (before_call);
  • keep: the ids of the earlier turns shown; without: fields left out of earlier turns, a list for every turn or Dict(turn => names) (context);
  • inputs: inputs replaced, by name (turn_start: the turn's; tool_call: the tool's);
  • block: the tool may not run, and why (tool_call);
  • output: the tool's result as the model is shown it (tool_result).
FunctAI.load_plugin — Function
load_plugin(path) -> Plugin

A plugin from a Julia file that defines plugin (a Plugin), read in a module of its own. Loading runs the file: load only code you trust. A file is read again when it changed. A plugins setting may name the file instead: configure!(plugins = ["plugins/modes.jl"]).

FunctAI.compaction — Function
compaction(; keep = 20, every = 10, summarize = nothing, lm = nothing, name = "compaction") -> Plugin

Keep a long conversation short: older turns are folded into a summary. When a turn ends and more than keep + every turns of its branch are not yet summarized, every turn but the last keep is folded into the summary (the earlier summary and those turns, given to summarize(earlier_summary, new_turns), new_turns a vector of Dict(input…, output…) rows; default: an AI function of FunctAI's, on lm when given). The summary is kept in the conversation (an entry at that turn, so each branch has its own), and the next turns are shown it, as a section of the instruction, with only the turns after it. What a turn was shown is in its record, so a rated turn is asked again with the same summary. every keeps the summary's text the same for every turns: a provider's prompt cache keeps its prefix meanwhile.

chat = conversation(tutor, "alex"; store = "tutoring/", plugins = [compaction(keep = 20, every = 10)])
FunctAI.delegate — Function
delegate(program; name, description, remember = true, effects) -> AITool

Another program as a tool: an assistant hands part of its work to it. Asked inside a conversation's turn, the program answers in a conversation of its own (<conversation>.<name>, in the same store), which follows the branch of the turn that asked: asked again later on that branch, it remembers what it was asked before; on another branch, it does not. Its calls are in the asking turn's call tree, under the tool call. Outside a conversation, or with remember = false, it is called plainly. effects defaults to :reads when every tool it has only reads, else unknown (counts as :changes).

researcher = delegate(research; description = "Look things up in the notes.")
@ai tools = [researcher] function assistant(request::String)::String
    "Help with the request."
end
FunctAI.ask — Function
ask(event::ToolCallEvent; reason = nothing, decide = nothing) -> Bool

Ask a person whether the tool may run: in a conversation the turn waits, saved, and goes on when someone answers; on a stream the call waits for approve!; a plain call refuses (approval-required). decide (a function of the Approval returning true, false or a reason) answers in place of a person. Returns true, or false (the model is then shown the refusal and the person's reason).

FunctAI.entries — Function
FunctAI.entries(event, kind) -> Vector

This plugin's entries of a kind on the branch of the turn the event is in ([] outside a conversation), oldest first: (turn, data, at) each.

Named entries: a NamedTuple, a Dict, or a vector of name => spec pairs.

entries(chat, plugin, kind; branch = head) -> Vector

A plugin's entries of a kind on the branch through branch (default: this view's head), oldest first: (turn, data, at) each.

FunctAI.remember! — Function
FunctAI.remember!(event, kind, data)

Keep an entry of this plugin at the turn the event is in (it then belongs to the branches through that turn). Outside a conversation it is kept nowhere: an ArgumentError.

remember!(chat, plugin, kind, data; turn = nothing)

Keep a plugin's entry in this conversation, at a turn (it then belongs to the branches through that turn), or with no turn (every branch): what the plugin needs later, never shown to the model by itself. A host keeps one too (switching a mode is an entry).

FunctAI.PluginError — Type
PluginError

A plugin refused or failed (contract/plugins.md): code is plugin-api, plugin-hook (no such hook), plugin-name, plugin-change (a change this hook cannot make, or a value that does not fit), plugin-failed (its handler threw: the call stops, since the change it was meant to make did not happen) or plugin-load; plugin and hook name where.

Serving and remote programs

FunctAI.serve — Function
serve(program; host = "127.0.0.1", port = 8080, keys, store, lm, approvals = :owner, block = true)

Serve a program over HTTP (contract/serving.md): its interface, calls, streams and conversations, to callers who see only its boundary.

routeanswer
GET /interfacethe program, described ({"functai_interface": 1, …})
GET /openapi.jsonthe same, as OpenAPI 3.1; GET /: a form
POST /call {"inputs": …}{"call", "outputs", "value"}; a refused input 422
POST /streamServer-Sent Events of the outside view
POST /conversations/<id>/turnsa turn, saved before it runs (after, request_id, wait)
GET /conversations/<id>/turns[/<turn>[/events]]the branch's turns, one turn, its events (Last-Event-ID resumes)
POST …/turns/<turn>/stop, …/approvals/<invocation>stop it; answer an approval (approvals = :caller)

Without keys it listens only on this machine (serve-keys otherwise). block = false serves on a task and returns the server (close(server) stops it). A caller that is a FunctAI call sends FunctAI-Parent: the served call names it as its parent, so the two logs make one call tree.

FunctAI.remote — Function
remote(url; key = nothing, timeout = 120) -> AIProgram

A program served elsewhere (serve, in any FunctAI language), used like a local one: its interface is the server's (GET /interface), its inputs are bound and checked here before anything is sent, and what comes back is checked against its outputs. Each call is logged here (program.kind "remote", program.remote the URL) and there; the server's record names this call as its parent, so the two logs make one call tree. It broadcasts over a column and evaluates as a local program does; its conversations are kept by its server (POST <url>/conversations/<id>/turns).

team = remote("https://example.org/team"; key = ENV["TEAM_KEY"])
team("I was charged twice for order B-2210.")
team.(tickets.message)
FunctAI.Service — Type
FunctAI.Service(program; keys, store, lm, approvals = :owner)

A program as an HTTP service, independent of any server: FunctAI.handle(service, method, path, headers, body) answers one request; serve runs it on HTTP.jl's server. program is an AI function, a program, or a saved folder (loaded with FunctAI.load).

  • keys: bearer keys a caller must send (a list, one key, or a file of one per line). None: no key, which serve allows only on 127.0.0.1.
  • store: where conversations are kept (as conversation(…; store)); nothing keeps them in this process's memory.
  • lm: the model every call uses.
  • approvals: who answers a tool call the program's approve rule asks about: :owner (in their own process; the caller sees the turn waiting), or :caller, through the approvals route.
FunctAI.handle — Function
FunctAI.handle(service, method, path, headers, body) -> Reply

Answer one request (headers a Dict of lower-case names): JSON in, JSON out; errors {"error": {"type", "code"?, "field"?}}, with a message only when it quotes the caller's own input (contract/serving.md, "Routes").

FunctAI.outside — Function
FunctAI.outside(events; answer_from = nothing) -> Vector{Event}

A whole form of a log as the outside view shows it (contract/streaming.md, "Views"): what a served program's caller may see.

FunctAI.RemoteError — Type
RemoteError

The server answered with an error: code is its code (or remote-<status>), status the HTTP status, type its error's type.

Long runs

FunctAI.clear_cache — Function
FunctAI.clear_cache(which = nothing)

Forget kept replies, so the next identical requests reach the model again: this process's memory (nothing, true), or the store a cache_replies value names (:disk, a path, a store).

FunctAI.reply_key — Function
FunctAI.reply_key(request, replicate = 0) -> String

The cache key of a request (contract/replies.md, "The key"): "sha256:" of the canonical JSON of {"functai_reply": 1, "request": <lm15's canonical request>, "replicate": n}. The same in every language, so a cache written by one is read by another.

FunctAI.ReplyStore — Type
FunctAI.ReplyStore

Where replies are kept. A store has FunctAI.reply(store, key) (the kept reply, or nothing) and FunctAI.keep_reply!(store, key, response), and may have claim_reply!(store, key, cancelled) / unclaim_reply!(store, key) (one flight per key) and discard_reply!(store, key). Any AbstractDict is a store too (configure!(cache_replies = Dict())): it keeps replies by key.

FunctAI.MemoryReplies — Type
FunctAI.MemoryReplies(capacity = 20_000)

Replies kept in this process's memory, at most capacity (the least recently used go first). cache_replies = true uses one per process.

FunctAI.DiskReplies — Type
FunctAI.DiskReplies(path = default; lease = 120)

Replies kept in one SQLite file (contract/replies.md, "The file"), shared by every process and thread that opens it, Python's, TypeScript's and R's included: writes are transactions, so nothing is half-written; a claim with a lease makes one flight per key across processes. The file and its folder are readable by their owner only.

FunctAI.quotes_found — Function
quotes_found(text, quotes) -> Bool or Vector{Bool}

Whether each quote is in the text, word for word: a judge written as an ordinary AI function (a score with the quotes it rests on) is only as good as its quotes, and one that invents evidence is caught here. White space, case, curly and straight quotes, dashes, and a quote's own surrounding quotation marks and final punctuation do not count; any other difference does (a changed word, a paraphrase, an invented sentence). Deterministic, and costs nothing.

source = "The parcel left Leeds on Monday. It was delayed by snow."
quotes_found(source, ["“It was delayed by snow”", "It was lost"])    # [true, false]
FunctAI.prune_calls — Function
FunctAI.prune_calls(older_than = "90d"; folder, keep_rated = true) -> NamedTuple

Delete the call log's day folders older than a time, keeping what ratings need (contract/calls.md, "The folder"). Before a day goes, every rated call in it is kept: the call, every call of its tree (a program's helpers), every call its saw names (the earlier turns a row replays) and their ratings are copied into one file at the folder's top level (kept-<host>-<pid>-<hex>.jsonl), which every reader reads. A row of rated made before pruning is made the same after. Returns (days, calls, kept): folders deleted, calls deleted, calls kept.

FunctAI.train_test — Function
train_test(rows; by = :conversation, test = 0.2, seed = 0) -> (train, test)

Two sets of rows with every group of rows on one side: train, test = train_test(rated(tutor)). Turns of one conversation depend on each other: a test row whose conversation is also in the training rows measures memory, not the program. A row with no group (conversation missing) is a group of its own. test is the share of groups in the test side (at least one group each side when there are two or more). Python's functai.split.

FunctAI.earlier_of — Function
FunctAI.earlier_of(call, records) -> Dict

What a rated call was shown before its own inputs, as data a row carries (contract/calls.md, "Rows that keep their context"): earlier (the turns it was shown, in order), conversation (its conversation's id, or nothing), sections (what plugins added to its instruction, when any) and, for a program's call, helpers (each call inside it that was shown earlier turns, in the order they started: {program, call, earlier}). Throws SawUnknown when the log cannot show them again (never a part).

FunctAI.inspect_history — Function
inspect_history(n = 1) -> Vector

The last n requests FunctAI sent (or answered from its cache), oldest first: (function_name, model, request, response, cached, error, time) each, with lm15's request and response (exactly what went to the provider and what came back; response is nothing when the provider failed). The last 500 are kept, in this process only; phistory prints them. For every call, kept on disk: the call log (log_calls = true, calls).

FunctAI.phistory — Function
phistory(n = 1)

The last n model calls as readable text: every message sent, the reply, its finish reason and tokens (a FunctAI.History that prints itself).

Baking

FunctAI.bake_examples — Function
FunctAI.bake_examples(f, rows; fixed, derived, reasoning, layout, validation = 0.1, seed = 0, weight = nothing, tag = nothing)

The training conversations of f on rows with known answers (contract/baked.md, "The examples table"): one row per conversation, function, messages (the prompt, then {"role": "assistant", "content": <reply>}: the loss belongs on the reply only), tag, weight, row_id, split ("train" or "validation", a seeded share of validation) and source ("data"). A Tables.jl table (a vector of NamedTuples). Turning messages into tokens is the student's own chat template's rule, so any trainer can train on them, and a model trained on them is called by baked with the same messages. weight and tag name columns of the rows.

FunctAI.export_examples — Function
FunctAI.export_examples(path, f, rows; options...) -> path

bake_examples written as JSON lines (function, messages, tag, weight, row_id, split, source), with <path>.meta.json beside it ({"functai_examples": 1, "student": null, "template": null, "functions": [<entry>]}, entries as in baked.json): what made them, so a model trained on them is adopted by checking its function's fingerprint. Any trainer reads the file (TRL's SFTTrainer with assistant_only_loss, Axolotl's chat template datasets, a service's upload).

FunctAI.bake_entry — Function
FunctAI.bake_entry(f; layout, reasoning = false, fixed = Dict(), derived = Dict(), rows = []) -> BakeEntry

What a student of f reads (contract/baked.md, "The examples"): f's signature without the inputs left out (fixed: one value in every row, its hash kept; derived: decided by another input, Dict("guidance" => "section"), a table of hashes kept), without reasoning unless the bake keeps it, laid out in f's layout with replies written from values, and no worked examples. rows are checked against fixed and derived.

FunctAI.baked — Function
FunctAI.baked(folder; url, model = nothing, api_key = nothing, timeout = 120) -> BakedModel

A model baked anywhere (Python's functai.bake, or a trainer that wrote baked.json format 2), its weights served by an OpenAI-compatible server at url (vllm serve <folder>/model serves /v1/chat/completions). configure(f; lm = student) runs f on it, laid out exactly as it was trained: the student's signature and layout, no worked examples, the chat template the server applies with thinking off. A call is refused when f changed since it was baked (baked-changed), or gives a fixed input another value (baked-fixed) or a derived one a pair the student never saw (baked-derived).

FunctAI.BakeError — Type
BakeError

A bake, or a call on a baked model, refused (contract/baked.md): code is baked-fixed (a call gives a fixed input another value), baked-derived (a derived input the student never learned for its source), baked-changed (the function changed since it was baked), baked-format (a folder of a format this reader does not know), or bake-rows (rows that cannot be examples).

Errors

FunctAI.FunctAIError — Type
FunctAIError

The supertype of every error FunctAI throws with a contract code: err.code is a short, stable word for what went wrong ("interface-input", "turn-waiting", "approval-required", …), the same in every language FunctAI is written in, and the one the call log records. Branch on it, not on the message:

try
    chat("Refund my order, please.")
catch err
    err isa FunctAI.FunctAIError && err.code == "turn-waiting" || rethrow()
    # a person has to approve a tool call first
end

Its subtypes say where it comes from: InterfaceError, LogContentError, JournalError, StoreRefusal, SawUnknown, LoadRefused, ConversationError, Waiting, ApprovalError, PluginError, ServeError, RemoteError.

FunctAI.ConversationError — Type
ConversationError

A conversation, or one of its turns, refused what was asked (contract/conversations.md). code is one of conversation-id, conversation-content (a store that keeps records, for a program a log_content setting keeps values of out of the log), conversation-signature (the program changed in a way earlier turns cannot be shown with; name an added reasoning in earlier_without), conversation-opaque, conversation-busy, conversation-nested, turn-unknown, turn-state, turn-unfinished or store-conflict; turn names the turn at fault.

Datasets

FunctAI.tickets — Function
FunctAI.tickets()

Customer support messages to a small homeware shop, with the team each belongs to: 80 rows, as a Tables.jl column table (DataFrame(FunctAI.tickets())).

columnwhat it holds
idthe message's number
messagewhat the customer wrote
channel"email" or "chat"
categorythe team: "shipping", "billing", "product" or "account"
order_idthe order number, like "A-1042" (a letter, a dash, four digits); missing when there is none

The house rules, which the labels follow:

  • Anything wrong with the delivery itself (late, lost, sent to the wrong place, the wrong item, something missing, or broken when it arrived) is shipping: the carrier pays.
  • Anything about money (charges, invoices, coupons, cards, and every request for money back, whatever the reason) is billing.
  • Problems that appear while using a product, and questions about products, are product.
  • Signing in, passwords, profile details, personal data and emails from the shop are account.

Examples

julia> t = FunctAI.tickets();

julia> length(t.message), t.category[1]
(80, "shipping")

See also FunctAI.field_notes, FunctAI.refunds.

FunctAI.field_notes — Function
FunctAI.field_notes()

Bird survey notes written by volunteers, with the species, count and behaviour of each: 60 rows, one species per note, from four sites between April and May 2026, as a Tables.jl column table.

columnwhat it holds
idthe note's number
site"Marsh boardwalk", "North field", "Creek trail" or "Old orchard"
dateYYYY-MM-DD
notewhat the volunteer wrote
speciesthe checklist's common name (American robin, black-capped chickadee, blue jay, northern cardinal, mallard, Canada goose, great blue heron, red-tailed hawk, downy woodpecker, song sparrow, American crow, barn swallow), or "other"
counthow many birds; missing when the note gives no number
behaviour"feeding", "nesting", "flying", "resting" or "calling"

The protocol, which the labels follow:

  • Species: the checklist's name. Nicknames count ("robin", "heron", "red-tail", "downy"); a species not on the checklist is "other".
  • Count: every bird seen or heard, young included. One bird named without a number ("a blue jay") is 1; "a pair" or "a couple" is 2; an approximate number ("about 40", "~25", "maybe 6") is that number; no number ("a few", "several", "a flock", "lots") is missing: never guess.
  • Behaviour: singing, calling and drumming are calling; building, sitting on a nest or bringing food to young are nesting; perched, swimming, roosting or standing still are resting.

Examples

julia> n = FunctAI.field_notes();

julia> length(n.note), count(ismissing, n.count) > 0
(60, true)
FunctAI.refunds — Function
FunctAI.refunds()

Refund requests to the homeware shop of FunctAI.tickets, with the decision its rules give: 120 rows, as a Tables.jl column table.

columnwhat it holds
idthe request's number
messagewhat the customer wrote
itemwhat they bought
pricewhat they paid, in dollars
days_since_deliveryfrom the order system, not the message
final_salebought on final sale (clearance)
state"unopened", "opened_unused", "used" (works, no longer wanted), "damaged" (on arrival), "wrong_item" or "faulty" (failed in normal use)
decision"approve" or "deny": what the rules below give

The refund rules, which decision follows exactly:

  • Damaged on arrival, or the wrong item (or part of the order missing): a refund within 60 days of delivery, final sale or not.
  • Faulty (it failed in normal use): a refund within 365 days, final sale or not.
  • Unopened, or opened but not used, and no longer wanted: a refund within 30 days, and never for a final-sale item.
  • Used and no longer wanted: no refund.

The messages were written by a language model from each row's facts, in varied tones and lengths; some say what happened only indirectly. The facts, and so the decisions, were drawn first.

Examples

julia> r = FunctAI.refunds();

julia> length(r.message), eltype(r.price), eltype(r.final_sale)
(120, Float64, Bool)