Architecture¶
Perspt is a Rust workspace with nine crates including a root integration crate. Version 0.5.9 implements the PSP-7 specification for the experimental SRBN agent.
Workspace Layout¶
perspt/ # Root: integration crate (perspt)
+-- crates/
| +-- perspt-core/ # Types, config, LLM provider, events, plugins
| +-- perspt-agent/ # SRBN orchestrator, agents, ledger, LSP, tools
| +-- 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
+-- tests/ # Integration tests
+-- docs/ # Sphinx documentation
Dependency Graph¶
![digraph crates {
rankdir=BT;
node [shape=box, style=rounded];
"perspt-cli" -> "perspt-core";
"perspt-cli" -> "perspt-agent";
"perspt-cli" -> "perspt-tui";
"perspt-cli" -> "perspt-store";
"perspt-agent" -> "perspt-core";
"perspt-agent" -> "perspt-store";
"perspt-agent" -> "perspt-policy";
"perspt-agent" -> "perspt-sandbox";
"perspt-tui" -> "perspt-core";
"perspt-tui" -> "perspt-agent";
"perspt-store" -> "perspt-core";
"perspt-policy" [label="perspt-policy\n(Starlark)"];
"perspt-sandbox" [label="perspt-sandbox"];
"perspt-dashboard" -> "perspt-store";
"perspt-dashboard" -> "perspt-core";
"perspt-cli" -> "perspt-dashboard";
}](../_images/graphviz-321fcfae7c3f5e661fcd71288479c9e72f7cc141.png)
Crate: perspt-core¶
The foundation crate. Re-exports all canonical types.
Modules:
types— All PSP-5 types (see PSP-5 Type Inventory below)config—Config { provider, model, api_key }events—AgentEvent(~30 variants),AgentAction,NodeStatus,ActionTypellm_provider—GenAIProviderwrapping thegenaicrate;EOT_SIGNALplugin—LanguagePlugintrait +PythonPlugin,RustPlugin,JsPluginmemory—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) -> Option<String>;
fn syntax_check_command(&self) -> Option<String>;
fn verifier_profile(&self) -> VerifierProfile;
fn owns_file(&self, path: &Path) -> bool;
// ... ~15 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
pub command: Option<String>, // Primary command
pub available: bool,
pub fallback_command: Option<String>,
pub fallback_available: bool,
}
Crate: perspt-agent¶
The experimental SRBN orchestrator and its subsystems.
Modules:
orchestrator—SRBNOrchestratorwithpetgraph::DiGraph<SRBNNode, Dependency>. Split into phase-focused sub-modules:mod(struct, constructors, run, helpers, tests),init(workspace bootstrap),solo(single-file mode),planning(architect interaction),verification(energy computation),convergence(stability loop),commit(ledger promotion),repair(correction prompts),bundle(artifact application).agent—Agenttrait +ArchitectAgent,ActuatorAgent,VerifierAgent,SpeculatorAgenttools—AgentTools(read_file, write_file, apply_diff, delete_file, move_file, run_command, search_code, etc.)prompts— Externalized prompt templates for architect (existing/greenfield) and actuator rolesledger—MerkleLedgeratopSessionStorelsp—LspClient(JSON-RPC over stdio)test_runner—TestRunnerTrait+PythonTestRunner,RustTestRunner,PluginVerifierRunnercontext_retriever—ContextRetrieverfor workspace search
Agent Trait:
#[async_trait]
pub trait Agent: Send + Sync {
async fn process(&self, node: &SRBNNode, ctx: &AgentContext)
-> Result<AgentMessage>;
fn name(&self) -> &str;
fn can_handle(&self, node: &SRBNNode) -> bool;
fn model(&self) -> &str;
fn build_prompt(&self, node: &SRBNNode, ctx: &AgentContext) -> String;
}
Orchestrator:
pub struct SRBNOrchestrator {
pub graph: DiGraph<SRBNNode, Dependency>,
pub context: AgentContext,
// + ledger, agents (4 tiers), tools, LSP clients, test runners,
// event/action channels, per-tier model names + fallbacks
}
The orchestrator drives the PSP-5 lifecycle:
detect_workspace()— Identify plugins and workspace stateplan_task()— Architect decomposes task into DAGexecute_dag()— Topological traversal with per-node verification loopverify_node()— Compute V(x) via plugin verifier profilesheaf_validate()— Cross-node contract checkingreview_node()— Interactive approval (unless--yes)execute_node()— ReturnsNodeOutcome::Completedwhen V(x) ≤ ε orNodeOutcome::Escalatedwhen retries are exhaustedcommit_node()— Record stable node in Merkle ledger
After all nodes are processed, the orchestrator derives SessionOutcome
from completed/escalated counts and emits a Complete event.
Crate: perspt-store¶
DuckDB-backed persistence. Not SQLite.
pub struct SessionStore {
conn: Mutex<Connection>, // duckdb::Connection
}
Tables:
sessions— Session metadata (id, task, working_dir, merkle_root, status)node_states— Per-node snapshotsenergy_records— Energy historyllm_requests— Full LLM request/response loggingstructural_digests— Content hashes for interface sealscontext_provenance— Provenance tracking per nodeescalation_reports— Classified escalationsrewrite_records— Graph rewrite audit trailsheaf_validations— Cross-node validation resultsprovisional_branches— Branch lifecyclebranch_lineage— Parent-child branch relationshipsinterface_seals— Sealed interface recordsbranch_flushes— Flush cascade recordstask_graph_edges— DAG edgesreview_outcomes— Human review decisionsverification_results— Full verification snapshotsartifact_bundles— Bundle JSON per nodeplan_revisions— Plan version historyfeature_charters— Scope governance recordsrepair_footprints— Correction audit trailbudget_envelopes— Session budget caps and usage
Crate: perspt-tui¶
Ratatui-based terminal UI with two modes:
ChatApp — Interactive chat with markdown rendering and streaming
AgentApp — Agent dashboard with DAG tree, energy display, review modal
Key components:
Component |
Purpose |
|---|---|
|
Main agent dashboard layout |
|
DAG visualization with node states |
|
Grouped diff viewer with approve/reject/correct |
|
Unified diff display |
|
LLM log browser |
|
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
}
PSP-5 Type Inventory¶
All canonical types live in perspt_core::types:
SRBN Core:
Type |
Description |
|---|---|
|
DAG node: goal, output_targets, contract, tier, monitor, state, node_class, owner_plugin, provisional_branch_id, interface_seal_hash |
|
TaskQueued → Planning → Coding → Verifying → Retry → SheafCheck → Committing → Completed / Failed / Escalated / Aborted / Superseded. Includes |
|
Interface, Implementation (default), Integration |
|
Architect, Actuator, Verifier, Speculator |
|
interface_signature, invariants, forbidden_patterns, weighted_tests, energy_weights |
|
energy_history, attempt_count, stable, stability_epsilon, max_retries, retry_policy |
|
Per-error-type counters: compilation, tool, review |
|
Success (all nodes completed), PartialSuccess (some escalated), Failed (none completed) |
|
Completed (V(x) ≤ ε) or Escalated (retries exhausted). Returned by |
Energy:
Type |
Fields |
|---|---|
|
v_syn (LSP), v_str (contracts), v_log (tests), v_boot (init), v_sheaf (cross-node) |
|
Critical (10.0), High (3.0), Low (1.0) |
|
test_name, criticality |
Task Planning:
Type |
Description |
|---|---|
|
Container for |
|
id, goal, output_files, dependencies, task_type, contract, command_contract, node_class |
|
Code, Command, UnitTest, IntegrationTest, Refactor, Documentation |
|
command, expected_exit_code, expected_files, forbidden_stderr_patterns |
Artifact Bundle:
Type |
Description |
|---|---|
|
artifacts: Vec<ArtifactOperation>, commands: Vec<String> |
|
Write { path, content } | Diff { path, patch } | Delete { path } | Move { from, to } |
|
entries: HashMap<String, OwnershipEntry>, fanout_limit |
Verification:
Type |
Description |
|---|---|
|
syntax_ok, build_ok, tests_ok, lint_ok, diagnostics_count, tests_passed/failed, degraded, stage_outcomes |
|
stage, passed, sensor_status, output |
|
Available | Fallback { actual, reason } | Unavailable { reason } |
|
Default, Strict, Minimal |
Context Management:
Type |
Description |
|---|---|
|
working_dir, history, merkle_root, complexity_k, session_id, auto_approve, defer_tests, token_budget, execution_mode, active_plugins, ownership_manifest |
|
max_tokens, max_cost_usd, tokens_used (in/out), cost_usd, per-1k rates |
|
byte_limit (100KB), file_count_limit (20) |
|
Per-node context scoping: owned_files, sealed_interfaces, structural_digests |
|
Assembled context with budget tracking |
|
Audit trail of what context each node received |
Escalation and Rewrite:
Type |
Description |
|---|---|
|
ImplementationError, ContractMismatch, InsufficientModelCapability, DegradedSensors, TopologyMismatch |
|
GroundedRetry, ContractRepair, CapabilityPromotion, SensorRecovery, DegradedValidationStop, NodeSplit, InterfaceInsertion, SubgraphReplan, UserEscalation |
|
node_id, category, action, energy_snapshot, stage_outcomes, evidence |
Sheaf Validation:
Type |
Description |
|---|---|
|
ExportImportConsistency, DependencyGraphConsistency, SchemaContractCompatibility, BuildGraphConsistency, TestOwnershipConsistency, CrossLanguageBoundary, PolicyInvariantConsistency |
|
validator_class, plugin_source, passed, evidence_summary, v_sheaf_contribution, requeue_targets |
Provisional Branches:
Type |
Description |
|---|---|
|
branch_id, node_id, state, parent_seal_hash, sandbox_dir |
|
Active → Sealed → Merged / Flushed |
|
node_id, sealed_path, artifact_kind, seal_hash, version |
|
parent_node_id, flushed_branch_ids, requeue_node_ids, reason |
|
child_node_id, parent_node_id, required_seal_paths |
Structural Digests:
Type |
Description |
|---|---|
|
Content hash of Interface artifacts (signatures, schemas, seals) |
|
Compressed summaries (IntentSummary, VerifierResults, DesignRationale) |
|
Signature, Schema, SymbolInventory, InterfaceSeal |
Planning and Budget:
Type |
Description |
|---|---|
|
Session-level budget caps: max_steps, max_revisions, max_cost_usd, usage counters |
|
Scope governance: max_modules, max_files, max_revisions, language_constraint |
|
Versioned plan: revision_id, sequence, plan, reason, supersedes, status (Active/Superseded/Cancelled) |
|
Correction audit: affected_files, applied_bundle, diagnosis, resolved flag |
|
Adaptive agent gating: LocalEdit (skip architect), FeatureIncrement (default), LargeFeature, GreenfieldBuild, ArchitecturalRevision. Methods: |
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 ~35 variants covering the full PSP-5 lifecycle:
Planning:
PlanReady,PlanGenerated,PlanRevisedExecution:
NodeSelected,BundleApplied,NodeCompletedVerification:
VerificationComplete,DegradedVerification,SensorFallbackSheaf:
SheafValidationCompleteBranches:
BranchCreated,InterfaceSealed,BranchFlushed,BranchMergedEscalation:
EscalationClassified,GraphRewriteAppliedContext:
ContextDegraded,ContextBlocked,ProvenanceDriftBudget:
BudgetUpdatedFile Ops:
FileDeleted,FileMovedUI:
ApprovalRequest,TaskStatusChanged,EnergyUpdated,LogLifecycle:
Complete,Error,ModelFallback,ToolReadiness
Data Flow¶
User Input
|
[perspt-cli] Parse args (clap)
|
[perspt-core] Config + Provider init
|
+---+---+
| |
chat agent
| |
[tui] [perspt-agent]
| |
| +-- SRBNOrchestrator
| | +-- detect_workspace() -> [plugins]
| | +-- plan_task() -> [Architect Agent]
| | +-- execute_dag() -> [Actuator Agent]
| | +-- verify_node() -> [LSP, TestRunner, Verifier Agent]
| | +-- sheaf_validate() -> [Sheaf Validators]
| | +-- commit_node() -> [MerkleLedger]
| | +-- derive SessionOutcome from completed/escalated counts
| | +-- emit Complete event with outcome
| |
| +-- AgentTools (filesystem, search, commands)
| +-- LspClient (JSON-RPC stdio)
| +-- TestRunner (plugin-driven)
| +-- ContextRetriever (workspace search)
| |
| +-- EventSender --> [perspt-tui AgentApp]
| +-- ActionReceiver <-- [perspt-tui ReviewModal]
|
[perspt-store] DuckDB persistence
[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.