// Architecture guide — long-form conceptual article
function Guide() {
  const shell = useShell();
  const tocItems = [
    { id: 'mental', label: 'The mental model' },
    { id: 'pieces', label: 'The four pieces' },
    { id: 'tenant', label: 'Tenants', level: 3 },
    { id: 'agents', label: 'Agents', level: 3 },
    { id: 'skills', label: 'Skills', level: 3 },
    { id: 'memory', label: 'Memory & context files', level: 3 },
    { id: 'lifecycle', label: 'Life of a message' },
    { id: 'twotool', label: 'The tool registry' },
    { id: 'isolation', label: 'How isolation actually works' },
    { id: 'recap', label: 'Recap' },
  ];
  return (
    <div className="app">
      <Topbar section="guides" theme={shell.theme} setTheme={shell.setTheme} onSearch={() => shell.setSearchOpen(true)} onMenuToggle={() => shell.setMobileMenuOpen(true)} />
      <div className="main">
        <Sidebar activeId="arch" mobileOpen={shell.mobileMenuOpen} onMobileClose={() => shell.setMobileMenuOpen(false)} />
        <article className="content">
          <div className="crumbs">
            <a href="index.html">Docs</a>
            <span className="sep">/</span>
            <a href="#">Concepts</a>
            <span className="sep">/</span>
            <span>Architecture</span>
          </div>

          <div className="eyebrow">Concepts · 10 min read</div>
          <h1 className="h1">Architecture</h1>
          <p className="lede">
            ClawAgen is a workspace for AI assistants that belong to the people who use them. Each
            staff member in your company gets their own tenant — a folder on disk where they design
            as many agents as they need, with their own skills, memory, and channel surfaces. This
            guide walks the whole stack, from the folder on disk up to the streamed reply.
          </p>

          <figure style={{margin:'32px 0 36px', padding:'36px 24px', background:'var(--bg-elev)', border:'1px solid var(--line)', borderRadius:12, maxWidth:660}}>
            <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:14, fontFamily:'var(--font-mono)', fontSize:11.5, textAlign:'center'}}>
              {['Channel','Agent runtime','Tenant filesystem'].map((s,i) => (
                <div key={s} style={{padding:'18px 10px', border:'1px solid var(--line)', borderRadius:8, background:'var(--bg)', color:'var(--ink)'}}>
                  <div style={{fontSize:22, marginBottom:6, color:'var(--accent-ink)'}}>{['◐','◉','◆'][i]}</div>
                  {s}
                </div>
              ))}
              <div style={{textAlign:'center', color:'var(--ink-3)'}}>message in</div>
              <div style={{textAlign:'center', color:'var(--ink-3)'}}>prompt + tools</div>
              <div style={{textAlign:'center', color:'var(--ink-3)'}}>md + sqlite</div>
            </div>
            <figcaption style={{textAlign:'center', marginTop:20, fontSize:12.5, color:'var(--ink-3)', fontStyle:'italic', fontFamily:'var(--font-serif)'}}>
              The round-trip of a single message.
            </figcaption>
          </figure>

          <h2 id="mental" className="h2">The mental model</h2>
          <p>
            If you've used <a className="inline" href="https://openclaw.sh" target="_blank" rel="noreferrer">OpenClaw</a>{' '}
            on your laptop, you already know this model — ClawAgen is the same idea, scaled for teams.
            One <strong>Tenant</strong> is a folder. Inside that folder, one or more <strong>Agents</strong>{' '}
            share the same <strong>Skills</strong> and <strong>Memory</strong> foundation, while each carries
            its own identity, toolset, and routing rules.
          </p>
          <p>
            The LLM is handed a curated registry of first-party tools — grouped by domain and
            gated by role. The admin agent sees the full set (file I/O, shell, web, memory, media,
            channels, sessions, cron, skills). The customer-facing agent sees a read-only + message
            subset — sensitive tools like <code>bash</code>, <code>write</code>, and <code>cron</code>
            are admin-only.
          </p>

          <h2 id="pieces" className="h2">The four pieces</h2>

          <h3 id="tenant" className="h3">Tenants</h3>
          <p>
            Each user is a tenant. A tenant is a directory <code>/data/tenants/&#123;accountId&#125;/</code>{' '}
            with its own SQLite file, its own Markdown context, and its own channel credentials. A query
            against tenant A physically cannot see tenant B — they open different database files.
          </p>
          <CodeBlock tabs={[{ label: 'layout.txt', raw: `/data/tenants/{accountId}/\n├── IDENTITY.md       # who the agent is\n├── SOUL.md           # values, tone\n├── USER.md           # facts about the owner\n├── AGENTS.md         # agent catalog\n├── TOOLS.md          # tool preferences\n├── HEARTBEAT.md      # dynamic state (updated every 5 min)\n├── MEMORY.md         # accumulating narrative\n├── BOOTSTRAP.md      # first-turn-only context\n├── data.db           # sqlite + sqlite-vec + FTS5\n├── skills/{name}/SKILL.md\n├── memories/*.md\n├── sessions/{id}.json\n└── config/\n    ├── personality.md\n    ├── providers.yaml\n    └── channels/*.yaml`,
            code: `/data/tenants/{accountId}/
├── IDENTITY.md       <span class="tok-com"># who the agent is</span>
├── SOUL.md           <span class="tok-com"># values, tone</span>
├── USER.md           <span class="tok-com"># facts about the owner</span>
├── AGENTS.md         <span class="tok-com"># agent catalog</span>
├── TOOLS.md          <span class="tok-com"># tool preferences</span>
├── HEARTBEAT.md      <span class="tok-com"># dynamic state (updated every 5 min)</span>
├── MEMORY.md         <span class="tok-com"># accumulating narrative</span>
├── BOOTSTRAP.md      <span class="tok-com"># first-turn-only context</span>
├── data.db           <span class="tok-com"># sqlite + sqlite-vec + FTS5</span>
├── skills/{name}/SKILL.md
├── memories/*.md
├── sessions/{id}.json
└── config/
    ├── personality.md
    ├── providers.yaml
    └── channels/*.yaml` }]} />

          <h3 id="agents" className="h3">Agents</h3>
          <p>
            A tenant can run as many agents as they need. One staff member might keep a single
            general-purpose assistant; another might maintain several — a research agent, an
            operations agent, a customer-response agent — each with its own <code>IDENTITY.md</code>,
            its own allowed tools, and its own channel bindings.
          </p>
          <p>
            Agents in the same tenant share <strong>Skills</strong> and <strong>Memory</strong> by
            default, so knowledge gathered by one is available to the others. What differs is voice,
            permissions, and routing. A <code>bindings.yaml</code> file maps{' '}
            <code>(channel, sender)</code> tuples to whichever agent should answer — so the same
            inbound surface can hit different agents depending on who is asking.
          </p>
          <p>
            Add an agent by creating its entry in <code>agents.yaml</code> and, optionally, its own
            <code> IDENTITY.md</code>. No code changes, no redeploy — the tenant's workspace picks it
            up on the next request.
          </p>

          <h3 id="skills" className="h3">Skills</h3>
          <p>
            A skill is a directory containing a <code>SKILL.md</code> file. The front-matter has a name,
            description, and optional trigger phrase. The body describes — in natural language — what
            the agent should do. Skill names and descriptions are always in the system prompt
            (compact); bodies load only when the agent calls <code>load_skill</code>.
          </p>

          <Callout type="note" title="Edit live">
            Skills are the source of truth. Edit <code>skills/weekly-summary/SKILL.md</code>, push the
            change, and the agent picks it up on the next turn — no redeploy, no compile step.
          </Callout>

          <h3 id="memory" className="h3">Memory & context files</h3>
          <p>
            Memory is not a single concept — it's a layered system of Markdown files.
            <code> IDENTITY.md</code>, <code>SOUL.md</code>, <code>USER.md</code> change rarely and define{' '}
            <em style={{color:'var(--ink-3)', fontStyle:'italic'}}>who</em> the agent is.{' '}
            <code>HEARTBEAT.md</code> is auto-updated every 5 minutes with dynamic state.{' '}
            <code>MEMORY.md</code> accumulates narrative turn-by-turn and is LLM-compacted nightly.
            The <code>memories/</code> directory holds key-value memories, indexed for hybrid search
            (vec cosine + FTS5 BM25 + Reciprocal Rank Fusion).
          </p>

          <h2 id="lifecycle" className="h2">Life of a message</h2>
          <p>When a message lands on an agent endpoint, the runtime goes through the same steps regardless of which agent is answering or which surface the request came from:</p>
          <ol>
            <li><code>authMiddleware</code> validates the API key against <code>platform.db</code> and resolves the <code>accountId</code>.</li>
            <li><code>resolveAgent</code> inspects <code>(accountId, channel, sender)</code> against the tenant's <code>bindings.yaml</code> and picks which agent should answer.</li>
            <li><code>prompt-builder</code> reads the tenant's context files (<code>IDENTITY.md</code> et al) and the chosen agent's identity, then assembles the system prompt.</li>
            <li>Skill names + descriptions are appended. The full bodies are <em style={{color:'var(--ink-3)', fontStyle:'italic'}}>not</em> included.</li>
            <li>The configured LLM is called with the agent's role-filtered tool registry (~28 first-party tools plus any tenant plugin tools).</li>
            <li>Tool calls stream back. Each tool has a typed handler that runs against the tenant DB/FS, the sandbox, or an external provider (web search, image gen, channel adapters).</li>
            <li>Text deltas stream to the client over SSE. When the model stops, the session is persisted to <code>sessions/&#123;id&#125;.json</code>.</li>
          </ol>
          <p>
            Steps 1–4 are under a few milliseconds (filesystem reads + in-memory lookups). Step 5
            dominates — an average turn is 1.5–4 seconds of streamed tokens. Step 7 writes on
            completion, not turn-by-turn. For a deeper trace with a worked example, see the{' '}
            <a className="inline" href="flow.html">Message flow</a> page.
          </p>

          <h2 id="twotool" className="h2">The tool registry</h2>
          <p>
            The brain runner aggregates every first-party tool from{' '}
            <code>agents/src/brain/tools/</code> and surfaces them to the LLM. Tools are grouped by
            domain and each one declares an <code>agents</code> allowlist:
          </p>
          <ul>
            <li><strong>File I/O:</strong> <code>read</code>, <code>ls</code>, <code>find</code>, <code>grep</code> (both agents) · <code>write</code>, <code>edit</code>, <code>apply_patch</code> (admin only)</li>
            <li><strong>Web:</strong> <code>web_search</code>, <code>web_fetch</code> (both)</li>
            <li><strong>Shell:</strong> <code>bash</code>, <code>exec</code> (admin only — sandbox + host-side command execution)</li>
            <li><strong>Memory & search:</strong> <code>memory_query</code>, <code>faq_search</code>, <code>session_status</code> (both) · <code>update_plan</code> (admin)</li>
            <li><strong>Media:</strong> <code>pdf</code> (both) · <code>image_generate</code> (admin)</li>
            <li><strong>Channels:</strong> <code>message</code> (both) — sends through the tenant's configured channel adapters</li>
            <li><strong>Sessions:</strong> <code>sessions_list</code>, <code>sessions_history</code>, <code>sessions_yield</code> (both) · <code>sessions_spawn</code>, <code>sessions_send</code>, <code>subagents</code> (admin)</li>
            <li><strong>Multi-agent:</strong> <code>agents_list</code> (both)</li>
            <li><strong>Ops:</strong> <code>cron</code>, <code>process</code> (admin only)</li>
            <li><strong>Skills:</strong> <code>load_skill</code> (both) — progressive disclosure, full body only on demand</li>
          </ul>
          <p>
            Per-tenant plugins extend the registry via{' '}
            <code>api.registerTool(accountId, tool)</code> — a plugin can ship an API client,
            a domain-specific aggregator, or a channel adapter and the LLM sees it on the next
            turn. First-party tools win on name collisions.
          </p>

          <CodeBlock tabs={[{ label: 'tool_schemas.json', raw: `// Excerpt of what the admin agent sees — 28 first-party tools + any plugin tools\n[\n  { "name": "read",         "description": "Read a file from the tenant filesystem.", ... },\n  { "name": "web_search",   "description": "Search the web via the configured provider.", ... },\n  { "name": "bash",         "description": "Run a shell command in the tenant sandbox.", ... },\n  { "name": "memory_query", "description": "Hybrid search over memories (vec + FTS5).", ... },\n  { "name": "message",      "description": "Send a reply through a connected channel adapter.", ... },\n  { "name": "cron",         "description": "Schedule a recurring task.", ... },\n  { "name": "load_skill",   "description": "Load a SKILL.md body by name.", ... }\n  /* ... 21 more ... */\n]`,
            code: `<span class="tok-com">// Excerpt of what the admin agent sees — 28 first-party tools + any plugin tools</span>
[
  { <span class="tok-str">"name"</span>: <span class="tok-str">"read"</span>,         <span class="tok-str">"description"</span>: <span class="tok-str">"Read a file from the tenant filesystem."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"web_search"</span>,   <span class="tok-str">"description"</span>: <span class="tok-str">"Search the web via the configured provider."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"bash"</span>,         <span class="tok-str">"description"</span>: <span class="tok-str">"Run a shell command in the tenant sandbox."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"memory_query"</span>, <span class="tok-str">"description"</span>: <span class="tok-str">"Hybrid search over memories (vec + FTS5)."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"message"</span>,      <span class="tok-str">"description"</span>: <span class="tok-str">"Send a reply through a connected channel adapter."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"cron"</span>,         <span class="tok-str">"description"</span>: <span class="tok-str">"Schedule a recurring task."</span>, ... },
  { <span class="tok-str">"name"</span>: <span class="tok-str">"load_skill"</span>,   <span class="tok-str">"description"</span>: <span class="tok-str">"Load a SKILL.md body by name."</span>, ... }
  <span class="tok-com">/* ... 21 more ... */</span>
]` }]} />

          <h2 id="isolation" className="h2">How isolation actually works</h2>
          <p>
            Tenant isolation is enforced at two layers. First, every SQL query routes through{' '}
            <code>getTenantDb(accountId)</code> which opens a specific SQLite file. Second, every
            filesystem path is joined onto <code>/data/tenants/&#123;accountId&#125;/</code> before use.
            There is no cross-tenant query path, because the DB handle itself is different.
          </p>
          <p>
            The <code>bash</code> tool runs in an ephemeral Docker sandbox (alpine + jq + ripgrep + fd + gh),
            mounted read-only everywhere except the tenant's own directory. Even if a prompt injection
            made the model try <code>rm -rf /</code>, it couldn't reach past the tenant boundary.
          </p>

          <h2 id="recap" className="h2">Recap</h2>
          <ul>
            <li><strong>Tenants</strong> are folders on disk, one per staff member, fully isolated.</li>
            <li><strong>Agents</strong> are configurable — each tenant runs one or many, each with its own identity and toolset.</li>
            <li><strong>Skills</strong> are Markdown — edit the file, behavior changes live.</li>
            <li><strong>Memory</strong> is layered Markdown + a hybrid-searchable vector/FTS index.</li>
            <li>The LLM sees a <strong>curated, role-gated tool registry</strong> (~28 first-party tools + per-tenant plugin tools) — not a flat 60-tool dump.</li>
          </ul>

          <p>Ready to build? Jump to the <a className="inline" href="api-reference.html">API reference</a> and start writing code.</p>

          <Feedback />
          <PageFoot
            prev={{ label: 'Quickstart', href: 'quickstart.html' }}
            next={{ label: 'API reference', href: 'api-reference.html' }}
          />
        </article>
        <TOC items={tocItems} />
      </div>
      <SearchOverlay open={shell.searchOpen} onClose={() => shell.setSearchOpen(false)} />
      <TweaksPanel visible={shell.tweaksVisible} theme={shell.theme} setTheme={shell.setTheme} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<Guide />);
