News: TypeScript¶
0.1.0 (2026-10-03)¶
Stages 1.2 to 5, plugins and baking's language-neutral half, as Python,
R and Julia have them (design/10, 11 and 12): every case of replies/,
conversations/, tools/, views/, context/, plugins/, baked/ and
saw/ of kind shown passes, and tools/crosslang.py checks
conversations, the disk reply cache and serving against Python.
- The reply cache (
contract/replies.md):cacheReplies: "disk"(or a folder or.sqlitepath) keeps replies in the SQLite file every language shares (node:sqlite);replicate: n; one flight per request, in the process and across processes (a claim with a lease); only a reply that was read is kept; a call whoselogContentdrops a field never reaches the disk. Its key is now the contract's (breaking: a store of your own holds replies under new keys, so it fills again once). fn.mapSettled(a long run goes on past a failure), a progress line formapandevaluatein a terminal (progress),quotesFound,pruneCalls(ratings outlive a cleanup), and the log's top-level kept files are read; for one call id, the record of the highestwriter.- Conversations (
fn.conversation(id, { store }),module.conversation): turns recorded before the call, branches (continueFrom), what each turn is shown (allTurns,lastTurns,without),sends(queue, refuse, branch),requestId, leases, stopping from any process, merging,render; stores in memory, in a folder (FolderStore: the files Python, R and Julia use, locked withflock), or your own. A module's code readsearlier(); helpers remember whatrememberssays. - Tools that ask first:
tool(name, { effects }), theapprovesetting (a function, or a rule),s.approve()/s.deny()on a stream, a turn that waits (Waiting) and resumes in any process, replaying the model replies and tool results it kept; a tool that may have run is never run again on its own (turn.resume({ results }),{ rerun }).approvalandapprovedevents;invocationon tool events and on the calls a tool makes. - Plugins (
Plugin, seven hooks, changes as data in the record'schanges,replayable: falsewhen a request was replaced), withcompaction()anddelegate()built from the public hooks. - Views and serving: the
outsideview (s.events({ view: "outside" }),answerFromfor a module),Service(afetchhandler) andserve()(Node's server) with the contract's routes, andremote(url): a served program used like a local one, one call tree across two logs. - Learning from conversations:
ratedgivesearlier,conversation(andhelpers), and countsnoContext;evaluateasks such rows again with their earlier turns (the call'ssawissaw_ofthe rated call); the few-shot optimizers never make them worked examples;split. - Records keep
steps(a call that ran tools or was a conversation's turn),conversation,invocation,writer,sections,changes,escalatedandprobabilities. - Escalation (
escalateTo,escalateBelow),Prediction.probabilitiesand.confidence. - Baking's language-neutral half:
bakeExamples,exportExamples,bakeEntry, andbaked(folder, { url }): a student trained anywhere, called as it was trained. randomSearch,instructionSearch,compare,inspectHistory,phistory,login/logins/logout(lm15's shared credentials: a saved login is used for its provider's calls).evaluatetakes modules and served programs.fn.render()returns a promise (breaking): the plugins around a call and its conversation's store may be asynchronous.-
A stopped call no longer waits for a tool that never returns: it ends
Cancelled(the tool may keep running). -
A reply cut off at the token limit (
contract/functions.md): it is sent again with twicemaxTokensonly when one was set. Without one it already had the model's whole limit (lm15's default, or the provider's own), and the old re-send with 2048 (a guessed 1024, doubled) only shrank it: it now raises at once. The refusal's hint says how many tokens went to thinking, what the limit was and whether it can be raised, and what lm15 changed in the request (a dropped thinking budget, the usual reasonthinking_budgetdoes not bound the thinking).
Stage 1.1 (design/09-stage1.1-decisions.md):
- Inputs are bound (breaking): an AI function's and a module's inputs
are converted to their declared type when the meaning is clear (
42to text is"42","5"to an integer is5, a record keeps only its fields), before any schema parses them; otherwiseInterfaceError(interface-input), recorded, and nothing is sent.null,undefinedorNaNfor an optional input is that input left out. The record holds the bound values (a text input given an array records the text sent). Outputs are checked, never converted. Records are closed. - Error messages quote the value at fault (cut after 80 characters),
except a value a
logContentsetting drops. - Defaults: a default may be a function of no arguments
(
t.string({ default: () => today() })), computed at each call that leaves the input out; the version counts it by the expression it returns (today(), as Python's), every other default by its value, and a saved folder keeps it (a node'sdefaults). Signatures leave out every default. Every function and module with a default gets a new version once. - The call log: a re-ask's
request_hashis the hash of what it sent;outputs.callsholds every tool call of the call;returnedis kept only when nothing is dropped; droppingcallskeepsreasoning;process.lmccandprocess.lm15. programObservers: false(a host's block orconfigure): a program's own observers get no event.- Ratings: with no person named (
by, or the caller'suser), a rating is made under the computer'saccountand kept on its own, so ratings on a shared account no longer replace each other. A program whose module is its file's base name (nodefinedIn) is known by its file too:rated(fn)takes only calls fromfn's file (two folders'summarize.tsare two programs);rated(fn, { anyFile: true })pools across files. - The sample value of a shape whose type is a list of types is the first
non-null one (it was
"example text"for any list).
Stage 1 of the contract (design/08-stage1-foundations.md):
module(name, { description, input, output | outputs, uses }, run)(breaking; wasmodule(name, run, { uses })): a module declares its interface, checked on every call (InterfaceError, logged);rungets its inputs by name and{ signal, callId }. Its version includes its interface; its code hash is nowsha256:-prefixed (every module's version changed once)..stream(),.using(),.interface.- Interfaces: every program has
.interfaceand.interfaceId(the call log'sprogram.interface); an AI function's is checked when it is defined;checkInterface(). An optional input's default is in its interface and left out of the signature (zod's.default(x)changes the version once).t.withDefault,t.json,t.opaque, and{ shape, desc, optional, opaque }for a field. - Call log format 2:
program.interface,saw([]),request_hash,described,journal;logContentby field, only removing, over every layer (SettingErrorfor a misspelled name); reading formats 1 and 2 (ratedby interface). - Stream events format 2:
tree,writer,seq,after,at, therequestevent (a field's text is its latest request's); a stream opened inside a tree shows the tree's numbers. Forms ("kept"), views, resuming (after,read),Follower,replay,keptLog. - Observers and journals (
observers,journalsettings):MemoryStore(claims, appends, batches, reads), best-effort and required journals with barriers,JournalError(journal-policy,journal-scope,journal-barrier,journal-endwithoutcome,eventandsettle()). - Saved folders: nodes carry their interface; loading takes optional
inputs from it and checks it;
describeSaved();save(module)writes the module's node and the AI functions it uses. The manifest is checked against the contract's schema. - Journals and observers never hold a call up nor reach it:
- A journal setting takes
timeout(ms, default 30,000: the longest an append is waited for, itssignalaborted then, and the longest a barrier waits) andbackoff(ms, default 50, doubling up to 2 s between resends). A store that throws (even before returning a promise), never answers, or answers anything but"kept","duplicate"or a refusal is no answer or a refusal: it can no longer spin the process, hang a call or crash it. A barrier waits for its own event (not for other calls' later events), and stops when the call is cancelled (start and tools; the end waits for its timeout). Each append gets fresh copies of its events. A failing journal is warned about once per outage, not once per process. - Observers are called off the call's turn, each with its own copy of
every event, from a queue of at most 10,000 events per observer (then
it loses events and sees the gap); an object with
postMessage(aWorker, aMessagePort) is an observer.flush()waits for observers and journals. (Before, observers ran inside the call and could change what the journal and the stream held.) JournalErrorhas no type parameter;outcome.doneis what the caller would have got: the answer fromfn(x)and a stream's result, thePredictionfromfn.predict(x)andstream.prediction.observersandjournalsettings that could only fail later refuse where they are set (TypeError), aslogContentdoes; a call's options are checked too, and a loaded function's ownlogContent(SettingError,log-content-field).- Records and events keep only a program's fields: a value given to a
module under another name is refused and never kept (not even under a
host's
"*": falseallowlist); inputs are recorded as given when the call starts (not a schema's transform, nor what the code later does to them). A module's argument that cannot be bound is a recorded refusal. Interface errors name the field and the kind of value, never the value. - An AI function's record holds every output the model gave,
calls(the tool calls of its last step,[]once it answers) included, as Python's does. A re-ask's exchange keeps therequest_hashof the rendered request it came from, as Python's does. - Closing a stream (or aborting its signal) while a module's code runs
ends the call
Cancelled, whatever the code returns after; a finished stream lets go of its caller's signal, and so does a retry's wait. - Declarations: a field that is not a shape refuses
interface-malformednaming it; an output cannot beoptional, nor an AI function's output opaque. One output, whatever its name, is the value, in the types too (outputs: { count: t.integer() }returns a number).t.string({ default })(andinteger,number,boolean) is typed as an input that may be left out;t.json,t.list,t.objectandt.recordtake extra keys as the other builders do (their words were dropped before). - Names JavaScript treats specially are data: a JSON member
__proto__, a required propertytoString, a field namedconstructor; an unknown shape keyword namedconstructorrefuses. This now holds for AI functions too: binding, preparing and sampling their inputs, reading rows and worked examples, and checking a model's object answers look at own properties only. Before, a required input namedtoStringorconstructorthat was left out was sent to the model as JavaScript function text, and one named__proto__lost its value. A Standard Schema's JSON Schema keeps a property named__proto__. With lmcc's D-58 (below), every name works as in Python, and the same requests are sent, saved and loaded both ways: an input or an output named__proto__; a JSON input whose value holds a member named__proto__(before, the member was dropped); a worked example that leaves out an input or output named like an Object member (before, the call failed); and a reply that leaves out an output named like an Object member (toString,valueOf, …) is asked again and refusedparse-missing-fields, by every adapter (before, it was read as"", and thejsonadapter refused a correct replyparse-ambiguous).bootstrapFewShotkeeps an input named__proto__, and a__proto__member of an output, in the worked examples it records (before, both were lost, in the saved folder too).gepa's default feedback reads rows by own members. - Members keep their order, as in Python: a value's members, even
names like
"10"that JavaScript lists first, keep the value's order in what a call sends (a text input given an object included), its record and its events (observers, journals,MemoryStore), the rowsratedreads back, the worked examples labeled and bootstrapped, and a saved folder, written and read (save,load). Values are copied, read and written with lmcc's helpers, neverstructuredClone,JSON.parseorJSON.stringify. JSON lm15 writes (aresponse_formatschema, a tool's parameters, aconfig) keeps the order too: FunctAI requires@lm15/lm151.0.0-rc.3 and lmcc 0.8.5, which keep it, so a request goes out with the same bytes as Python's. - FunctAI hands lm15 what lmcc built or parsed through lmcc's bridge
(
lmcc/lm15, D-59): a request and itsConfigwithrequest(), a savedconfigand a cached reply withtoLm15. lmcc's record of member order is lm15's, so it reaches the provider; an lm15 without it is given plain copies; an integer past 2^53 goes as lm15's own number. Before, ajson-adapter function whose output shape had an integer-like property after another (fromlmcc.parseJson, or a saved folder, Python's included), or a tool with such parameters, threw aTypeErrorat every render and call; a cached reply read back that way was skipped with a warning. A cached reply a store keeps as text is read with lm15'sparseJson(wasJSON.parse, which lost the order of a tool call's input and the digits of a big integer). - The JSON FunctAI writes (a text input given an object, call log lines,
saved folders) visits every array index again, a hole written
null: before,[, "B"]was written[,"B"](a call log line and a saved folder no reader took), and["A", ,]andnew Array(2)lost elements. A boxed primitive is written as its value (new Number(42)was{}; aStringobject given to a text input is its text), and recorded as its value in the call log; a value that holds itself is refused withJSON.stringify'sTypeError(was a stack overflow). - An integer past 2^53 in an answer, or anywhere in a value, is recorded
as its digits (before, as a description; a JSON answer holding one was
recorded as
{"$type": "Object", ...}). An array's hole, orundefined, in a recorded value isnull(anundefinedmade the whole value a description). - lmcc comes from npm (
lmcc^0.8.5), no longer from a checkout of lmcc beside this repository:npm install functaiinstalls everything it needs. - functai needs lmcc with decision D-58 (lmcc 0.8.5 or later): it refuses to start on an lmcc without its helpers (lmcc 0.8.4 as published on npm). The workarounds for the older kernel are gone: values are ordinary objects again, and nothing is refused for its names.
- A function loaded from a saved folder keeps its fields' type names as
saved (
dict,str): its signature's fingerprint is the saved one, so a worked example recorded by another language (a Python bootstrap) is replayed as the model wrote it. Before, it was written again from its values, another request than the saving language's, and a folder whose recorded reply was spelled otherwise refusedsaved-differs. - A call whose code rejects with no reason (
Promise.reject(),throw undefined) fails as any other: itsfailedevent is kept and shown, and its record says it failed. Before, it made no terminal event and no record, and its caller got an unrelatedTypeError. A tool that throwsundefinedis reported to the model as an error. - An AI function records its inputs as given even when a schema changes them in place (a nested object edited by a transform): they are written as JSON before the schema runs.
- A schema whose validation returns another realm's promise (a
vmcontext, an iframe) is awaited. - Observers each have their own queue and share of the delivery time: a slow observer falls behind and loses events alone, never the others.
- A journal writer whose round of resends gave up tries again later on
its own (1, 2, 4, … 60 s apart, for as long as the process lives) and at
the next event. It gives up on a log only for memory: when all writers
hold more than 100,000 unconfirmed events, the one holding the oldest
gives up its log (warned once per outage), and lets go at once: its
append under way is aborted and no longer waited for (even with
timeout: Infinity), and no resend follows.flush()sends what is not confirmed once more (again after a round that was under way when it began), and returnsfalsewhile a journal still holds events it did not confirm, or once after a writer gave up on events. (Before, it saidtruewhile a tree's end was lost for good.) - A journal's
timeoutandbackoffabove 2,147,483,647 ms (a timer's limit) refuse where they are set;timeout: Infinitystill waits for ever.JournalError.settle({ signal })can be stopped, also by an abort the store's own read makes. - A builder's extra keys never replace what it makes (
t.list(x, { items })is aTypeError), and a builder'sdefaultis typed as its value.ModuleResultof outputs built at run time (Record<string, …>) is a record, not one value. replay()andFollower.recover()stop at an event of a format they do not know (stopped);recover()from a readable source follows again;Follower.forget(tree).- Validation started for a call's inputs never leaves a rejection unhandled.
npm testhas a time limit per test.sawOfandkeepsSawread what a call saw.-
An object argument is the inputs by name when every key is an input's or it holds the one required input's name (else, that input's value); input errors are
InterfaceErrors (aTypeError). -
The API follows TypeScript:
ai("mood", { description, input, output })(the name first;input, as fortool); a call takes its inputs by name, typed, or a one-input function its value alone, and nothing else, so a missing, misspelled or mistyped input is a compile error.tool("name", { description, input }, run)typesrun's input. - A call's options:
fn(input, { signal, lm, temperature, ... }), anAbortSignalthat cancels it and settings for that call only. fn.map(inputs, { concurrency }): every answer, in order.- Rows are typed in
evaluate,labeledFewShot,bootstrapFewShotandgepa: a row missing an input, or anexpectedcolumn the rows lack, is a compile error. The few-shot optimizers takeexpectedtoo. gepareturns{ fn, trials, calls, reflections }(trials(fn)is gone).calls()gives typedLoggedCalls (camelCase;recordis the line as written);rated().leftOutis camelCase.- Any Standard Schema with JSON Schema (zod 4, valibot, arktype, ...) is a field, typed by its output type.
- Optional inputs: a field whose schema accepts a missing value may be
left out:
t.optional(...)and zod's.optional()are sent as null (the shape says so, so the version is Python's forx: T | None = None),.default(x)asx;.nullable()must still be given. Given values are checked (and parsed) by their Standard Schema before any call. With one required input, its value alone is the call; with none, no argument. - The reply cache:
cacheReplies: true(memory, the last 20,000) or any store withget/set/delete, given plain JSON;clearCache(). As Python'scache_replies, and like it now: an unreadable reply is not kept, andgepa's teacher is never answered from it. A store that fails is skipped with one warning. -
tests/types.tspins the types (tscchecks it; each@ts-expect-errormust stay an error). -
gepa(fn, rows, { teacher }): the instruction rewritten from the function's mistakes, as Python'sGEPAand R'sgepa()do (design/04-gepa.md);trials(fn)gives the search. withSettings({ caller })adds to the enclosing block's caller instead of replacing it, so an evaluation inside an optimization is logged as both.
The first TypeScript implementation of FunctAI, held to the same contract
as the Python package (../contract): every function, score, rating and
saved-folder case, and a check against Python itself (../tools/crosslang.py).
-
A temperature or top_p a model does not take (GPT-6, the o-series and GPT-5, Claude 5) is left out of its requests, with one warning, as in Python (
contract/models.json,fixed_sampling). -
ai({ name, description, inputs, output | outputs, ...settings }): a typed function whose body a model writes. Shapes witht, zod 4 or JSON Schema. The layoutsxml(default),chatandjson, templates,module: "cot", tools. - The same function has the same version and signature as in Python.
- Unreadable replies are asked again (the contract's words); values are checked against their shapes; transient provider errors are re-sent.
- Streaming (
fn.stream): the answer's text as it arrives, and every event ofcontract/streaming.md. - The call log,
rate,rated,calls: the same folder and records as Python. evaluate,exactMatch,interval: the same scores and ranges as Python.- The call log's
program.fileand.lineare the line that calledai(), found by who called rather than by file path: right when bundled (with--enable-source-maps, the original file), on Windows paths,file:URLs with spaces, and in a project whose own folder is namedfunctai. labeledFewShot,bootstrapFewShot.load/fromManifest: run an AI function Python saved;save/toManifest.module(name, fn, { uses }): code that calls AI functions, logged as one call with theirs as children.