# Agent Skills & Plan Mode Guide

Two `BaseAgent`-framework capabilities that ship together (they share one migration): **Skills** — reusable capability bundles an agent can activate mid-run — and **Plan Mode** — a per-request human-in-the-loop gate that makes an agent get its plan approved before it acts.

**Audience**: developers building/configuring agents, authoring skills, or wiring skills/plan-mode into a chat surface.

Both are **agent-framework** features, not chat-widget features. They work for any agent invocation — the conversations UI, headless/scheduled runs, the API — because the logic lives in `BaseAgent` / `AIEngineBase`, and the chat surface is just one caller.

---

## Part 1 — Skills

### 1.1 The mental model

A **Skill** (`MJ: AI Skills`) is a bundle of three things:

| Piece | Column / table | Effect when the skill activates |
|---|---|---|
| **Instructions** | `AISkill.Instructions` (NVARCHAR MAX) | Appended to the agent's context for the rest of the run |
| **Actions** | `MJ: AI Skill Actions` (junction → `Action`) | Added to the agent's run — described to the model and executable — when the row's `ExposeToModel` is set (the default). With it off, the action stays bundled for SKILL.md export and tooling but is left out of the run entirely; application code invokes it through the Actions API (see §1.3) |
| **Sub-agents** | `MJ: AI Skill Sub Agents` (junction → `AIAgent`) | Added to the agent's available sub-agents |

The point is **write-once, grant-to-many**: instead of copy-pasting the same instruction block + action set into every agent's system prompt, you author it once as a Skill and grant it to any agent that should have it. Skills are also **shareable** and **portable** (see §1.6, §1.7).

Activation is **progressive-disclosure**: an agent only ever sees a skill's **name + description** in its prompt catalog. It sees the full `Instructions` only after it chooses to activate the skill — so a large library of skills costs very few prompt tokens until one is actually used.

Activation is **not** a nested agent run. It's an in-loop step that appends instructions and widens the tool surface, then continues the same run.

### 1.2 The three-layer gate

Whether a given agent may activate a given skill is resolved by `AIEngineBase.GetSkillsForAgent(agent)` ([`packages/AI/BaseAIEngine/src/BaseAIEngine.ts`](../packages/AI/BaseAIEngine/src/BaseAIEngine.ts)) through three independent gates — **all** must pass:

```
include skill  ⟺  AIAgent.AcceptsSkills ≠ 'None'    (per-agent gate)
              AND  AISkill.Status = 'Active'          (catalog gate)
              AND  (AcceptsSkills = 'All'  OR  an Active MJ: AI Agent Skills grant exists)   (grant gate)
```

- **`AIAgent.AcceptsSkills`** — `None` (default; opt-in, so existing agents are unaffected) · `All` (any Active skill) · `Limited` (only skills granted via `MJ: AI Agent Skills`).
- **`AISkill.Status`** — only `Active` skills are activatable. `Deprecated` retires a skill without deleting it (defaults to `Active` so a freshly authored skill works immediately for its owner).
- **`MJ: AI Agent Skills.Status`** — per-grant `Active`/`Pending`/`Revoked`, so a grant can be disabled without unlinking (only consulted when `AcceptsSkills = 'Limited'`).

At runtime a fourth, orthogonal gate also applies: an activated skill's bundled **Actions** are still subject to `Action.Status`, and bundled **sub-agents** to `AIAgent.Status` — a deprecated Action never flows through any skill.

`Action`/`Agent` bundle IDs are read via `GetSkillActionIDs(skillID)` / `GetSkillSubAgentIDs(skillID)`; the engine returns IDs only and lets callers resolve the full entities against their own `ActionEngineServer` / `AIEngine` caches (no cross-package dependency).

### 1.2a The double activation gate (v5.45) — availability vs. trigger

The §1.2 gates answer *"may this agent+user use this skill at all?"* (**availability**). A second, orthogonal dimension answers *"who may pull the trigger?"* (**activation**), added after live testing showed an `AcceptsSkills='All'` agent correctly — but surprisingly — self-activating a skill the user never requested:

| Column | Values | Default | Governs |
|---|---|---|---|
| `AISkill.ActivationMode` | `Auto` / `RequestedOnly` | **`RequestedOnly`** | May this *skill* ever be self-activated? |
| `AIAgent.SkillActivationMode` | `Auto` / `RequestedOnly` | **`RequestedOnly`** | May this *agent* ever self-activate skills? |

**Self-activation** (the skill appearing in the agent's prompt catalog + an agent-initiated `Skill` step) requires **`Auto` on BOTH sides** — resolved by `AIEngineBase.GetAutoActivatableSkillsForAgent(agent, user?)`, which is `GetSkillsForAgent(...)` further filtered by the two dials. The catalog injection, `validateSkillNextStep`, and `resolveSkillActivations` all use this auto set.

**The requested path is NOT gated by ActivationMode.** A user's explicit `/skill` mention (→ `ExecuteAgentParams.requestedSkillIDs` → `preActivateRequestedSkills`) honors a `RequestedOnly` skill — that's the mode's whole point — subject to all §1.2 availability gates.

Because both defaults are `RequestedOnly`, the **Auto × Auto "super agent" posture** (an agent that expands its own tool surface at runtime) always requires two deliberate opt-ins and can never arise accidentally — the defense against *skill leakage into an agent*. Configuration recipes:

- **Research assistant** that should reach for web/search/visualization skills on its own judgment: agent `SkillActivationMode='Auto'` + those skills `ActivationMode='Auto'`.
- **Consequential skill** (e.g. Communications — it *sends things*): keep `ActivationMode='RequestedOnly'`. No agent, regardless of posture, can self-activate it; users must `/skill` it in explicitly, and the skill's own confirm-before-send instructions still gate the act.
- **Locked-down agent**: `SkillActivationMode='RequestedOnly'` — its prompt catalog is empty; skills only enter its runs when the user asks.

### 1.2b Layering an application policy — `filterAvailableSkills`

MJ's gates are User/Role and agent based; they have no notion of a tenant, a licence or an
entitlement. An application that needs one overrides **one** protected hook:

```typescript
protected override async filterAvailableSkills(
    skills: MJAISkillEntity[], purpose: SkillAvailabilityPurpose,
    agent: MJAIAgentEntityExtended, contextUser?: UserInfo,
): Promise<MJAISkillEntity[]> {
    const usable = await myLicensing.usableSkillIDs(contextUser);   // cached — this runs every prompt turn
    return skills.filter(s => usable.has(s.ID));                     // subset only; on error return []
}
```

`BaseAgent` calls it after its own gates and before anything activates, at all three availability
sites: the prompt **catalog** (`purpose: 'catalog'`), a model-initiated Skill step's validation and
execution (`'auto-activation'`), and a user's explicit request (`'requested'`). So a skill the policy
refuses is never offered, never self-activated, and refused (with the usual system note) when asked
for. The default is the identity: without an override, MJ's gates are the whole policy.

Two notes for override authors (and one on logs: a policy that throws *persistently* logs that error on every prompt turn via the catalog site — that is the fail-closed behaviour working, and the pattern to recognise). It is called at four sites (catalog on every prompt turn, Skill-step
validation, Skill-step execution, requested pre-activation), so cache your lookups. And it is
server-side: the composer's `/skill` warning (`conversation-agent.service.ts`) checks only MJ's own
`GetSkillsForAgent`, so a client can still send a request the policy then refuses — it surfaces as
the refusal note, not as a picker omission.

### 1.3 The runtime flow (Loop agent)

```
gatherPromptTemplateData()  ── injects skill CATALOG (name+description only) into the prompt
        │
        ▼
LLM emits  nextStep.type='Skill', skills:[{name}]
        │
        ▼
LoopAgentType.DetermineNextStep  ── → BaseAgentNextStep.step='Skill', skillActivations:[{name}]
        │
        ▼
validateSkillNextStep  ── fuzzy-matches names vs GetSkillsForAgent; unknown/disallowed → Retry
        │
        ▼
executeSkillStep  ── for each newly-activated skill:
        ├─ recordSkillActivationStep()   → AIAgentRunStep (StepType='Skill')
        ├─ buildSkillActivationMessage() → appends Instructions to conversationMessages
        └─ enableSkillCapabilities()     → pushes 'specific'-scoped add ActionChange/SubAgentChange
                                          (Actions: the skill's ExposeToModel rows only — GetSkillExposedActionIDs)
        │
        ▼
executePromptStep  ── loop continues; next turn's gatherPromptTemplateData picks up the widened tool surface
```

Key implementation notes (all in [`base-agent.ts`](../packages/AI/Agents/src/base-agent.ts)):

- **`'Skill'` is a non-terminal step.** Like `'ClientTools'`, it is deliberately **not** part of the generated `AIAgentRun.FinalStep` union (that column is DB-CHECK-constrained to terminal outcomes). `BaseAgentNextStep.step` is typed off `FinalStep`, so the switch sites use an explicit `'Skill' as typeof …step` cast. It **is** in the `AIAgentRunStep.StepType` CHECK — that column records what executed, a different concern from a run's final outcome.
- **`ExposeToModel` decides whether a bundled action joins the run at all** (v6.1.x, #4226). `enableSkillCapabilities` pushes `GetSkillExposedActionIDs(skill.ID)` — the rows with the flag set (default) — onto `params.actionChanges`. The run's effective action set is one list that is both the prompt's tool surface and the execution allow-list, so a row with the flag **off** is not described to the model *and* cannot be executed by the agent through any path (`Actions` step, a `PreProcessActionStep`, a loop's action lookup all resolve against that set and reject it). That is deliberate: hiding it from the prompt alone would still let the model call it by name the moment a skill's Instructions mention it. The row keeps the action associated with the skill for SKILL.md export and tooling; application code — a menu button in the skill's reply — invokes it directly through the Actions API, whose authorization is its own (bundling grants nothing there). Because the agent never runs such an action, skill attribution on the Actions step does not apply to it.
- **Tool-surface widening uses `scope: 'specific'` targeting the activating agent's own ID** (`agentIds: [agent.ID]`), pushed onto `params.actionChanges` / `params.subAgentChanges`. This applies to the activating agent **at any depth** (a sub-agent that activates a skill still gets its tools — a `'root'` scope would only apply at depth 0) and never cascades to that agent's own sub-agents (via `filterActionChangesForSubAgent`). Because `params` is the same object for the whole run, every later turn's `gatherPromptTemplateData()` re-applies it automatically.
- **Idempotent re-activation.** `_activatedSkillIDs` tracks what's already active; re-requesting an active skill is a harmless no-op (no duplicate instructions, no duplicate change entries).
- **`_effectiveSubAgents`** mirrors `_effectiveActions` so a runtime-added sub-agent (from a skill) validates correctly in `validateSubAgentNextStep`. (This closed a pre-existing gap in the `subAgentChanges` mechanism that affected any consumer, not just Skills.)

`executeSkillStep` is decomposed into `resolveSkillActivations` / `buildSkillActivationMessage` / `enableSkillCapabilities` / `recordSkillActivationStep`, all `protected` — override any one for fine-grained control (custom instruction formatting, a licensing check on activation, cascading grants via `'all-subagents'`, etc.) without re-implementing the step.

### 1.3a Observability — `AIAgentRunStep.Skills` (v5.45)

Every step touched by a skill records the linkage on `AIAgentRunStep.Skills` — a JSON-typed column (`Array<AgentSkillInvocation> | null`, generated accessor `SkillsObject`) so skill involvement is **auditable per step**, not inferred:

```typescript
type AgentSkillInvocation = {
    SkillID: string;
    SkillName: string;                       // identity as of activation
    ActivationType: 'requested' | 'auto';    // /skill mention vs. agent self-activation
    Provenance: {                            // the gate values that admitted it
        AgentAcceptsSkills: string;
        SkillActivationMode: string;
        AgentSkillActivationMode: string;
        RequestedBy: 'user-request' | 'agent-decision';
    };
    Reason?: string;                         // agent-stated rationale (auto only)
}
```

Population rules (all in `createStepEntity` + the activation paths):

| Step type | `Skills` contains |
|---|---|
| **Skill** | The activation(s) that step performed — with `Reason` when the model self-activated (the loop response's `skills:[{name, reason}]` now carries an optional one-sentence rationale) |
| **Prompt** | The **full set of skills in effect** for that turn — prompt injection is never invisible |
| **Actions** / **Sub-Agent** | The skill(s) through which the executed tool became available. **`NULL` means the tool was a native grant** — native authority takes precedence over skill attribution (`getSkillAttributionForAction` / `getSkillAttributionForSubAgent`) |
| anything else | `NULL` |

The runtime type twin is `AgentSkillInvocation` in `@memberjunction/ai-core-plus`; the CodeGen JSON-type interface lives at `metadata/entities/JSONType-interfaces/AgentSkillInvocation.ts` — keep them in sync.

**UX**: the agent-run form renders this end to end — `Skill`/`Plan` step nodes get dedicated icons, any step with `Skills` shows per-skill chips (auto-activations get a warning accent — the agent expanded its own surface), and the step drill-in panel gains a **Skills tab** showing each invocation's activation type, provenance-of-authority grid, and reason.

### 1.4 Realtime and proxy agents

- **Realtime** agents don't run the Loop; a skill's instructions are appended at session build (session-static), not activated in-loop.
- **Proxy** agents delegate their whole loop to a remote system; skills are the remote's concern.

### 1.5 Prompt template

The Loop system prompt template ([`metadata/prompts/templates/system/loop-agent-type-system-prompt.template.md`](../metadata/prompts/templates/system/loop-agent-type-system-prompt.template.md)) gates the entire Skills section on `{% if skillCount > 0 %}` — agents with no available skills see nothing about skills, keeping their prompt unchanged. `skillCount` / `skillsCatalog` come from `AgentContextData` (populated in `gatherPromptTemplateData`).

### 1.6 Permissions: full agent-parity, open-by-default

Skills use the **same dedicated-table permission model as AI Agents** — `MJ: AI Skill Permissions` (`AISkillPermission`) is the exact sibling of `AIAgentPermission`: a row grants a **User** (`UserID`) **or** a **Role** (`RoleID`) — never both, never neither (a server-side validator enforces this; see below) — one of `CanView` / `CanRun` / `CanEdit` / `CanDelete`. A skill's owner is its `CreatedByUserID`.

There are **two access paths over the same table**, with different defaults and purposes (the general pattern is documented in the **[Unified Permissions Guide](UNIFIED_PERMISSIONS_GUIDE.md#3-the-two-access-paths-dont-confuse-them)**):

| Path | Class | Default (no rows) | Used for |
|---|---|---|---|
| **Runtime helper** | `AISkillPermissionHelper` (`@memberjunction/ai-engine-base`) | **Open** — anyone can View + Run; only owner Edits/Deletes | The hot path: the `/skill` picker filter and the server-side run-time guard |
| **Unified provider** | `AISkillPermissionProvider` (`@memberjunction/core-entities`, `@RegisterClass(PermissionProviderBase, 'MJAISkillPermissionProvider')`, domain `AI Skill Permissions`) | **Closed** — explicit grants only | Sharing Center / audit via the `PermissionEngine` |

Action mapping (both paths): `CanView → Read`, `CanRun → Execute`, `CanEdit → Update`, `CanDelete → Delete`. The helper applies the hierarchy `Delete → Edit → Run → View` and is a **synchronous cache hit** because `AIEngineBase` caches `SkillPermissions` alongside `Skills`.

- **Grantee-exclusivity validator**: `MJAISkillPermissionEntityServer` (`@memberjunction/core-entities-server`) overrides `Validate()` to reject a row with both or neither grantee — the deterministic, version-controlled equivalent of the LLM-generated `ValidateRoleIDAndUserIDExclusive` on `AIAgentPermission`. It fires on every server save path, so the sharing UI, Remote Operations, and scripts are all covered.
- **The permission filter**: `AIEngineBase.GetSkillsForAgent(agent, user?)` takes an **optional** user. Without it, you get pure agent-gating (§1.2). With it, the result is additionally intersected with the user's Run permission — so the same call is the single source of truth for "what may this agent activate *for this user*". `BaseAgent` passes `params.contextUser`, so the skills **catalog the model sees is already permission-filtered** — an agent is never even offered a skill the acting user can't run.

**Sharing** is gated behind the dedicated **`Can Share Skills`** authorization (`MJ: Authorizations`); authoring/using your own skills never requires it. In the UI, the generated `MJ: AI Skills` form gets a **Manage Permissions** grid + Export/Import SKILL.md from the `AISkillSharingPanel` (a `BaseFormPanel` slot component), and the permission grid itself is a skill-scoped mirror of the agent permissions grid (`SkillPermissionsPanel`/`SkillPermissionsDialog` in `@memberjunction/ng-agents`). The generated form's related-entity grid also edits `MJ: AI Skill Permissions` directly. (The earlier `AI Skills` *Resource Type* / `MJ: Resource Permissions` sharing was retired — skills now own their permission table, exactly like agents.)

### 1.6a `/skill` invocation — user-requested pre-activation

A user can activate a skill for a message by typing **`/skill-name`** in the conversation composer — the exact sibling of `@agent` and `#entity` mentions (the mention system's `MENTION_TRIGGERS` now includes `/`). The picker (`MentionAutocompleteService`) lists **only Active skills the user has Run permission for** (filtered through `AISkillPermissionHelper`, open-by-default), and each chip renders with the skill's own `IconClass` + `Color` UX metadata. Selected skill IDs are collected from the composer and threaded as **`ExecuteAgentParams.requestedSkillIDs`** (a `string[]`) down the same client→resolver→runtime chain as `planMode`:

```
/skill chips → message-input.component (collectRequestedSkillIDs)
  → conversation-agent.service.invokeSubAgent(requestedSkillIDs)   [or ConversationAgentRunner.ProcessMessage]
  → RunAgentFromConversationDetailParams.RequestedSkillIDs
  → GraphQL $requestedSkillIDs:[String!] → RunAIAgentResolver
  → ExecuteAgentParams.requestedSkillIDs
  → BaseAgent.preActivateRequestedSkills()   ← guarded pre-activation at run start
```

`preActivateRequestedSkills` runs once at run start (**root agent only**), and activates each requested skill **only if it survives the guard**: it must be in `GetSkillsForAgent(agent, contextUser)` — i.e. the agent accepts it **and** the user may run it. Anything else is silently dropped, so a client can never smuggle in a skill the user or agent isn't entitled to. Surviving skills are activated through the same `recordSkillActivationStep` / `enableSkillCapabilities` / `buildSkillActivationMessage` machinery as a model-initiated `Skill` step, so their Instructions + bundled tools take effect from turn 1. Plan Mode is unaffected — pre-activation widens the tool surface, but the plan-approval gate still blocks *executing* those tools until approved.

### 1.6b Skills and Scoped Search — the principal comes from the run

`Scoped Search` takes an optional `AISkillID` input: the skill on whose behalf the search runs. The
skill is a **principal** — its `SearchScopeAccess` / `MJ: AI Skill Search Scopes` can deny or widen a
scope, and `ScopeDimensionResolver` binds it as `Principals.SkillID` into a dimension's expansion
query (the mechanism behind "invoking this skill changes the content"). Inside a Loop agent that
input is written by the model, so it cannot be the authority on which skill is active.

`BaseAgent.ExecuteSingleAction` therefore stamps **`Context.ActiveSkillIDs`** — the skills the run has
activated so far — onto every action call. `Scoped Search` reads it:

| inside a run | outcome |
|---|---|
| no `AISkillID` named, exactly one skill active | that skill is the principal |
| no `AISkillID` named, several active | no principal (verbose log) — the caller must say which |
| `AISkillID` named and active in the run | accepted (the loaded skill's id is threaded) |
| `AISkillID` named but NOT active in the run | `INVALID_PARAM` — the model may not widen its own reach |

Outside a run (no `ActiveSkillIDs` on the context) the explicit input is the only source, unchanged.
An app that gates activation with its own policy reads the same state through the protected
`ActivatedSkillIDs` getter on `BaseAgent` rather than re-deriving it.

### 1.6c Conversation-scoped skills — a mode that survives the turn

By default a skill is active for the run that activated it (`AISkill.ActivationScope = 'Run'`). A
skill that behaves as a **mode** — a persona, an assistant whose reply carries a menu pressed on the
next turn — sets `ActivationScope = 'Conversation'`. Then:

- when it activates in a run that has a `conversationId` (requested or self-activated), `BaseAgent`
  writes an `MJ: Conversation Skills` row (`Status = 'Active'`, `ActivatedByRunID` = the run);
- at the start of every later root run in that conversation, Active rows join `requestedSkillIDs`
  before the gates — so the skill is re-activated with its Instructions and tools, subject to every
  availability gate on that run;
- a persisted skill a gate refuses this turn is skipped without a note (the user did not mention it)
  and its row stays Active — retiring a mode is explicit, never a side effect of a gate miss;
- the app's "leave the mode" gesture calls `BaseAgent.EndConversationSkill(conversationId, skillId,
  user)`; a later activation re-uses the row.

Each step is a protected hook (`loadConversationSkillIDs`, `persistConversationSkillActivation`) and
fails soft. Precedent: `UserRoutine.RequestedSkillIDs`, the same idea keyed on a routine.

### 1.7 SKILL.md — portable import/export

A skill can be exported to / imported from a portable **SKILL.md** file, so skills move across MJ instances:

```markdown
---
name: Report Builder
description: Generates formatted business reports from query results
category: Reporting
actions:
  - Run Query
  - Generate PDF
codeOnlyActions:
  - Generate PDF
subAgents:
  - Report Formatter Agent
---

Instructions body — plain markdown, appended to an accepting agent's system
prompt when the skill is activated.
```

`codeOnlyActions` (optional) names the `actions` bundled with `ExposeToModel = 0` — see §1.3. Absent key: no
opinion, surviving rows keep their flag. Present but empty (`codeOnlyActions:` with no items, or `[]`): nothing is
code-only — delete the last name and keep the key to put that action back into the run.

**Why names, not IDs**: Action/sub-agent references in the frontmatter are **names**, because names are the only stable cross-instance reference. On import, names are resolved against the target instance's catalog; anything that doesn't resolve becomes a **non-fatal warning** (the skill still imports with whatever did resolve) — a skill authored elsewhere may reference actions this instance doesn't have.

Two layers, in `@memberjunction/ai-agents`:

- **`SkillMarkdownConverter`** — pure, dependency-free `Parse` / `Serialize`. Fully unit-tested in isolation. Hand-rolled parser (the frontmatter shape is small and fixed) rather than a YAML dependency.
- **`SkillImportExportService`** — orchestrates against the DB: `Config()`s the AI + Action engines (idempotent) for name↔ID resolution, creates/updates the `MJ: AI Skills` row, and resyncs the two junction sets. `AISkillAction.ExposeToModel` round-trips through the optional `codeOnlyActions` frontmatter list (names, a subset of `actions`): export writes it only when the skill has rows with the flag off, so older files are byte-identical; import applies it when present. A file without the key expresses no opinion, and the resync — which is delete-and-recreate — then carries each surviving row's flag across, so re-saving a skill's SKILL.md never turns a code-only action model-callable; only a genuinely new row takes the column default.

Both are exposed as typed, provider-routed **Remote Operations** — `AISkill.ExportMarkdown` / `AISkill.ImportMarkdown` (see [REMOTE_OPERATIONS_GUIDE](REMOTE_OPERATIONS_GUIDE.md)) — so the browser calls `new AISkillExportMarkdownOperation().Execute(input, { provider })` with no bespoke resolver/GraphQL client.

---

## Part 2 — Plan Mode

### 2.1 The mental model

Plan Mode makes an agent **present a plan and get human approval before it executes any Actions or Sub-Agents**. It's a per-request opt-in built on the **existing** `MJ: AI Agent Requests` human-in-the-loop (HITL) pause/resume infrastructure — it adds no new persistence mechanism.

Three switches, resolved once per run in `BaseAgent.resolvePlanModeGate`:

| Switch | Column / param | Default | Meaning |
|---|---|---|---|
| **Capability** | `AIAgent.SupportsPlanMode` (BIT) | `1` (ON, opt-out) | Whether this agent *can* use plan mode at all |
| **Per-request** | `ExecuteAgentParams.planMode` | `undefined` (OFF) | Whether *this run* should be gated |
| **Mandate** (v5.45) | `AIAgent.RequirePlanMode` (BIT) | `0` (OFF) | Forces plan mode on **every** root run of this agent, regardless of the per-request flag. `SupportsPlanMode` is irrelevant when set. |

Plan Mode is **active** when the agent is the **root** (depth 0) **AND** (`RequirePlanMode` **OR** (`SupportsPlanMode` **AND** `planMode === true`)). Because both the per-request toggle and the mandate default OFF, **default runtime behavior is unchanged** — nothing is gated unless a caller opts in or an admin mandates it. Set `RequirePlanMode` on high-consequence agents (anything with outbound-communication capabilities) where human review must not depend on the caller remembering a toggle. Realtime agents are seeded `SupportsPlanMode = 0` (they skip plan mode structurally); Remote Proxy agents will be too when that type ships.

Every plan-mode run is stamped **`AIAgentRun.PlanMode = 1`** (v5.45) — the run-header UX renders a "Plan Mode" chip from it, and it's the anchor for future plan-drift audits (approved plan vs. steps that actually executed).

### 2.2 The gate

While Plan Mode is **active and not yet approved**, `validateNextStep` demotes any `Actions` or `Sub-Agent` next step to `Retry` with an explanatory message — the agent literally cannot execute until it presents a plan. **Everything else stays allowed**: `Chat` (ask a clarifying question first), `Skill` (load a skill's instructions before planning), `ForEach`/`While`, `ClientTools`, `Retry`.

**After approval, skill activations are blocked** (v5.45): a post-approval agent-initiated `Skill` step is demoted to `Retry` directing the agent to present an *updated plan* — otherwise the agent could widen its tool surface beyond what the human reviewed. The rule's shape: activations are legal only **before** approval (pre-activated `/skill` requests land before planning; pre-approval self-activations are visible in the plan), so the reviewed plan always reflects the full capability set.

The prompt template surfaces a "Plan Mode — REQUIRED before you may act" section, gated on `{% if planModeActive and not planApproved %}`.

### 2.3 The HITL flow

```
Plan Mode active, unapproved
        │
        ▼
LLM emits  nextStep.type='Plan', plan:'…'
        │
        ▼
executePlanStep:
   ├─ records an AIAgentRunStep (StepType='Plan')      ← the UI reads this to render a plan-approval card
   ├─ createFeedbackRequest(...) with an editable       ← reuses the EXISTING MJ: AI Agent Requests flow
   │    plan-approval AgentResponseForm (textarea + Approve/Reject buttongroup)
   └─ terminates the run (returns a 'Chat'-shaped terminal step)   ← awaits the human
        │
        ▼
human Approves / edits / Rejects  → MJAIAgentRequestEntityServer.Save() auto-resumes (lastRunId set)
        │
        ▼
resolvePlanModeGate on the RESUMED run:
   ├─ re-enables planMode (because the request originated from a 'Plan' step)
   └─ reads the request's status:  Approved/Responded → approved (proceed)   ·   Rejected → unapproved (re-plan)
```

Two subtleties worth calling out (both are load-bearing correctness points):

- **The step returned to the framework is `'Chat'`, not `'Plan'`.** `'Plan'` is only an intermediate classification and the `AIAgentRunStep.StepType` audit value the UI keys on. The *terminal* step must stay within `AIAgentRun.FinalStep`'s DB-CHECK-constrained vocabulary, which deliberately excludes `Plan`/`Skill`/`ClientTools` (same reason as §1.3). So a plan-approval pause is represented with the same terminal shape `executeChatStep` uses.
- **Rejection forces a re-plan** because `resumeAgent` re-enables `planMode` **only** when the resume originated from a `Plan` run-step. Without that, the gate would be off on the resumed run and a rejected plan would silently execute anyway. Regular Chat-clarification resumes are unaffected — they leave `planMode` off.

`resolvePlanModeGate`, `validatePlanNextStep`, `executePlanStep`, and `buildPlanApprovalForm` are all `protected` — override to change eligibility rules, plan-quality validation, or the approval card's layout.

---

## 3. Schema at a glance

**v5.44** — Migration [`V202606301200__v5.44.x__Agent_Skills_And_Plan_Mode.sql`](../migrations/v5). New tables: `AISkill`, `AISkillAction`, `AISkillSubAgent`, `AIAgentSkill`, `AISkillPermission` (User **xor** Role grantee + `CanView`/`CanRun`/`CanEdit`/`CanDelete`). Additive columns: `AIAgent.SupportsPlanMode` (default 1), `AIAgent.AcceptsSkills` (default `'None'`), `AISkill.IconClass` + `AISkill.Color` (UX metadata for the `/skill` picker). `AIAgentRunStep.StepType` CHECK extended with `'Plan'` and `'Skill'`. Composition junctions (`AISkillAction`/`AISkillSubAgent`) are intentionally **status-less** — member lifecycle is governed by `Action.Status`/`AIAgent.Status`; `Status` lives only on the grant (`AIAgentSkill`) and the catalog (`AISkill`). The `MJ: Permission Domains` catalog row for `AI Skill Permissions → MJAISkillPermissionProvider` is seeded via metadata sync (`metadata/permission-domains/`), not the migration.

**v5.45** — Migration [`V202607020314__v5.45.x__AISkill_ActivationMode.sql`](../migrations/v5). Additive columns: `AISkill.ActivationMode` + `AIAgent.SkillActivationMode` (both `'Auto'`/`'RequestedOnly'`, default **`'RequestedOnly'`** — the double activation gate, §1.2a), `AIAgent.RequirePlanMode` (BIT, default 0 — mandatory plan mode, §2.1), `AIAgentRun.PlanMode` (BIT, default 0 — run-level stamp), `AIAgentRunStep.Skills` (NVARCHAR MAX, JSON-typed `Array<AgentSkillInvocation>` — per-step observability, §1.3a). The `Skills` JSON-type binding is seeded via metadata sync (`metadata/entities/.entity-field-jsontype-agent-run-step-skills.json` + `JSONType-interfaces/AgentSkillInvocation.ts`).

---

**v6.1.x** — Migration [`V202609111449__v6.1.x__Skill_Action_Expose_To_Model.sql`](../migrations/v6).
Additive: `AISkillAction.ExposeToModel` (BIT NOT NULL, default 1). `1` = today's behaviour; `0` keeps the
action bundled (SKILL.md export, tooling) but out of the agent's run — not described to the model, not
executable by the agent. See §1.3; SKILL.md carries it as `codeOnlyActions` (§1.7).

**v6.1.x** — Migration [`V202609031400__v6.1.x__Conversation_Scoped_Skill_Activation.sql`](../migrations/v6).
Additive: `AISkill.ActivationScope` (`'Run'`/`'Conversation'`, default `'Run'`) and the
`ConversationSkill` table (`MJ: Conversation Skills`: ConversationID, SkillID, Status
`Active`/`Ended`, ActivatedByRunID, EndedAt; UNIQUE per conversation+skill). See §1.6c.

## 4. Where to look

| Concern | File |
|---|---|
| Skill resolution + gate + user filter | `packages/AI/BaseAIEngine/src/BaseAIEngine.ts` (`GetSkillsForAgent(agent, user?)`, `GetAutoActivatableSkillsForAgent(agent, user?)` — the double gate, `SkillPermissions`, `GetSkillActionIDs`/`…SubAgentIDs`) |
| Skill observability (provenance capture + per-step attribution) | `packages/AI/Agents/src/base-agent.ts` (`buildSkillInvocation`, `getSkillAttributionForAction`/`…ForSubAgent`, `createStepEntity` `skills` param); type in `packages/AI/CorePlus/src/agent-types.ts` (`AgentSkillInvocation`) |
| Skill observability UX (run header chip, step chips, Skills tab) | `packages/Angular/Explorer/core-entity-forms/src/lib/custom/ai-agent-run/` (`ai-agent-run.component.*`, `ai-agent-run-step-node.component.*`, `ai-agent-run-step-detail.component.*`) |
| Skill permission runtime helper | `packages/AI/BaseAIEngine/src/AISkillPermissionHelper.ts` (open-by-default, cached) |
| Skill permission unified provider | `packages/MJCoreEntities/src/custom/PermissionProviders/AISkillPermissionProvider.ts` (+ `index.ts` `LoadPermissionProviders`) |
| Grantee-exclusivity validator | `packages/MJCoreEntitiesServer/src/custom/MJAISkillPermissionEntityServer.server.ts` |
| Skill step + Plan Mode runtime + pre-activation | `packages/AI/Agents/src/base-agent.ts` (`executeSkillStep` family, `preActivateRequestedSkills`, `resolvePlanModeGate`, `executePlanStep`); `ExecuteSingleAction` stamps `Context.ActiveSkillIDs` |
| Skill/Plan next-step parsing | `packages/AI/Agents/src/agent-types/loop-agent-type.ts`, `loop-agent-response-type.ts` |
| Step-type union + params | `packages/AI/CorePlus/src/agent-types.ts` (`BaseAgentNextStep`, `AgentSkillActivationRequest`, `ExecuteAgentParams.planMode` / `.requestedSkillIDs`) |
| `/skill` composer UX | `packages/Angular/Generic/conversations/src/lib/services/mention-autocomplete.service.ts` (`getSuggestions(…, '/', targetAgentId)` → `skillsForTarget` → `skill-picker-narrowing.ts` `IntersectAcceptedSkills`; the host binds `TargetAgentId` on `mj-ai-composer` — `mj-message-input` binds `pickerTargetAgentId`: an `@agent` chip in the draft, else its resolved agent), `components/mention/mention-editor.component.ts`, `components/message/message-input.component.ts` |
| `requestedSkillIDs` transport | `AgentsClient/src/generic/AgentClientTypes.ts` + `AgentClientSession.ts`, `GraphQLDataProvider/src/graphQLAIClient.ts`, `MJServer/src/resolvers/RunAIAgentResolver.ts`, `ConversationsRuntime/src/agent-runner/ConversationAgentRunner.ts` |
| SKILL.md | `packages/AI/Agents/src/SkillMarkdownConverter.ts`, `SkillImportExportService.ts`, `operations/AISkillMarkdownOperations.ts` |
| Plan-approval resume | `packages/AI/Agents/src/MJAIAgentRequestEntityServer.ts` |
| Sharing / permission grid / import UI | `packages/Angular/Explorer/core-entity-forms/src/lib/panels/ai-skill-sharing/`, `packages/Angular/Generic/agents/src/lib/{services/skill-permissions.service,components/skill-permissions-*}` |
| Governance metadata | `metadata/authorizations/` (`Can Share Skills`), `metadata/permission-domains/` (`AI Skill Permissions`) |
| Tests | unit: `packages/AI/Agents/src/__tests__/{skill-step,plan-mode-gate,loop-agent-type,SkillMarkdownConverter}.test.ts`, `packages/AI/BaseAIEngine/src/__tests__/AISkillPermissionHelper.test.ts`, `packages/MJCoreEntities/src/__tests__/PermissionProviders/AISkillPermissionProvider.test.ts`, `packages/MJCoreEntitiesServer/src/__tests__/MJAISkillPermissionEntityServer.test.ts`; integration: `packages/MJServer/integration-test-scripts/ai-skills-tests.ts` |
