Agents
Agents are alpha and not part of the frozen v1 contract. The agent
kind, the ai.teild.dev vocabulary (llm, thread, message), and the
/agents chat and call wire are all alpha and unfrozen at v1. They are marked
alpha in the docs and in API discovery, and
they may change, or be superseded, without counting as a v1 wire break. Build
on the frozen core (entities, the API, functions, triggers) with confidence;
treat agents as a preview.
An agent is a callable whose body is an LLM loop, run host-side. It has
one identity and the same four ways in as a function: a
trigger delivery (a callable reference whose type is agent), the call
API, a sub-agent call, and chat. Its actor is agent.<name>.<group>, and its dispatch stamps the
extension tier on its writes, exactly like a function's. A bundled agent obeys
its bundle's lifecycle: while the
bundle is disabled or uninstalled, every entry refuses with a guard error.
The agent schema kind is a first-class core kind, identity
agent.core.teild.dev, like every other schema kind. Its runtime vocabulary,
by contrast, lives in its own group.
The llm, thread, and message vocabulary is consolidated into a single
ai.teild.dev group, so everything AI lives in one place. Their full type
identities are llm.ai.teild.dev, thread.ai.teild.dev, and
message.ai.teild.dev. There is no leftover agents.teild.dev group.
The manifest
data carries:
description(required), model-facing: the agent is its own tool card wherever it appears as a sub-agent.prompt(required, at most 64 KiB): the row is the prompt store, and the changelog's full retention is its version history.llm(required): anllmdata-entity id, resolved at dispatch.tools:, the callables the model may invoke (below).agents:, sub-agent identities (self-reference is a load error).budgets:bounds one run:maxTurns(default 8, max 64),maxToolCalls(default 32, max 256),deadlineSeconds(default 120, max 600), anddepth(default 3, max 3).emit:, the allowlist for the agent's writes, and which*requesttypesproposemay emit. Empty means the agent writes nothing.- optional
reads:, the function envelope's read shape verbatim (types plus calls/rows budgets).
Because schema is entities, a parsed agent projects to a row the console lists
and creates like any other, with first-class columns (name, group,
description, prompt, llm, functions, subagents) mirroring the
manifest and a definition json carrying the authoritative envelope.
Tools
A tools: entry is a bare string, one of the two built-ins or a function
identity, or a {callable, name, description} alias that recolors this agent's
prompt context without changing the function's canonical description. Tool
names are unique per agent.
queryis the capability-scoped read, and requiresreads:. A get outside the allowlist answers like an absent id; list and search clamp to the remaining row budget; a blown budget is a tool error the model sees.proposeis the reviewed write, and requiresemit:to nameentitypatchrequest.core.teild.dev. It lands oneentitypatchrequest, never a direct mutation. It carries anop(patchdefault,create, ordelete) and arationale, and its diff is validated and normalised at propose time, so a malformed proposal (a wrapper-less diff, an immutable or unknown key, a secret-typed property) is a tool error the model sees, never a bad request reaching the owner's inbox.- A function tool runs through the same runner invoke a host call uses. Its effects pass both envelopes: decoded against the function's capabilities, then held whole to the agent's emit. One effect outside the agent's emit fails the whole tool call as a result the model sees, and nothing applies.
Applied effects land in their own transaction under the agent's actor, each row
recording the triggering seq as its cause. Creation and deletion still flow through
the reviewed request: propose emits the request, and accepting it is the
write.
Sub-agents, budgets, and the emit ceiling
Sub-agents are child invocations with their own budgets and a child thread
carrying the parent edge. agentDepth is 0 at the root and increments per
hop, checked against the calling agent's budgets.depth (hard-capped at 3), so
the default chain refuses the hop that would run at depth 4, as a tool error
the caller sees.
The emit ceiling rides every hop. A child's effective emit is its own
declared emit intersected with the caller's effective emit, enforced against
function-tool effects and propose alike. An empty-emit parent delegating to a
write-capable child yields a child that writes nothing: no chain of children can
ever write a type its narrowest ancestor could not. Accepting a proposed
request is authorized as the transitive write too, so a function or agent that
drives an accept must have the concrete written type in its effective emit, or
the accept refuses. An owner's acceptance stays unbounded.
Threads, messages, and cost
The conversation state is the run: there is no separate run entity. A thread
is written as the loop runs under the agent's actor, carrying agent, llm,
mode, status (running then ok/overbudget/error), agentDepth, the
tallies (turns, toolCalls, the token counts, costUSD), and
startedAt/finishedAt. A message carries role, content, turn, and the
tool-call audit, plus the required thread edge.
Cost rolls up onto the root thread: every loop on a chain adds to one
shared tally, so the root thread's numbers include every descendant while a
child thread carries only its own. Pricing is data on the llm row, never a
table in code. The loop terminates on the final tool-free reply, any budget, or
its deadline; over-budget is a settled outcome (thread overbudget with a
reason), never a park. A loop error (the LLM transport failing) rides the
ordinary retries and parks, each attempt a fresh thread but not fresh
idempotency keys, so an effectful tool that honors keys never double-fires
across a retry.
LLM rows
llm rows are pure data over the host-native OpenAI-compatible transport:
name, provider, baseURL, model, defaults (temperature), pricing,
and an optional secret apiKey. A row that declares its own baseURL must
declare its own apiKey, or it refuses to resolve: the host gateway key never
travels to a row-defined endpoint. Three well-known rows, cheap, mid, and
strong, seed at tenant open create-only, each pinning a concrete gateway
model id and its pricing pair, so an owner's re-tiering or deliberate delete
stands. Exotic
providers wrap as function tools, never as the transport.
Calling an agent
POST …/agents/{name}/call with {"input": …} runs the loop once: the input
becomes the first user message, and the answer carries reply, thread,
status, effects with its effectsByAction breakdown, turns,
toolCalls, and the token and costUSD tallies. POST …/agents/{name}/chat
with {"thread"?, "message"}
opens or continues a thread and streams the loop: application/x-ndjson, one
JSON object per line keyed by kind (thread first, delta carrying text
per streamed token, toolStarted and toolFinished around each dispatch, one
done carrying the settled result under result).
A loop that fails after the 200 status line has already gone out terminates
the stream with an error event instead, the same choice the
changelog makes, so a failure is never a
done with no result. A
continued thread replays its prose history and keeps one running tally on the
thread row; one active turn per thread is enforced by a lease, so a second
concurrent turn is refused as a conflict.
Next: the extensions catalog, what ships today.