Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

InvariantWhere it is enforced, on both implementations
Identity comes from the gateway, never from conversation textgateway headers, checked by the agent before any work
The tool list is a capability grantthe MCP server's tool registry and the agent's allow-list
Facts come from authoritative stateMCP tools over PostgreSQL; neither agent touches the database
A state change needs a signed human approvalthe 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 pseudonymOpenTelemetry → 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 assertedthe 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 AgentRunner drives a small, serialisable run. The agent bounds it with a model-call budget and a deterministic tool-call budget of exactly max_tool_calls.
  • Tool policy. Every model-proposed call passes one pure function, ToolPolicy::decide, through Rig's AgentHook::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 on main.
  • 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)
Rolerecommended production referencecomparative learning implementation
Loop, tool calling, streamingprovidedprovided
Guardrail flows, NER maskingprovidedwritten by hand; PII narrower
Tool policyspread over configuration and parsingone function
Execution statehiddenan explicit enum
Agent image2.01 GB71 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:

  1. 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::decide on Rig. Then send limit: "5" and explain why one agent coerces it and the other refuses.
  2. 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.
  3. 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.
  4. Hold the contract constant. Run make eval-all on both branches and compare the results, which are tagged provenance.agent.runtime (nat or rig-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: