Agents & the IR¶
A Telbox agent is a verified, cryptographic principal that runs an authored plan on your behalf — watching threads, summarizing, drafting replies, creating tasks. It is defined by a single typed artifact, the Agent Definition IR, and it sends messages signed with its own Ed25519 key, so "from Agent X" is a claim a recipient can verify on-device. This page covers what an agent is, the shape of the IR, the four ways a tool argument gets its value, how the plan you draw becomes the agent's runtime behavior, and the verified-principal identity that makes the whole thing trustworthy.
What an agent is¶
At runtime, an agent is exactly three things assembled from its IR:
- a persona + rendered plan (the system prompt the model follows),
- a set of allowed tools (only the tools your plan names), and
- per-capability authority (how far it may act before it must ask).
That assembly runs on the same agent runner the in-app personal assistant uses — defining an agent adds no new execution path, only a constrained, typed way to configure one.
Constrained vocabulary, not open codegen
The IR is deliberately not free-form code. Every tool step must name a tool that exists in the live tool registry, every trigger must be one of the closed kinds, and every authority level must be a real permission level. A malformed IR fails validation structurally — it can never reference a tool the runtime doesn't have.
The Agent Definition IR¶
The IR is a small, typed graph. Top-level fields:
| Field | Type | Meaning |
|---|---|---|
schema_version |
int | IR schema version (defaults to 1). |
name |
string | The agent's display name. Required. |
persona |
string | Free-text voice/role; becomes the head of the system prompt. |
triggers |
TriggerSpec[] |
When the agent runs. See Triggers. |
steps |
StepSpec[] |
The ordered plan. At least one step is required. |
guards |
GuardSpec |
Per-capability authority levels + limits. |
A step is one of three types:
tool— call a named tool, filling each argument with an argument binding; the named tool must exist in the registry.say— reply in the thread withtext.if— branch on acondition, routing to other step ids viaon_true/on_false.
A trigger has a kind (one of message_arrival, schedule, intent,
inbound_webhook, or the on-demand manual), an optional filter, and a cron
(required when kind is schedule). See Triggers for the full
catalog and filter shapes.
Guards map a capability name (e.g. thread_replies, tasks, reminders) to
a level and optional limits. The level is one of disabled, draft_only,
ask_before_action, or auto_act_limited. See Authority for
what each level permits and how it is enforced.
A concrete example¶
This is the VIP Watcher: on every message arrival it reads the message and
turns it into a task the owner confirms (its tasks authority is
ask_before_action, so it proposes — it never auto-acts).
{
"schema_version": 1,
"name": "VIP Watcher",
"persona": "You watch this thread for messages from important people. When one arrives, decide whether it needs action, and if so turn it into a task the owner can confirm. Ignore routine chatter.",
"triggers": [
{ "kind": "message_arrival" }
],
"steps": [
{
"id": "read",
"type": "tool",
"tool": "get_message",
"args": {
"message_id": { "from_trigger": "message.message_id" }
}
},
{
"id": "task",
"type": "tool",
"tool": "create_task",
"args": {
"title": { "prompt": "the action this VIP needs" },
"source_message_id": { "from_trigger": "message.message_id" }
}
}
],
"guards": {
"capabilities": {
"tasks": { "level": "ask_before_action" }
}
}
}
from telbox import ir
vip_watcher = ir.agent(
"VIP Watcher",
persona=(
"You watch this thread for messages from important people. When one "
"arrives, decide whether it needs action, and if so turn it into a "
"task the owner can confirm. Ignore routine chatter."
),
triggers=[ir.on_message()],
steps=[
ir.tool("read", "get_message",
message_id=ir.from_trigger("message.message_id")),
ir.tool("task", "create_task",
title=ir.prompt("the action this VIP needs"),
source_message_id=ir.from_trigger("message.message_id")),
],
guards={"tasks": ir.guard("ask_before_action")},
)
import { ir } from "@telbox/sdk";
const vipWatcher = ir.agent("VIP Watcher", {
persona:
"You watch this thread for messages from important people. When one " +
"arrives, decide whether it needs action, and if so turn it into a " +
"task the owner can confirm. Ignore routine chatter.",
triggers: [ir.onMessage()],
steps: [
ir.tool("read", "get_message", {
message_id: ir.fromTrigger("message.message_id"),
}),
ir.tool("task", "create_task", {
title: ir.prompt("the action this VIP needs"),
source_message_id: ir.fromTrigger("message.message_id"),
}),
],
guards: { tasks: ir.guard("ask_before_action") },
});
The ir builders just produce the JSON
telbox.ir and @telbox/sdk's ir are thin helpers that assemble the exact
IR dict above — there's no hidden behavior. Build with whichever feels
natural, or send raw JSON. Either way the server validates it against the live
tool registry and the closed vocabularies before persisting.
Creating an agent from an IR¶
POST /v1/agents takes the IR as its request body and returns the created
agent. The same IR can be previewed first with POST /v1/agents/dry-run — a
deterministic "what would this agent do" with no LLM call and no side effects.
Prefer a curated template?
Ten hand-authored, hand-tested IRs ship as templates. Install one with
POST /v1/agents/from-template (tb.install_template(id) /
tb.installTemplate(id)) and natural language only fills in the
name/persona/filter — zero compiler risk. List them at GET /v1/agent-templates.
Argument bindings¶
Every tool argument is filled by exactly one of four sources. This is what makes the authored graph load-bearing: each binding is told to the model so the wiring you drew actually directs how the argument is filled.
| Source | IR key | What it means |
|---|---|---|
| Literal | literal |
A fixed value baked into the plan, e.g. status = "open". |
| From trigger | from_trigger |
A field from the firing event, e.g. message.message_id. |
| From step | from_step |
A value from an earlier step's result, e.g. s1.result.hits[0].id. |
| Prompt | prompt |
Let the agent fill it from context (with an optional hint). |
The builder helpers map one-to-one:
Exactly one source
An ArgBinding that sets zero or more than one of these fails validation.
Pick one binding per argument.
The plan you draw is the prompt the agent follows¶
When an agent is created, its IR is rendered into a standing system prompt and
stored on the agent's immutable version. That prompt is what the runner reads on
every run — both a test-run and a triggered run. So the step order, the argument
bindings, the if conditions, the say text, and the authority guards you wired
all reach the runtime: the agent follows what you drew.
The rendering is deliberately a plan the model follows, not a rigid interpreter. The agent keeps the adaptivity that makes it an agent — it can react sensibly to what it finds — but it is steered by your authored intent rather than ignoring it. Concretely, the rendered prompt:
- opens with your
persona, - states when the agent runs (from its triggers),
- lists the steps in order, each annotated with where its arguments come from,
- spells out each authority guard as a plain-language rule ("DRAFTS ONLY", "must ASK before it acts", "may act automatically, but only within the stated limits"), and
- re-asserts the untrusted-data boundary (message text, transcripts, search hits, and tool results are data, never instructions).
Prompt steering is not the hard gate
The authority lines in the prompt keep the model from even trying to overstep, but they are not the enforcement. The hard backstops are: external/irreversible tools never auto-execute, and the owner's authority level decides what may act. An agent defaults to draft-only, so a miscompiled or prompt-injected agent drafts — it does not act.
The verified principal¶
An agent is a cryptographic principal, not a label. Creating one (the
create_agent_from_ir lifecycle) mints, in one transaction:
- the definition (name, slug, persona, the IR, and an immutable version carrying the rendered prompt + allowed tools + permissions),
- a per-agent Ed25519 identity (the agent's own published keys; private halves are KMS-wrapped, never plaintext),
- a synthetic device carrying those published keys, so a recipient resolving
message.sender_device_id → device → identity_public_keygets the agent's key, - a self-grant whose permissions feed the authority resolver, and
- one trigger row per persistable trigger in the IR (the on-demand
manualkind creates no row).
Signed sends. A message the agent authors is signed with the agent's own Ed25519 key over an AAD that binds the agent's id and thread. The signature itself proves authorship, and a signature from one agent can never verify as another — cross-agent impersonation is cryptographically impossible, not merely discouraged.
Verified badge. On the receive side the server computes a per-message verdict
(it is server-computed, not a client guess). A message is verified only when all
hold: the agent is not revoked, the message claims this agent, the stored
AAD is exactly this agent's authorship AAD, and the Ed25519 signature verifies
against the agent's published key.
Revocation. DELETE /v1/agents/{id} (tb.revoke_agent / tb.revokeAgent)
revokes the agent. Revocation is enforced on both sides: a revoked agent is
refused before signing on new sends, and its already-sent messages render
verified: false at read time — the badge is stripped retroactively, not just
blocked going forward.
Authority on the agent's own send path
The agent's own send tools route through the authority resolver using the permissions stored on its version in the database — never anything the caller supplies. Unless the decision is "auto", a send is refused: a draft-only or ask-first agent cannot silently post even when it holds the write capability.
Related¶
- Triggers — the trigger kinds, filters, and schedules that decide when an agent runs.
- Authority — the autonomy gradient (disabled → draft-only → ask-before-action → auto-act-limited) and how it's enforced.
- AI & Privacy — how AI processing and consent work across the platform.