The backend seam
TheBackend ABC is the contract; the skeleton re-exports it as the registration
seam and ships the callback glue that chains a backend result back into the
runtime. Concrete worker backends — for example ones built on Celery, RQ, or arq —
are external plugins that implement the ABC and
register through tai42_app.backends.register_backend when the
manifest names the backend_module.
The backend runs as its own process:
backend_module requires the bus: set TAI_BUS_REDIS_URL
or the server refuses to start.
It also requires a shared hooks registry:
set HOOKS_REDIS_URL (or TAI_DEFAULT_REDIS_URL), whatever the worker count. The
backend process raises platform events of its own and matches them against its own
hooks registry; an in-memory one there holds none of the hooks registered through
the server, so those events would run no hook. tai serve, tai backend, a config
reload and a config write all refuse a backend_module over an in-memory hooks
registry.
Three ways a tool runs off the request path
- Background — submit a tool run and poll its result, for a run that outlives one request. The background tool-runs HTTP surface loads as a router module.
- Scheduled — schedule a tool to run later or on a cadence. A schedule names
the base
tool_name, itstool_kwargs, and a typedcadence(an interval or a crontab). The tool must carry the backend’sschedule_taskextension to be scheduled on a cadence — see the schedule-a-tool guide for attaching it. - Distributed — the worker backend spreads runs across workers.
The backend tools
Every backend offers the same 22backend_* tools, tagged backend. The kit builds
them once over the backend’s seams, so each tool takes one argument list on every
backend; a backend supplies only what its engine reads and writes.
The ten schedule tools read and write the backend’s schedule store:
A schedule tool’s status reply is
{"status": ..., "name": name}, and a name with no
schedule replies {"status": "not_found", "name": name}. An update given no new cadence
and no next run writes nothing and replies
{"status": "skipped", "name": name, "message": "No changes provided; nothing updated."}.
What a backend’s store cannot represent is refused on every write: a relative interval on
a backend that honours none, and a disabled schedule on a backend that holds none. On such
a backend every stored schedule is enabled already, so backend_enable_schedule replies
enabled and writes nothing, and backend_disable_schedule raises
UnsupportedBackendTool, leaving the schedule as it is.
The twelve task and worker tools read the backend’s own views of its tasks and workers:
backend_ping_worker, backend_list_active_workers, backend_task_status,
backend_task_result, backend_cancel_task, backend_active_tasks,
backend_reserved_tasks, backend_scheduled_tasks, backend_registered_tasks,
backend_list_failed_tasks, backend_worker_stats and backend_worker_queues. Each
replies what the backend reports, in the backend’s own shape and words.
backend_task_result replies Task <id> is not ready (status: <status>) while the task
is unfinished, and raises a failed task’s failure.
worker_name.backend_ping_worker,backend_active_tasks,backend_registered_tasks,backend_worker_statsandbackend_worker_queuestake an optionalworker_namethat narrows the call to one worker. A backend that cannot address one worker refuses anyworker_namebutNone.backend_cancel_task(task_id, terminate=False). Has one meaning on every backend. Withoutterminate, a task that has not started is kept from running, and a running task is left to finish. Withterminate=True, a running task is stopped as well. A backend that cannot stop a running task refusesterminate=True, and one that cannot cancel a running task without stopping it refusesterminate=Falsefor a running task. When a worker answers that it cannot stop the task it runs,terminate=Trueis refused for that task rather than leaving it running behind a reply that says otherwise. Each refusal raisesUnsupportedBackendTool. The reply says what was done; a stop the workers did not confirm within the time the backend waits for it is reported as not confirmed, with the status the task reached.- What a backend cannot serve. A tool, or a form of one, that a backend cannot serve is
still registered, and raises
UnsupportedBackendToolnaming the backend and the tool when it is called. Each backend’s page in the Plugins section lists what it refuses.
Queue labels
The<tool>_sync_task and <tool>_async_task branches take an optional queue
argument: a label of letters, digits, _ and - naming a worker pool. A task sent with
queue: gpu goes to the queue <QUEUE_NAME>.gpu, where <QUEUE_NAME> is the backend’s
own queue setting (ARQ_QUEUE_NAME, CELERY_QUEUE_NAME, RQ_QUEUE_NAME). Any other
value is refused before anything is queued, and a task sent with no queue stays on
<QUEUE_NAME>. A preset can fix queue in its arguments like any other argument. A tool
with a queue parameter of its own cannot take either branch.
A worker serves one queue. Start it with a label to serve that label’s queue alone:
--queue serves <QUEUE_NAME>. A task on a queue no worker
serves waits: a sync_task raises TimeoutError after the job budget, and an
async_task id stays queued. Recurring schedules carry no queue, so a stack that runs
schedules keeps a worker without a label.
Chaining a job with a callback
A job does not wait for another job. It chains the next one withcallback_kwargs, an
option of the sync_task and async_task branches. The callback runs as its own job,
only when the job it follows succeeded, on that job’s queue (its label included). On a
backend whose callback job reads the job it follows back from the engine, a callback job
that finds that job unfinished or failed raises, so a broken chain is a failed job, never
a silent one. Inside a job, sync_task and backend_task_result given a timeout raise
RuntimeError instead of waiting.
The job budget
Every backend bounds a dispatched job by one setting, the dispatch group’stask_timeout (<PREFIX>_TASK_TIMEOUT, 300 seconds by default): it is the longest a
job may run on the worker, and how long a synchronous dispatch waits for its result.
It covers every job the worker runs — a dispatched task, a schedule fire, a callback.
Celery applies it as task_time_limit, RQ’s worker sets it as the timeout of every job
it runs (whatever timeout the job was stored with), and arq as the worker’s
--job-timeout default. The worker binds it when it starts, so a change converges
through a process recycle.
A backend never retries a tool body. A tool that wants retries declares a
ToolRetryPolicy, applied by
the platform’s dispatch seam on every door. What a backend still retries is its own
transport — publishing a message on connection loss, reconnecting to its broker.
Writing a backend
A backend plugs its engine into three seams declared intai42_contract.backend, and the
kit builds every tool over them. What the seams cannot express, the backend’s engine does
not offer, and the tools refuse it the same way on every backend.
- The task door. A
TaskDoorqueues a dispatched call on the engine (submit), names its job (task_id) and waits for its result (wait, raisingTaskWaitTimeoutErrorwhen the job is unfinished at its timeout). Itstask_optsdeclare the backend’s own task options beside the ones every backend honours (countdown,expires,eta,callback_kwargs,queue). - The schedule store. A
ScheduleStoreis the engine’s durable home of schedules:read,read_all,exists,delete,set_enabled,run_nowandupdate, read back asStoredSchedule. ItsScheduleWriterpart is the one writer every write door shares (write), and its capabilities say what it can represent:relative_intervalsandholds_disabled. - The worker inspection. A
WorkerInspectionanswers the task and worker views. The kit’sBackendInspectionbase refuses each view withUnsupportedBackendTooluntil the backend overrides it;addresses_workersandterminatessay whether the backend can narrow a view to one worker and stop a running task.
register_task_branches (the
sync_task, async_task and schedule_task BACKEND extensions over its door and
writer), register_schedule_tools (over its store) and register_inspection_tools (over
its inspection). A schedule’s tool rides inside the stored job kwargs under the backend’s
own tool-name key; the kit splits it out into the record’s tool on export and list, and
injects it back on import.
The kit’s conformance checks certify a backend in its own tests:
check_backend_declarations checks its runtimes, and check_tool_surface checks that
every backend_* tool is registered under the backend tag and no other one is.
tai42_kit.utils.runtime.normalize_schedule parses the friendly specs a schedule tool
accepts (seconds, a crontab string, {"type": "interval", "every": 5, "period": "minutes"}) into the contract’s cadence.
See Backends for the seams and
Backend dispatch for the branch tools,
their task options and the queue labels.
Routing a tool to a backend
A tool is routed to a backend with aBACKEND-kind
extension — the extension kind whose job is to
send a tool’s execution through a backend rather than running it inline. The tool’s
definition does not change; the extension decides where it runs.
A persist-across-jobs backend — a Celery prefork pool, whose forked children
hold their own in-memory caches for the life of the child — turns its pool over
once after any template-eviction or registry-mutating fleet op reaches it over
the worker bus, so a child never keeps serving a stale
compiled template or a stale registry from before the change.
A specific worker backend’s setup — its broker, queues, and concurrency — lives in
that backend package’s own repository, reachable from the
Plugins section. The platform documents the backend contract,
not any one engine.

