Architecture¶
Perspt is a Rust workspace of fourteen crates plus a dev-only xtask
automation crate. Eight crates make up the running program, four crates form
the reusable platform layer (SDK, prompt codegen, and two domain packages),
one crate is the optional feature-gated benchmark harness, and one root crate
ties them together. Version 0.6.6 implements the PSP-9 governed candidate
runtime and the PSP-10 typed prompt, search, and integration layers.
Workspace Layout¶
perspt/ # Root: integration crate (perspt)
+-- crates/
| +-- perspt-core/ # Types, config, LLM provider, events, plugins, prompts
| +-- perspt-agent/ # Governed candidate runtime, tool loop, verifier, search
| +-- perspt-tui/ # Ratatui TUI (chat + agent + review modal)
| +-- perspt-cli/ # Clap CLI entry point, subcommands
| +-- perspt-store/ # DuckDB session store
| +-- perspt-policy/ # Starlark policy engine
| +-- perspt-sandbox/ # Command sandboxing
| +-- perspt-dashboard/ # Axum web dashboard
| +-- perspt-sdk/ # Domain-neutral SRBN platform SDK
| +-- perspt-prompt-macros/ # Build-time prompt section codegen
| +-- perspt-coding/ # Coding domain package (first domain)
| +-- perspt-research/ # Research domain package (second domain)
| +-- perspt-benchmark/ # Optional credentialed evaluation harness
+-- xtask/ # PSP code-rule checker (dev-only)
+-- tests/ # Integration tests
+-- docs/ # Sphinx documentation
Dependency Graph¶
![digraph crates {
rankdir=BT;
node [shape=box, style=rounded];
"perspt-cli" -> "perspt-core";
"perspt-cli" -> "perspt-sdk";
"perspt-cli" -> "perspt-tui";
"perspt-cli" -> "perspt-agent";
"perspt-cli" -> "perspt-coding";
"perspt-cli" -> "perspt-prompt-macros";
"perspt-cli" -> "perspt-research";
"perspt-cli" -> "perspt-store";
"perspt-cli" -> "perspt-dashboard";
"perspt-cli" -> "perspt-benchmark" [style=dotted, label="feature: benchmark"];
"perspt-core" -> "perspt-sdk";
"perspt-core" -> "perspt-prompt-macros";
"perspt-agent" -> "perspt-core";
"perspt-agent" -> "perspt-sdk";
"perspt-agent" -> "perspt-coding";
"perspt-agent" -> "perspt-research";
"perspt-agent" -> "perspt-policy";
"perspt-agent" -> "perspt-sandbox";
"perspt-agent" -> "perspt-store";
"perspt-tui" -> "perspt-core";
"perspt-tui" -> "perspt-agent";
"perspt-tui" -> "perspt-store";
"perspt-store" -> "perspt-core";
"perspt-policy" -> "perspt-core";
"perspt-sandbox" [label="perspt-sandbox"];
"perspt-coding" -> "perspt-sdk";
"perspt-coding" -> "perspt-prompt-macros";
"perspt-research" -> "perspt-sdk";
"perspt-prompt-macros" -> "perspt-sdk";
"perspt-dashboard" -> "perspt-store";
"perspt-dashboard" -> "perspt-sdk";
"perspt-benchmark" -> "perspt-agent";
"perspt-benchmark" -> "perspt-core";
"perspt-benchmark" -> "perspt-sdk";
"perspt-benchmark" -> "perspt-store";
}](../_images/graphviz-e3b709628773ff06ce9846e01c664b10a2799ea2.png)
PSP-9 / PSP-10 Overview¶
PSP-9 replaced the multi-agent orchestrator with a single governed candidate
runtime: every model-issued tool call becomes a typed proposal, a
deterministic admissibility kernel (in perspt-sdk) decides whether it may
affect the reversible candidate workspace, and every gate is evaluated on the
re-measured candidate, never on the model’s account of it. Every event lands
in a hash-chained durable ledger (perspt-store), so sessions replay and
resume deterministically. PSP-10 adds typed prompt section libraries with
build-time codegen (perspt-prompt-macros), the bounded search forest with
exact no-good learning, graph staging behind a global integration gate, and
the optional benchmark harness (perspt-benchmark).
Crate: perspt-core¶
The foundation crate. Re-exports all canonical types.
Modules:
types- Core shared types, split into submodulestypes/{context,model,plan,policy,verification,workspace}.rs(see Core Type Inventory below)config-Config { provider, model, api_key, ... }plus per-tier model overridesevents-AgentEvent(33 variants),AgentAction,NodeStatus,ActionTypellm_provider-GenAIProviderwrapping thegenaicrate;EOT_SIGNALportfolio-ModelPortfoliowith provider handles and declared capsplugin-LanguagePlugintrait +PythonPlugin,RustPlugin,JsPluginprompts- Typed prompt section libraries (prompts/*/) compiled at build time byperspt-prompt-macrosmemory-ProjectMemoryloaded from.perspt/memory.tomlnormalize- Model and provider name normalization
Key Plugin Types:
pub trait LanguagePlugin: Send + Sync {
fn name(&self) -> &str;
fn detect(&self, path: &Path) -> bool;
fn get_init_action(&self, opts: &InitOptions) -> ProjectAction;
fn test_command(&self) -> String;
fn syntax_check_command(&self) -> Option<String>;
fn verifier_profile(&self) -> VerifierProfile;
fn owns_file(&self, path: &str) -> bool;
// ... ~25 methods total
}
Plugins provide verifier profiles with fallback chains:
pub struct VerifierProfile {
pub plugin_name: String,
pub capabilities: Vec<VerifierCapability>,
pub lsp: LspCapability,
}
pub struct VerifierCapability {
pub stage: VerifierStage, // SyntaxCheck | Build | Test | Lint | Format
pub command: Option<String>, // Primary command
pub available: bool,
pub fallback_command: Option<String>,
pub fallback_available: bool,
}
Crate: perspt-agent¶
The governed PSP-9/PSP-10 agent runtime.
Modules:
runtime-Psp9AgentRuntime: work-graph planning, bounded dispatch, node assembly, adjudication, staging and integration, resume, and the bounded search forest (runtime/search/)toolloop- The SRBN tool loop: each model-issued tool call becomes a typed proposal; the deterministic admissibility kernel decides whether it may affect the candidatecandidate-CandidateWorkspace: reversible coding-candidate overlaymeasure-CodingCandidateMeasurer: full verifier suite at gate boundaries, cheap syntax-only pass at mutation boundariesverifier- Governed verifier sandbox: compiler/test/lint processes run under a deny-network profile with a read allow-listtools-AgentToolsexecutor and sandboxing, theCandidateHandlerRegistryexecution plane (tools/handlers/), and first-party tool families (tools/families/{db,system}.rs)transport-GenAiTransport: the only adapter between the SDK’s provider-neutral contract andperspt-core’sgenaidriverturn- Universal actor turn runner (worker, explorer, architect, adjudicator, evidence summarizer, capability probe)grant- Persistent grant signing-key resolutionprobe-probe_route/ProbeReport: behavioral provider probespromote- Descriptor-relative workspace promotion on Unix; native write-through replacement plus best-effort reparse-point rejection on Windowsrealize-SnapshotRealizer: content-addressed workspace statesexploration- Deterministic, read-only repository orientationexternal_tools- Official-SDK MCP 2026-07-28 client (stdio/stateless HTTP; tools, resources, prompts, roots, sampling, elicitation, subscriptions, MRTR/tasks), with local admission, replay, and separate agent/read-only-chat lifecycleslsp-LspClient(JSON-RPC over stdio)
Runtime Flow:
Psp9AgentRuntime::run() drives one governed session:
Exploration - deterministic read-only repository map;
--exploration-onlyadds an interactive explorer tool loop and stops before any mutationPlanning - one forced-tool-choice architect turn, restricted to the privileged
update_graphtool, produces the work graph; the host never fabricates multi-node graphsDispatch - bounded multi-node scheduling (
runtime/dispatch.rs);--max-parallel-nodessets the concurrency (default 1; above 1 requires--yes)Tool loop - per node, the governed loop (
toolloop/) turns each model tool call into a typed proposal, admits it through the deterministic kernel, and realizes it against the reversible candidate overlay (candidate.rs)Measurement -
measure.rsre-measures the candidate through the plugin verifier suite inside the sandboxed verifier (verifier.rs), or through explicitly acknowledged host-user execution in native Windows reduced- isolation mode; the gate is evaluated on the re-measured candidate, never on the model’s account of it. The default evolving-test policy measures the resulting tests; backward-compatible and protected external evidence are explicit additions, never implicit assumptions about every task’s contractAdjudication - a tool-free validator reviews only the realized diff and records an uncalibrated verdict (
runtime/adjudicate.rs)Staging and integration - node winners stage into a graph workspace and must pass the global integration gate before descriptor-relative promotion (
runtime/integrate.rs,promote.rs). Unix promotion holds ancestor directory descriptors; Windows uses write-through native replace operations and rejects observed reparse points, without claiming the same race-resistant boundary.Recording - every event lands in the durable hash-chained ledger (
runtime/recorder.rs); interrupted sessions resume from the newest durable checkpoint with exactly the remaining budgets (runtime/resume.rs)
Failed attempts feed the bounded search forest (runtime/search/):
sequential, at most three branch identities, one branch attempt (quantum) at
a time, with exact no-good learning that suppresses only byte-identical
repeated attempts.
Crate: perspt-store¶
DuckDB-backed persistence. Not SQLite.
pub struct SessionStore {
conn: Mutex<Connection>, // duckdb::Connection
}
DuckDB Schema and Tables¶
The schema (crates/perspt-store/src/schema.rs) is applied through an
idempotent transactional migration; schema_migrations records the
applied version. Eleven tables:
Table Name |
Key Columns |
Purpose |
|---|---|---|
|
|
Life cycle of agent sessions |
|
|
Applied schema versions |
|
|
The durable canonical event stream; hash-chained |
|
|
Content-addressed artifact bytes |
|
|
Single-writer authority fencing per session |
|
|
Durable conversation checkpoints for resume |
|
|
External (MCP) effect intents and results, idempotent |
|
|
Capability grant policies |
|
|
Adjudication verdicts per candidate |
|
|
Conformal calibration epochs |
|
|
Calibration samples; |
Row types live in store/rows.rs (SessionRecord) and
store/psp9_ledger.rs (Psp9LedgerRow, Psp9VerdictRow,
Psp9CalibrationEpochRow, Psp9ExternalEffectRow). repair.rs backs
perspt db repair for recovering a database with a poisoned WAL.
Crate: perspt-tui¶
Ratatui-based terminal UI with two modes:
ChatApp - Interactive chat with streaming, LaTeX math transpilation, ASCII table wrapping, and markdown saving
AgentApp - Agent dashboard with work-graph tree, energy display, review modal
The agent TUI is entered through run_agent_tui_with_runtime
(agent_app.rs), which owns the Psp9AgentRuntime for the session and
wires its event and action channels.
Key components:
Component |
Purpose |
|---|---|
|
Main agent dashboard layout |
|
Work-graph visualization with node states |
|
Grouped diff viewer with approve/reject/correct |
|
Unified diff display |
|
TUI-side energy component display types |
|
60fps cap, adaptive rendering |
Crate: perspt-policy¶
Starlark policy evaluation:
pub struct PolicyEngine {
policies: Vec<FrozenModule>,
policy_dir: PathBuf,
}
pub enum PolicyDecision {
Allow,
Prompt(String),
Deny(String),
}
Utility functions:
sanitize_command(cmd)->SanitizeResult(split, validate, filter)validate_workspace_bound(cmd, working_dir)- Ensure commands stay in scopevalidate_artifact_mutation(path, workspace_root, operation)- Protect root project files from delete/move
Crate: perspt-sandbox¶
Process isolation with active timeout enforcement:
pub trait SandboxedCommand: Send + Sync {
fn execute(&self) -> Result<CommandResult>;
fn display(&self) -> String;
fn is_read_only(&self) -> bool;
}
pub struct BasicSandbox {
program: String,
args: Vec<String>,
working_dir: Option<PathBuf>,
timeout: Duration, // Active: spawn + poll + kill on deadline
}
Core Type Inventory¶
All canonical shared types live in perspt_core::types, re-exported from
types/mod.rs:
Workspace (``types/workspace.rs``):
Type |
Description |
|---|---|
|
Interface, Implementation (default), Integration |
|
Default (compile + tests), Strict (adds lint), Minimal (syntax only) |
Model (``types/model.rs``):
Type |
Description |
|---|---|
|
Architect, Actuator, Verifier, Speculator; |
Energy (``types/context.rs``):
Type |
Fields |
|---|---|
|
v_syn (LSP), v_str (contracts), v_log (tests), v_boot (commands),
v_sheaf (cross-node); |
Task Planning (``types/plan.rs``):
Type |
Description |
|---|---|
|
Container for |
|
id, goal, context_files, output_files, dependencies, task_type, contract, command_contract, node_class, dependency_expectations |
|
Code, Command, UnitTest, IntegrationTest, Refactor, Documentation |
|
interface_signature, invariants, forbidden_patterns, tests |
|
name, criticality (informational string label) |
|
required_packages, setup_commands, min_toolchain_version |
|
command, expected_exit_code, expected_files, forbidden_stderr_patterns, working_dir |
Verification and Context (``types/verification.rs``):
Type |
Description |
|---|---|
|
Available | Fallback { actual, reason } | Unavailable { reason } |
|
stage, passed, sensor_status, output |
|
Signature, Schema, SymbolInventory, InterfaceSeal |
|
Content hash of a compile-critical structural artifact (signatures, schemas, seals) |
|
Condensed summary with hash; |
|
byte_limit (100KB default), file_count_limit (20) |
|
Per-node context boundary: owned_files, sealed_interfaces, structural/summary digests, dependency commits |
|
The bounded, reproducible context assembled for a node |
|
Audit trail of the digests and files a node’s context used |
Policy (``types/policy.rs``):
Type |
Description |
|---|---|
|
Allow, Deny, RequireApproval |
|
Allow, Deny |
Events System¶
The event system uses unbounded tokio channels:
// In perspt_core::events::channel
pub type EventSender = UnboundedSender<AgentEvent>;
pub type EventReceiver = UnboundedReceiver<AgentEvent>;
pub type ActionSender = UnboundedSender<AgentAction>;
pub type ActionReceiver = UnboundedReceiver<AgentAction>;
AgentEvent has 33 variants (crates/perspt-core/src/events.rs):
Planning:
PlanReady,PlanGenerated,PlanRevised,FallbackPlannerExecution:
NodeSelected,BundleApplied,NodeCompletedVerification:
VerificationComplete,DegradedVerification,SensorFallbackSheaf:
SheafValidationCompleteBranches:
BranchCreated,InterfaceSealed,BranchFlushed,BranchMerged,DependentUnblockedEscalation:
EscalationClassified,GraphRewriteAppliedContext:
ContextDegraded,ContextBlocked,StructuralDependencyMissing,ProvenanceDriftBudget:
BudgetUpdatedFile Ops:
FileDeleted,FileMovedUI:
ApprovalRequest,TaskStatusChanged,EnergyUpdated,LogLifecycle:
Complete,Error,ModelFallback,ToolReadiness
The sheaf and branch variants are vestigial: the TUI still renders them for older ledger streams, but the PSP-9 runtime no longer emits them.
Data Flow¶
User Input
|
[perspt-cli] Parse args (clap)
|
[perspt-core] Config + Provider init
|
+---+---+
| |
chat agent
| |
[tui] [perspt-agent]
| |
| +-- Psp9AgentRuntime
| | +-- exploration -> read-only repository map
| | +-- plan -> architect turn (update_graph)
| | +-- dispatch -> bounded multi-node scheduler
| | +-- tool loop -> proposals -> admissibility kernel
| | +-- candidate overlay -> reversible workspace mutations
| | +-- verifier/measure -> sandboxed deterministic verification
| | +-- adjudicate -> diff-only validator verdict
| | +-- integrate/promote -> staging, integration gate, promotion
| | +-- recorder -> hash-chained ledger events
| |
| +-- GenAiTransport (provider-neutral model plane)
| +-- CandidateHandlerRegistry (open execution plane + tool families)
| +-- ExternalToolRuntime (governed MCP)
| +-- LspClient (JSON-RPC stdio)
| |
| +-- EventSender --> [perspt-tui AgentApp / perspt-dashboard]
| +-- ActionReceiver <-- [perspt-tui ReviewModal]
|
[perspt-store] DuckDB persistence (ledger, checkpoints, verdicts)
[perspt-policy] Starlark rule evaluation
[perspt-sandbox] Process isolation
Streaming Contract¶
Both chat and agent mode use the same streaming protocol:
LLM requests stream chunks over
mpsc::UnboundedSender<String>End-of-response signaled by
EOT_SIGNAL(<|EOT|>)Provider sends EOT - UI never adds its own
UI batches channel messages, handles first EOT, ignores duplicates
Streaming buffer updates the last assistant message live
Pending inputs queue until EOT is received
Warning
Never block the UI select loop. Spawn LLM work on tokio tasks and send results via the channel.