> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nelieo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Core Concepts: NSP State, Actions, and Runtime Probes

> The NSP data model explained: SubstrateState, SSF keys, confidence scores, action schemas, reversibility tiers, runtime probes, and app lifecycle.

NSP exposes every running application through a unified data model built from three primitives: structured state, typed actions, and a schema that ties them together. Understanding these primitives is the key to writing reliable agents — it tells you exactly what data you can read, what operations you can call, and what safety guarantees you can rely on.

## The Substrate Model

NSP treats every running application as a **substrate** — a live, addressable data source you can read from and write to in real time. The substrate model has three components.

<CardGroup cols={2}>
  <Card title="State" icon="database">
    A typed JSON snapshot of the application's current data, normalized to the Substrate Schema Format (SSF) with stable dot-notation keys.
  </Card>

  <Card title="Actions" icon="bolt">
    Typed function signatures discovered from the runtime that your agent can invoke directly — no UI interaction, no mouse clicks.
  </Card>

  <Card title="Schema" icon="diagram-project">
    A persistent, versioned registry of discovered state structures and action mappings, stored on disk and reused automatically across daemon restarts.
  </Card>
</CardGroup>

## SubstrateState (SSF)

Every application's state is represented as a `SubstrateState` object — the core data structure that flows through every part of NSP. You get one of these back from `GET /substrate/v1/state/{pid}`.

```json theme={null}
{
  "capture_id": "0196f4a2-3b5c-7d00-8e1f-9a0b2c3d4e5f",
  "app_id": "gmail-chrome-12847",
  "app_name": "Gmail",
  "pid": 12847,
  "runtime": "v8",
  "schema_version": "1.4.2",
  "captured_at": "2026-07-20T13:00:00Z",
  "confidence": 0.97,
  "context": {
    "url": "https://mail.google.com/mail/u/0/#inbox",
    "view": "gmail_inbox",
    "window_title": "Inbox - Gmail"
  },
  "objects": {
    "inbox.unread_count": 47,
    "inbox.total_count": 1284,
    "inbox.emails[0].id": "msg_18821af3",
    "inbox.emails[0].from": "alice@example.com",
    "inbox.emails[0].subject": "Q3 Performance Report",
    "inbox.emails[0].is_read": false,
    "inbox.emails[0].has_attachments": true
  },
  "actions": { },
  "probe_metadata": {
    "capture_latency_ms": 28,
    "total_latency_ms": 51,
    "cache_hit": false
  }
}
```

### Field Reference

| Field            | Type          | Description                                                                  |
| ---------------- | ------------- | ---------------------------------------------------------------------------- |
| `capture_id`     | UUIDv7        | Time-ordered unique ID for this specific capture                             |
| `app_id`         | string        | Stable identifier tied to the app — survives process restarts                |
| `app_name`       | string        | Human-readable name, e.g. `"Gmail"`                                          |
| `pid`            | integer       | OS process ID at the time of capture                                         |
| `runtime`        | enum          | `v8` / `jvm` / `clr` / `native` / `unknown`                                  |
| `schema_version` | semver string | Schema version in use; a version bump means key mappings changed             |
| `confidence`     | float         | Probe quality score from 0.0 to 1.0                                          |
| `context`        | object        | Current navigational context — URL, view name, window title                  |
| `objects`        | object        | Flat dot-notation key/value map of all readable app state                    |
| `actions`        | object        | Available action schemas keyed by action name                                |
| `probe_metadata` | object        | Diagnostic fields: capture latency (ms), total latency, and cache hit status |

## Confidence Score

Every `SubstrateState` carries a `confidence` field between `0.0` and `1.0` that represents how certain the probe is in the semantic accuracy of its labels. The daemon uses this score as a safety gate before allowing action execution.

| Range         | Meaning                                              | Action Gating                               |
| ------------- | ---------------------------------------------------- | ------------------------------------------- |
| `0.0 – 0.39`  | Unknown — probe is initializing or no schema matched | No actions allowed                          |
| `0.40 – 0.79` | Partial — some objects labeled, schema incomplete    | Read-only actions only                      |
| `0.80 – 0.94` | Good — most objects labeled with high accuracy       | `reversible_write` actions allowed          |
| `0.95 – 1.00` | Excellent — full semantic coverage confirmed         | All actions, including `irreversible_write` |

**Runtime confidence baselines** tell you what to expect once a probe is warm:

| Runtime         | Typical Confidence Range |
| --------------- | ------------------------ |
| V8 / JavaScript | `0.95 – 0.99`            |
| JVM / Java      | `0.93 – 0.97`            |
| CLR / .NET      | `0.93 – 0.97`            |
| Native C++      | `0.40 – 0.75`            |

<Tip>
  If you see confidence below the expected baseline for a V8 or JVM app, the probe is likely still in its cold-start phase. Wait a few seconds and re-poll — confidence climbs as the schema cache warms up.
</Tip>

## Action Schema

Every action available on a substrate is described by an `ActionSchema` that tells your agent exactly what parameters to pass, what to expect back, and how risky the operation is.

```json theme={null}
{
  "send_reply": {
    "signature": "fn(thread_id: string, body: string, cc?: string[]) -> bool",
    "reversibility": "irreversible_write",
    "description": "Send a reply to an email thread",
    "confidence": 0.99,
    "parameters": [
      {
        "name": "thread_id",
        "type_name": "string",
        "required": true,
        "description": "Thread ID from inbox.emails[N].thread_id"
      },
      {
        "name": "body",
        "type_name": "string",
        "required": true,
        "description": "Reply body text"
      },
      {
        "name": "cc",
        "type_name": "array",
        "required": false,
        "description": "Optional CC email addresses"
      }
    ]
  }
}
```

The `confidence` on an action schema reflects how reliably NSP can invoke it — separate from the state-level confidence. Both must meet their respective thresholds before execution proceeds.

## Reversibility

Every action is classified into one of three reversibility tiers. This is a **safety contract enforced at the daemon and SDK level**, not a suggestion or annotation.

<AccordionGroup>
  <Accordion title="read — No state change">
    The action reads data without modifying any application state. There is no confidence gate for read actions — they are safe to call at any confidence level.

    **Examples:** `get_thread`, `search`, `list_contacts`, `read_document`, `get_calendar_events`

    ```python theme={null}
    # No restrictions — callable at any confidence
    results = await session.execute("search", parameters={"query": "Q3 report"})
    ```
  </Accordion>

  <Accordion title="reversible_write — Undoable change">
    The action modifies application state in a way that can be undone. The daemon requires `confidence >= 0.80` before dispatching.

    **Examples:** `archive`, `label`, `move_to_folder`, `mark_read`, `star`, `create_draft`

    ```python theme={null}
    # Allowed when confidence >= 0.80
    result = await session.execute(
        "archive",
        parameters={"email_id": "msg_18821af3"},
    )
    ```
  </Accordion>

  <Accordion title="irreversible_write — Permanent change">
    The action causes a permanent side effect that cannot be undone. The daemon requires `confidence >= 0.95` **and** the SDK requires `verify=True` with a `verify_expression`. Calling `execute` without these raises `NSPIrreversibleNotConfirmedError`.

    **Examples:** `send_reply`, `delete_permanently`, `purchase`, `submit_form`, `send_message`

    ```python theme={null}
    # This raises NSPIrreversibleNotConfirmedError — verify=False is not allowed
    await session.execute("send_reply", parameters={...}, verify=False)

    # Correct — provide verify=True and a verify_expression
    result = await session.execute(
        "send_reply",
        parameters={
            "thread_id": "thread_abc123",
            "body": "Thanks, I'll review this shortly.",
        },
        verify=True,
        verify_expression="inbox.emails[0].is_read == true",
    )
    ```

    After executing the action, NSP waits 200 ms, then polls the app state up to 10 times — evaluating your `verify_expression` each cycle — before returning. The response includes a `verification` object with the outcome, which changed fields were observed, and the total latency.
  </Accordion>
</AccordionGroup>

## Runtime Probes

Each runtime family uses a fundamentally different technical method to reach the application's internal state. Understanding which probe applies to your target app helps you predict accuracy, cold-start time, and confidence baselines.

### V8 / JavaScript

The V8 probe connects to Chrome or any Electron app via the **Chrome DevTools Protocol (CDP)** remote debugging port (enabled with `--remote-debugging-port=9222`). It uses a fast-path state extraction method that returns in 5–50 ms during normal operation. On first encounter with a new app, NSP falls back to a full heap snapshot to build the schema — this takes up to \~47 seconds but runs only once. All subsequent reads use the fast path against the cached schema.

**Covered apps:** Gmail, Slack, VS Code, Notion, GitHub Desktop, Salesforce, WhatsApp Desktop, Discord, Shopify, HubSpot, and any Electron-based application.

### JVM / Java

The JVM probe attaches to a running Java process without requiring a restart, reads live object fields using the JVM's native instrumentation interface, and converts Java objects to SSF JSON using class type metadata. No code changes to the target application are needed.

**Covered apps:** SAP ERP, Eclipse, IntelliJ IDEA, Jenkins, Elasticsearch, ArduPilot GCS (Java build).

### CLR / .NET

The CLR probe injects a managed bootstrapper into the target .NET process and reads field values and method return values from the CLR heap, communicating results back to the daemon over an inter-process channel. No changes to the target application are required.

**Covered apps:** Mission Planner, WinForms apps, WPF apps, Visual Studio, Microsoft Office (Excel, Word, PowerPoint), and any .NET 4.x or .NET 6+ desktop app.

### Native / C++

The native probe reads raw process memory from compiled binaries that carry no semantic metadata. NSP layers a **Visual-Memory Grounding** pass on top — correlating memory regions with a local vision model to produce human-readable semantic labels.

**Covered apps:** Bloomberg Terminal, Photoshop, AutoCAD, Chrome internals (C++ layer). Expect confidence in the `0.40–0.75` range rather than the high-nineties seen for managed runtimes.

## App Lifecycle

An app moves through a well-defined state machine from the moment NSP detects its process to the moment it exits. Understanding this lifecycle helps you reason about cold-start delays, poll frequency, and WebSocket event timing.

```text theme={null}
App Starts
   │
   ▼
Daemon detects new process (PID, exe name, command line)
   │
   ▼
Runtime classified: V8 / JVM / CLR / Native
   │
   ▼
Known app matched → app_id and app_name assigned
   │
   ▼
Probe attaches (cold start: 3–90 s on first encounter)
   │
   ▼
First SubstrateState cached (confidence 0.80+)
   │
   ▼
Fast poll loop (default 2 s interval, ~50 ms per read)
   │
   ▼
State change notifications broadcast to WebSocket subscribers
   │
   ▼
App exits → probe detached → state removed from cache
```

<Note>
  The cold probe delay (3–90 seconds) only occurs the first time NSP encounters an app. On every subsequent start, the daemon reuses the cached schema from disk and reaches an active probe state in under a second.
</Note>

## State Object Keys

NSP stores all state values in a **flat dot-notation map** rather than nested JSON. This design means your agent can reference any specific value with a single deterministic string — no recursive traversal, no path ambiguity.

```python theme={null}
# Access a nested value with a single key
subject = session.get_str("inbox.emails[0].subject")

# Query all keys under a prefix
inbox_keys = session.state.keys_with_prefix("inbox.")
```

Keys are **stable across poll cycles** for a given app and schema version. If `inbox.emails[0].subject` resolves to the email subject today, it resolves to the email subject on every subsequent read until the schema version changes. When `schema_version` increments (a semver bump in the `SubstrateState`), re-validate any hardcoded key references in your agent.
