Skip to main content
Storage is where a server keeps content, and the ResourceManager is how tools and templates read it. The manager loads content by storage id, url, or a raw file — as text or as media — and renders Jinja templates fetched through the active storage provider.

The storage provider

Storage is a provider behind one ABC. The contract owns the Storage 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.
It returns each resource as text or media. Media content is produced as typed content parts, so a resource that is an image or other binary flows through the same interface as text. Accessing the manager before a storage provider is registered raises the moment a stored resource is first touched.

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 add rendered_* 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’s locale 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:
Storage is dead by default — the skeleton ships no provider. With none registered, 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:
Stored content is one of the sections the backup surface can export and restore. A module may mark a prefix of the store as not backed up, so backups leave the objects under it out, or as unlisted, so no storage or template listing shows them while they stay readable and deletable by id. Inbound media carries both marks; see inbound media for how a plugin declares one.
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.
See the manage-stored-content guide for the media in-and-out workflow and the Python SDK reference for the Storage ABC and the ResourceManager.