Alternative implementation: the same agent on Rig and Rust
Canonical implementation: NVIDIA NeMo Agent Toolkit (NAT) on main. The labs and walkthrough run on it.
Comparative implementation: rust-agent, the same agent service written in Rust on Rig 0.44 with explicit policy code. It is a learning extension, not a replacement.
Part I teaches the production-agent architecture primarily through the canonical NAT implementation. Comparative notes on the Rig implementation already appear throughout: in the learning path, the concepts, the labs, the request walkthrough and the reference chapters. This section brings those comparisons together and adds a dedicated Rust learning extension. It asks what changes when the same architecture is built with a much thinner framework. The answer separates two things the NAT implementation necessarily blurs: what a production agent needs, and what a particular agent toolkit happens to provide.
Same lesson, two implementations
This is the book's first, and most concrete, test of a principle that runs through every part: the implementation may change; the engineering property should survive.
flowchart TB
L["The same engineering lesson<br/>identity · capability · authority · evidence"]
L --> N["NeMo Agent Toolkit<br/>Python · main"]
L --> R["Rust + Rig<br/>rust-agent"]
N --> P["The same architectural properties<br/>one gateway · one MCP server · one contract · one evaluation"]
R --> P
The educational question is:
Can you still recognise the architecture after replacing the framework?
The Rust version is not here because Rust is inherently better, nor because NAT is a poor choice. NAT shows what productive use of a mature framework looks like: much of the agent is configuration, and a great deal comes for free. Rig removes most of that abstraction, so behaviour NAT provides has to be written as ordinary code. Comparing the two shows which properties belong to the architecture and which to the framework. The same comparison applies when you move an agent to a managed platform, another vendor's SDK or your own code.
The two implementations are not at full parity, and the book does not pretend otherwise. They share every component except the agent, satisfy the same contract and run the same evaluation suites, but some behaviour differs on purpose: PII masking is narrower on Rig, tool-argument checking is stricter, and NAT remains the recommended production reference. The differences are listed in NAT vs Rig.
Where the material lives
All of the template's documentation, including the Rig material, lives on
main and is imported into this book from there. The rust-agent branch adds
one commit that replaces the Python agent with the Rust one. Rust source is
linked directly on GitHub.
What stays invariant
The Rig agent replaces only the box labelled agent in the architecture. Everything else is identical on the two branches and enforces the same rules:
| Invariant | Where it is enforced, on both implementations |
|---|---|
| Identity comes from the gateway, never from conversation text | gateway headers, checked by the agent before any work |
| The tool list is a capability grant | the MCP server's tool registry and the agent's allow-list |
| Facts come from authoritative state | MCP tools over PostgreSQL; neither agent touches the database |
| A state change needs a signed human approval | the MCP server verifies the token, consumes a nonce and re-derives state in one transaction |
| One correlated trace per request; per-user attribution only as an opt-in pseudonym | OpenTelemetry → MLflow; raw gateway identity headers are redacted from spans, and a per-user pseudonym is exported only with OTEL_TRACE_USER_ID=true (off by default) |
| Behaviour is measured, not asserted | the same four evaluation suites and gates |
The two agents also share one boundary, specified as the
agent-service contract:
routes, authentication order, the SSE vocabulary the UI consumes, error classes
and provenance. The gateway and UI cannot tell which agent is behind them. They
also share byte-identical prompts, so prompt_sha256 is equal on both.
What NAT gives you
NAT is a high-level toolkit. On main it provides the ReAct loop, tool calling
over MCP, the interaction (pause) protocol, NeMo Guardrails flows with
model-based checks, Presidio's NER-based PII masking, telemetry exporters and
evaluation integrations. You configure behaviour rather than write it. The price
is visible in Extending the template:
several local modules exist only to close gaps in the toolkit's behaviour, and
one trace per request needs access to private NAT attributes.
What Rig makes explicit
Rig provides the agent loop, a model client and MCP tools, and little else. What NAT configures, the Rig implementation writes as ordinary, testable code:
- The agent loop. Rig's
AgentRunnerdrives a small, serialisable run. The agent bounds it with a model-call budget and a deterministic tool-call budget of exactlymax_tool_calls. - Tool policy. Every model-proposed call passes one pure function,
ToolPolicy::decide, through Rig'sAgentHook::on_dispatch. The function checks a closed registry, accepts only a JSON object with no unknown fields, refuses coercion ("5"is not an integer), and routes mutations into the approval gate. The MCP server still re-validates and stays authoritative. - Human-in-the-loop. The run suspends on a pending ticket. The human's answer
is checked against its owner and the offered choices, and only then becomes a
VerifiedDecision, a type with a private constructor. The token minted from it is the one the MCP server already verifies onmain. - Guardrails. The input rail uses the same deny patterns, allow templates and
classifier prompt as
main, written as pure functions, and fails closed when the classifier is unreachable. The output rail redacts secrets and masks PII inside a bounded streaming window. Its PII recognisers are deterministic: stricter where an entity has a checksum, and narrower than Presidio where it does not.
The execution state machine
NAT keeps a request's execution state inside its runner. The Rig agent writes it as an enum with legal transitions:
Received ──► ScreeningInput ──► Running ◄──► AwaitingApproval
│ │
▼ ▼
Blocked Completed / Failed / Cancelled
A blocked request cannot await approval, and a finished run cannot resume, because the type has no such transitions. The lesson transfers to any runtime: name the states of a request and make illegal transitions impossible, even when the framework hides its own state. → Rust lesson 10
Comparison, not a contest
NAT vs Rig goes through each concern and labels the two realisations as equivalent, stricter, narrower or different. It also lists every deliberate behavioural difference. The short version:
NAT (main) | Rig (rust-agent) | |
|---|---|---|
| Role | recommended production reference | comparative learning implementation |
| Loop, tool calling, streaming | provided | provided |
| Guardrail flows, NER masking | provided | written by hand; PII narrower |
| Tool policy | spread over configuration and parsing | one function |
| Execution state | hidden | an explicit enum |
| Agent image | 2.01 GB | 71 MB |
The boundary that matters most does not move. The MCP server, not the agent, decides what may change in the database.
Comparative exercises
Each lab works on both implementations, and each has a short On the Rig implementation section. These exercises use the pair deliberately:
- Find the decision point. Lab 02 asks for a tool outside the allow-list.
Find the code that refuses it on each implementation: configuration on NAT,
ToolPolicy::decideon Rig. Then sendlimit: "5"and explain why one agent coerces it and the other refuses. - Draw the state machine NAT does not show you. Using lab 09, list the states a request passes through on NAT while an approval is pending. Compare your list with the Rig enum above, and name one transition NAT permits that the enum forbids.
- Measure the PII trade-off. Run the guardrail scenarios of lab 07 on both implementations. Record which entities each one masks, and when the masked answer reaches the browser.
- Hold the contract constant. Run
make eval-allon both branches and compare the results, which are taggedprovenance.agent.runtime(natorrig-rust). Any difference must be explained by the list of deliberate differences or reported as a defect.
Where to go next
The two chapters of this section:
- NAT vs Rig: the full concern-by-concern comparison.
- Rust learning extension: thirteen lessons that rebuild the agent on Rig, from calling a model to tracing the whole lifecycle. Do them after the labs.
The boundary both implementations satisfy is specified in the agent-service contract, part of the Part I reference.
Optional deep reading, kept in the template repository as its engineering record:
- Mapping the NAT agent's responsibilities: the responsibility mapping, parity checklist, Rig API investigation and implementation plan written before the Rust agent was.
- How
rust-agenttracksmain: the single-commit maintenance model and its parity and drift checks. - The Rust source itself, on the
rust-agentbranch.