The storage provider
Storage is a provider behind one ABC. The contract owns theStorage ABC and its
root-delete guard; the skeleton adds a registry that holds the one active provider.
A concrete provider — a local filesystem or object store — ships as a separate
plugin and registers through tai42_app when the
manifest names its storage_module.
A provider’s list(directory=None) answers every stored path when no directory is given;
given a directory, it answers only the paths beneath it (those starting with
directory_prefix(directory)), read from the backend by that prefix rather than by listing
the whole store, and [] for a directory holding nothing. A blank directory raises
ValueError. load_object(path) answers a StoredObject, the bytes and the content type
stat reports; its default makes two reads, load_bytes and stat, and a backend that
answers both in one read overrides it.
The ResourceManager
The ResourceManager layers on the active provider and resolves a resource from one of three sources:- a storage id — content stored under the provider,
- a url — content fetched from an address, or
- a raw file — content read directly.
Templates are stored resources
A template is one kind of stored resource. The ResourceManager renders Jinja templates fetched through the provider, with a compiled-template cache in front of the render step. Render mixins addrendered_* methods to the contract’s field
models, so a stored template resolves in place wherever a field expects rendered
content.
A template write, a template delete, a directory delete — through tai templates or
the raw tai storage doors alike — a backup restore, or a manual
tai templates clear-cache evicts that compiled-template cache on every worker
over the worker bus, not only on the process that made the
change — so no worker keeps serving a template compiled from stale content, or keeps
treating a just-created template (or locale variant) as missing.
A stored template named by id is checked when the thing that names it is saved, as a tool
name is: a preset’s input and output schema, the slots of a state_binding (on a preset, a
hook, a schedule or a per-target conversation config), a state declaration or state
template, and every templated text a preset’s, a hook’s or a schedule’s tool arguments name
by id: each input field of the tool typed as a templated text, at any depth (an agent’s
system prompt, user message and response format, a sub-agent’s prompt, each entry of a list
of templated texts), and the places a tool declares in its own config. The check reads the
bare default id, also for a text rendered with a locale: a template stored only as locale
variants (greeting@he-IL and no greeting) is refused. An id that is not stored, or that
is not a canonical key, is refused (400, or 422 on the states routes) with the line
stored id '<id>' at <site> is not a stored template. The same check runs wherever a stored
record names a template by id: a hook’s condition, its subject’s key expression and its door
expressions; a schedule’s door expressions; a conversation route’s door expressions and reply
expression; the condition of an API key, a principal or a role. A restore runs it too, and
restores the storage section before every section whose records it checks.
A stored object that something names by id cannot be deleted: the delete, through
tai storage, tai templates or the Storage screen, is refused (409) with one line per
holder, for example hook 'h' names 'jq/gate' at condition, and the answer’s holders
list; a directory delete is refused when any object under it is named. The summarization
setting is among the holders: a stored id in SUMMARIZATION_MIDDLEWARE_SUMMARY_PROMPT holds
its template (setting 'SUMMARIZATION_MIDDLEWARE_SUMMARY_PROMPT' names '<id>' at summary_prompt); that id is resolved when the summarizer is built, not checked at the env
write. Change or remove the holders, then delete. The refusal shows the caller’s view: the
holders under a feature tag the caller’s role cannot read come back as one count per kind and
tag (see Holders a role cannot read). A plugin
that stores records naming templates registers a referee with
tai42_app.storage.register_template_delete_referee, a coroutine called with the frozenset
of ids a delete would remove that answers a list of ReferenceHolder entries, one per place a
record names one, each carrying the feature tag its record lives under; a record that stores a tool name and its arguments reads the template
ids those arguments name with await tai42_app.tools.kwargs_template_refs(tool, kwargs), the
platform’s own reading (a tool that is not registered names none).
A plugin that caches something built from rendered templates follows the same eviction.
tai42_app.storage.template_reads() is a context manager that records, into the set it
yields, every template id a render inside the block reads — a by-id render’s whole locale
fallback chain and every {% include %}, {% extends %} and {% import %} target. A
handler registered with @tai42_app.storage.on_template_evicted is called with a
TemplateEviction (one template id, a directory prefix, or every template; its covers(id)
answers whether an id is dropped) on every process that drops template content, the one
that made the write included. Keeping the recorded ids beside a cached entry and dropping
the entries the eviction covers refreshes exactly what read the changed content. The
Babelfish flows engine caches its flows this way: a template write refreshes exactly the
cached flows that read it, on every worker.
Locale variants
A template renders against the subject’slocale
when the render carries one — the template author never selects a language. A locale-scoped
variant lives at "{id}@{subtags}" (for example greeting@he-IL, greeting@he), and a render
for a locale walks a fallback chain from the most specific subtag form to the least, then to
the bare default id: he-IL resolves greeting@he-IL → greeting@he → greeting, returning
the first stored variant. When none of the chain is stored — no variant and no bare default —
the render refuses loudly, naming the template and locale, rather than silently rendering the
wrong language. A None locale (the locale-agnostic callers — hooks, authz, access control)
renders the bare id unchanged.
The locale also drives the list_format filter: a template writes {{ names | list_format }}
(with an optional CLDR list style, standard by default) and the separator and final connective
come from the resolved locale’s CLDR list patterns. It reads the locale the render resolved, never a
template argument; a render that resolved no locale, or a locale with no CLDR data, raises loudly.
Managing stored content
Two CLI groups reach stored content.tai templates is the higher-level,
template-aware surface (render, cache), while tai storage is the raw
provider-resource surface — thin wrappers over the authed /api/storage* routes:
tai storage info reports the empty state (present: false, a 200
so a UI renders it cleanly) while every CRUD door answers an honest 501 rather
than a fabricated default. A store key is a relative, /-separated name with no
empty, . or .. segment and no backslash. The storage file actions, the template
doors, both entry doors and restore refuse any other spelling with a 400 (a per-row
error on restore). The CLI and the Studio refuse a key with a . or .. segment
before sending, since their HTTP clients would rewrite it, and send every other
spelling, showing the server’s 400. Upload takes exactly one of --text (verbatim)
or --base64 (decoded bytes), and an existing id is overwritten.
The presence answer (GET /api/storage / tai storage info) is a core,
always-mounted read: it is answerable in every deployment, including one that mounts
no storage management surface, so a consumer can always tell “no provider” apart from
“cannot ask”. The answer names the mounted management surfaces (surfaces: storage,
templates), and it is open to the storage grant and the templates grant.
The template-aware surface is the same content viewed as renderable resources:
A specific storage provider’s configuration — its bucket, credentials, or paths —
lives in that provider package’s own repository, reachable from the
Plugins section. The platform documents the storage
contract, not any one provider.
Storage ABC and the ResourceManager.
