~9 min readgrounded in apps/web/app/api/chat/route.ts · apps/web/lib/chat/tools/* · packages/contracts/src/{chat,sse}.ts
The agent loop and its tools·
Every conversation is one POST /api/chat. The route builds a Strands Agent for that request — the tiny's system prompt, the caller's memory, and a tool list that depends on who is asking and from where — runs it, and streams the result back as Server-Sent Events. Nothing about the agent is cached between requests; the conversation history arrives in the body.
One request, step by step·
- Resolve the tiny. The
x-tiny-nameheader names it. The route fetches its record from the worker: system prompt, knowledge, data, an optional OpenAPI schema or list of skills, MCP servers. - Resolve the caller. The
tiny_sessioncookie, if present, gives a signed-in user. Anonymous callers still get an agent — with tools that answerLogin requiredwhere an account is needed (below). - Retrieve context. The user's most recent and most relevant server-side memories are pre-loaded into the prompt; the rest are reachable through
recall. Tinys retrieved from the universe contribute a compact summary (name, URL, the first 300 characters of prompt and data, skill names) — their full schemas become tools, not prompt text. - Choose the model. Per-request
x-tiny-model-*headers win; otherwise the user's synced model config; otherwise the deployment default (TINY_MODEL_PROVIDERand its key). See Configure models. - Mount tools. Built-ins first, then the user's forged tools, then the operations derived from the tiny's own schema and from retrieved tinys, then MCP clients. Names are deduplicated with built-ins winning — the registry throws on a duplicate, and a public tiny must not be able to shadow
learnwith a skill of the same name. Tools the user disabled withmanage_toolsare dropped here. - Stream. Each agent event is normalised into one SSE frame with a monotonic
seq, so a client can detect a dropped frame. A: pingcomment goes out every 15 s. The stream ends withdata: [DONE]. - Recover from overflow, once. If the model rejects the context as too long and there are at least four history messages, the route drops the older half, emits
{"type":"contextCompacted","dropped":N}and retries with a fresh agent. Tool-name pairings learned before the retry are kept so results still render with their names. - Cancel on disconnect. When the client goes away,
agent.cancel()propagates to the model provider — upstream inference stops, not just the loop.
The route runs on the Edge runtime with maxDuration = 300 seconds.
Request and headers·
Body: { "messages": [{ "role": "user" | "assistant" | "system", "content": string | block[] }] }.
| Header | Purpose |
|---|---|
x-tiny-name |
which tiny answers |
x-tiny-session |
a client session id; tiny-ios / tiny-android identify the native apps and change which tools are mounted |
x-tiny-system-prompt |
an override for the tiny's prompt |
x-tiny-metadata (legacy x-tiny-ip) |
client metadata the prompt can mention |
x-tiny-key |
the key of a private tiny |
x-tiny-mcp-servers |
MCP server configuration to mount as tool clients |
x-tiny-model-provider, x-tiny-model-id, x-tiny-model-api-key, x-tiny-model-base-url, x-tiny-model-max-tokens, x-tiny-model-region, x-tiny-model-additional-fields |
bring-your-own model for this request |
x-tiny-x402-settled |
proof that a paid message was settled (payments only) |
These names are the CHAT_HEADERS constants in packages/contracts/src/chat.ts. Providers the contract enumerates: openai, bedrock, gemini/google, vercel, anthropic, openrouter, groq, deepseek, mistral, xai, perplexity, custom.
The stream·
Frames are data: {json} blank-line separated, where the JSON is one of the events in packages/contracts/src/sse.ts plus seq:
type |
Carries | When |
|---|---|---|
modelMessageStartEvent / modelMessageStopEvent |
stopReason on stop |
a model turn begins / ends |
modelContentBlockDeltaEvent |
textDelta, reasoningDelta, toolInputDelta, citationsDelta |
tokens as they arrive |
modelContentBlockStartEvent |
toolStart {name, toolUseId} |
the model begins a tool call |
modelContentBlockStopEvent |
— | a block closes |
beforeToolCallEvent |
toolCall {name, toolUseId, input} |
a tool is about to run |
toolStreamUpdateEvent |
toolStream {toolUseId, name, data} |
a tool reports progress |
afterToolCallEvent |
toolResult {name, toolUseId, status, content, error} |
a tool finished |
toolResultBlock |
toolResultBlock {toolUseId, status, content} |
the result as the model sees it |
modelMetadataEvent |
usage {inputTokens, outputTokens, totalTokens}, metrics, modelId |
after each model turn |
contextCompacted |
dropped |
the overflow retry above |
agentResultEvent |
stopReason |
the agent is done |
error |
error (string) |
something failed |
Then data: [DONE]. The smoke test from the deploy guide shows the shape end to end:
curl -s -N -X POST https://<your-app>/api/chat -H 'content-type: application/json' \
-H 'x-tiny-name: tiny' -H 'x-tiny-session: s1' \
-d '{"messages":[{"role":"user","content":"Reply with exactly the word: pong"}]}'
Tools·
The mount list is one array in apps/web/app/api/chat/route.ts; the names below are exact. account marks tools that are mounted for everyone but answer Login required without a session, because what they touch belongs to a user.
Tinys and the universe·
| Tool | What it does |
|---|---|
create_ai |
make a new tiny: name, systemPrompt, systemKnowledge, optional data, worker (an OpenAPI URL — its operations become tools), hook, hero, logo, theme, tagline, chips, intro_vibe |
modify_ai |
change any of those on a tiny you own; empty strings clear media and theme, an empty chips array restores the defaults |
get_tiny |
read a tiny by name |
list_tiny |
list tinys, paginated and filterable |
retrieve |
semantic search over the universe's public tinys — the results also become tools for the rest of the turn |
ask_tiny |
talk to another tiny: a nested agent with that tiny's prompt, knowledge and data answers your message |
spawn_agents |
run independent sub-agent tasks in parallel (8 concurrently, the rest queue; each has http; sub-agents cannot spawn) |
http |
fetch a URL |
Memory·
| Tool | Where | What it does |
|---|---|---|
learn account |
server — D1 + Vectorize | store one self-contained fact; supersedes closes the fact it replaces and links the two. Capacity 5000 entries × 2000 chars; a full store rejects the write instead of evicting |
recall account |
server | semantic search over everything learned |
unlearn account |
server | close a memory — bitemporal, it leaves listings and recall but survives as history |
memory_graph account |
server | the subgraph around a memory: supersedes trails, part_of / relates_to / about clusters |
memory_conflicts account |
server | same subject and relation pointing at different facts in one scope; list and resolve |
remember / forget |
the browser | a short local note that survives history clears; delete by substring |
manage_messages |
the browser | stats, drop a range, compact a range of the stored conversation |
Devices and hardware·
| Tool | Mounted for | What it does |
|---|---|---|
use_device account |
everyone | reach an enrolled device — a tiny-vercel laptop over the relay mailbox, or an endpoint device over its HTTPS API |
nicla_take_photo, nicla_take_video, nicla_listen, nicla_status |
everyone | a Nicla Vision worn as a necklace, over the internet |
nicla_voice_status, nicla_voice_wakes, nicla_voice_record, nicla_voice_transcripts, nicla_voice_transcript |
everyone | the Nicla Voice recorder — status and wakes read the registry and event ring, recording rides the phone's relay mailbox, transcripts are D1 reads |
flipper_status, flipper_listen, flipper_files, flipper_find |
everyone | a Flipper Zero plugged into one of the user's machines or paired to their phone |
screenshot |
native apps | capture the phone's screen; the user is asked first and a recording indicator shows |
generate_image |
iOS | on-device image generation |
meta_take_photo, meta_record_video, meta_listen, meta_glasses_status |
iOS and Android | Meta glasses paired to the phone |
The page and the phone·
| Tool | Mounted for | What it does |
|---|---|---|
render_ui |
everyone | render a React component (React.createElement; recharts available) in the chat; native apps get a props-only variant so the model cannot emit code-only calls the phone would degrade |
speak |
everyone | on-device text-to-speech with a playback card |
suggest_followups |
everyone | 2–4 clickable follow-up chips |
set_theme |
everyone | live theme — presets tiny, cyberpunk, ocean, forest, sunset, dracula, nord, amber, or custom accent and background hex; persists when signed in |
customize_page |
the tiny's owner only | inject CSS and/or JS (≤ 8 KB each) into the page; persist:true saves it. Owner-only on purpose: it runs arbitrary code in the app's origin, next to the session cookie |
add_map_marker, remove_map_marker, fly_to_marker, fly_to_location, clear_map_markers, tour_markers |
everyone | the live map — the web bridge, or the iOS / Android map screens |
vibrate, flashlight, copy_to_clipboard, set_brightness, play_sound, schedule_alert, cancel_alerts, open_url |
everyone | device actions; native apps execute them, browsers ignore most |
Messages, jobs, Telegram·
| Tool | What it does |
|---|---|
send_message account |
a direct message to another user by @login or one of their tiny slugs; stored in their inbox and pushed |
read_messages account |
inbox overview, or one conversation (marks it read) |
schedule account |
background jobs that run without the user: recurring */Nm, */Nh or daily@HH:MM (UTC), or one-shot run_in_minutes; list, delete; max 10 per user |
telegram account |
connect a bot: setup (BotFather token + tiny slug), status, allow_chat, disable, remove; the worker polls it every minute |
use_telegram account |
call any Telegram Bot API method with the connected bot |
Extending the toolset·
| Tool | What it does |
|---|---|
create_tool account |
forge a personal tool from one JavaScript arrow function (≤ 4 KB; sandboxed fetch to public HTTPS hosts with a 10 s timeout; no process, require, eval, globalThis); callable as my_<name> from the next message. The code is public on the user's profile |
remove_tool account |
delete one forged tool |
install_tool account |
install a tool from a raw.githubusercontent.com URL; the owner must be in TOOL_REPO_ALLOWLIST (default strands-agents) or trusted by the user with /tools trust <owner> |
marketplace account |
browse everyone's public forged tools, install one (re-validated in the sandbox), check_updates on GitHub-installed tools pinned by SHA |
manage_tools account |
list, disable, enable tools for this user. The protected set can never be disabled: manage_tools, manage_messages, learn, unlearn, recall, create_tool, remove_tool, marketplace |
my_* |
the user's forged tools, mounted every turn |
| dynamic | operations parsed from the tiny's OpenAPI schema or skills, and from retrieved tinys; names sanitised and deduplicated |
| MCP | clients for the servers named in x-tiny-mcp-servers |
Money·
Always mounted, but every route they call answers 404 {"error":"payments disabled"} until PAYMENTS_ENABLED="true" is set on both the app and the worker. See Payments.
| Tool | What it does |
|---|---|
wallet account |
read-only balance and ledger |
set_price account |
price a tiny per message, or a forged tool as a one-time purchase; 0 makes it free; a flat $0.001 platform fee per sale |
pay_x402 |
pay a priced tiny or any x402 endpoint — returns a quote the user confirms in the UI |
make_payment account |
send to another user by @login — also a quote; the agent cannot approve it |
Count: on a web session, before forged, dynamic and MCP tools, 62 named tools are mounted. Native apps add screenshot and the four meta_* tools, iOS adds generate_image, the tiny's owner adds customize_page.