# Personalities & agent-direct prompts

A **personality** is a named overlay of prompts on top of an existing
agent. Same agent, same LLM, same `systemPrompt` — but a different bundle
of additional context injected at chat time.

The mental model is a VLAN on top of ethernet: ethernet works on its own,
and a VLAN tag adds segmentation without replacing the underlying link.
Without a personality, an agent runs exactly as before. With one selected,
its bound prompts get appended to the system block.

## What goes into the system block

When you call an agent's chat endpoint, mcpd assembles the system block in
this order (top wins by appearing first in the prompt):

```
agent.systemPrompt
+ agent-direct prompts          (Prompt.agentId == agent.id, priority desc)
+ project prompts               (Prompt.projectId == agent.projectId, priority desc)
+ personality-bound prompts     (PersonalityPrompt[chosen],   priority desc)
+ systemAppend (per-call override, --system-append)
```

Picking a personality is per-turn. Either:

- pass `--personality <name>` on the CLI (or `personality: "<name>"` in
  the chat request body), or
- set `agent.defaultPersonalityId` on the agent — used when no
  `--personality` flag is given.

Without either, today's behavior holds: agent + project prompts only.

## Three prompt scopes

A `Prompt` row attaches to **at most one** of `projectId` or `agentId`.
Every prompt fits exactly one of these slots:

| Scope          | `projectId` | `agentId` | Where it shows up                                          |
|----------------|-------------|-----------|------------------------------------------------------------|
| Global         | `null`      | `null`    | Any chat (passes the personality's "in scope" check)       |
| Project        | set         | `null`    | All agents whose `projectId` matches                       |
| Agent-direct   | `null`      | set       | Only this agent. Always-on overlay, no toggle              |

Personality bindings (`PersonalityPrompt`) further select which of those
prompts get injected when that personality is active. The service layer
enforces a scope rule: a prompt can only be bound to a personality if
it's already in-scope for that agent (agent-direct, agent's project, or
global). Foreign-project prompts are rejected with HTTP 400.

## CLI

```fish
# Make a personality on an existing agent
mcpctl create personality grumpy --agent reviewer --description "Be terse and slightly grumpy"

# Add an agent-direct prompt (always-on for this agent)
mcpctl create prompt always-terse --agent reviewer --content "Always be terse." --priority 8

# Bind the prompt to the personality (HTTP for now — CLI subcommand to come)
PERSONALITY_ID=$(mcpctl get personalities -o json | jq -r '.[] | select(.name=="grumpy") | .id')
PROMPT_ID=$(mcpctl get prompts always-terse -o json | jq -r '.[0].id')
curl -sf -H "Authorization: Bearer $(jq -r .token ~/.mcpctl/credentials)" \
     -H "Content-Type: application/json" \
     -X POST "https://mcpctl.ad.itaz.eu/api/v1/personalities/${PERSONALITY_ID}/prompts" \
     -d "{\"promptId\": \"${PROMPT_ID}\", \"priority\": 9}"

# Use it
mcpctl chat reviewer --personality grumpy
> what's wrong with this code?

# Make it the default for this agent
mcpctl edit agent reviewer
# … in the YAML editor, set:
#   defaultPersonality:
#     name: grumpy
```

The chat banner shows which personality (if any) is active before the
first prompt:

```
────────────────────────────────────────────────────────────
Agent: reviewer — code review agent
LLM: qwen3-thinking   Project: code-quality
Personality: grumpy (--personality)
System prompt:
  You are a senior code reviewer. Be terse...
────────────────────────────────────────────────────────────
```

## Web UI

The browser editor at `https://mcpctl.ad.itaz.eu/ui/` covers the same
operations with Monaco for prompt editing:

- **Projects → :name → prompts**: project-scoped prompt CRUD.
- **Agents → :name → Direct prompts**: agent-direct prompt CRUD.
- **Agents → :name → Personalities**: list, create, drill into a
  personality to bind/unbind prompts. The "attach prompt" picker only
  shows in-scope candidates (agent-direct, same-project, or global).

The web UI uses the same bearer token as the CLI — paste a session
token (`mcpctl auth login` writes one to `~/.mcpctl/credentials`) or
mint a long-lived PAT (`mcpctl create mcptoken …`). The token is kept
in `localStorage`; logout clears it.

## API surface

```
GET    /api/v1/personalities
GET    /api/v1/personalities?agent=<name>
GET    /api/v1/agents/:agentName/personalities
POST   /api/v1/agents/:agentName/personalities
GET    /api/v1/personalities/:id
PUT    /api/v1/personalities/:id
DELETE /api/v1/personalities/:id
GET    /api/v1/personalities/:id/prompts
POST   /api/v1/personalities/:id/prompts        body: { promptId, priority? }
DELETE /api/v1/personalities/:id/prompts/:promptId

GET    /api/v1/agents/:agentName/prompts        # agent-direct prompts only
POST   /api/v1/prompts                          # body: { name, content, agent: <name>, priority? }
                                                # XOR with project: <name>
```

Chat request body now accepts an optional `personality: "<name>"`. RBAC:
all personality endpoints inherit `agents:view/edit/create/delete`. There
is no separate `personalities` resource in RBAC bindings — managing a
personality is part of managing the parent agent.

## See also

- [agents.md](./agents.md) — the parent resource.
- [chat.md](./chat.md) — `mcpctl chat` flow + LiteLLM-style flags.
