# Profiles

Profiles define the agent's identity, tools, and behaviour for a specific domain.

## Profile definition (`navi/profiles/base.py`)

Each profile is loaded from a directory under `navi/profiles/<id>/`:
- `config.json` — all fields below
- `system_prompt.txt` — domain-specific instructions
- `subagent_system_prompt.txt` — injected into subagents spawned from this profile (optional)

---

## All `config.json` fields

### Identity

| Key | Type | Default | Description |
|---|---|---|---|
| `id` | str | **required** | Unique profile identifier (matches directory name) |
| `name` | str | **required** | Human-readable name shown in UI |
| `description` | str | **required** | Longer description shown in profile picker |
| `short_description` | str | `""` | One-line summary injected into every profile's system prompt (cross-profile awareness) |
| `full_description` | dict | `{}` | Structured dict: `specialization`, `when_to_use`, `key_tools` keys |

### LLM

| Key | Type | Default | Description |
|---|---|---|---|
| `llm_backend` | str | `"ollama"` | Backend key: `"ollama"`, `"openai"` |
| `model` | str or list[str] | `["gemma4:31b-cloud"]` | Model priority list — first available wins. String is accepted and auto-wrapped. |
| `temperature` | float | `0.7` | Sampling temperature for main loop calls |
| `max_iterations` | int | `10` | Hard cap on tool-calling iterations per turn |
| `top_k` | int \| None | `None` | Ollama sampling top_k |
| `top_p` | float \| None | `None` | Ollama sampling top_p |
| `num_thread` | int \| None | `None` | CPU threads for local inference. `None` = Ollama default |

### Tools

| Key | Type | Default | Description |
|---|---|---|---|
| `tools` | `ToolConfig` | `{}` | **Required.** Explicit tool configuration with two scopes: `agent` and `subagent`. Each scope has `native: list[str]` (built-in and user tool names) and `mcp: dict[str, list[str]]` (MCP server groups). |

`tools.agent` controls what the main loop sees. `tools.subagent` controls what sub-agents spawned from this profile see. If `tools.subagent` is empty, it falls back to `tools.agent`.

`spawn_agent` may receive an optional `profile_id`. If omitted, the subagent uses the parent session's current profile. If provided, the subagent uses the selected profile's model, prompt, planning flags, and `tools.subagent` fallback.

### MCP tool groups

Inside `tools.{agent,subagent}.mcp`, each key is an MCP server name and each value is a list of group names (or `"*"` for all groups). Named groups resolve to concrete tools via the server's config in `mcp_servers.d/`. Example:

```json
{
  "mcp": {
    "navi-web": ["search", "browse", "request"]
  }
}
```

This exposes the tools `mcp__navi-web__web_search`, `mcp__navi-web__web_view`, and `mcp__navi-web__http_request`. `*` expands to every tool advertised by that server.

### Deprecated tool fields

Older configs used `enabled_tools`, `subagent_tools`, and `mcp_servers` as flat top-level fields. The loader still auto-migrates them into `tools.agent` / `tools.subagent` for backward compatibility, but new profiles should use the explicit `tools` structure.

When `tools.subagent` is non-empty, only MCP groups listed there are exposed to the sub-agent. This prevents a profile's main MCP servers from leaking into restricted sub-agent contexts.

### Thinking mechanics

| Key | Type | Default | Description |
|---|---|---|---|
| `think_enabled` | bool | `true` | Pass `think=True` to LLM on every call (extended reasoning). Disable for latency-sensitive profiles. |
| `iteration_budget_enabled` | bool | `true` | Inject remaining iteration count into context so the model knows when to wrap up. |
| `goal_anchoring_enabled` | bool | `true` | Inject a goal-reminder system message every N iterations to prevent drift. |
| `goal_anchoring_interval` | int | `5` | N for goal anchoring. |
| `anti_stall_enabled` | bool | `true` | Detect looping without todo progress and inject a hard warning. |
| `anti_stall_threshold` | int | `8` | Consecutive iterations without progress before stall warning fires. |
| `step_validation_enabled` | bool | `false` | Reserved flag — todo validation is unconditional in the current implementation. |
| `adaptive_replan_enabled` | bool | `false` | When a todo step is marked failed, trigger a re-planning pass. Depends on `step_validation_enabled`. |

### Bounded autonomy

Two independent flags that keep an autonomous agent inside the user's literally requested scope. Both default `false` (legacy "free flight" behavior — explore broadly, finish discovered work). Enabled on `navi_code`.

| Key | Type | Default | Description |
|---|---|---|---|
| `scope_boundary_enabled` | bool | `false` | Inject a standing `[Scope boundary]` system message telling the agent to act strictly within the requested scope — do not expand to sibling/parent directories or projects, and do not execute discovered TODO/roadmap/milestone/backlog docs unless explicitly asked. Also gates the memory-facts scope filter (see below). |
| `observe_skips_plan_enabled` | bool | `false` | When Phase 1 analysis classifies the request as `MODE: observe` (look/read/explain/inspect/list/find — no changes requested), skip Phase 2/3 — no multi-step execution plan, no auto-todo, no "execute step by step" prompt. The agent gathers info with tools and answers directly. `act` requests still plan normally. |

The memory-facts scope filter: when `scope_boundary_enabled` is on, `_memory_facts_msg` drops memory facts whose value is an absolute path outside the session `cwd` (e.g. a stale `project_root` pointing at another project). Facts inside the session cwd and non-path facts are kept. No facts are deleted — only injection is filtered. See [`docs/mechanics.md`](mechanics.md).

### Planning

The planning pipeline runs before the main tool-calling loop and produces a structured execution plan. It has three phases:

- **Phase 1 — Analysis**: reformulates the task, identifies subtasks and unknowns. Can output `DIRECT` to skip to execution immediately.
- **Phase 2 — Structured review**: one LLM call critiques the Phase 1 analysis through Critic / Pragmatist / Detailer sections and emits Plan Adjustments. Runs only when Phase 1 signals `REFLECT: yes`.
- **Phase 3 — Execution plan**: assigns each subtask to `TOOL / AGENT / SELF` and uses adaptive plan depth.

| Key | Type | Default | Description |
|---|---|---|---|
| `planning_enabled` | bool | `false` | Run the planning pipeline on every user message (not just the first). First-message planning always runs regardless of this flag. |
| `planning_mandatory` | bool | `false` | `true` — the `DIRECT` shortcut is never offered to the model; all three phases always run. `false` — the model can output `DIRECT` in Phase 1 to skip straight to execution. First-message planning is always forced regardless of this flag. |
| `planning_phase1_enabled` | bool | `true` | Enable Phase 1 (task analysis). When disabled, Phase 3 runs without analysis context. |
| `planning_phase2_enabled` | bool | `false` | Enable Phase 2 structured review. Adds one LLM call only when Phase 1 signals `REFLECT: yes`. |
| `planning_phase3_enabled` | bool | `true` | Enable Phase 3 (structured execution plan). When disabled, only Phase 1 (analysis) runs. |

### Sub-agent planning

| Key | Type | Default | Description |
|---|---|---|---|
| `subagent_think_enabled` | bool \| None | `None` | Extended reasoning for sub-agents. `None` = inherit `think_enabled` from parent profile. |
| `subagent_planning_enabled` | bool | `false` | Sub-agents spawned from this profile also run the planning pipeline before their tool loop. |
| `context_providers` | list[str] | `[]` | Extra context providers to inject for this profile (by name). Global providers are always injected. |
| `is_admin_only` | bool | `false` | If `true`, profile is hidden from non-admin users in the profile list. |
| `is_subagent_only` | bool | `false` | If `true`, profile can only be used via `spawn_agent`; `switch_profile` is blocked. Useful for narrow specialist agents that should never become the main session profile. |

### Compression

Per-profile overrides for context compression. When set, they take precedence over the global `settings` (`CONTEXT_KEEP_RECENT`, `CONTEXT_SUMMARY_MAX_TOKENS`) inside `compress_context` / `compress_session` and the summary system prompt. The active profile is propagated to the compressor via `Agent.set_profile()` and to the post-turn `CompressionWorker` via `WorkerContext.profile`.

| Key | Type | Default | Description |
|---|---|---|---|
| `compression_keep_recent` | int \| None | `None` | Override `CONTEXT_KEEP_RECENT` — number of recent turns kept verbatim. `navi_code` uses `12`. |
| `compression_max_tokens` | int \| None | `None` | Override `CONTEXT_SUMMARY_MAX_TOKENS` — max output tokens for the summary LLM call. |
| `compression_prompt_file` | str \| None | `None` | Extra instructions appended to the summary system prompt (loaded from the profile dir). |

See [`sessions.md`](sessions.md) for the full compression algorithm and [`mechanics.md`](mechanics.md) for the catalog.

### Sub-agent system prompt

| Key | Type | Default | Description |
|---|---|---|---|
| `subagent_system_prompt` | str | `""` | Inline sub-agent system prompt. If empty, the loader reads `subagent_system_prompt.txt` from the profile directory. Prepended on top of the parent's `system_prompt` when `inherit_system_prompt=True`. |

### Unknown keys

`AgentProfile` uses `model_config = {"extra": "allow"}`, so unknown keys in `config.json` are accepted and preserved (round-tripped through the admin panel's GET/PUT) rather than rejected. This lets profiles carry custom metadata without code changes. |

---

## Active profiles

| ID | Name | Models (priority order) | Temp | Planning |
|---|---|---|---|---|
| `secretary` | Personal Secretary | gemma4:31b-cloud, qwen3.5:397b-cloud, kimi-k2.6:cloud, gemma4:26b-a4b-it-q4_K_M, qwen3.6:27b | 0.45 | Yes |
| `server_admin` | Server Administrator | gemma4:31b-cloud → gemma4:26b-a4b-it-q4_K_M | 0.3 | Yes |
| `developer` | Developer | gemma4:31b-cloud, qwen3.5:397b-cloud, kimi-k2.6:cloud, gemma4:26b-a4b-it-q4_K_M, qwen3.6:27b | 0.35 | Yes |
| `tool_developer` | Tool Developer | gemma4:31b-cloud → gemma4:26b-a4b-it-q4_K_M | 0.35 | Yes |
| `discuss` | Discussion | gemma4:31b-cloud → gemma4:26b-a4b-it-q4_K_M | 0.85 | No |
| `modeler_3d` | 3D Modeler | gemma4:26b-a4b-it-q4_K_M → gemma4:31b-cloud | 0.35 | Yes |
| `navi_code` | Navi Code | gemma4:26b-a4b-it-q4_K_M → gemma4:31b-cloud | 0.35 | Yes |

All profiles share a base tool set. User tools from `tools/enabled.json` are merged in at runtime.

### `navi_code`

Terminal-first local coding assistant. Designed for the Navi Code CLI and single-user local deployments:

- **Native tools:** `todo`, `scratchpad`, `reflect`, `switch_profile`, `list_profiles`, `filesystem`, `code_exec`, `terminal`, `memory`, `list_tools`, `tool_manual`, `spawn_agent`, `schedule_recall`, `manage_recall`.
- **MCP tools:** disabled (`"mcp": {}`) for the terminal experience.
- **Excluded:** `share_file`, `content_publish`, `ssh_exec`, `gmail`, `image_view`, `mcp__navi-web`.
- **Planning:** Phase 1 and Phase 3 enabled, Phase 2 disabled to reduce latency. Phase 1 classifies the request as `MODE: observe | act`.
- **Bounded autonomy:** `scope_boundary_enabled` and `observe_skips_plan_enabled` are both `true` — the agent stays within the requested scope (does not climb to sibling projects or execute discovered milestone/TODO docs) and `observe` requests (look/read/explain) skip the execution plan and just answer. Flip both off to reproduce the legacy "free flight" behavior.
- **Safety:** an authoritative backend permission gate for destructive tool calls is specified in [`permissions.md`](permissions.md) (designed, not yet implemented). The old prompt-level "confirm before destructive ops" nudge and the client-side permission dialog were removed.

Use it with `NAVI_DEFAULT_PROFILE_ID=navi_code` so `POST /sessions` without a `profile_id` creates a `navi_code` session automatically. See [`docs/navi_code.md`](navi_code.md) for the full local-terminal setup.

---

## System prompt construction

The LLM sees (injected fresh on every call, never stored in session):

```
{persona.txt content}

---

{profile system_prompt.txt content}
```

`persona.txt` — global layer: personality, CONTEXT FIRST principle, self-extension rules, scratchpad/todo/memory instructions, delegation rules.

`system_prompt.txt` — domain layer: tool priorities, workflow, safety rules for this profile.

---

## Adding a profile

1. Create directory `navi/profiles/my_profile/`
2. Add `config.json` (minimal example):
```json
{
  "id": "my_profile",
  "name": "My Profile",
  "description": "...",
  "short_description": "...",
  "model": ["gemma4:31b-cloud", "gemma4:26b-a4b-it-q4_K_M"],
  "temperature": 0.5,
  "max_iterations": 20,
  "tools": {
    "agent": {
      "native": ["todo", "scratchpad", "filesystem", "terminal"],
      "mcp": {
        "navi-web": ["search"]
      }
    },
    "subagent": {
      "native": ["todo", "filesystem", "terminal"],
      "mcp": {}
    }
  },
  "planning_enabled": true,
  "planning_mandatory": false,
  "planning_phase1_enabled": true,
  "planning_phase2_enabled": false,
  "planning_phase3_enabled": true,
  "think_enabled": true,
  "iteration_budget_enabled": true,
  "goal_anchoring_enabled": true,
  "goal_anchoring_interval": 5,
  "anti_stall_enabled": true,
  "anti_stall_threshold": 8,
  "step_validation_enabled": false,
  "adaptive_replan_enabled": false,
  "scope_boundary_enabled": false,
  "observe_skips_plan_enabled": false,
  "subagent_planning_enabled": false
}
```
3. Add `system_prompt.txt` with domain-specific instructions.
4. Optionally add `subagent_system_prompt.txt`.
5. The profile is auto-discovered at startup — no registration needed.

---

## Profile switching

`switch_profile` tool updates `session.profile_id` in the DB. After each tool execution batch, `run_stream()` checks for a profile change and reloads profile + tools. Takes effect on the next LLM call.

Rules (in persona): don't switch for a single off-topic question; switch when the domain clearly changes; never switch back and forth repeatedly.
