Pulse/docs/ASSISTANT_SAFETY.md
rcourtman 63fc89f701 Write the deep-dive docs the AI pages promised
AI.md, AI_AUTONOMY.md and PULSE_PRO.md linked nine times into
docs/architecture/, which .gitignore marks as not for public release. The
targets were never missing, they were deliberately unpublished, so every one
of those links was dead for anybody but the maintainer.

Write the three promised documents against the code and publish them under
docs/ where the rest of the shipped set lives.

PATROL_ARCHITECTURE.md covers a run end to end. The interesting part is that
deterministic signal detection runs after the model, not before, so unmatched
signals catch what the model failed to file. Signal types, the thresholds
those signals derive from your own alert settings, and every condition in
Finding.ShouldInvestigate are documented from internal/ai/patrol_signals.go
and internal/ai/findings.go.

ASSISTANT_SAFETY.md documents the session state machine in
internal/ai/chat/fsm.go, its four states, the tool classification it runs on,
and its invariants. No write without a validated target, no second write
before the first is verified, no final answer about an unverified change, and
no attempt count that wears the gate down.

ASSISTANT_ARCHITECTURE.md covers the agentic loop around that machine, the
three-phase pipeline, why only execution parallelises and at what cap, the
read-before-write batch that must stay ordered, the look-before-asking gate
and its bound, and the stable error codes.

Also removed two older pointers into the same private directory, in API.md
and UPGRADE_v6.md, and the two references to ENTITLEMENT_MATRIX.md in
PULSE_PRO.md. That file exists locally and is a monetization document, so
publishing it is not a documentation decision.

Unresolvable intra-doc links are now 2 of 218, both internal release-control
documents deliberately withheld from the shipped set.

Contract-Neutral: documentation only
2026-08-03 22:20:57 +01:00

100 lines
4.1 KiB
Markdown

# Pulse Assistant safety architecture
The state machine that governs what the Pulse Assistant is allowed to do
during a chat session, the tool classification it runs on, and the invariants
it holds.
The point of this machine is structural. Prompt wording can be argued with by
a model, and drifts as prompts are edited. These rules are enforced in code,
in `internal/ai/chat/fsm.go`, so neither a model nor a future prompt change
can talk its way past them.
## Tool kinds
Every tool call is classified before it runs.
| Kind | Meaning |
|---|---|
| `resolve` | Discovery and query tools that find resources |
| `read` | Read-only tools such as logs, metrics, status, and config |
| `write` | Mutating tools such as restart, stop, start, delete, and file write |
| `user_input` | Interactive tools that ask you something |
Classification is `ClassifyToolCall`, which delegates to the shared
`agentcapabilities` classifier so the Assistant and the rest of the agent
surface agree on what counts as a write.
## States
A session starts in `RESOLVING` and moves between four states.
| State | What it means |
|---|---|
| `RESOLVING` | No validated target yet, so resources must be discovered first |
| `READING` | A target is established and querying is allowed |
| `WRITING` | Transitional, entered around a mutation |
| `VERIFYING` | A write happened and evidence has not been gathered since |
Transitions on a successful tool call are as follows.
- A `resolve` or `read` in `RESOLVING` moves the session to `READING`.
- A `write` from any state moves the session to `VERIFYING`, records the tool
and timestamp, and clears the read-after-write flag.
- A `resolve` or `read` while in `VERIFYING` sets read-after-write, which is
what satisfies the verification requirement.
- A `user_input` call does not advance state at all, because asking you a
question is neither discovery nor verification.
`CompleteVerification` returns a verified session from `VERIFYING` to
`READING` so further writes become possible.
## The invariants
**No writing without a validated target.** A `write` attempted in `RESOLVING`
is blocked. The model must establish what it is acting on before it acts.
**No writing again until the last write is verified.** A `write` attempted in
`VERIFYING` is blocked until a read or resolve has run since the write.
**No final answer about an unverified change.** `CanFinalAnswer` refuses while
the session is in `VERIFYING` with no read-after-write. The Assistant cannot
tell you it restarted something and then decline to look at whether the
restart worked.
**Repeated attempts do not wear the gate down.** Consecutive blocked writes in
`VERIFYING` increment a counter, and that counter is telemetry only. There is
no attempt threshold after which the verification requirement is waived.
**Reads are never blocked.** No state blocks a `read`, `resolve`, or
`user_input`. The machine constrains mutation and the claims made about
mutation, not information gathering.
## Blocked calls and recovery
A blocked call returns an `FSMBlockedError` carrying the state, the tool, the
tool kind, a reason, and a recoverable flag. It surfaces to the model with the
stable code `ErrCodeFSMBlocked` rather than as prose, so the model can branch
on the code.
Blocks are recoverable rather than terminal. The session tracks a pending
recovery per blocked operation, and a later successful call of the same tool
clears it. Pending recoveries expire after ten minutes.
## Resetting
`Reset` returns the session to `RESOLVING` and clears all tracking, which is
what a full session clear does.
`ResetKeepProgress` is the softer variant used when context is cleared but
pinned items are kept. It drops verification tracking and moves a `VERIFYING`
session back to `READING`, without discarding that a target was established.
Note that `WroteThisEpisode` means "wrote at all during this session" rather
than "wrote during the current verification cycle", and `CompleteVerification`
deliberately leaves it set.
## Related reading
- [AI features](AI.md) for the overview and configuration.
- [Patrol deep dive](PATROL_ARCHITECTURE.md) for the scheduled analysis
runtime, which is a separate loop from the Assistant.