PlanCover Docs
Operations

Guardrails

The rules that do not bend, and what each one is protecting against.

These are constraints on the system, not style preferences. Each exists because the failure it prevents is expensive.

Architecture is not changed silently

A boundary moved without discussion is a boundary nobody can rely on. Propose it first.

The model gets no direct access

No database connection, no raw HTTP client, no network. It receives tool results that a Python service has already authorised, validated and sanitised.

Every external call is bounded

A timeout and explicit error handling on every call. Only idempotent reads may retry. Tool steps, model iterations, retries and retrieval size all carry hard limits — there is no unrestricted agent loop. Every one of them is listed below, with the setting that configures it.

Nothing is claimed until it is confirmed

An action is reported as successful only after the upstream PlanCover API says so. "It looks like that went through" is not an acceptable answer about a claim.

Typed schemas at every boundary

Pydantic models in, Pydantic models out. Routes hold no business logic; graph definitions hold no data access.

State stays out of the process

Per-user and per-conversation state lives in Postgres. The process is disposable, which is what makes horizontal scaling and clean restarts possible.

The bounds

Every bound is on an edge, in a node, or in the executor — never in a prompt. A model cannot be asked nicely to stop.

BoundSettingDefaultEnforced by
Tool loop iterationsGRAPH_MAX_TOOL_STEPS4State counter, checked on the edge out of gather
Model calls per turnGRAPH_MAX_MODEL_CALLS6State counter, checked at the top of gather
Framework backstopGRAPH_RECURSION_LIMIT24Top-level recursion_limit; the framework default of 1000 is not a backstop
Per-node ceilingNODE_TIMEOUT_SECONDS45add_node(timeout=…) — except confirm, which waits on a human
Per-tool timeoutTOOL_TIMEOUT_SECONDS10The executor, overridable per tool
Concurrent tool callsTOOL_MAX_PARALLEL3A per-run semaphore; PlanCover allows 60 requests a minute
Tool result sizeTOOL_MAX_RESULT_CHARS4000The sanitizer, on the way to the model
History replayedMAX_HISTORY_TURNS / MAX_HISTORY_CHARS10 / 8000The context builder, at prompt-build time
Evidence usedMAX_EVIDENCE_CHUNKS / MAX_EVIDENCE_CHARS6 / 6000The RAG service and the context builder
Chunks retrievedRETRIEVAL_TOP_K8The RAG service, capped by MAX_EVIDENCE_CHUNKS
Answer lengthMAX_RESPONSE_TOKENS1024A model argument
Request size—8000 chars, 50 turnsPydantic, at the HTTP boundary
Messages per memberRATE_LIMIT_MESSAGES_PER_MINUTE20A fixed-window counter in Postgres
Confirmation windowCONFIRMATION_TTL_SECONDS600Checked before the pause, after it, and at the confirm endpoint
Heartbeat intervalSSE_HEARTBEAT_SECONDS15The SSE layer
Disconnect graceDRAIN_GRACE_SECONDS5Drain first, then a hard cancel

Running out of a budget is never a hang. A spent tool-step budget answers every outstanding tool call with a notice and routes to the answer; a spent model-call budget winds the loop down deliberately rather than waiting for the recursion backstop, which would be a hard error. The backstop sits above both, so it is only ever reached by a bug.

Security-sensitive code

Authentication, sessions, the tool executor, sanitizers and webhook handlers are flagged for human review before they merge. Automated review is a supplement, never the gate.

On this page