Skip to content

Latest commit

 

History

History
129 lines (102 loc) · 8.53 KB

File metadata and controls

129 lines (102 loc) · 8.53 KB

Architecture

High-level map of web-agent. Updated 2026-05-18.

Layers

┌──────────────────────────────────────────────────────────────┐
│  Browser (React 19 + Vite + Tailwind)                        │
│  ─────────────────────────────────────────────────────────── │
│  src/App.tsx ─ src/ui/components/{Terminal, ChatInput,       │
│                Sidebar, FilesPopup, MemoryTab, ProfileEditor}│
│  src/ui/stores/{runtime, profile, settings}-store  (Zustand) │
│                              │                                │
│                              ▼                                │
│  src/core/orchestrator.ts   ─ lifecycle, terminal, storage   │
│  src/core/workspace.ts      ─ WebContainer FS / shell        │
│  src/core/persistence.ts    ─ idb-keyval + OPFS              │
│  src/core/credential-vault  ─ AES-GCM encrypted API keys     │
│                              │                                │
│                              ▼                                │
│  src/agent/adapter.ts       ─ spawns embedded agent runtime  │
│                                in Nodebox / WebContainer     │
│                              │   stdout/stdin IPC markers    │
│                              ▼                                │
│  src/agent/runtime/         ─ Node-style agent (excluded     │
│    ├─ turn.ts                 from tsc; built via            │
│    ├─ tools/registry.ts       scripts/build-embed-runtime)   │
│    ├─ llm/streaming.ts                                       │
│    ├─ memory/* (sql.js)                                      │
│    └─ logging, channels, ...                                 │
│                              │                                │
│                              ▼                                │
│  HTTPS to LLM provider (OpenRouter / Ollama / custom)        │
│  via /api/llm/<provider> reverse proxy (vite.config.ts)      │
└──────────────────────────────────────────────────────────────┘

Runtime boundary

The "agent" runs inside the browser tab in a Nodebox/WebContainer sandbox — not on a server. src/agent/adapter.ts spawns it as a Node-like process, then communicates through stdout/stdin.

IPC marker protocol

The embedded runtime cannot directly call fetch (CORS / no origin). To make HTTP requests it writes framed markers to stdout:

<<<WEBAGENT_PROXY_REQ:<id>>>{"method":"POST","url":"…","headers":{…},"body":"…"}<<<END_WEBAGENT_PROXY_REQ>>>

adapter.ts parses these markers (search for WEBAGENT_PROXY_REQ in that file). Non-streaming requests go through /api/proxy (CORS proxy, dev: vite.config.ts; prod: scripts/cors-proxy-server.mjs or Caddy). Streaming LLM requests use same-origin fetch to /api/llm/... when shouldUseIpcStream in src/agent/runtime/llm/streaming.ts applies. Responses are written back to stdin:

<<<WEBAGENT_PROXY_RESP:<id>>>{"status":200,"body":"…"}<<<END_WEBAGENT_PROXY_RESP>>>

The same framing is used for streaming LLM responses (ipcProxyStreamRequest in src/agent/runtime/llm/streaming.ts).

Storage layers

Layer Backed by Versioning Purpose
Profiles idb-keyval envelope {version,…} Profile CRUD (src/core/profiles.ts)
Credentials idb-keyval key-based PBKDF2 + AES-GCM API keys
Settings (UI) idb-keyval none sidebar width, theme, etc.
Workspace files OPFS none WebContainer FS
Agent memory sql.js (WASM) column-add migrations facts, learnings, jobs, snapshots
Debug log OPFS JSONL none tool calls, errors

Agent loop

  1. src/core/orchestrator.ts boots a profile → adapter.ts spawns runtime.
  2. User input → src/agent/runtime/turn.ts:agentTurn().
  3. streamOpenAI() issues a request (HTTP direct or IPC-framed).
  4. Streamed chunks parsed → tool calls extracted.
  5. runTools() (registry.ts) executes built-ins or capability tools.
  6. Result spillover exceeds inline caps → written under memory/snapshots/ (see env: WEBAGENT_MAX_TOOL_RESULT_INLINE_CHARS default 48k per item, WEBAGENT_MAX_TURN_INLINE_CHARS default 256k per tool round). Unwrapped snapshot read_file results are never re-spilled.
  7. Tool loop guardrails (tool-loop-guardrails.ts) detect repeated tool failures and idempotent no-progress reads per turn (Hermes-style deterministic guardrails). Configure via VITE_WEBAGENT_TOOL_LOOP_* in .env (see .env.example and docs/agent-notes.md).
  8. Max 64 rounds per turn (WEBAGENT_MAX_AGENT_ROUNDS).

AbortController per turn; /stop triggers abortCurrentTurn().

Tool registry

  • Built-in tools: src/agent/runtime/tools/builtins/ — registered at module load.
  • Capability tools: src/capabilities/tools/<id>/{manifest.json, handler.js} — loaded lazily; skipped if name collides with a built-in (warns to console + JSONL debug log).
  • Tool catalog re-exported to browser via src/agent/tool-catalog.ts for emoji/icon hints in the UI.
  • Python execution: run_python sends an IPC request from the Nodebox agent runtime to the browser adapter, which lazy-loads a Pyodide worker after boot and mirrors the selected workspace cwd into Pyodide's virtual FS for the run. This is CPython-in-Wasm, not host Python: no subprocess, system pip, native sockets, or arbitrary compiled wheels.

Skills

  • In the sandbox: user/imported skills live under .webagent/skills/<category>/<slug>/SKILL.md (install via skill action=manage import_dir after extract_archive). Bundled procedures are seeded each launch under .webagent/capabilities/skills/<id>/ (read-only; included in skill action=list — do not copy user skills there).
  • In the host repo: contributors edit src/capabilities/skills/<id>/SKILL.md; the adapter copies them into .webagent/capabilities/skills/ on profile boot.
  • Remote imports auto-append a Web Agent execution compatibility section via memory/skill-compat.ts so skills.sh hosts map to built-in tools (web_fetch, web_post, file tools, etc.).
  • Every turn injects a Workspace & filesystem directory map (workspace-map.ts → system prompt + .webagent/workspace-map.md).

Build pipeline

scripts/build-embed-runtime.mjs   → dist/agent-runtime/*.js   (compiled agent code,
                                                                imported as ?raw strings)
vite build                        → dist/assets/*             (browser bundle)

Chunk strategy (vite.config.ts):

Chunk Contents
sqljs sql.js + WASM
xterm @xterm/* terminal
nodebox @codesandbox/nodebox (if used)
markdown markdown-it
icons lucide-react
react-vendor react, react-dom, scheduler
zustand state library

Heavy panels (FilesPopup, MemoryTab, ProfileEditor) are loaded via React.lazy so they don't block first paint.

Where to look first

Task Start here
Add a tool src/agent/runtime/tools/builtins/
Add a capability skill src/capabilities/skills/<id>/SKILL.md
Modify the agent loop src/agent/runtime/turn.ts
Add a channel (Telegram, …) src/capabilities/channels/<id>/
New LLM provider src/core/providers/<id>.json + manifest
UI panel src/ui/components/
Persistence change src/core/profiles.ts + bump STORAGE_SCHEMA_VERSION