diff --git a/LICENSE b/LICENSE index 49559f0..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,3 @@ -Swarm Map -Copyright (C) 2025-2026 Juniper Bevensee and contributors - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..0c4e4e4 --- /dev/null +++ b/NOTICE @@ -0,0 +1,19 @@ +Swarm Map +Copyright (C) 2025-2026 Juniper Bevensee and contributors + +A commons, public goods project of NimbleCo (https://www.nimbleco.ai/). +Formerly Hermes Swarm Map / HSM. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. See the LICENSE file for the full license text. + +Why this file exists +-------------------- +The project name and copyright line used to sit at the top of LICENSE. GitHub +classifies a repository's license by matching LICENSE against known license +texts, and the prepended header was enough to make it report "Other / +NOASSERTION" — which contradicted the AGPL v3 claim in the README and on the +website. LICENSE is now the verbatim GNU AGPL v3 text so the classifier can +recognise it, and the project's own attribution lives here instead. diff --git a/README.md b/README.md index 33f35df..5376aa3 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ A commons, public goods project of [NimbleCo](https://www.nimbleco.ai/). -**Multiplayer admin and orchestrator platform for Hermes.** Deploy, manage, and monitor multiple [Hermes Agent](https://github.com/NimbleCoAI/hermes-agent) instances from one dashboard — with built-in multi-tenant security, model cascades, and platform connections. +**Multiplayer admin and orchestrator platform for heterogeneous agent runtimes.** One dashboard for two shapes of agent: [Hermes](https://github.com/NimbleCoOrg/hermes-agent-mt) agents, where each agent is its own container, and [Letta](https://github.com/letta-ai/letta) agents, where memory-first agents live as rows on one shared server. Multi-tenant security, model cascades, and platform connections come from the same control plane either way. -*First of its kind, a point and click GUI for not just managing Hermes runtimes, but also who can do what and where. Solves the multi-tenant Hermes problem. View the godhead of complexity without derealizing. Share compute.* +*First of its kind, a point and click GUI for not just managing agent runtimes, but also who can do what and where. Solves the multi-tenant agent problem. View the godhead of complexity without derealizing. Share compute.* + +**Runtime support is not symmetrical yet.** Hermes is the mature path; Letta support is read-only plus send-a-message. See [Runtimes](docs/runtimes.md) for the exact shipped/not-shipped line — we would rather you read a boring matrix than discover the gap after deploying. Screenshot 2026-05-26 at 5 01 19 pm -*Calm UX showing a variety of config settings for different hermes harness runtimes* +*Calm UX showing a variety of config settings across agent harness runtimes* --- @@ -17,7 +19,9 @@ A commons, public goods project of [NimbleCo](https://www.nimbleco.ai/). AI agents are most useful when they're always on — running on a server, reachable from your phone, remembering context across conversations. But running *multiple* agents across *multiple* platforms for *multiple* users? That's where it gets hard. -Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet of Hermes agents — each with its own personality, memory, platform connections, and budget. Everything a single agent can do, but multiplied and multiplayer. +Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet of agents — each with its own personality, memory, platform connections, and budget. Everything a single agent can do, but multiplied and multiplayer. + +It also gets harder when your agents aren't all the same *shape*. A Hermes agent is a container you can start, stop, rebuild, and attach messaging surfaces to. A Letta agent is a row in a shared server's database with a memory-first architecture — no container of its own, nothing to `docker restart`. Swarm Map models both behind one runtime seam (`ContainerRuntimeAdapter` for container runtimes, a separate REST provider for Letta) instead of pretending everything is a container. ## What People Build With It @@ -41,6 +45,31 @@ Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet 💰 **Budget Enforcement** — Set monthly spend limits per API key. Agents self-throttle when budget is exceeded. +🧩 **Two Runtimes, One Seam** — Hermes (container-per-agent) and Letta (memory-first agents as rows on one shared server) in the same fleet view. Feature parity is partial and documented in [Runtimes](docs/runtimes.md); the six bullets above describe the Hermes path. + +--- + +## Runtimes + +Swarm Map manages two agent runtimes. They are **not** at parity, and the docs say so on purpose. + +| | **Hermes** | **Letta** | +|---|---|---| +| Agent is | a container | a row on a shared server | +| Deploy from the wizard | yes | yes (brings the shared server up, creates the agent) | +| Start / stop / restart / rebuild | yes | n/a — no container to control (the *server* is a container and does have these) | +| Read agent state | yes | yes — fleet list, core-memory blocks, model handle | +| Send a message from the UI | yes | yes | +| Edit memory, reconfigure, delete agent | yes | **not shipped** — the Letta REST surface in Swarm Map is read-only plus send-message | +| Messaging surfaces (Signal / Telegram / Mattermost / Discord) | yes, direct | only via a Hermes front that proxies a group conversation to the agent | +| Git-backed memory files (memfs) | n/a | **not enabled** — we never request `git_enabled`, so agents take the server default (`false`) | +| Group approval + audit | yes | inherited when reached through a Hermes front | +| Budget enforcement | yes | **does not cover proxied Letta turns** — see below | + +**Budget caveat, stated plainly.** Group approval and the audit trail inherit cleanly through a Hermes front, because they gate the turn before it runs. Budget does **not** inherit — on any path. Swarm Map's monthly-spend check sums `input_tokens` / `output_tokens` from each agent's `state.db` `sessions` table, and the Letta bridge never writes those columns: it records only `last_prompt_tokens` (context size). So proxied Letta spend is not counted and a soft cap is never approached. Do not deploy a Letta agent behind a Hermes front expecting the front's budget cap to bound it. + +Full detail, including the route inventory that backs these claims: [docs/runtimes.md](docs/runtimes.md). + --- ## Quick Start @@ -53,7 +82,7 @@ pnpm seed # first run: writes settings + tier config pnpm dev # http://localhost:3000 ``` -On first launch, the setup wizard detects your Hermes compose directories automatically. Point it at your agent directory and go. +On first launch, the setup wizard detects your Hermes compose directories automatically. Point it at your agent directory and go. Choosing the Letta runtime in the wizard instead brings up the shared Letta server and creates the agent on it. **New here?** Read the [Getting Started guide](docs/getting-started.md) for a full walkthrough. @@ -61,13 +90,15 @@ On first launch, the setup wizard detects your Hermes compose directories automa - **Node.js 18+** - **Docker** running locally (used for container management) -- **Hermes Agent** instances — see [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) +- For the Hermes runtime: **Hermes Agent** instances — Swarm Map deploys the multi-tenant fork [NimbleCoOrg/hermes-agent-mt](https://github.com/NimbleCoOrg/hermes-agent-mt), a fork of [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) +- For the Letta runtime: a **Letta server** — the wizard brings one up for you via `docker/letta-compose.yml` --- ## Features - **Discovery** — auto-detect Hermes agent containers and compose files +- **Letta fleet view** — list agents on a configured Letta server, read their core memory, send a message (read-only otherwise) - **Per-agent configuration** — edit env vars, SOUL.md personas, surface connections - **Model cascade** — ordered fallback chains across providers (Anthropic, OpenAI, Bedrock, etc.) - **Surface management** — connect agents to Telegram, Signal, Mattermost @@ -155,6 +186,18 @@ Any AI agent (Claude Code, Hermes, etc.) can orchestrate your fleet via the REST | `GET` | `/api/audit` | Audit log (`?who=&what=&since=`) | | `GET/PUT` | `/api/settings` | App settings | +Letta runtime — **read-only plus send-message.** This is the complete surface; there is no `PATCH`, `PUT`, or `DELETE` for Letta agents: + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/letta/harnesses` | Letta server + its hosted agents, as harness rows | +| `GET` | `/api/letta/agents` | List agents on the Letta server | +| `GET` | `/api/letta/agents/:id/blocks` | Core-memory blocks (read-only) | +| `GET` | `/api/letta/agents/:id/files` | Context-file view, when the server exposes one | +| `POST` | `/api/letta/agents/:id/messages` | Send a message, get the turn back | + +Creating a Letta agent goes through `POST /api/setup/deploy` with the Letta runtime selected. Editing memory, reconfiguring, and deleting Letta agents are not shipped. + ```bash # Examples curl http://localhost:3000/api/harnesses @@ -183,6 +226,7 @@ Settings are stored at `~/.hermes-swarm-map/settings.json`. API keys are encrypt ## Documentation - [Getting Started](docs/getting-started.md) — deploy your first agent in 5 minutes +- [Runtimes](docs/runtimes.md) — Hermes vs Letta: what's shipped, what isn't, and the evidence - [Migrating Existing Agents](docs/migrating.md) — upgrade path for existing Hermes users - [Platform Setup](docs/platforms.md) — Signal, Telegram, Mattermost, Google Workspace guides - [Image vs HSM Boundary](docs/architecture/image-vs-hsm-boundary.md) — what belongs in the Docker image vs HSM scaffolding @@ -194,4 +238,6 @@ Settings are stored at `~/.hermes-swarm-map/settings.json`. API keys are encrypt ## License -[AGPL v3](LICENSE). You can use, modify, and deploy this software freely. If you modify it and expose it over a network (e.g., as a hosted service), you must make your modified source code available under the same license. Self-hosting for your own agents requires no source disclosure. +Swarm Map — Copyright (C) 2025-2026 Juniper Bevensee and contributors. See [NOTICE](NOTICE). + +Licensed under [AGPL v3](LICENSE). You can use, modify, and deploy this software freely. If you modify it and expose it over a network (e.g., as a hosted service), you must make your modified source code available under the same license. Self-hosting for your own agents requires no source disclosure. diff --git a/app/(setup)/setup/wizard/page.tsx b/app/(setup)/setup/wizard/page.tsx index f63f3d4..de09044 100644 --- a/app/(setup)/setup/wizard/page.tsx +++ b/app/(setup)/setup/wizard/page.tsx @@ -449,7 +449,7 @@ export default function WizardPage() { /> Letta swarm - A stateful agent on the shared Letta server (memfs memory). No container of its own; messaging comes later via a Hermes front. + A memory-first agent on the shared Letta server — a row, not a container of its own. Read-only in Swarm Map after deploy, plus sending a message; messaging surfaces come later via a Hermes front. @@ -587,7 +587,7 @@ export default function WizardPage() {

- Letta agents use memfs memory (git-backed context files) and have no per-agent fallback model or bundled Ollama — those are Hermes-only. + Letta agents keep their memory in core-memory blocks on the shared server. Letta's git-backed memory files (memfs) are not enabled on this deploy path yet, so the context-files view will be empty. Letta agents also have no per-agent fallback model and no bundled Ollama — those are Hermes-only.

@@ -689,6 +689,9 @@ export default function WizardPage() {

A Letta agent is a stateful brain on the shared server — it's reached over REST, not Signal/Telegram/Slack directly. Multiplayer messaging arrives via a separate Hermes front that proxies a group conversation to this agent (v1 bridge). Nothing to configure here.

+

+ Through that front, group approval and the audit trail carry over — they gate the turn before it runs. Budget enforcement does not carry over at all. The spend check sums the token columns an agent writes to its own state DB, and the Letta bridge never writes them, so proxied turns count as zero spend on every path. Do not rely on the front's budget cap to bound a Letta agent. +

)} diff --git a/components/harness/letta-agent-detail.tsx b/components/harness/letta-agent-detail.tsx index 1ef943c..df4c638 100644 --- a/components/harness/letta-agent-detail.tsx +++ b/components/harness/letta-agent-detail.tsx @@ -11,14 +11,20 @@ * Lifecycle buttons → Send message (Reset/Delete deferred to a later phase) * Overview CPU/mem → model handle + agent identity * Logs tab → message / turn viewer (send + rendered reply) - * Memory rows → core-memory blocks + memfs context-file view (A3) + * Memory rows → core-memory blocks (+ a context-file view, if enabled) * - * MEMFS (2026-07-21 research, memory/specs/2026-07-21-letta-memfs-api-and-a3.md): - * a modern Letta agent's memory is a git-backed file tree; `system/` files are - * pinned into the prompt, the rest open on demand. We surface BOTH the - * core-memory blocks (`/core-memory/blocks`) and the live file view (`/files`). - * Everything here is read-only except "send a message". Block/file WRITES and - * the memfs↔REST live-sync semantics need a live server (Phase 4). + * MEMFS IS NOT ENABLED — DO NOT WRITE COPY THAT PROMISES IT. + * The 2026-07-21 research (memory/specs/2026-07-21-letta-memfs-api-and-a3.md) + * describes memfs as a git-backed memory file tree with `system/` files pinned + * into the prompt. That is Letta's capability, not ours yet: we never send + * `git_enabled` (zero occurrences in executable code — only doc comments like + * this one), so agents created by our deploy path take the server default, + * observed as `git_enabled: false`. So the `/files` view below is + * normally EMPTY, and its empty state must say "not enabled" rather than imply + * the feature is there and the agent simply has no files. + * + * Everything here is read-only except "send a message". Block/file WRITES, + * enabling memfs, and the memfs↔REST live-sync semantics are all unshipped. */ import { useState } from 'react' @@ -77,8 +83,8 @@ export function LettaAgentDetail({ harness }: { harness: Harness }) {

{isServer - ? 'Letta server (a real container). Hosts N agents as Postgres rows — start/stop/logs via Docker are available on the server, agents are managed over REST.' - : 'Letta agent — a row in the Letta server’s Postgres, not a container. No CPU/memory, no lifecycle buttons; managed over REST.'} + ? 'Letta server (a real container). Hosts N agents as Postgres rows — start/stop/logs via Docker are available on the server; the agents themselves are reached over REST.' + : 'Letta agent — a row in the Letta server’s Postgres, not a container. No CPU/memory, no lifecycle buttons. Read-only over REST, plus sending a message: editing memory, reconfiguring and deleting are not shipped.'}

{/* Overview: model handle + identity, not CPU/mem (design §4a) */} @@ -131,14 +137,18 @@ function ServerAgentsList() { ) } -/** Core-memory block viewer — the seed layer that projects into memfs system/ files (design §4a). */ +/** + * Core-memory block viewer. This is the agent's actual live memory surface on + * our deploy path, because memfs is not enabled — the "blocks project into + * system/ files" story only applies once git-backed memory is turned on. + */ function MemoryBlocks({ agentId }: { agentId: string }) { const { data: blocks, loading, error } = useApi(`/api/letta/agents/${agentId}/blocks`) return (

Core memory (blocks)

- Read-only. Under memfs these blocks project into system/ files (e.g. persona.md). Editing lands with the live server (Phase 4). + Read-only — editing memory blocks from Swarm Map is not shipped. These blocks are this agent's memory: git-backed memory files (memfs) are not enabled, so nothing is projected into system/ files yet.

{loading &&

Loading…

} {error &&

Couldn't read memory: {error}

} @@ -170,25 +180,44 @@ function MemoryBlocks({ agentId }: { agentId: string }) { } /** - * memfs context-file view — the "what's in this agent's memory right now" - * surface (GET /v1/agents/{id}/files). `system/` files are pinned into the - * prompt every turn; others open on demand. The open/pinned marker is a - * CONTEXT-LOADING indicator, NOT an access-control boundary (memfs has no - * per-file ACL — see the airlock note in the spec). + * Context-file view (GET /v1/agents/{id}/files) — Letta's memfs surface. + * + * HONESTY: memfs is NOT enabled on our deploy path (`git_enabled: false`), so + * this list is empty for every agent we create. The panel therefore leads with + * an explicit "not enabled" state instead of an empty box under a heading that + * promises git-backed memory. The file list still renders if a server does + * expose one, so this stays useful for externally-created agents. + * + * When files ARE present: `system/` files are pinned into the prompt every + * turn; others open on demand. The open/pinned marker is a CONTEXT-LOADING + * indicator, NOT an access-control boundary (memfs has no per-file ACL — see + * the airlock note in the spec). */ function ContextFiles({ agentId }: { agentId: string }) { const { data: files, loading, error } = useApi(`/api/letta/agents/${agentId}/files`) const isSystem = (name?: string) => !!name && (name.startsWith('system/') || name.includes('/system/')) + const hasFiles = !!files && files.length > 0 return (
-

Context files (memfs)

-

- The agent's git-backed memory file tree. Pinned = a system/ file loaded into the prompt every turn; open = currently in context. This is a loading indicator, not a permission boundary. -

+
+

Context files (memfs)

+ {!hasFiles && !loading && ( + + not enabled + + )} +
+ {hasFiles && ( +

+ The agent's git-backed memory file tree. Pinned = a system/ file loaded into the prompt every turn; open = currently in context. This is a loading indicator, not a permission boundary. +

+ )} {loading &&

Loading…

} {error &&

Couldn't read context files: {error}

} {files && files.length === 0 && ( -

No context files (agent may predate memfs, or files aren't exposed on this server).

+

+ Git-backed memory files are not enabled for agents Swarm Map deploys, so there is nothing to show here. Core memory (blocks) above is this agent's live memory. Enabling memfs is not shipped yet. +

)} {files && files.length > 0 && (
diff --git a/docs/migrating.md b/docs/migrating.md index 891cc12..81a8e0b 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -8,7 +8,7 @@ Already running Hermes agents? HSM can manage them — and optionally upgrade th HSM's wizard deploys Docker containers, but if you're running Hermes bare-metal (via `hermes gateway` or `pip install`), you can still use HSM for fleet management. HSM discovers agents by scanning data directories — Docker is optional for monitoring. For the full multi-tenant upgrade, you have two paths: -1. **Stay bare-metal:** Apply the Tier 2 changes manually (env vars + plugins). Install the MT fork via `pip install -e ".[all]"` from the [hermes-agent-mt repo](https://github.com/NimbleCoAI/hermes-agent). Memory scoping works the same way regardless of deployment method. +1. **Stay bare-metal:** Apply the Tier 2 changes manually (env vars + plugins). Install the MT fork via `pip install -e ".[all]"` from the [hermes-agent-mt repo](https://github.com/NimbleCoOrg/hermes-agent-mt). Memory scoping works the same way regardless of deployment method. 2. **Move to Docker:** The wizard containerizes your agent with security hardening. Your data directory is mounted as a volume — same files, just managed by Docker now. ## Tier 1: Fleet Management (No Changes) @@ -68,6 +68,11 @@ Restart your agent. It will now check with HSM before responding in new groups. Switch to the MT fork image for per-context memory isolation — the core feature. +> **Canonical image path:** `ghcr.io/nimblecoorg/hermes-agent-mt:latest`. Use this one. +> It is what the wizard deploys, what `lib/seed.ts` defaults to, and the only path we +> publish to. If you have a compose file, script, or runbook pinning the image under any +> other namespace, repoint it here. + ### What changes | Before | After | @@ -200,10 +205,10 @@ Yes. Switch the image back to `nousresearch/hermes-agent:latest`. Context-scoped No. If you're using the Docker image, it's a simple image swap. Your data directory is the same. No code merges needed. **What if I'm not using Docker?** -Install the MT fork via pip: `pip install -e ".[all]"` from the [hermes-agent-mt repo](https://github.com/NimbleCoAI/hermes-agent). Your data directory works the same way — the fork reads the same config.yaml, SOUL.md, and memories. HSM can still manage your agent for monitoring and config, even without Docker. +Install the MT fork via pip: `pip install -e ".[all]"` from the [hermes-agent-mt repo](https://github.com/NimbleCoOrg/hermes-agent-mt). Your data directory works the same way — the fork reads the same config.yaml, SOUL.md, and memories. HSM can still manage your agent for monitoring and config, even without Docker. **What if I customized the Hermes source code?** -If you built from source with custom modifications, you'd need to rebase your changes onto the MT fork. The MT fork has 27 patches — mostly adapter improvements. Conflicts are typically minimal. See the [rebase journal](https://github.com/NimbleCoAI/hermes-agent) for guidance. +If you built from source with custom modifications, you'd need to rebase your changes onto the MT fork. The MT fork has 27 patches — mostly adapter improvements. Conflicts are typically minimal. See the [rebase journal](https://github.com/NimbleCoOrg/hermes-agent-mt) for guidance. **Will the import wizard modify my original files?** No. It copies your data directory to a new location and applies changes to the copy. Your original is never touched. diff --git a/docs/runtimes.md b/docs/runtimes.md new file mode 100644 index 0000000..34a8e85 --- /dev/null +++ b/docs/runtimes.md @@ -0,0 +1,141 @@ +# Runtimes: Hermes and Letta + +Swarm Map manages two kinds of agent. They are different shapes, not two skins on one +model, and they are **not at feature parity**. This page states the line precisely and +shows the evidence, so nobody has to discover the gap after deploying. + +## The two shapes + +**Hermes — container per agent.** An agent is a Docker container with its own data +directory, `.env`, `SOUL.md`, memory files, plugins, and messaging surfaces. You can +start it, stop it, restart it, rebuild it, read its logs, and attach it to Signal, +Telegram, Mattermost or Discord. Container verbs mean something here. + +**Letta — memory-first agents as rows on one server.** The Letta *server* is a container. +The agents are rows in that server's Postgres. There is no per-agent container, so there +is nothing to restart, no CPU or memory to graph, no per-agent logs. You reach an agent +over the server's REST API. + +Swarm Map keeps these separate on purpose. `ContainerRuntimeAdapter` +(`lib/services/runtime-adapter.ts`) is the seam for container runtimes; Letta is +deliberately *not* registered there and goes through its own REST provider +(`lib/services/letta-agent-provider.ts`, `lib/services/letta.ts`). The agent detail page +renders a dedicated view (`components/harness/letta-agent-detail.tsx`) rather than +threading `runtime === 'letta'` branches through the Hermes page. + +## What ships today + +| Capability | Hermes | Letta | +|---|---|---| +| Deploy from the wizard | yes | yes — brings the shared server up and creates the agent | +| Appears in the fleet list | yes | yes | +| Start / stop / restart / rebuild / purge | yes | **n/a** — no container. The *server* has these. | +| Container logs | yes | **n/a** | +| CPU / memory / health | yes | **n/a** — the detail view shows model handle + identity instead | +| Read agent state over the API | yes | yes | +| Read core memory | yes (files) | yes — core-memory blocks, read-only | +| Git-backed memory files (memfs) | n/a | **not enabled** (see below) | +| Send a message from the UI | yes | yes | +| Edit memory / reconfigure / delete agent | yes | **not shipped** (see below) | +| Messaging surfaces | yes, direct | only via a Hermes front that proxies a group conversation | +| Group approval policy | yes | inherited through a Hermes front | +| Audit log | yes | inherited through a Hermes front | +| Budget enforcement | yes | **no** — proxied turns are never counted (see below) | +| Model cascade / fallback provider | yes | no — single model handle, no per-agent fallback | +| Bundled Ollama | yes | no | + +## Three things that are easy to overclaim + +### 1. The Letta API surface is read-only plus send-message + +Swarm Map's Letta routes are exactly these five: + +| Method | Route | +|---|---| +| `GET` | `app/api/letta/harnesses/route.ts` | +| `GET` | `app/api/letta/agents/route.ts` | +| `GET` | `app/api/letta/agents/[id]/blocks/route.ts` | +| `GET` | `app/api/letta/agents/[id]/files/route.ts` | +| `POST` | `app/api/letta/agents/[id]/messages/route.ts` | + +Four reads and one message send. There is **no** `PATCH`, `PUT` or `DELETE` anywhere under +`app/api/letta/`. Verify it yourself: + +```bash +for f in $(find app/api/letta -name route.ts); do + echo "--- $f" + grep -nE '^export (async )?(function|const) (GET|POST|PATCH|PUT|DELETE)' "$f" +done +``` + +So: editing memory blocks, changing an agent's model or configuration, and deleting an +agent are **not shipped**. Creation is not part of this surface either — it happens through +`POST /api/setup/deploy` with the Letta runtime selected, which calls `deployLettaAgent` +(`lib/services/letta-deploy-templates.ts`). The one delete call in the codebase +(`services.letta.deleteAgent`) exists solely to roll back an orphaned agent when a deploy +fails partway; it is not a user-facing delete. + +Anything that describes Swarm Map as offering Hermes-equivalent lifecycle or memory +management for Letta agents is wrong. + +### 2. memfs / git-backed "Context files" is not enabled + +Letta's modern agents can keep memory as a git-backed file tree (memfs), with `system/` +files pinned into the prompt and the rest opened on demand. Swarm Map reads that view +(`GET /v1/agents/{id}/files`) and will render it if a server exposes one — but **we do not +enable it**: + +- `git_enabled` has **zero** occurrences in this repository's executable code — we never + request it, so agents get the server default. (`grep -rn git_enabled --include='*.ts' + --include='*.tsx' .` hits only doc comments and this page.) +- That default is `false`: freshly created Letta agents come up with `git_enabled: false` + (observed against a live server, 2026-07). + +Consequence: for every agent Swarm Map creates, the "Context files (memfs)" panel is +empty. It is labelled **not enabled** in the UI, with core memory identified as the live +memory surface, so the panel does not read as an empty promise. Enabling memfs, and the +memfs↔REST write/live-sync semantics, are unshipped. + +If you are writing copy about Letta memory in this product, say **core-memory blocks**. +Say memfs only with "not enabled yet" attached. + +### 3. Budget does not inherit at all + +A Hermes front can proxy a group conversation to a Letta agent. Two of the door's three +guarantees carry over cleanly, and one does not carry over on *any* path: + +- **Group approval — inherits.** The policy check gates the turn *before* it runs, so a + proxied turn passes through the same approval gate as any other. +- **Audit — inherits.** The turn is recorded on the same audit path. +- **Budget — does not inherit.** Swarm Map's spend check + (`handleBudgetCheck` in `app/api/harnesses/[id]/policy/route.ts`) compares key budgets + against a monthly cost derived in `lib/services/usage.ts`, which sums + `input_tokens` / `output_tokens` from the agent's `state.db` `sessions` table. The Letta + bridge in hermes-agent-mt (`gateway/run.py`, `_run_agent_via_letta`) never writes those + columns — the only usage it records is `last_prompt_tokens`, which tracks context size, + not billable spend. So proxied Letta turns contribute **zero** to the monthly total and + the soft cap is never approached, let alone tripped. + +Two things this is **not**: + +- It is **not** a streaming artefact. Gateway token streaming is **off** unless an operator + turns it on: it follows the top-level `streaming` config + (`StreamingConfig.enabled` defaults to `false` in `gateway/config.py`), and the + `display.streaming: true` that Swarm Map writes into `config.yaml` is **CLI-only** — + `gateway/display_config.py` explicitly skips `display.streaming` when resolving gateway + streaming. The default proxied turn therefore takes the *blocking* client. +- It is **not** fixed by forcing the blocking path. The blocking client does extract real + `prompt_tokens` / `completion_tokens` from the Letta payload, but it lands them in + `last_prompt_tokens`, which the budget query does not read. (Enabling streaming makes it + strictly worse — a streamed turn reports zero tokens even there — but that is a second + problem, not the cause.) + +Do not write "the bridge inherits the door's approval, audit and budget", and do not write +"budget inherits on the non-streaming path". Write: approval and audit inherit, budget does +not. + +## Related + +- [Getting Started](getting-started.md) +- [Image vs HSM Boundary](architecture/image-vs-hsm-boundary.md) +- [Roadmap](ROADMAP.md)