Agent internals
This document covers the internal architecture of the quorum-rs crate for developers who want to use it as a Rust library or understand how the reference agent works under the hood.
For running an agent from the CLI, install the quorum binary via cargo install quorum-rs — the CLI ships in the same crate as the SDK.
Library Quick Start
use quorum_rs::agents::{AgentConfig, ProposerEvaluatorAgent};
use quorum_rs::llms::OpenAICompatibleModel;
use quorum_rs::prompts::defaults::DefaultPromptSet;
use quorum_rs::workers::{NatsNsedWorker, NatsNsedWorkerExt, WorkerConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Configure the agent
let agent_config = AgentConfig {
name: "my-agent".into(),
provider_id: "together_ai".into(),
model_name: "MiniMaxAI/MiniMax-M2.5".into(),
..Default::default()
};
// 2. Build LLM backend
let model = OpenAICompatibleModel::new(
"https://api.together.xyz/v1".into(),
std::env::var("TOGETHER_AI_API_KEY").unwrap(),
None,
);
// 3. Create the agent with default prompts
let agent = ProposerEvaluatorAgent::new(
agent_config,
Box::new(model),
Box::new(DefaultPromptSet::new()),
vec![], // propose-phase tools
vec![], // evaluate-phase tools
);
// 4. Connect to NATS and start processing tasks
let config = WorkerConfig::new(
"nats://localhost:4222".into(),
"sphera_jobs".into(),
"my_agent_consumer".into(),
);
// from_agent() auto-wires extensions (user tool handler, chat support)
let worker = NatsNsedWorker::from_agent(agent, config).await?;
worker.run().await
}
Architecture
The orchestrator and agent are decoupled over NATS JetStream:
flowchart LR
O["Orchestrator (Admin)<br/>creates stream 'sphera_jobs'"]
A["Agent Process<br/>subscribes via NatsNsedWorker"]
O <-->|NATS| A| Direction | Subject | Purpose |
|---|---|---|
| Orchestrator → Agent | nsed.{session}.task.{agent}.propose |
Dispatch propose task |
| Orchestrator → Agent | nsed.{session}.task.{agent}.evaluate |
Dispatch evaluate task |
| Agent → Orchestrator | nsed.{session}.result.{round}.{agent}.{action} |
Publish result |
| Orchestrator → All | nsed.jobs.manifest.{job_id} |
Broadcast job manifest |
| Agent → Orchestrator | nsed.jobs.ack.> |
Acknowledge manifest |
The orchestrator can run without any agents. Agents can join and leave at any time — the orchestrator dispatches tasks to whichever agents are online.
Modules
| Module | Key Types | Purpose |
|---|---|---|
agents |
ProposerEvaluatorAgent, UserToolHandler, NatsUserToolHandlerFactory |
ReAct-style agent with tool-use loop + direct chat() method + ChatCapable impl |
providers |
ProviderFactory, ProviderRegistry, cli_base |
Dispatch registry mapping provider.type → factory that builds the agent (third parties register their own); cli_base holds the exec/mcp shared subprocess spawn + timeout helpers. See About the provider registry. |
llms |
OpenAICompatibleModel, RateLimiter |
LLM client (any compatible API) with streaming + rate limiting |
llms::strategies |
NativeStrategy, HarmonyStrategy, XmlRegexStrategy |
Provider-specific request/response adaptation |
llms::simulated |
SimulatedModel |
Deterministic model for testing |
prompts |
DefaultPromptSet |
Benchmark-validated XML-structured prompts |
tools::context |
ReadProposalTool, ReadCritiquesTool, ReadOwnProposalTool |
RAG tools for reading deliberation history |
tools::sandbox |
ListFilesTool, ReadFileTool, WriteFileTool, ExecuteCommandTool |
Sandboxed execution tools |
tools::user_call |
UserCallTool |
External tool forwarding via NATS KV |
llm_repair |
repair_tool_calls, extract_xml_tool_calls, strip_thinking_prefix, ... |
Multi-stage JSON repair for unreliable model output (6 stages: truncation repair → escape repair → lossy sanitization → conversational/markdown extraction → merged field splitting → thinking-token prefix stripping) |
workers |
NatsNsedWorker, WorkerConfig, NatsScratchpadStore, JobManifest, NatsNsedWorkerExt, NatsNsedWorkerStatusExt |
NATS JetStream worker runtime + ergonomic extension traits |
status |
AgentStatusSnapshot, EventLogEntry, SharedAgentStatus |
Real-time agent status types |
status::server |
StatusServer |
Embedded HTTP dashboard with chat + config API (feature: status-server) |
How the Agent Works
The ProposerEvaluatorAgent implements the NsedAgent trait with two main methods, plus a direct chat method:
propose(ctx)-- Generates a solution proposal:- Runs a ReAct loop: system prompt -> tool calls -> observations -> ... -> final answer
- Uses
generate_structured_outputto extract JSON from the model's response - Supports context tools (read previous proposals, critiques) and user-defined tools
evaluate(ctx)-- Evaluates peer proposals:- Receives all proposals from the current round via
AgentContext - Uses batch evaluation prompts to score each proposal
- Returns evaluations with scores, justifications, structured claim assessments, disagreement points, stance, and per-category quality scores
- Extensive serde alias coverage handles LLM field-name hallucination (11+ aliases per field)
- Receives all proposals from the current round via
chat(messages)-- Direct conversation with the agent's LLM:- Bypasses the NSED deliberation protocol entirely
- Uses the agent's persona with an
<internal_voice>wrapper that signals the LLM to respond naturally - Accepts a full conversation history (
Vec<ChatCompletionRequestMessage>) - Used by the dashboard chat interface (
POST /api/chat)
NATS Worker Lifecycle
- Connect --
NatsNsedWorker::new()connects to NATS and creates per-agent KV buckets for idempotency and scratchpad storage. - Subscribe -- Binds durable pull consumers for task and manifest subjects.
- Manifest ACK -- When the orchestrator broadcasts a job manifest listing this agent, the worker sends an
agent_acceptedevent. - Task Processing -- For each incoming task:
- Deduplicates via idempotency KV bucket
- Publishes
agent_workingSSE event - Attaches
NatsScratchpadStorefor persistent memory - Runs
proposeorevaluatebased on the action in the subject - Publishes result (or
agent_erroron failure)
Status Dashboard & Event Log
The status module provides real-time agent monitoring, gated behind the status-server feature flag.
Architecture
flowchart TD
W["NatsNsedWorker<br/>(push events)"] -->|push events| S["SharedAgentStatus<br/>Arc<RwLock<Snapshot>>"]
S -->|poll every 2s| SS["StatusServer (axum)<br/>GET /api/status<br/>GET /api/config<br/>POST /api/chat"]AgentStatusSnapshot— Shared state with identity, counters, current job, recent tasks (max 20), and event log (max 200 entries). Defined inquorum-rs.EventLogEntry— Timestamped lifecycle event (event_type,job_id,detail). Defined inquorum-rs.StatusServer::run(port, status, chat_agent, agent_config)— Spawns an embedded axum HTTP server. UsesOption<Arc<dyn ChatCapable>>for chat support.
Event Types
The worker pushes events at each lifecycle transition:
| Event | When | Detail |
|---|---|---|
connected |
Worker starts listening | "NATS connected, listening for tasks" |
agent_accepted |
Manifest received, agent selected | "Accepted job manifest ({task})" |
agent_working |
Task execution begins | "Round {n} {action}" |
task_complete |
Task finishes successfully | "{action} ok {ms}ms" |
agent_error |
Task fails | "{action} failed: {error}" |
heartbeat |
Every 10s | "idle uptime {n}s" or "busy uptime {n}s" |
Chat API
The POST /api/chat endpoint accepts { messages: [{ role, content }] } and forwards the conversation to agent.chat(). The system prompt includes the agent's persona with an <internal_voice> block that bypasses NSED deliberation constraints.
LLM Strategies
The OpenAICompatibleModel delegates provider-specific quirks to strategy implementations:
| Strategy | When Used | What It Does |
|---|---|---|
NativeStrategy |
Default for most providers | Standard tool calling API, handles Cloudflare/vLLM/Together quirks |
HarmonyStrategy |
gpt-oss via harmony engine |
Encodes tool schemas into system prompt for models without native tool support |
XmlRegexStrategy |
vLLM with guided_regex |
Uses XML-based tool calling with regex-guided decoding |
Rust Configuration Types
AgentConfig
AgentConfig {
name: "my-agent".into(), // Unique agent identifier
provider_id: "together".into(), // Provider for strategy selection
model_name: "meta-llama/...".into(), // Model identifier
max_iterations: 10, // Max ReAct loop iterations
context_window: 128_000, // Max context tokens
temperature: Some(0.7), // Sampling temperature
task_precision: TaskPrecision::Standard, // Controls structured output strictness
..Default::default()
}
WorkerConfig
WorkerConfig::new(
"nats://localhost:4222".into(), // NATS server URL
"sphera_jobs".into(), // JetStream stream name
"agent_consumer".into(), // Durable consumer name
)
.with_subject_prefix("nsed".into()) // Subject namespace (default: "nsed")
.with_scratchpad_retention(86400 * 7) // Scratchpad TTL in seconds (default: 7 days)
Trait / implementation map
The agent contract is defined by traits in quorum_rs::agents and friends. The reference implementations shipped in this crate map as follows:
| Trait | Reference impl |
|---|---|
NsedAgent |
ProposerEvaluatorAgent |
AiModel |
OpenAICompatibleModel, SimulatedModel, SimpleOpenAIModel |
PromptSet |
DefaultPromptSet |
Tool |
ReadProposalTool, UserCallTool, sandbox tools, … |
UserToolHandlerTrait |
UserToolHandler |
UserToolHandlerFactory |
NatsUserToolHandlerFactory |
ChatCapable |
ProposerEvaluatorAgent (via impl ChatCapable) |
ChatStrategy |
NativeStrategy, HarmonyStrategy, XmlRegexStrategy |
Custom agents can implement NsedAgent directly. To start from a ready-made ReAct loop with rate-limited LLM access, default prompts, and built-in tools, use ProposerEvaluatorAgent (see how-to/agent-development.md).