From ce02068b90da085a69e0d5d069e5451b0d4ed9e6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 16:08:57 +0000 Subject: [PATCH] docs: align incident triage, layout migration, and planner vs governance Rewrite the incident-triage tutorial onto pause-on-veto + resume_run, document migrate-layout copy semantics, and stop treating planner_mode as the governance switch. Also drop fabricated CLI policy flags and the missing llms-txt reference page. Co-authored-by: Sara Loera --- docs/docs.json | 3 + docs/guides/configure-planner-modes.mdx | 98 +++-- docs/guides/configure-shared-storage.mdx | 100 +++-- docs/guides/write-policies.mdx | 23 +- docs/quickstart.mdx | 6 +- docs/reference/cli.mdx | 38 +- docs/reference/configuration.mdx | 21 +- docs/reference/llms-txt.mdx | 36 ++ docs/tutorials/first-policy.mdx | 21 +- docs/tutorials/incident-triage.mdx | 467 +++++++++-------------- llms.txt | 11 +- 11 files changed, 435 insertions(+), 389 deletions(-) create mode 100644 docs/reference/llms-txt.mdx diff --git a/docs/docs.json b/docs/docs.json index 955c7ba..abd3d71 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -49,8 +49,11 @@ "group": "Tutorials", "pages": [ "tutorials/hello-episode", + "tutorials/first-episode", + "tutorials/first-policy", "tutorials/langgraph-episode", "tutorials/governed-side-effects", + "tutorials/incident-triage", "tutorials/trace-based-evals" ] }, diff --git a/docs/guides/configure-planner-modes.mdx b/docs/guides/configure-planner-modes.mdx index fd2fa3e..315644c 100644 --- a/docs/guides/configure-planner-modes.mdx +++ b/docs/guides/configure-planner-modes.mdx @@ -9,8 +9,12 @@ Noēsis supports two planner modes that control the level of governance and obse | Mode | Description | Use when | | --- | --- | --- | -| `meta` (default) | Full governance with MetaPlanner and PreActGovernor | You need auditability, vetoes, and insight KPIs | -| `minimal` | Skips governance, uses legacy stepwise planner | Quick sanity checks or smoke tests | +| `meta` (default) | MetaPlanner + Direction search / directives | Production planning, intuition steering, richer insight | +| `minimal` | Simple planner; skips Direction search | Smoke tests, or when you only need a short plan | + + +`planner_mode` chooses the planner. **Governance is separate:** `governance_mode` (`off` / `audit` / `enforce`) and `governance_pause_on_veto`. Pause-on-veto is covered with `planner_mode="minimal"` and `governance_mode="enforce"` in `tests/runtime/test_run_lifecycle.py`. + ## Setting the planner mode @@ -20,12 +24,13 @@ Noēsis supports two planner modes that control the level of governance and obse import noesis as ns # Set globally -ns.set(planner_mode="meta") # Full governance (default) -ns.set(planner_mode="minimal") # Skip governance +ns.set(planner_mode="meta") # MetaPlanner (default) +ns.set(planner_mode="minimal") # Simple planner -# Check current mode -config = ns.config() -print(config.planner_mode) +# Read the current snapshot (ns.config() is not a public API) +config = ns.get() +print(config["planner_mode"]) +print(config["governance_mode"]) ``` ### Environment variable @@ -135,34 +140,48 @@ print(f"Governance events: {len(governance)}") ## Minimal mode -Minimal mode skips governance for faster execution: +Minimal mode uses a simple planner and, **with the default `governance_mode="off"`**, emits no direction or governance events: ```python import noesis as ns -ns.set(planner_mode="minimal") +ns.set(planner_mode="minimal") # governance_mode still defaults to "off" -episode_id = ns.run("Quick sanity check") +episode_id = ns.run("Quick sanity check", intuition=False) -# No governance events events = list(ns.events.read(episode_id)) governance = [e for e in events if e["phase"] == "governance"] -assert len(governance) == 0 # No governance in minimal mode +assert len(governance) == 0 ``` -### What minimal mode skips +That assertion matches `tests/governance/test_pre_act.py` (`planner_mode="minimal"` without an enforce override). + +### What minimal mode changes by default -- MetaPlanner and PreActGovernor -- Direction and governance events -- Policy-based vetoes -- Some insight metrics (veto_count, plan_adherence) +- Uses `MinimalPlanner` instead of `MetaPlanner` +- Skips Direction search when `governance_mode` is `off` +- Keeps the cognitive loop and artifact pack (`events.jsonl`, `summary.json`, `state.json`) -### What minimal mode keeps +### What it does **not** disable -- The cognitive loop (observe → interpret → plan → act → reflect → learn) -- Event timeline (`events.jsonl`) -- Summary artifacts (`summary.json`) -- Basic metrics (success, act_count) +Setting `governance_mode="enforce"` still runs `PreActGovernor` before act, including pause-on-veto: + +```python +class NoopGraph: + def invoke(self, payload): + return payload + +ns.set( + planner_mode="minimal", + governance_mode="enforce", + governance_pause_on_veto=True, +) +episode_id = ns.solve( + "Danger operation: delete production database", + using=NoopGraph(), + intuition=False, +) +``` ## Inspecting governance metrics @@ -229,13 +248,13 @@ You can switch modes between episodes: ```python import noesis as ns -# Run with full governance +# Run with MetaPlanner ns.set(planner_mode="meta") governed_ep = ns.run("Critical operation", intuition=True) -# Run quick test without governance +# Simple planner; governance still follows governance_mode ns.set(planner_mode="minimal") -test_ep = ns.run("Quick test") +test_ep = ns.run("Quick test", intuition=False) # Back to governed mode ns.set(planner_mode="meta") @@ -247,29 +266,28 @@ another_governed_ep = ns.run("Another critical operation", intuition=True) Toggle modes from the command line: ```bash -# Meta mode (default) +# Meta planner (default) noesis run "Sensitive task" -# Minimal mode via environment -NOESIS_PLANNER=minimal noesis run "Quick test" +# Minimal planner (does not by itself set governance_mode=off) +noesis run "Quick test" --planner minimal +# equivalent: NOESIS_PLANNER=minimal noesis run "Quick test" ``` ## When to use each mode ### Use meta mode for: -- Production workloads -- Compliance-sensitive operations -- Operations requiring audit trails -- Tasks with intuition policies -- Any operation that might need to be vetoed +- Production planning that needs Direction + intuition steering +- Tasks that should record directive diffs and richer insight KPIs ### Use minimal mode for: - Development and testing - Quick sanity checks - Benchmarking adapter performance -- Operations that don't need governance overhead + +To actually block side effects, set `governance_mode="enforce"` regardless of planner mode. To pause instead of seal on veto, also set `governance_pause_on_veto=True`. ## Troubleshooting @@ -278,20 +296,20 @@ NOESIS_PLANNER=minimal noesis run "Quick test" You're likely in `minimal` mode. Check with: ```python - config = ns.config() - print(config.planner_mode) # Should be "meta" + config = ns.get() + print(config["planner_mode"]) # "meta" or "minimal" + print(config["governance_mode"]) ``` Set to meta mode: `ns.set(planner_mode="meta")` - Ensure Governance is enforcing: - + Ensure Governance is enforcing. Planner mode is not enough: + ```python - ns.set(planner_mode="meta") ns.set(governance_mode="enforce") - ns.run("task", intuition=True) + ns.run("Danger operation: delete production database", intuition=False) ``` In `governance_mode="audit"` or `off`, veto-like risk signals can still appear in direction/intuition artifacts, but they do not block actuation. diff --git a/docs/guides/configure-shared-storage.mdx b/docs/guides/configure-shared-storage.mdx index 3e4a88c..aa7fc5b 100644 --- a/docs/guides/configure-shared-storage.mdx +++ b/docs/guides/configure-shared-storage.mdx @@ -3,65 +3,77 @@ title: "Configure shared episode storage" description: "Point Noēsis at a shared location via network volumes, S3 sync, Docker mounts, or Kubernetes PVCs." --- -By default, Noēsis writes episodes under `./.noesis/episodes//`. In a team or service setting, you usually want a **shared** location so everyone can see/replay the same traces. +By default, Noēsis writes **flat** episode bundles under `.noesis/episodes/ep_/`. In a team or service setting, you usually want a **shared** location so everyone can see/replay the same traces. -This guide shows four common setups: - -1. Network volume (NFS/SMB) -2. Local + S3 sync -3. Docker bind mount -4. Kubernetes PersistentVolumeClaim +This guide shows four common setups, the `runs_dir` resolver pitfall, and how to migrate a legacy `runs/` tree. Pick one of these patterns for your environment—you don’t need all four. +## Canonical layout + +`resolve_noesis_paths()` (`noesis/runtime/paths.py`) treats `runs_dir` as the episodes root only when: + +- the path is named `episodes` **and** its parent is named `.noesis`, or +- the path itself is named `.noesis` + +Any other `runs_dir` resolves to `{runs_dir}/.noesis/episodes` (and `{runs_dir}/.noesis/processes`, `{runs_dir}/.noesis/index`). + +``` +.noesis/ + episodes/ep_/ # episode bundles (flat, no label/ nesting) + processes/ # process registry JSON + index/ # EpisodeIndex (ttl_days=30); vacuum drops index rows only +``` + +Verified by `tests/runtime/test_episode_dir_layout.py` (no `_episodes/` directory under `episodes/`). + ## A) Shared dev: network volume +Mount a volume whose **parent is `.noesis`** if you want the mount path to *be* the episodes root: + ```bash -# Example: shared volume mounted at /srv/noesis/episodes -ls /srv/noesis/episodes -# (empty or existing runs) +# Host path whose parent directory is named .noesis +export NOESIS_RUNS_DIR=/srv/noesis/.noesis/episodes +ls /srv/noesis/.noesis/episodes ``` -Configure your app/runtime to use it as the base runs directory, e.g.: +```python +import noesis as ns -```bash -# pseudo-config -NOESIS_RUNS_DIR=/srv/noesis/episodes +ns.set(runs_dir="/srv/noesis/.noesis/episodes") ``` -Result: `/srv/noesis/episodes///` is shared across the team. + +`NOESIS_RUNS_DIR=/srv/noesis/episodes` does **not** keep that path as the episodes root. The parent is `noesis`, not `.noesis`, so new bundles land at `/srv/noesis/episodes/.noesis/episodes/ep_/`. + ## B) Shared dev: local + S3 (or any object store) -Don’t write directly to S3; sync a local directory. +Don’t write directly to S3; sync the canonical `.noesis` tree. ```bash -# App writes episodes locally -NOESIS_RUNS_DIR=/var/noesis/episodes +export NOESIS_RUNS_DIR=/var/noesis/.noesis/episodes -# Periodic sync job (cron/CI) -aws s3 sync /var/noesis/episodes s3://my-bucket/noesis-runs --delete - -# Optionally pull shared episodes down -aws s3 sync s3://my-bucket/noesis-runs /var/noesis/episodes +aws s3 sync /var/noesis/.noesis s3://my-bucket/noesis-layout +aws s3 sync s3://my-bucket/noesis-layout /var/noesis/.noesis ``` Pattern: local path for fast replay/debugging, S3 as central archive. -## C) Docker: bind-mount the runs directory +## C) Docker: bind-mount the `.noesis` root ```bash -mkdir -p /srv/noesis/episodes +mkdir -p /srv/noesis/.noesis/episodes docker run \ - -v /srv/noesis/episodes:/app/.noesis/episodes \ + -v /srv/noesis/.noesis:/app/.noesis \ -e NOESIS_RUNS_DIR=/app/.noesis/episodes \ my-noesis-service:latest ``` -Inside the container, Noēsis writes to `/app/.noesis/episodes/...`; on the host, you see `/srv/noesis/episodes/...`. +Inside the container, bundles are `/app/.noesis/episodes/ep_...`. On the host they appear under `/srv/noesis/.noesis/episodes/`. ## D) Kubernetes: PersistentVolumeClaim @@ -88,17 +100,45 @@ spec: image: my-noesis-service:latest env: - name: NOESIS_RUNS_DIR - value: /var/noesis/episodes + value: /var/noesis/.noesis/episodes volumeMounts: - name: noesis-runs - mountPath: /var/noesis/episodes + mountPath: /var/noesis/.noesis volumes: - name: noesis-runs persistentVolumeClaim: claimName: noesis-runs-pvc ``` -Each pod writes episodes under `/var/noesis/episodes`, backed by the shared PVC. +Each pod writes under `/var/noesis/.noesis/episodes`, backed by the shared PVC. + +## Migrate a legacy `runs/` tree + +Older layouts stored episode directories directly under `runs/` (or `runs_dir` itself) instead of `.noesis/episodes/`. `noesis migrate-layout` **copies** those bundles; it does not delete the source. + +```bash +noesis migrate-layout +noesis migrate-layout --json +``` + +Behavior (`noesis/infrastructure/layout_migration.py`): + +- Legacy episode roots considered (when they exist and are not the canonical episodes dir): `runs_dir`, `runs_dir/runs`, `{workspace}/runs`, `{root}/runs`, `{root.parent}/.noesis/runs` +- Copies only directories whose names start with `ep_` +- Copies process `*.json` files from legacy process dirs +- Skips a target that already exists +- Writes `.noesis/MIGRATED_FROM.json` (or `{layout.root}/MIGRATED_FROM.json`) + +### Pitfalls + +| Symptom | Cause | Fix | +| --- | --- | --- | +| Nested `label/ep_...` dirs were not copied | Migrator only copies top-level `ep_*` directories | Flatten or copy those bundles into `.noesis/episodes/` yourself | +| `episodes_copied` is 0 | Legacy root missing, or names do not start with `ep_` | Confirm the source path with `noesis migrate-layout --json` | +| Duplicate IDs already in the new layout | Target `ep_*` directory exists | Migration skips; inspect both copies before deleting legacy data | +| CLI still cannot find an old episode | `NOESIS_RUNS_DIR` points at a different root | Align `runs_dir` with the layout you migrated into | + +See the [CLI migrate-layout runbook](/reference/cli#migrate-a-legacy-layout). --- diff --git a/docs/guides/write-policies.mdx b/docs/guides/write-policies.mdx index b4e52e5..a8f9f41 100644 --- a/docs/guides/write-policies.mdx +++ b/docs/guides/write-policies.mdx @@ -242,13 +242,9 @@ pytest test_policy.py -v ## Wiring policies -### CLI +### Python (supported) -```bash -noesis run "my task" --intuition my_module:MyPolicy -``` - -### Python +The shipped `noesis run` command has no `--intuition` or `--policy` flag. Attach a policy instance on `ns.run` / `ns.solve`: ```python import noesis as ns @@ -257,6 +253,18 @@ from my_policy import MyPolicy episode_id = ns.run("my task", intuition=MyPolicy()) ``` +Then inspect with the CLI: + +```bash +noesis events "$EPISODE_ID" --phase intuition +noesis events "$EPISODE_ID" --phase direction +noesis explain "$EPISODE_ID" +``` + +### Config aliases + +`policy_aliases` in `noesis.toml` maps short names to `module:Class` import specs. That mapping is consumed by the legacy argparse `solve` helper (`noesis/cli/commands/solve.py`), not by `noesis run`. + ### Multiple policies Chain multiple policies by creating a composite: @@ -311,7 +319,8 @@ def advise(self, state: dict) -> IntuitionEvent | None: ### Policy not being called -- Verify `intuition=True` or `intuition=MyPolicy()` is passed to `ns.run()` +- Pass `intuition=MyPolicy()` (or `intuition=True` for the default policy) to `ns.run()` / `ns.solve()` +- Do not expect `noesis run --intuition ...` to work; that flag is not on the shipped CLI ### Veto not blocking diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index b48ed36..75e0a1c 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -108,16 +108,14 @@ Every episode creates a structured artifact directory (written under `.noesis/ep events.jsonl # append-only timeline manifest.json # SHA-256 + size ledger learn.jsonl # optional learning payloads - _episodes/ # optional episode index (best-effort) - episodes.jsonl ``` -The `_episodes/` index is written on a best-effort basis at the end of runs and may be missing or empty (for example, if indexing fails due to permissions). +The runtime index lives at `.noesis/index/` (`ttl_days=30`), not under `episodes/_episodes/`. `tests/runtime/test_episode_dir_layout.py` asserts `_episodes` is not created inside the episodes directory. Use `jq` for pretty-printing JSON artifacts: `cat .noesis/episodes/ep_01JH6Z2V9Q2K6Y6N0QZ7K2QW8C/summary.json | jq .` -To change the artifact location, set `NOESIS_RUNS_DIR` or in Python call `ns.set(runs_dir="./my-runs")`. +To change the artifact location, set `NOESIS_RUNS_DIR` or in Python call `ns.set(runs_dir="./my-runs")`. Unless that path is `.noesis` or `.noesis/episodes`, bundles are written to `{runs_dir}/.noesis/episodes`. ## What's next? diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 0305b08..b972b74 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -156,14 +156,40 @@ noesis explain ep_01JH6Z2V9Q2K6Y6N0QZ7K2QW8C # Interactive terminal UI noesis browse -# Migrate old storage layout +# Migrate old storage layout (copy, do not delete) noesis migrate-layout +noesis migrate-layout --json # Help noesis help noesis help events ``` +## Migrate a legacy layout + +`noesis migrate-layout` copies episode directories and process JSON from legacy roots into the canonical `.noesis/` tree. Source files are left in place. + +```bash +noesis migrate-layout --json +``` + +JSON fields (`MigrationResult.to_dict()`): + +| Field | Meaning | +| --- | --- | +| `episodes_copied` | Number of `ep_*` directories copied | +| `processes_copied` | Number of process `*.json` files copied | +| `legacy_roots` | Absolute paths that were scanned | +| `warnings` | Per-entry copy failures | + +Constraints: + +- Only **directories named `ep_*`** are treated as episodes. Nested `label/ep_...` trees are not flattened. +- Existing destination paths are skipped (no overwrite). +- A marker file `MIGRATED_FROM.json` is written at the layout root. + +After migration, confirm with `noesis ps` and `noesis processes`. Full layout notes: [Configure shared episode storage](/guides/configure-shared-storage). + ## Command constraints ### Verification flags on `run` @@ -223,6 +249,16 @@ Cause: `textual` is not installed in the current environment. Fix: install UI dependencies, then re-run `noesis browse`. +### `migrate-layout` copied 0 episodes + +Cause: no legacy root contained a top-level `ep_*` directory, or destinations already exist. + +Fix: + +- run `noesis migrate-layout --json` and inspect `legacy_roots` / `warnings` +- remember the copier does not recurse into `label/` folders +- check `NOESIS_RUNS_DIR` so you are migrating the tree you think you are + ### `invalid cli version (expected cli/MAJOR.MINOR)` on `run --json` Cause: unsupported `--cli-version` value. diff --git a/docs/reference/configuration.mdx b/docs/reference/configuration.mdx index fbe8631..f8ce50f 100644 --- a/docs/reference/configuration.mdx +++ b/docs/reference/configuration.mdx @@ -410,23 +410,40 @@ episode_id = ns.run( seed=42, # reproducibility seed tags={"env": "staging"}, # metadata tags intuition=True, # enable/disable intuition + process="docs-smoke", # process registry grouping ) ``` -Global configuration (via `ns.set()`) applies to all episodes. Per-call parameters like `seed`, `tags`, and `intuition` are set at run time. +Global configuration (via `ns.set()`) applies to all episodes. Per-call parameters like `seed`, `tags`, `process`, and `intuition` are set at run time, not via `ns.set()`. +## Keys that are **not** configuration + +`ns.set()` only accepts `ALLOWED_CONFIG_KEYS` (`noesis/domain/config/settings.py`). Unknown names raise `ValueError`. + +These are **not** config keys: + +| Attempt | What to use instead | +| --- | --- | +| `ns.set(label="production")` | Group runs with `ns.run(..., process="production")` / `noesis run ... --process` | +| `ns.set(seed=42)` | Pass `seed=` to `ns.run` / `ns.solve` | +| `ns.set(log_prompts=True)` | `prompt_provenance_enabled` + `prompt_provenance_mode` | + +Episode directories are flat `.noesis/episodes/ep_/`. There is no `label/` nesting. + ## CLI configuration Most CLI configuration is done via environment variables: ```bash -NOESIS_RUNS_DIR=./artifacts noesis run "task" +NOESIS_RUNS_DIR=./.noesis/episodes noesis run "task" NOESIS_PLANNER=minimal noesis run "task" NOESIS_DIRECTION_MIN_CONFIDENCE=0.8 noesis run "task" ``` +`NOESIS_RUNS_DIR=./artifacts` stores bundles at `./artifacts/.noesis/episodes` because the parent directory is not named `.noesis`. See [shared storage](/guides/configure-shared-storage). + ## Configuration validation Noēsis validates configuration on startup: diff --git a/docs/reference/llms-txt.mdx b/docs/reference/llms-txt.mdx new file mode 100644 index 0000000..605eacc --- /dev/null +++ b/docs/reference/llms-txt.mdx @@ -0,0 +1,36 @@ +--- +title: "llms.txt" +description: "LLM-friendly indexes for Noēsis docs." +--- + +Noēsis publishes a small index of public docs pages so coding agents can discover the current site without scraping every MDX file. + +## Files + +| File | Role | +| --- | --- | +| [`llms.txt`](https://github.com/saraeloop/noesis/blob/main/llms.txt) (repo root) | Curated list of **published** docs URLs | +| Mintlify `/llms.txt` / `/llms-full.txt` | Generated from `docs/docs.json` navigation when the docs site is built | + +Keep the root `llms.txt` aligned with pages that actually exist under `docs/` **and** are listed in `docs/docs.json`. Unpublished, deprecated, or removed pages (for example `tutorials/guarded-langgraph-agent`) should not appear there. + +## What to include + +- Get started, tutorials, how-to guides, explanation, and reference pages that are in the sidebar +- One-line descriptions that match the page frontmatter + +## What to omit + +- Deprecated stubs +- Internal notes under `docs/internal/` +- Schema JSON under `docs/schema/` (those are generated artifacts) + +## Preview locally + +From `docs/`: + +```bash +npx --yes mintlify@latest broken-links +``` + +Run that from `docs/`, not the repo root. Root execution reports false broken absolute links. diff --git a/docs/tutorials/first-policy.mdx b/docs/tutorials/first-policy.mdx index fde7819..2d95301 100644 --- a/docs/tutorials/first-policy.mdx +++ b/docs/tutorials/first-policy.mdx @@ -121,29 +121,32 @@ python test_policy.py ## Step 3: Wire the policy into an episode -Use your policy with the CLI: - -```bash -noesis run "SELECT * FROM users" --intuition my_policy:SafetyGuard -``` - -Or with Python: +Attach the policy in Python. The shipped `noesis run` entrypoint (`noesis/cli/__main__.py`) has no `--intuition` or `--policy` flag. ```python import noesis as ns from my_policy import SafetyGuard -# Run with the policy attached episode_id = ns.run( "SELECT * FROM users", intuition=SafetyGuard(), ) -# Check what happened summary = ns.summary.read(episode_id) print(f"Direction applied: {summary['flags'].get('direction', {}).get('applied', 0)}") ``` +Inspect the same episode from the CLI after it finishes: + +```bash +noesis view "$EPISODE_ID" +noesis events "$EPISODE_ID" --phase direction +``` + + +`policy_aliases` in `noesis.toml` is a config mapping used by the legacy `solve` argparse helper. It does not add a policy flag to `noesis run`. + + ## Step 4: Inspect policy decisions When a policy intervenes or vetoes, the events timeline shows the decision: diff --git a/docs/tutorials/incident-triage.mdx b/docs/tutorials/incident-triage.mdx index f9ad50d..9729a5c 100644 --- a/docs/tutorials/incident-triage.mdx +++ b/docs/tutorials/incident-triage.mdx @@ -1,109 +1,100 @@ --- -title: "Build an incident triage dashboard" -description: "Create a production-ready incident response system with Noēsis, complete with guardrails and human approval loops." +title: "Incident triage with pause-on-veto" +description: "Build an SRE-style incident loop with intuition policies, enforce-mode governance, and same-run resume after review." --- -This tutorial walks you through building an incident triage dashboard that showcases Noēsis in a production-minded SRE scenario. You'll implement the full cognitive loop with guardrails, human approvals, and learning proposals. +This tutorial wires a small incident-response loop onto the canonical Noēsis runtime: detect, plan, govern, then either seal or pause. Approval continues the **same** episode with `ns.resume_run(...)`. It does not start a follow-up `ns.run(...)`. ## What you'll build -A Gradio-based control room that: - -- Detects and classifies incidents -- Proposes response actions with governance -- Requires human approval for high-risk operations -- Captures learning signals for future improvement +- A mock detector and responder (swap for Prometheus, PagerDuty, LangGraph later) +- An intuition policy for org-specific incident rules +- Enforce-mode governance with pause-on-veto +- A review step that resumes from `run.checkpoint` on the same episode ID ## Prerequisites -- Completed the [first policy tutorial](/tutorials/first-policy) -- Understanding of the [cognitive loop](/explanation/cognitive-loop) -- Familiarity with Python async patterns (helpful but not required) - -## Step 1: Understand the architecture +- Completed [Your first policy](/tutorials/first-policy) +- Familiarity with the [cognitive loop](/explanation/cognitive-loop) and [human-in-the-loop](/guides/human-in-the-loop) continuation contract -The incident triage system follows the Noēsis cognitive loop: +## Architecture ```mermaid flowchart TD - A[Detect incident] --> B[Interpret signals] - B --> C[Plan response] - C --> D{Governance check} - D -->|approved| E[Execute action] - D -->|needs review| F[Human approval] - F --> E - E --> G[Reflect on outcome] - G --> H[Learn for next time] + A[Detect incident] --> B[Plan response] + B --> C[ns.run / ns.solve] + C --> D{Governance} + D -->|allow or audit| E[Act and seal] + D -->|enforce veto + pause-on-veto| F[run.interrupt then run.checkpoint] + F --> G[Human review] + G -->|approve| H["ns.resume_run same episode"] + G -->|reject| I[Leave the run paused and unsealed] ``` | Component | Noēsis concept | Production swap | | --- | --- | --- | -| Detector | Observe phase | Prometheus/Datadog queries | -| Classifier | Interpret phase | LLM + retrieval | -| Responder | Plan + Act phases | LangGraph plan generator | -| Reviewer | Governance | Slack/ServiceNow approval | -| Policy | Intuition | Your org's guardrails | +| Detector | Observe input | Prometheus / Alertmanager / PagerDuty | +| Planner | Your code before `ns.run` | LangGraph / runbook lookup | +| Intuition policy | `DirectedIntuition.advise` | Org change-control rules | +| Pre-act gate | `governance_mode="enforce"` | Built-in `PreActGovernor` | +| Review | `run.checkpoint` + `ns.resume_run` | Slack / ServiceNow on the same run ID | + + +`ns.run(...)` and `ns.solve(...)` seal the episode when they terminate. After `final.json` exists, `ns.interrupt` / `ns.checkpoint` / `ns.resume_run` raise `RunSealedError`. Do not start a second episode to represent approval. + + +## Step 1: Write the incident policy -## Step 2: Create the policy +Intuition can hint, intervene, or veto. **Pause-on-veto is a Governance runtime mode**, not an intuition patch. Returning `{"requires_approval": True}` does not pause the run. -Create a policy that enforces your organization's incident response rules: +`advise` receives a snapshot with `task` and `tags` (see `_build_snapshot` in `noesis/usecases/episode_runner.py`). It does not receive a free-form `proposed_action` key unless you put that value in `tags`. + +Create `prod_guard.py` (same shape as `examples/incident_triage/prod_guard.py`): ```python prod_guard.py +import re import noesis as ns from noesis.intuition import IntuitionEvent +_DESTRUCT = re.compile(r"\b(drop\s+db|delete\s+all|shutdown\s+cluster|wipe)\b", re.I) + class IncidentPolicy(ns.DirectedIntuition): - """Guards incident response actions.""" - + """Org rules that steer planning. Fail-closed gates still live in Governance.""" + __version__ = "1.0" - - # Risk thresholds - HIGH_RISK_ACTIONS = {"rollback", "restart", "scale_down", "delete"} - REQUIRE_APPROVAL = {"rollback", "delete"} - + def advise(self, state: dict) -> IntuitionEvent | None: - action = state.get("proposed_action", "").lower() - severity = state.get("severity", "low") - has_canary = state.get("canary_scope", False) - - # Hard veto: never allow deletes in production - if action == "delete" and state.get("environment") == "production": + task = str(state.get("task") or "").strip() + tags = state.get("tags") or {} + action = str(tags.get("proposed_action") or "").lower() + + if _DESTRUCT.search(task) or tags.get("risk") == "high": return self.veto( - advice="Blocked: delete operations forbidden in production.", + advice="Blocked: destructive or high-risk incident action.", target="plan", - rationale="Production deletes require manual execution with audit trail.", + rationale="Dangerous ops require a privileged change window.", ) - - # Require human approval for high-risk + high-severity - if action in self.HIGH_RISK_ACTIONS and severity in ("high", "critical"): - if not has_canary: - return self.intervene( - advice="Requires approval: high-risk action without canary scope.", - patch={"requires_approval": True, "scope": "canary_first"}, - target="plan", - rationale="High-severity incidents need canary validation before full rollout.", - ) - - # Warn about off-hours changes - if self._is_off_hours() and action in self.HIGH_RISK_ACTIONS: + + if action in {"rollback", "restart", "scale_down", "delete"} and self._is_off_hours(): return self.hint( - advice="Caution: executing high-risk action outside change window.", + advice="Caution: high-risk action outside the change window.", target="plan", rationale="Consider waiting for on-call handoff.", ) - + return None - + def _is_off_hours(self) -> bool: from datetime import datetime + hour = datetime.now().hour return hour < 9 or hour > 18 ``` -## Step 3: Build the detector +Direction records steering (`applied` / `blocked`). Governance is the layer that can stop actuation. See [Faculties](/explanation/faculties). -Create a mock detector that simulates incident detection: +## Step 2: Detect and plan (mocks) ```python detector.py from dataclasses import dataclass @@ -120,31 +111,17 @@ class Incident: def detect_incidents() -> list[Incident]: - """ - In production, replace with: - - Prometheus/Alertmanager queries - - Datadog/New Relic API calls - - PagerDuty webhook events - """ return [ Incident( id="INC-001", title="API latency spike in checkout service", severity="high", service="checkout-api", - signals={ - "p99_latency_ms": 2500, - "error_rate": 0.05, - "affected_users": 1200, - }, + signals={"p99_latency_ms": 2500, "error_rate": 0.05, "affected_users": 1200}, ) ] ``` -## Step 4: Build the responder - -Create a responder that proposes actions: - ```python responder.py from dataclasses import dataclass @@ -158,289 +135,199 @@ class ResponsePlan: rationale: str -def plan_response(incident, state: dict) -> ResponsePlan: - """ - In production, replace with: - - LangGraph plan generator - - Runbook lookup - - LLM-based reasoning - """ - # Simple heuristic-based planning +def plan_response(incident) -> ResponsePlan: if incident.signals.get("error_rate", 0) > 0.1: return ResponsePlan( action="rollback", target=incident.service, parameters={"to_version": "v1.2.3"}, confidence=0.85, - rationale="Error rate exceeds threshold; rollback to last stable version.", + rationale="Error rate exceeds threshold; roll back to last stable version.", ) - + if incident.signals.get("p99_latency_ms", 0) > 2000: return ResponsePlan( action="scale_up", target=incident.service, parameters={"replicas": 10}, confidence=0.75, - rationale="Latency spike suggests capacity issue; scaling up.", + rationale="Latency spike suggests capacity pressure.", ) - + return ResponsePlan( action="monitor", target=incident.service, parameters={"duration_minutes": 15}, confidence=0.9, - rationale="Signals within acceptable range; continuing to monitor.", + rationale="Signals within range; keep watching.", ) ``` -## Step 5: Wire it together with Noēsis +## Step 3: Run with enforce + pause-on-veto + +`planner_mode` chooses the planner. `governance_mode` is independent. Pause-on-veto is tested with `planner_mode="minimal"` and `governance_mode="enforce"` in `tests/runtime/test_run_lifecycle.py`. -Create the main orchestration: +The built-in `PreActGovernor` (`noesis/domain/faculties/governance.py`) vetoes when the goal or a plan step contains `danger`, or matches `veto|destroy|shutdown|wipe`. `write|delete|drop` is an **audit** (action still proceeds). `rollback` / `scale_up` / `monitor` are not veto keywords by themselves. ```python incident_triage.py +from pathlib import Path + import noesis as ns from detector import detect_incidents from responder import plan_response from prod_guard import IncidentPolicy -def run_triage(): - """Run incident triage with full cognitive loop.""" - - # Detect incidents - incidents = detect_incidents() - - for incident in incidents: - # Build initial state - state = { - "task": f"Respond to incident: {incident.title}", - "incident_id": incident.id, - "severity": incident.severity, - "service": incident.service, - "signals": incident.signals, - "environment": "production", - } - - # Plan the response - plan = plan_response(incident, state) - state["proposed_action"] = plan.action - state["confidence"] = plan.confidence - - # Run through Noēsis with policy +def checkpoint_id_from_events(events: list[dict]) -> str | None: + for event in reversed(events): + if event.get("phase") == "runtime" and event.get("event_type") == "run.checkpoint": + checkpoint_id = (event.get("payload") or {}).get("checkpoint_id") + if isinstance(checkpoint_id, str) and checkpoint_id: + return checkpoint_id + return None + + +def run_triage() -> None: + ns.set( + planner_mode="minimal", + governance_mode="enforce", + governance_failure_policy="fail_closed", + governance_pause_on_veto=True, + ) + + for incident in detect_incidents(): + plan = plan_response(incident) + task = ( + f"Respond to {incident.id}: {plan.action} on {incident.service}. " + f"{incident.title}" + ) + episode_id = ns.run( - state["task"], + task, intuition=IncidentPolicy(), + process="incident-triage", tags={ "incident_id": incident.id, "severity": incident.severity, + "proposed_action": plan.action, + "environment": "production", }, ) - - # Check outcomes - summary = ns.summary.read(episode_id) - events = list(ns.events.read(episode_id)) - - # Find governance decisions - governance = [e for e in events if e["phase"] == "governance"] - direction = [e for e in events if e["phase"] == "direction"] - - print(f"\nIncident: {incident.id}") - print(f" Action: {plan.action}") - print(f" Status: {summary['metrics'].get('success', 'unknown')}") - - if direction: - status = direction[-1]["payload"].get("status") - if status == "blocked": - print(f" Blocked: {direction[-1]['payload'].get('advice')}") - elif "requires_approval" in str(direction[-1].get("payload", {})): - print(" Awaiting human approval") + events = list(ns.events.read(episode_id)) + from noesis.io import paths -if __name__ == "__main__": - run_triage() -``` + run_dir = Path(paths(episode_id)["dir"]) + sealed = (run_dir / "final.json").exists() + checkpoint_id = checkpoint_id_from_events(events) -## Step 6: Add a Gradio UI (optional) + print(f"Incident {incident.id} episode={episode_id}") + print(f" proposed_action={plan.action} sealed={sealed}") -Create a visual control room: + if checkpoint_id and not sealed: + print(f" paused at checkpoint {checkpoint_id} (no act, no terminate)") + print(" review events with: noesis events", episode_id, "--phase governance") + continue -```python gradio_app.py -import gradio as gr -import noesis as ns -from incident_triage import run_triage - - -def triage_ui(incident_prompt: str, severity: str, intuition_mode: str): - """Run triage and return results for display.""" - - # Configure Noēsis - ns.set(planner_mode="meta" if intuition_mode == "Full governance" else "minimal") - - # Run the episode - episode_id = ns.run( - incident_prompt, - intuition=True, - tags={"severity": severity, "source": "gradio"}, - ) - - # Gather results - summary = ns.summary.read(episode_id) - events = list(ns.events.read(episode_id)) - - # Format timeline - timeline = "\n".join([ - f"[{e['phase']}] {e.get('payload', {}).get('status', 'ok')}" - for e in events - ]) - - # Format metrics - metrics = summary.get("metrics", {}) - metrics_text = f""" - Success: {metrics.get('success', 'N/A')} - Plans: {metrics.get('plan_count', 0)} - Actions: {metrics.get('act_count', 0)} - Vetoes: {metrics.get('veto_count', 0)} - """ - - return episode_id, timeline, metrics_text - - -# Build the interface -with gr.Blocks(title="Incident Triage Dashboard") as app: - gr.Markdown("# 🚨 Incident Triage Dashboard") - gr.Markdown("Powered by Noēsis cognitive loop") - - with gr.Row(): - with gr.Column(): - prompt = gr.Textbox( - label="Incident description", - placeholder="API latency spike in checkout service...", - lines=3, - ) - severity = gr.Dropdown( - choices=["low", "medium", "high", "critical"], - value="medium", - label="Severity", - ) - mode = gr.Radio( - choices=["Full governance", "Minimal (no guardrails)"], - value="Full governance", - label="Mode", - ) - run_btn = gr.Button("Run Triage", variant="primary") - - with gr.Column(): - episode_out = gr.Textbox(label="Episode ID") - timeline_out = gr.Textbox(label="Event Timeline", lines=10) - metrics_out = gr.Textbox(label="Metrics", lines=6) - - run_btn.click( - triage_ui, - inputs=[prompt, severity, mode], - outputs=[episode_out, timeline_out, metrics_out], - ) + if sealed: + summary = ns.summary.read(episode_id) + print(f" sealed success={summary.get('metrics', {}).get('success')}") if __name__ == "__main__": - app.launch() + run_triage() ``` -Run it: +Group related pages with `process=` / `noesis --process`. Episode bundles stay flat: `.noesis/episodes/ep_/`. + +## Step 4: Review a paused run + +Paused enforce vetoes emit `action_candidate → governance → run.interrupt → run.checkpoint`. There is no `act`, no `terminate`, and no `final.json` / `manifest.json` yet. `state.json` outcomes use `status="partial"`. + +Inspect: ```bash -pip install gradio -python gradio_app.py +noesis view ep_01JH6Z2V9Q2K6Y6N0QZ7K2QW8C +noesis events ep_01JH6Z2V9Q2K6Y6N0QZ7K2QW8C --phase governance +noesis explain ep_01JH6Z2V9Q2K6Y6N0QZ7K2QW8C ``` -## Step 7: Human-in-the-loop approval +Continue the **same** run after approval: -When the policy requires approval, you need a way to capture human decisions: +```python review.py +from pathlib import Path -```python approval.py import noesis as ns +from noesis.io import paths -def await_approval(episode_id: str) -> bool: - """ - In production, replace with: - - Slack interactive message - - ServiceNow approval workflow - - PagerDuty acknowledgment - """ - # For demo, simulate approval - print(f"Episode {episode_id} requires approval.") - response = input("Approve? (y/n): ") - return response.lower() == "y" - - -def run_with_approval(): - """Example of human-in-the-loop pattern.""" - - episode_id = ns.run( - "Rollback checkout-api to v1.2.3", - intuition=True, - tags={"requires_review": True}, - ) - - # Check if approval is needed +def resume_if_paused(episode_id: str, *, approved: bool) -> str | None: + run_dir = Path(paths(episode_id)["dir"]) + if (run_dir / "final.json").exists(): + print(f"{episode_id} is sealed; interrupt/resume will raise RunSealedError") + return None + events = list(ns.events.read(episode_id)) - needs_approval = any( - "requires_approval" in str(e.get("payload", {})) - for e in events - ) - - if needs_approval: - approved = await_approval(episode_id) - if approved: - # Re-run with approval flag - episode_id = ns.run( - "Rollback checkout-api to v1.2.3 [APPROVED]", - intuition=True, - tags={"approved": True}, - ) - - return episode_id -``` + checkpoint_id = None + for event in reversed(events): + if event.get("phase") == "runtime" and event.get("event_type") == "run.checkpoint": + value = (event.get("payload") or {}).get("checkpoint_id") + if isinstance(value, str) and value: + checkpoint_id = value + break + if checkpoint_id is None: + print(f"{episode_id} has no run.checkpoint event") + return None -## What you've built + if not approved: + print(f"rejected; leave {episode_id} paused and unsealed") + return None + + # Same episode ID. For ns.run / minimal checkpoints, omitting using= is allowed. + return ns.resume_run(episode_id, checkpoint_id=checkpoint_id) +``` - -You now have a complete incident triage system with: -- Incident detection and classification -- Policy-based guardrails -- Human approval workflows -- Full observability through Noēsis artifacts - + +`resume_run` continues the paused episode; it does not rewrite the original goal. If Governance still vetoes that goal (for example the task still contains `danger`), the run can pause or terminate again. Approval is not a bypass of `PreActGovernor`. + -## Artifacts produced +For a side-effect that must itself be gated, use [`ns.governed_act(...)`](/tutorials/governed-side-effects) (`kind="shell"` or `kind="adapter"`) with the same `governance_pause_on_veto` setting. Pause still raises `NoesisVeto` and leaves that governed episode unsealed. -Every triage run produces: +## Constraints and pitfalls -| File | Contents | +| Pitfall | What actually happens | | --- | --- | -| `events.jsonl` | Full timeline with phases, agent IDs, advice, status | -| `summary.json` | Success metrics, latencies, learn proposal counts | -| `state.json` | Current plan state and cognitive context | -| `learn.jsonl` | Learning signals for policy improvement | +| Start `ns.run("... [APPROVED]")` after the first episode | New episode ID; original sealed/paused run is not continued | +| Call `ns.interrupt` after `ns.run` returns a sealed episode | `RunSealedError` | +| Treat intuition `intervene(..., patch={"requires_approval": True})` as a pause | Run continues unless Governance vetoes | +| Expect `planner_mode="minimal"` to disable Governance | Governance follows `governance_mode`; pause-on-veto works in minimal | +| Set `NOESIS_RUNS_DIR=/srv/noesis/episodes` and look for bundles there | Unless the parent directory is `.noesis`, artifacts land under `{runs_dir}/.noesis/episodes` | +| `noesis run TASK --intuition my_policy:Class` | Shipped `noesis run` has no `--intuition` / `--policy` flag; attach policies in Python | -## Production considerations +## Artifacts - -The mocks in this tutorial are deterministic for demo purposes. For production: +| File | Allow / audit / sealed veto | Paused enforce veto | +| --- | --- | --- | +| `events.jsonl` | Includes `terminate` | `run.interrupt` then `run.checkpoint`; no `act` | +| `state.json` | Terminal outcomes | `outcomes.status="partial"` | +| `final.json` + `manifest.json` | Present (sealed) | Absent until continuation or a later terminal path | +| `checkpoints//checkpoint.json` | Not the veto-pause path | Present when pause-on-veto fires | -- Replace `detect_incidents()` with real monitoring integrations -- Replace `plan_response()` with LLM-based planning (LangGraph, etc.) -- Replace approval simulation with Slack/ServiceNow workflows -- Add authentication and audit logging - +## Production notes + +- Replace the detector/planner mocks with your monitoring and runbook stack. +- Keep approval on the paused episode ID (`resume_run`), not a linked follow-up episode. +- Do not hand-edit `events.jsonl` or checkpoint files before resume; checkpoint consistency checks are fail-closed. +- `examples/incident_triage/` ships Gradio/Streamlit viewers around `incident_graph`. Those UIs inspect artifacts after a run; they simulate in-graph approval and still call the legacy `ns.run_using` helper. Use this tutorial's pause-on-veto path when you need same-run continuation. +- Export veto and pause counts with the [metrics guide](/guides/export-metrics). ## Next steps - - Deep dive into approval patterns. + + Pause, checkpoint, and resume contracts. - - Send Noēsis metrics to your observability stack. + + Gate shell/adapter actions at the OS boundary. diff --git a/llms.txt b/llms.txt index df1f502..72c60fb 100644 --- a/llms.txt +++ b/llms.txt @@ -5,24 +5,23 @@ - [Quickstart](https://docs.noesis.systems/quickstart): Get your first Noēsis episode running in under 5 minutes. ## Tutorials +- [Hello Episode: See Your Agent Think in 5 Minutes](https://docs.noesis.systems/tutorials/hello-episode): Run a tiny agent through Noēsis, open the artifacts, and read the timeline that shows every phase. - [Your first episode](https://docs.noesis.systems/tutorials/first-episode): Learn how to run, inspect, and understand Noēsis episodes step by step. - [Your first policy](https://docs.noesis.systems/tutorials/first-policy): Learn how to create intuition policies that guide and guard your agent episodes. -- [Guarded LangGraph Agent: Vetoing Dangerous File Deletes](https://docs.noesis.systems/tutorials/guarded-langgraph-agent): Wrap a LangGraph agent with Noēsis so every tool call is observable and dangerous deletes get vetoed. -- [Hello Episode: See Your Agent Think in 5 Minutes](https://docs.noesis.systems/tutorials/hello-episode): Run a tiny agent through Noēsis, open the artifacts, and read the timeline that shows every phase. -- [Build an incident triage dashboard](https://docs.noesis.systems/tutorials/incident-triage): Create a production-ready incident response system with Noēsis, complete with guardrails and human approval loops. +- [LangGraph Episode: Trace the Cognitive Loop](https://docs.noesis.systems/tutorials/langgraph-episode): Run a LangGraph agent with Noesis and capture cognitive artifacts. +- [Governed Side Effects: Action Candidates + Veto](https://docs.noesis.systems/tutorials/governed-side-effects): Use ns.governed_act to enforce governance at the side-effect boundary. +- [Incident triage with pause-on-veto](https://docs.noesis.systems/tutorials/incident-triage): Build an SRE-style incident loop with enforce-mode governance and same-run resume after review. - [Trace-Based Evals: Safety & Success in 100 Lines](https://docs.noesis.systems/tutorials/trace-based-evals): Score your guarded agent over a dataset using Noēsis traces, not just final answers. ## How-to guides - [Add a memory port](https://docs.noesis.systems/guides/add-memory-port): Plug long-term memory into Noēsis using SQLite, your DB, or a custom store. - [Adopting Noēsis in real systems](https://docs.noesis.systems/guides/adopting-noesis): Move from local traces to team-wide observability and evals without surprises. -- [Configure planner modes](https://docs.noesis.systems/guides/configure-planner-modes): How to toggle between meta and minimal planner modes for different governance levels. +- [Configure planner modes](https://docs.noesis.systems/guides/configure-planner-modes): How to toggle between meta and minimal planner modes without mixing them up with governance_mode. - [Configure shared episode storage](https://docs.noesis.systems/guides/configure-shared-storage): Point Noēsis at a shared location via network volumes, S3 sync, Docker mounts, or Kubernetes PVCs. - [Export metrics](https://docs.noesis.systems/guides/export-metrics): How to send Noēsis insight metrics to your observability stack. - [Human-in-the-loop](https://docs.noesis.systems/guides/human-in-the-loop): How to add human approval workflows to your Noēsis episodes. - [Integrate adapters](https://docs.noesis.systems/guides/integrate-adapters): How to connect Noēsis to LangGraph, CrewAI, or your own agent runtime. - [Write intuition policies](https://docs.noesis.systems/guides/write-policies): How to create, test, and deploy policies that guide and guard your agent episodes. -- [Use Noēsis in Cursor](https://docs.noesis.systems/guides/use-noesis-in-cursor): Run episodes, add guardrails, and inspect traces without leaving your editor. -- [Use GitHub Copilot with Noēsis](https://docs.noesis.systems/guides/use-copilot): Pasteable Noēsis cheat sheet for Copilot to guide users through the API and CLI without hallucinations. ## Explanation - [Core concepts](https://docs.noesis.systems/explanation/core-concepts): Understand the fundamental building blocks of Noēsis: episodes, faculties, and artifacts.