Skip to content

Repository files navigation

Appwrite Agent

POC agent engine for Appwrite Cloud /v1/agent, built on LangGraph.

Stateless by design. Cloud (or any proxy) owns conversation persistence, Realtime, and auth. The UI/client owns MCP OAuth and credentials. This service is the agent runtime only — every turn receives the context it needs over the API.

What you get

  • One-shot router + subagents (platform, researcher, planner) via LangGraph create_react_agent — each turn routes once (with an optional planner fallback if the primary agent stalls)
  • Platform agent with official agent-skills vendored under .agents/skills/
  • Safe tools by default — no host shell for the model
  • Tools: calculator, current_time, web_search, browser_fetch, http_get, appwrite_skill, sandbox_exec stub, console (Console UI protocol), clarify (structured follow-ups), memory (lasting preferences), plus MCP tools from credentials on the turn
  • FastAPI + shadcn chat UI
  • Console protocol (appwrite.console/v1) — agent → Console metadata for theme, navigation, toasts, resource cards, lists, and usage charts
  • Clarify protocol (appwrite.clarify/v1) — agent → Console structured prompts (choice / confirm / text) when details are missing or deletes need confirmation

Agent architecture

Each POST /api/turn is one independent run. The engine builds tools for that turn (built-ins + MCP from the request), routes once with structured output, then streams a single LangGraph ReAct subagent.

flowchart TD
  req["POST /api/turn<br/>message · history · attachments · mcp_connections · llm"]
  prep["Build tools<br/>built-ins + MCP write-guard wrappers"]
  route["Supervisor router<br/>structured Route: next + reason"]
  finish["FINISH<br/>emit final_answer"]
  plat["platform ReAct agent"]
  res["researcher ReAct agent"]
  plan["planner ReAct agent"]
  fallback{"Primary stalled?<br/>researcher / platform only"}
  done["SSE done · optional conversation_title"]

  req --> prep --> route
  route -->|FINISH| finish --> done
  route -->|platform| plat
  route -->|researcher| res
  route -->|planner| plan
  plat --> fallback
  res --> fallback
  plan --> done
  fallback -->|yes| plan
  fallback -->|no| done
Loading
Role Responsibility When it runs Tools
Supervisor Pick one subagent (or answer directly) Every turn None — structured Route (next, reason, optional final_answer)
platform Appwrite product specialist — SDKs, CLI, Auth, TablesDB, Storage, Functions, Sites, Messaging, permissions, Cloud vs self-hosted, and live project ops via MCP Product/SDK/CLI questions and in-project mutations when MCP is connected appwrite_skill + shared tools + MCP + console + clarify
researcher Open-web fact finding News, lookups, calc, fetch public pages Shared tools + MCP
planner Plans, structured guidance, Console UI side-effects, sandbox proposals; also the fallback if platform/researcher stall Non-Appwrite planning, Console UI-only asks, or recovery after a stalled primary Shared tools + MCP + console + clarify

Shared tools: calculator, current_time, web_search, browser_fetch, http_get (raw HTTPS text/JSON), sandbox_exec (stub), console (UI protocol), clarify (structured follow-ups), memory (set/forget lasting preferences). MCP tools from mcp_connections are attached to every subagent; create mutations are write-guarded (dedupe / already_exists recovery).

Console protocol. After MCP create/update/delete (or when the user asks to change theme / navigate / open a dialog), agents call the console tool with a JSON action list. For list/query answers (databases, users, …) they emit resource_list instead of markdown tables. For usage metrics they emit chart (e.g. metric network.requests with an interval). The tool returns an appwrite.console/v1 envelope on tool_end; the Console parses it into cards, lists, and shell side-effects. See docs/console-protocol.md.

Clarify protocol. When a required detail is missing or a destructive action needs confirmation, agents call the clarify tool with choice / confirm / text prompts instead of guessing IDs or permissions. The tool returns an appwrite.clarify/v1 envelope; the Console renders the form and the next user turn carries the answers. See docs/clarify-protocol.md.

Memory protocol. When the user asks to remember or forget lasting preferences/instructions, agents call the memory tool (type=set / type=forget). The tool returns an appwrite.memory/v1 envelope; Cloud persists successful calls into agentMemories and injects active memories on later turns.

Turn shape: route once → stream one subagent → optional planner fallback → done. History is trimmed to the last 12 turns server-side. No durable graph checkpoint — Cloud/proxy owns conversation state.

Quick start

cp .env.example .env
# set AGENT_API_KEY and LLM_API_KEY

# Stamp the image so /health and startup logs show which build is running
export AGENT_BUILD_ID="$(git rev-parse --short HEAD)"
export AGENT_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

docker compose up --build -d

curl -s http://127.0.0.1:8000/health
# → {"status":"ok","build_id":"abc1234","build_time":"2026-08-01T17:56:00Z"}

curl -sN -H "X-Session-API-Key: $AGENT_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"message":"What is 17*19?","history":[]}' \
  http://127.0.0.1:8000/api/turn

Rebuild image only (tag used by cloud compose)

cd /path/to/eldadfux/openhands
docker build \
  --build-arg AGENT_BUILD_ID="$(git rev-parse --short HEAD)" \
  --build-arg AGENT_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -t ghcr.io/eldadfux/openhands:dev \
  .

# Then recreate the cloud compose service that pulls that tag:
# cd cloud && docker compose --profile agent up -d --force-recreate appwrite-agent
# curl -s http://127.0.0.1:8000/health && docker logs appwrite-agent 2>&1 | head -20

API

Method Path Notes
GET /health Liveness (+ build id/time)
GET /ready Env LLM_API_KEY present?
GET /api/meta Auth — runtime inspection (secrets masked)
POST /api/title Auth — short topic title for a conversation opener
POST /api/turn Auth — one turn (SSE)

POST /api/turn requires either env LLM_API_KEY or a per-turn llm.api_key. /ready only checks the env default.

Turn request

{
  "message": "What is 17*19?",
  "history": [
    { "role": "user", "content": "Hi" },
    { "role": "assistant", "content": "Hello!" }
  ],
  "attachments": [
    { "name": "notes.txt", "mime": "text/plain", "content_base64": "..." }
  ],
  "mcp_connections": [
    {
      "id": "appwrite",
      "name": "Appwrite",
      "url": "https://mcp.appwrite.io/",
      "tokens": { "access_token": "...", "refresh_token": "..." },
      "client_info": { "client_id": "..." }
    }
  ],
  "llm": {
    "api_key": "...",
    "model": "openai/gpt-5.6",
    "base_url": "https://api.openai.com/v1",
    "temperature": 0.2
  }
}
Field Required Notes
message one of message / attachments User text for this turn
history no Prior {role, content} pairs (last 12 kept server-side)
attachments no Inline files via content_base64
mcp_connections no Full MCP URL + tokens + client_info for this turn
llm no Per-turn credential/model override (see below)

llm override. Optional object merged over env LLM_* for this turn only. Omitted fields keep the env defaults. Cloud uses this when a conversation selects a user-owned model (agentModels); omit it to use the shared Appwrite default. The key is never logged. GPT-5 reasoning models and the o-series omit custom temperature and run on the Responses API (use_responses_api) so function tools work; chat models like gpt-4o keep Chat Completions + temperature.

Title request

{ "message": "How do I create a database?", "assistant_message": "..." }

Returns { "title": "..." }. Uses the env LLM only (no per-turn llm override).

SSE events from /api/turn include status, route, subagent_start / subagent_end, answer_start, tool_start / tool_end, token, mcp_credentials (refreshed tokens for the client to store), conversation_title (first turn only), done, error, and a final complete.

MCP OAuth (client-owned)

The engine does not run OAuth. The UI (or production proxy) does:

  1. Discover protected-resource + authorization-server metadata
  2. Dynamic client registration (public client + PKCE)
  3. Browser authorize → callback at {origin}/oauth/mcp/callback
  4. Store tokens + client_info in localStorage
  5. Send them on every /api/turn as mcp_connections

Production Appwrite should own steps 1–4 and replay credentials on each turn the same way.

Environment

Variable Required Purpose
AGENT_API_KEY yes (prod) Clients send X-Session-API-Key
LLM_API_KEY yes* Default model provider API key (* or supply llm.api_key per turn)
LLM_MODEL no Default openai/gpt-5.6 (overridable via llm.model)
LLM_BASE_URL no Optional OpenAI-compatible base URL (overridable via llm.base_url)
WEB_SEARCH_ENABLED no Headless browser web search (default true)
ATTACHMENTS_MAX_BYTES no Max inline attachment size (default 10MB)
ATTACHMENTS_MAX_PER_MESSAGE no Max attachments per turn (default 8)
STREAM_TOOL_INPUT_CHARS no Cap tool-input preview on SSE (default 100000)
STREAM_TOOL_OUTPUT_CHARS no Cap tool-output preview on SSE (default 500000)

Security

  • Do not expose this container on a public Gateway/HTTPRoute.
  • The model cannot run host shell commands.
  • Unauthenticated mode (empty API key) is for local smoke tests only.

Appwrite skills

./scripts/update-appwrite-skills.sh
docker compose up --build -d agent

Local (no Docker)

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
export LLM_API_KEY=... AGENT_API_KEY=...
uvicorn app.main:app --reload --port 8000

About

Stateless agent runtime for Appwrite

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages