---
name: public-agent-manager
description: "Create, edit, clone, list, preview, and delete public agents, each a self-contained directory with its own identity, personality, knowledge, and model. Use when asked to manage, configure, or set up public agents."
---

# Public Agent Manager

Invoked by the admin agent directly.

Create, edit, clone, list, preview, and delete public agents. Each public agent is a self-contained directory under `agents/` with its own identity, personality, knowledge, and model.

## Agent Directory Structure

Each agent lives at `{accountDir}/agents/{slug}/`:

| File | Purpose | Required |
|------|---------|----------|
| `config.json` | Model, display name, status, image identity, knowledge keywords | Yes |
| `IDENTITY.md` | Boundaries, behavioural rules — copied verbatim from the Rubytech template | Yes |
| `SOUL.md` | Personality and the operator-defined role — tone, voice, warmth, greeting, and the role the operator gives the agent at creation | Yes |
| `KNOWLEDGE.md` | The business's public-facing facts at landing-page detail — pricing, FAQs, services | Yes |
| `KNOWLEDGE-SUMMARY.md` | Auto-generated summary when KNOWLEDGE.md exceeds context budget | No |
| `assets/` | Per-agent images (logo, avatar, icon) | No |

`SOUL.md` and `KNOWLEDGE.md` are authored entirely client-side by you at agent creation and must be **non-empty before the agent goes active** — a public spawn whose SOUL or KNOWLEDGE is empty or missing is refused at spawn (the visitor sees an unavailable state, never a dud agent), so neither ships as a template nor is seeded by the installer. `IDENTITY.md` is copied verbatim from the Rubytech-controlled template, never drafted.

## Slug Rules

- Lowercase a-z, 0-9, hyphens only
- 3-50 characters, must start with a letter
- Reserved (rejected): `admin`, `api`, `assets`, `agent-assets`, `brand`, `bot`, `privacy`
- Shadowed by a static route, also rejected: `public`, `chat`, `graph`, `sessions`, `data` — a slug equal to one of these would 404 at `/{slug}`. If the operator asks for one, reject it and name the collision.
- Public agents are optional — the platform operates without any.

## config.json

```json
{
  "slug": "sales",
  "displayName": "Sales Agent",
  "model": "claude-haiku-4-5",
  "status": "active",
  "knowledgeKeywords": [],
  "image": "/agent-assets/sales/agent-image.png",
  "imageShape": "circle",
  "showAgentName": true
}
```

### Toolless by construction

The public agent has no tools. It cannot search the graph, read files, run commands, or load skills mid-conversation. Everything it knows is the KNOWLEDGE.md and SOUL assembled into its system prompt at spawn time. There is no operator toggle for this — it is the fixed posture for every public agent on every channel. If the agent needs to surface graph content created after deployment, refresh its KNOWLEDGE.md (see Update knowledge below); the agent itself never reaches the live graph.

### knowledgeKeywords

Topic keywords the agent subscribes to. During an `update-knowledge` refresh, nodes matching these keywords are included automatically — even if they are not directly tagged for the agent.

Maximum 5 keywords. Present during creation as a free-text input. Examples: `["climate science", "sustainability"]`, `["pricing", "services"]`.

### Agent Image

An optional image (avatar, icon, or logo) displayed in the public chat header instead of the account-level brand logo. Managed via the `agent-image` MCP tool.

- `image` — URL path to the image (e.g. `/agent-assets/sales/agent-image.png`). Set automatically by the `agent-image` tool.
- `imageShape` — `"circle"` for avatars and icons, `"rounded"` for logos. Controls the CSS border-radius applied to the image in the chat header.
- `showAgentName` — `true`: chat header displays `displayName`; `false` (default): header shows the business name from branding data; `"none"`: no name displayed at all (logo-only header, useful when the logo already contains the brand name).

**Fallback chain:** agent image → account brand logo → platform default icon. When no image is configured, the existing brand logo behaviour is preserved.

During creation (Step 6) and editing, offer image upload when the user has an image ready. Use the `agent-image` tool with `action: "set"`, providing the file path and the appropriate shape. If the user wants the agent's name shown in the header, set `showAgentName: true` in config.json.

### Knowledge Tagging

Nodes in the graph can be directly tagged for a specific agent by adding the agent's slug to the node's `agents` array property. This is the explicit tagging mode — the admin decides exactly which nodes an agent can see. Direct tagging and keyword subscriptions (`knowledgeKeywords`) are independent routing mechanisms — each is configured and approved separately, and neither implies the other.

To tag nodes: read the node's current `agents` array via `memory-search` (no `agentSlug` param), confirm with the operator which nodes get the new slug, and the recorder folds the slug onto each `agents` array in its next turn. To untag: same shape — read the current array, confirm removal, and the recorder rewrites the array without the slug. To see what's tagged for an agent: search with `memory-search` (no `agentSlug` param) and identify results whose `agents` property contains the target slug.

### Model Options

Present as a `select` field within the agent configuration `form` during creation:

| Model | ID | Suitability | Cost |
|-------|-----|------------|------|
| Haiku | `claude-haiku-4-5` | Fast, cost-effective. FAQ, routing, high-volume. | Low |
| Sonnet | `claude-sonnet-5` | Balanced. Nuanced conversation, qualification, support. | Standard |
| Opus | `claude-opus-5[1m]` | Maximum intelligence. Complex sales, consultative selling. | Premium |

## SOUL.md Scope Enforcement

SOUL.md is personality and role. When the user provides content for SOUL.md, route content that belongs elsewhere to the correct file:

- **Facts, pricing, services, FAQs** → KNOWLEDGE.md
- **Personality and the operator-defined role — how the agent sounds and what the operator has tasked it to do** → SOUL.md (the role is whatever the operator specifies at creation; the public agent is toolless, so there are no plugins to embed)
- **Boundaries and procedural rules** → IDENTITY.md (Rubytech-controlled and copied verbatim — the agent's hard limits live there, not in SOUL)

SOUL.md answers "what does this agent feel like to talk to, and what is it here to do?" — tone, warmth, formality, greeting, and the operator-defined role the agent was created to fill. SOUL.md must not restate constraints from IDENTITY.md. When user-provided content could belong to either file, apply this test: "does this restrict what the agent does?" → IDENTITY.md. "Does this describe how the agent sounds?" → SOUL.md. If content does both (e.g. "always respond politely"), route to SOUL — the constraint is a consequence of tone, not an operational boundary. Present SOUL.md via `document-editor` for the user to review and approve. Follow the document-editor encoding constraint in IDENTITY.md when generating content.

## KNOWLEDGE.md Population

KNOWLEDGE.md is the public agent's baked, static knowledge surface — the facts the agent should always have on hand without a tool call. It is assembled into the system prompt at invocation time.

Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. The public agent is toolless: KNOWLEDGE.md is its **only** knowledge source. There is no graph access mid-conversation. Every gap in KNOWLEDGE.md is a question the agent cannot answer, so the KNOWLEDGE.md you build below is the whole of what the agent will know — populate it thoroughly.

The `memory-search` calls described below are **your** admin-time tool for populating KNOWLEDGE.md, not something the public agent ever runs. All `memory-search` queries for knowledge population use admin permissions — do not pass `agentSlug`. The `agentSlug` parameter is a runtime enforcement mechanism for the public agent's MCP server, not an admin-time discovery tool. At creation time, the agent may have zero tagged nodes — searching with `agentSlug` would return nothing.

Query `memory-search` multiple times with targeted terms across all categories relevant to the agent's role. A single broad query will miss knowledge stored under specific labels or entity types. Common categories:

- Services, pricing, availability
- FAQs
- Team / people (public-facing details only)
- Policies (returns, cancellations, terms)
- Products / catalogue
- Location, hours, contact info

For each category relevant to the agent, confirm that results were returned or that the graph genuinely has no data for that category. Write results as structured markdown under clear headings.

After writing KNOWLEDGE.md, check the token budget. If the total system prompt exceeds 50% of the model's context window, summarise KNOWLEDGE.md and write the summary to `KNOWLEDGE-SUMMARY.md`. Present the summary to the user for review.

## Context Budget

The system prompt must leave room for conversation. After assembling all files, estimate the total token count. Present the breakdown to the user:

```
IDENTITY.md:     800 tokens
SOUL.md:         400 tokens
KNOWLEDGE.md:  8,200 tokens
─────────────────────────
Total:         9,400 / 40,000 (24%)
```

Warn if total exceeds 50% of the model's context window.

## Operations

### Create from Template

When template context is provided (a path to a template directory containing `template.json` and a `SOUL.md` seed), the Create operation pre-populates its steps from the template. The user reviews and approves every file — the template is a starting point, not an automatic deployment.

Read the template's `template.json` and seed files. If `template.json` is unreadable or missing required fields (`suggestedSlug`, `displayName`), report the error and fall back to the standard Create flow.

The following Create steps are modified when a template is provided:

- **Step 1** — use `suggestedSlug` from `template.json` as the initial slug. If an agent already exists at that slug, ask the user to choose a different name.
- **Step 2** — skip. The agent name and role come from `template.json` `displayName` and `description`.
- **Step 4** — unchanged: `IDENTITY.md` is always the Rubytech-controlled template copied verbatim, never the agent template's own identity.
- **Step 5** — present the template's `SOUL.md` content via `document-editor` instead of drafting from conversation. The user can edit before approving.
- **Step 6** — pre-populate form defaults: `model` from `template.json` (or `claude-haiku-4-5` if absent), `knowledgeKeywords` from `template.json` (or empty).

All other steps (3, 7-13) proceed identically — knowledge discovery, direct tagging, KNOWLEDGE.md generation, config write, budget check, default-setting, and confirmation are unchanged.

After creation, no template metadata persists in the agent's files. The resulting agent is indistinguishable from one created manually. The user owns it completely.

### Create

1. **Pre-creation check** — verify the target directory (`agents/{slug}/`) does not exist or is empty before proceeding. If remnant files exist from a prior incomplete deletion, flag the issue to the user: show what files remain and offer to clean them up before starting fresh. Do not silently inherit stale files — a leftover `config.json` from a previous agent would bypass the creation gate.
2. Ask for the agent's name (becomes the slug) and role description
3. **Knowledge discovery** — search the graph with admin permissions (no `agentSlug` — the agent doesn't exist yet) to find available knowledge. Present available content grouped by category for the user to select.
4. **Install `IDENTITY.md`** — copy the Rubytech-controlled template verbatim to `agents/{slug}/IDENTITY.md`. It is fixed (the toolless public directive) and is never drafted, edited, or presented for review.
5. Present `SOUL.md` via document-editor with `filePath: "agents/{slug}/SOUL.md"` (draft personality and the operator-defined role from user conversation — tone plus the role the operator describes for the agent). Write on approval. Must be non-empty; an empty SOUL is refused at spawn.
6. Present agent configuration via a single `form` component with these fields:
   - `knowledgeKeywords` (`text`): label "Knowledge keywords (max 5, comma-separated)", placeholder with examples relevant to the agent's role
   - `model` (`select`): label "Model", options with descriptions:
     - `claude-haiku-4-5` — "Haiku — Fast, cost-effective. FAQ, routing, high-volume."
     - `claude-sonnet-5` — "Sonnet — Balanced. Nuanced conversation, qualification, support."
     - `claude-opus-5[1m]` — "Opus — Maximum intelligence. Complex sales, consultative selling."
   - `showAgentName` (`select`): label "Chat header title", options: `false` — "Business name (default)", `true` — "Agent display name", `"none"` — "None (logo only)"
   The `model` field should have a `defaultValue` of `"claude-haiku-4-5"`.
   Wait for the user's response before proceeding.
   After the form is submitted, ask the user if they have an image (avatar, icon, or logo) for the agent. If yes, ask the user to provide the file and select the shape (circle for avatars/icons, rounded for logos), then use the `agent-image` tool to upload it.
7. **Direct slug tagging (optional, gated)** — keyword subscriptions (configured in Step 6) and direct slug tagging are independent routing mechanisms. Keyword subscriptions route content by topic — any matching node is visible, now or in the future. Direct slug tagging routes specific nodes by identity — only explicitly tagged nodes are visible via the slug search path. Neither implies the other.

   Ask the user whether any specific knowledge nodes should be tagged with this agent's slug. Explain that direct tagging makes specific nodes always visible to this agent regardless of keyword matches, and is useful for content that doesn't match any keyword but should still be available. If keyword subscriptions already cover the desired content, tagging is unnecessary.

   - If the user approves: present the knowledge discovery results from Step 3 for selection, read each node's current `agents` array first, and confirm the new slug-tag set with the user. The recorder folds the slug onto each node's `agents` array in its next turn.
   - If the user declines: skip tagging entirely. Do not propose tagging without explicit approval.
8. **KNOWLEDGE.md generation** — populate from the now-tagged set plus keyword matches using the `update-knowledge` skill workflow. KNOWLEDGE.md is required and must be non-empty; an empty or missing KNOWLEDGE is refused at spawn.
9. Write `config.json` with selected model, status "active", and `knowledgeKeywords`. This is the last gated write — placed after IDENTITY.md, SOUL.md, and KNOWLEDGE.md to prevent cascade failure if one gate stalls.
10. Check context budget — auto-summarise if over threshold
11. **Project the agent into the graph** — delegate to the `specialists:database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project?accountId=<the literal ACCOUNT_ID value you read>`." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE. The `accountId` query parameter is required on every write to this route (project and delete). The route no longer infers an account from device state, so it returns 400 without one and 403 when the caller is not entitled to the account it named. Source the value from the `ACCOUNT_ID` environment variable of this session (`echo $ACCOUNT_ID`), and substitute the literal id into the URL before delegating, since the specialist you delegate to does not share your shell.
12. **Set the default if none exists.** Read `defaultAgent` from `account-manage`. If it is empty, set it to this new slug via `account-update` (`field: "defaultAgent"`, `value: "{slug}"`) — otherwise the agent answers at `/{slug}` but the bare root serves nothing. If a default already exists, do not change it silently: tell the operator the current default and ask whether to switch the root to this new agent; call `account-update` only if they say yes.
13. Confirm creation: "Agent created and reachable at `/{slug}`." If you set or changed the default in step 12, add: "The site root also serves this agent." If a different default remains, add: "The site root still serves `{currentDefault}`."

### List

Read `defaultAgent` from `account-manage` to identify the current default. Read all directories under `agents/`. For each, read `config.json` and estimate total token budget. Present as a structured summary, marking the default agent:

**sales** — Sonnet Standard * default
11,500 / 200,000 tokens (6%) · active

**support** — Haiku Low cost
3,200 / 40,000 tokens (8%) · active

**hello** — Haiku Low cost
9,800 / 40,000 tokens (25%) · active

### Clone

Copy the source agent's directory to a new slug. The user modifies what's different — typically SOUL.md and KNOWLEDGE.md.

### Edit

Read any file in the agent's directory and present it via document-editor with `filePath` set to the file's path. Write back on approval. The document-editor includes a Download button — the user can review, edit, then download the file as `.md` without further agent action.

For agent image changes:
- Use the `agent-image` tool with `action: "set"` to upload or replace the image
- Use the `agent-image` tool with `action: "remove"` to remove the image (falls back to brand logo)
- To change the shape, use `agent-image` with `action: "set"` and the new `imageShape`
- To change `showAgentName` (`true`, `false`, or `"none"`), update `config.json` directly

For knowledge scope changes:
- Show currently tagged nodes (search `memory-search` without `agentSlug`, identify results whose `agents` property contains the agent's slug) and active keyword subscriptions (from `config.json`).
- Allow adding/removing direct tags. Read each node's current `agents` array first, confirm the change with the operator, and the recorder updates the arrays in its next turn.
- Allow adding/removing keyword subscriptions (update `knowledgeKeywords` in `config.json`).
- After changes, offer to refresh KNOWLEDGE.md using the `update-knowledge` skill.

**After every Edit operation that touches IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY/`config.json` (including `knowledgeKeywords` or direct-tag mutations), delegate to `specialists:database-operator` to re-project: POST `/api/admin/agents/{slug}/project?accountId={accountId}`.** Without re-projection the on-disk files diverge from the graph state — the operator sees stale `:Agent` properties and stale `USES_KNOWLEDGE` edges in /graph. Loud-fail: surface non-2xx errors verbatim.

### Delete

Deletion removes the entire agent directory (`agents/{slug}/`) — not individual files. A partial deletion leaves ghost state that poisons future creation into the same slug.

**Confirmation:** Before removing, show the user exactly what will be deleted — the slug, directory path, and the files present in it. Proceed only after explicit confirmation.

**Default agent handling:** Read `defaultAgent` from `account.json` via `account-manage`. If the agent being deleted is the current default:
- Scan `agents/` for other non-admin directories containing `config.json`. If other agents exist, offer to set one as the new default via `account-update`.
- If no other agents remain (or the user declines to set a new default), clear the `defaultAgent` field by calling `account-update` with `field: "defaultAgent"`, `value: ""`. Tell the user that visitors to the root URL will see a "no agents" message until a new agent is created.

**Cleanup sequence:**
- Issue `DELETE /api/admin/agents/{slug}?accountId={accountId}` — the route removes the `:Agent` node and its four owned `:KnowledgeDocument` projections FIRST, then deletes the directory. Loud-fail: if graph cleanup throws, files are preserved and a 500 is returned with the error text. Re-issuing the DELETE is safe (idempotent on both layers). Operator-tagged docs survive the projection delete — only the `USES_KNOWLEDGE` edges from this Agent are removed.
- Verify the directory no longer exists
- Report what was removed (list the files that were in the directory)

**Graph tag cleanup (optional):** Knowledge nodes in the graph may have the deleted agent's slug in their `agents` array. These tags become orphaned after deletion — no agent will query them, so they are inert. Offer the operator the choice: clean up the orphaned tags (read each affected node's current `agents` array first, confirm the slug removal, and the recorder rewrites the arrays in its next turn) or leave them. If the operator declines, note that the tags are harmless and can be cleaned up later.

### Set as Default

Change which agent serves the root URL (`public.maxy.bot/`).

1. Read the current `defaultAgent` from `account-manage`
2. Confirm the change with the user — show the current default and the proposed new default
3. Call `account-update` with `field: "defaultAgent"`, `value: "{slug}"`
4. Report the result — the tool response includes both old and new default slug

The new default takes effect on the next page load. The previous default agent remains accessible at its `/{slug}` URL.

### Test Greet

Invoke the agent with `[New session. Greet the visitor.]` and stream the response to the admin chat. The admin sees exactly what a visitor would see on first load.

## Runtime Surface

Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. They are toolless by construction: no filesystem access, no MCP tools, no subagents, no `memory-search`. Every public spawn resolves an empty `--allowed-tools`, runs in `dontAsk`, and carries a per-spawn `permissions.deny` covering every native, harness, and memory-MCP tool on every channel. Everything the agent needs to say comes from its directory files (IDENTITY.md, SOUL.md, KNOWLEDGE.md) assembled into the system prompt at invocation time — nothing else is reachable.

## URL Routing

On the `public.*` subdomain:
- `public.maxy.bot/` → the agent set as `defaultAgent` in account.json
- `public.maxy.bot/{slug}` → named agent
