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
RoleResponsibilityWhen it runsTools
SupervisorPick one subagent (or answer directly)Every turnNone — structured Route (next, reason, optional final_answer)
platformAppwrite product specialist — SDKs, CLI, Auth, TablesDB, Storage, Functions, Sites, Messaging, permissions, Cloud vs self-hosted, and live project ops via MCPProduct/SDK/CLI questions and in-project mutations when MCP is connectedappwrite_skill + shared tools + MCP + console + clarify
researcherOpen-web fact findingNews, lookups, calc, fetch public pagesShared tools + MCP
plannerPlans, structured guidance, Console UI side-effects, sandbox proposals; also the fallback if platform/researcher stallNon-Appwrite planning, Console UI-only asks, or recovery after a stalled primaryShared 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 runningexport 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

MethodPathNotes
GET/healthLiveness (+ build id/time)
GET/readyEnv LLM_API_KEY present?
GET/api/metaAuth — runtime inspection (secrets masked)
POST/api/titleAuth — short topic title for a conversation opener
POST/api/turnAuth — 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
}
}
FieldRequiredNotes
messageone of message / attachmentsUser text for this turn
historynoPrior {role, content} pairs (last 12 kept server-side)
attachmentsnoInline files via content_base64
mcp_connectionsnoFull MCP URL + tokens + client_info for this turn
llmnoPer-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

VariableRequiredPurpose
AGENT_API_KEYyes (prod)Clients send X-Session-API-Key
LLM_API_KEYyes*Default model provider API key (* or supply llm.api_key per turn)
LLM_MODELnoDefault openai/gpt-5.6 (overridable via llm.model)
LLM_BASE_URLnoOptional OpenAI-compatible base URL (overridable via llm.base_url)
WEB_SEARCH_ENABLEDnoHeadless browser web search (default true)
ATTACHMENTS_MAX_BYTESnoMax inline attachment size (default 10MB)
ATTACHMENTS_MAX_PER_MESSAGEnoMax attachments per turn (default 8)
STREAM_TOOL_INPUT_CHARSnoCap tool-input preview on SSE (default 100000)
STREAM_TOOL_OUTPUT_CHARSnoCap 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