Files
pulse/docs/ASSISTANT_ARCHITECTURE.md
T
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

4.4 KiB

Pulse Assistant deep dive

How the Assistant's agentic loop executes tool calls, for readers who want more than the overview in AI features.

The safety state machine has its own page. See Pulse Assistant safety architecture for the states, transitions, and invariants. This page covers the loop that runs around it.

The three-phase pipeline

Each provider turn can return several tool calls at once. The loop processes them in three phases, and the split matters because only one of the three is safe to parallelise.

Phase 1, pre-check, runs sequentially. This is where the state machine gate, loop detection, and budget checks happen. Every call is judged before any call runs.

Phase 2, execute, runs in parallel. Independent calls run concurrently through goroutines, with concurrency capped at four.

Phase 3, post-process, runs sequentially. Streaming output, state machine transitions, and knowledge extraction happen in a deterministic order, so concurrent execution cannot produce non-deterministic session state.

What is not allowed to run in parallel

Parallelism is bounded by real ordering requirements rather than applied uniformly.

A same-turn read-before-write dependency forces sequential execution. If one turn contains both a patrol_get_findings read and a finding lifecycle write, the batch runs in order, because the write's deduplication and assessment precondition is established by that read. Letting them race inside one turn would mean writing against a precondition that had not been checked. Independent reads and independent finding writes still run in parallel. The provider's original call order stays authoritative.

Interactive input is also excluded. pulse_question never runs in parallel with other tools, since asking you something is not an independent operation.

The look-before-asking gate

The Assistant is discouraged from asking you a question before it has tried to find the answer. If the model attempts to ask without having attempted any tool call, the attempt is blocked and it is pushed to look first.

The gate is bounded rather than absolute. It allows at most two blocks per turn, after which the question goes through. A model that genuinely cannot proceed without input is not trapped in a loop, and any real tool attempt satisfies the gate immediately.

Structured errors

Failures reach the model as stable machine-readable codes rather than as prose, so it can branch on the failure instead of parsing an English sentence. The codes are declared in internal/agentcapabilities/errors.go and include resource_not_found, operator_state_not_set, operator_state_invalid, invalid_finding_request, finding_not_found, finding_action_not_allowed, patrol_unavailable, invalid_action_request, capability_not_found, action_execution_unavailable, action_actor_unavailable, and missing_id.

A call blocked by the state machine uses the same mechanism, returning the fsm_blocked code with the state and tool that were involved.

The same codes are published in the capability manifest at /api/agent/capabilities, and a contract test fails the build if a handler can emit a code the manifest does not declare, or the manifest declares a code no handler emits. See agent integrations.

Grounded execution

Several guardrails exist to keep the model's claims tied to evidence it actually gathered.

The state machine supplies the structural half. A write moves the session into verification, and the Assistant cannot deliver a final answer about that write until it has read something afterwards.

The prompts supply the rest. Instructions repeated across the agentic prompts tell the model to treat infrastructure names, labels, logs, and other collected values as untrusted data rather than as instructions, and not to invent evidence, root cause, verification, remediation, or a claim that an action was taken.

Prompt instructions are the weaker of the two, which is exactly why the verification requirement lives in code instead. Where a guarantee needs to hold, it is enforced structurally.