Skip to content

~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·

  1. Resolve the tiny. The x-tiny-name header names it. The route fetches its record from the worker: system prompt, knowledge, data, an optional OpenAPI schema or list of skills, MCP servers.
  2. Resolve the caller. The tiny_session cookie, if present, gives a signed-in user. Anonymous callers still get an agent — with tools that answer Login required where an account is needed (below).
  3. 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.
  4. Choose the model. Per-request x-tiny-model-* headers win; otherwise the user's synced model config; otherwise the deployment default (TINY_MODEL_PROVIDER and its key). See Configure models.
  5. 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 learn with a skill of the same name. Tools the user disabled with manage_tools are dropped here.
  6. Stream. Each agent event is normalised into one SSE frame with a monotonic seq, so a client can detect a dropped frame. A : ping comment goes out every 15 s. The stream ends with data: [DONE].
  7. 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.
  8. 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.