# Managed Agents — Knowledge API Contract

Approved agent training knowledge is served to the runtime from an **immutable, versioned Knowledge
Cache** inside Numu. **Which files make up an agent's package — their paths, load order, and
required/optional flags — is defined exclusively by `AGENT_KNOWLEDGE_REGISTRY.json`** (see
`config/agent_knowledge_registry.json`). Nothing about folder names, file names, file count, or load
order is hardcoded; the loader and runtime read only what the registry declares.

```
SharePoint / OneDrive          Knowledge Loader              Knowledge Cache            Agent Runtime (in Numu)
(long-term source of truth) → (registry → fetch exact    →  (immutable versions,  →  reads active knowledge,
                               paths, validate, checksum)     runtime source of truth)   stamps it on the AgentRun
```

- **SharePoint is the long-term source of truth**; the Numu **Knowledge Cache is the runtime source
  of truth**. Make is **orchestration only** — the runtime executes inside Numu.
- The source driver is env-switchable with **no code change** (`AGENT_KNOWLEDGE_SOURCE`):
  `local` (v01 default, reads exact registry paths from a configurable root) or `graph` (SharePoint
  via Microsoft Graph, reuses `GraphTokenProvider`; inert until the Azure app is granted
  `Files.Read.All`/`Sites.Read.All` + `AGENT_KNOWLEDGE_GRAPH_DRIVE_ID`). Both drivers run the **same
  pipeline** — only the fetch backend changes.
- Every `(agent_key, environment)` has its **own** immutable version sequence, active pointer, and
  rollback. Reads/refresh/rollback default to `default_environment` when none is supplied.
- This subsystem is **separate** from Handoffs (operational record) and AgentRuns (execution). It
  **never** touches the decision / policy / approval / expected-state / native-action / handoff /
  execution-request pipeline.

## The registry (`config/agent_knowledge_registry.json`)

The single authority for package composition. It is validated on load and **rejects**: empty
`relative_path`, duplicate `relative_path`, duplicate `load_order`, wildcard/recursive globs
(`*`/`?`), absolute paths, and `..` path traversal. Files are always loaded in ascending `load_order`.

```json
{
  "registry_version": "1.0",
  "agents": {
    "intake_triage": {
      "enabled": true,
      "agent_code": "AUTO-025",
      "stage": "intake_triage",
      "package_version": "v01",
      "files": [
        { "relative_path": "00 Shared Agent Knowledge/NUMU_CORE_SKILL.md", "load_order": 1, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/PROMPT_INDEX.md", "load_order": 2, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/numu-intake-triage_SKILL.md", "load_order": 3, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/DECISION_RULES.md", "load_order": 4, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/DECISION_EXAMPLES.md", "load_order": 5, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/NOTE_GUIDE.md", "load_order": 6, "required": true },
        { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/HANDOFF_SCHEMA.md", "load_order": 7, "required": true }
      ]
    }
  }
}
```

A package may span multiple folders (as above: shared core + the agent's `04 Prompts`). A `required`
file that is missing/empty fails the refresh (keeps the previous active version); an optional file is
simply skipped when absent.

## Authentication & envelope

All read endpoints live under `/api/v1/ai/*` and require **`McpAuth:full`** (OAuth `numu:write`+ or
Sanctum `numu:full`), plus `ai.cb` (circuit breaker) and `throttle:600,1`. Every endpoint accepts an
optional `?environment=` query param (defaults to `default_environment`).

Success: `{ "data": <payload>, "meta": { "request_id", "correlation_id", "actor": { "id", "email" } } }`
Error:   `{ "error": { "code", "message", "details" } }` (e.g. `404 not_found`).

**Metadata vs runtime-content split.** `GET /knowledge`, `/knowledge/meta`, and `/knowledge/versions`
return **metadata + a file manifest** (per-file `relative_path`, `load_order`, `required`, `checksum`,
`size_bytes` — **no file bodies**). The full file **content** is returned only by the explicit
`GET /knowledge/runtime` and `GET /knowledge/{version}` endpoints.

---

## 1. Active package (metadata + manifest) — `GET /api/v1/ai/agents/{agent_key}/knowledge`

The active version's metadata and an ordered file **manifest** (no bodies). Cheap to poll / display.

`200` example:
```json
{
  "data": {
    "agent_key": "intake_triage",
    "environment": "production",
    "package_id": 42,
    "agent_code": "AUTO-025",
    "stage": "intake_triage",
    "knowledge_version": "v2",
    "package_version": "v01",
    "registry_version": "1.0",
    "checksum": "99c2b4f0…",
    "file_count": 7,
    "total_size_bytes": 18422,
    "source_type": "local",
    "status": "active",
    "loaded_at": "2026-07-02T10:20:14+00:00",
    "source_stale": false,
    "files": [
      { "relative_path": "00 Shared Agent Knowledge/NUMU_CORE_SKILL.md", "load_order": 1, "required": true, "checksum": "…", "size_bytes": 2210 },
      { "relative_path": "AUTO-025 - Startup Intake Triage Agent - v01/04 Prompts/PROMPT_INDEX.md", "load_order": 2, "required": true, "checksum": "…", "size_bytes": 980 }
    ]
  },
  "meta": { "request_id": "…", "correlation_id": "…", "actor": { "id": 1, "email": "…" } }
}
```
`404` when the agent has **no active knowledge** in that environment — the runtime MUST NOT run the
agent (Numu does not couple execution to knowledge).

## 1b. Runtime content — `GET /api/v1/ai/agents/{agent_key}/knowledge/runtime`

Same metadata **plus the full ordered file content** — this is what the runtime loads into the LLM
context. `files[]` carries `relative_path`, `load_order`, and `content`, in load order:
```json
{ "data": { "knowledge_version": "v2", "…": "…",
  "files": [
    { "relative_path": "00 Shared Agent Knowledge/NUMU_CORE_SKILL.md", "load_order": 1, "content": "# Numu Core Skill\n…" },
    { "relative_path": "…/PROMPT_INDEX.md", "load_order": 2, "content": "…" }
  ] } }
```

## 2. Change-detection — `GET /api/v1/ai/agents/{agent_key}/knowledge/meta`

Lightweight metadata for polling — **no files**. Compare `checksum` (or `knowledge_version`) to decide
whether to re-pull the runtime content.

```json
{ "data": {
    "agent_key": "intake_triage", "environment": "production",
    "knowledge_version": "v2", "package_version": "v01", "registry_version": "1.0",
    "checksum": "99c2b4f0…", "file_count": 7,
    "loaded_at": "2026-07-02T10:20:14+00:00", "source_type": "local", "source_stale": false
} }
```
`404` when no active knowledge exists.

## 3. Version history — `GET /api/v1/ai/agents/{agent_key}/knowledge/versions`

The full **immutable** history for the environment (newest first), no file bodies — for audit.

```json
{ "data": [
  { "package_id": 42, "version": "v2", "package_version": "v01", "registry_version": "1.0", "environment": "production", "checksum": "99c2b4f0…", "file_count": 7, "status": "active",     "is_active": true,  "source_type": "local", "loaded_at": "…", "loaded_by": 1 },
  { "package_id": 40, "version": "v1", "package_version": "v01", "registry_version": "1.0", "environment": "production", "checksum": "fcde6e19…", "file_count": 7, "status": "superseded", "is_active": false, "source_type": "local", "loaded_at": "…", "loaded_by": 1 }
] }
```

## 4. Exact immutable version (content) — `GET /api/v1/ai/agents/{agent_key}/knowledge/{version}`

Returns the **exact** immutable content of any version — including superseded ones — so a historical
AgentRun can be reconstructed. `files[]` is the ordered content list (as in §1b). Route order:
`/runtime`, `/versions`, `/meta` register **before** `/{version}` so those words are never captured as
a version id. `GET …/knowledge/v1` while `v2` is active returns the **original v1 content**
(`status: superseded`). `404` for an unknown version.

---

## 5. AgentRun knowledge contract

`POST /api/v1/ai/agent-runs` accepts these additional (nullable) traceability fields — copied from the
`/knowledge` (or `/knowledge/runtime`) response the runtime just used, plus what the runtime stamps:

| Field | Type | Meaning |
|---|---|---|
| `knowledge_version`          | string(40) | the package version the runtime used |
| `knowledge_checksum`         | string(64) | that version's ordered-bundle checksum |
| `knowledge_package_id`       | integer    | the exact cache row id (the package id) |
| `knowledge_registry_version` | string(40) | registry version in force at load |
| `knowledge_loaded_at`        | datetime   | when the runtime loaded the package |
| `source_stale`               | boolean    | runtime served last-known-good after a source failure |
| `runtime_model`              | string(80) | model that produced the decision (e.g. `claude-opus-4-8`) |
| `prompt_contract_version`    | string(40) | the prompt/output contract version |

**Runtime contract:** fetch `/knowledge` (or `/knowledge/meta`) first and copy the knowledge fields
onto every run. The values persist on `agent_runs` and resolve back to exact content via §4, so
**every AI decision remains reproducible**. Purely additive — run-create/decision/execution logic is
unchanged; `AgentRunService::create` passes these through as fillable.

---

## 6. Version numbering — monotonic `MAX + 1`

- A new version number is `v{ MAX(existing numeric version for this (agent, environment), incl.
  superseded) + 1 }`.
- **Numbers are never reused.** A no-op refresh (unchanged ordered-bundle checksum) creates **no** new
  version.
- **Rollback never renumbers.** Example: `v1, v2, v3`, rollback→`v1`, refresh (new content) → **`v4`**.
- `unique(agent_key, environment, version)` enforces this at the DB level.

## 7. Rollback semantics — pointer-only (per environment)

`POST /admin/agents/{agent}/knowledge/{environment}/{version}/activate` (perm `ai.knowledge.rollback`)
makes an existing immutable version the active pointer **within that environment**:

- Flips `is_active`/`status` **only** — the previous active becomes `superseded`, the target becomes
  `active`.
- **Never creates or edits content**; history and version numbers are unchanged; other environments
  are untouched.
- Audited as `agent.knowledge_rollback`.

## 8. Immutability guarantees

The cache is **append-only** and content-immutable (mirrors the Handoff model):

- After insert, **only `is_active` and `status`** may change.
- Editing content (`files_json`, `checksum`, `version`, `source_path`, …) throws
  `RuntimeException: Agent knowledge versions are immutable; …`.
- Deleting throws `RuntimeException: Agent knowledge versions are append-only and cannot be deleted.`
  (there is **no delete endpoint**).
- Historical versions are retained forever + queryable. The stored `files_json` keeps each file's
  `relative_path`, `load_order`, `required`, `checksum`, `size_bytes`, and `content` — a full replay.

## 9. Audit chain guarantees

- A **refresh** with changed content appends a new immutable version and supersedes the prior active;
  prior content is never rewritten. Failure — missing/empty required file (`agent.knowledge_failed`),
  transport error (`agent.knowledge_failed`), or content validation failure
  (`agent.knowledge_validation_failed`) — **keeps the previous active version intact**. Unchanged
  content records `agent.knowledge_unchanged`; a successful load records `agent.knowledge_loaded`.
- Any AgentRun's `knowledge_version` (env-scoped) or `knowledge_checksum` (cross-env, checksum-unique)
  resolves to the exact content that produced its decision, for as long as the record exists.

## 10. Content safety validation (per file)

Fetched content is validated fail-closed (any failure keeps the previous active + records
`agent.knowledge_validation_failed`): max file size (`AGENT_KNOWLEDGE_MAX_FILE_SIZE`, default 1 MB),
valid UTF-8, no null bytes, no executable/script payload (`MZ`/`ELF`/`#!`/`<?php`/`<?=`), no embedded
markup/script (`<script`, `<iframe`, `javascript:`, `<object`, `<embed`). Package **composition** is
validated by the registry (see above); this layer only guards file **content**.

## 11. Admin & permissions

On the agent detail page (`Settings → AI Agents → {agent}`), the **Knowledge** card shows, per
environment, the active version + package version + file count + checksum + loaded-at, a **Refresh
Knowledge** button (environment-scoped), and the immutable version history with an **Activate
(rollback)** button per prior version.

| Action | Route | Permission |
|---|---|---|
| Refresh (load new version) | `POST /admin/agents/{agent}/refresh-knowledge` | `ai.knowledge.refresh` |
| Rollback (activate prior version) | `POST /admin/agents/{agent}/knowledge/{environment}/{version}/activate` | `ai.knowledge.rollback` |

## 12. Configuration (`config/agent_knowledge.php`, all env-driven)

| Key | Env | Notes |
|---|---|---|
| `source` | `AGENT_KNOWLEDGE_SOURCE` | `local` (default) \| `graph` |
| `registry_path` | `AGENT_KNOWLEDGE_REGISTRY_PATH` | the package registry JSON (default `config/agent_knowledge_registry.json`) |
| `environments` / `default_environment` | `AGENT_KNOWLEDGE_DEFAULT_ENV` | `dev` / `staging` / `production`; default `production` |
| `max_file_size` | `AGENT_KNOWLEDGE_MAX_FILE_SIZE` | per-file byte cap (default 1 MB) |
| `local.base_path` | `AGENT_KNOWLEDGE_LOCAL_PATH` | root that contains the registry's relative paths (dev: synced OneDrive path) |
| `graph.drive_id` / `graph.root_path` | `AGENT_KNOWLEDGE_GRAPH_DRIVE_ID` / `AGENT_KNOWLEDGE_GRAPH_ROOT` | SharePoint drive + root prefix |

Package composition (files, order, required) is **not** in this config — it lives in the registry JSON
(`registry_path`). Real seeded `agent_key`s: `intake_triage`, `prescreen`, `founder_response_analysis`,
`screening_call_analysis`, `initial_due_diligence`, `committee_analysis`, `investment_memo` (only
`intake_triage` is enabled in the registry for v01; the rest are added as their packages are approved).
