Most agent frameworks make you learn their abstractions: prompt templates, tool schemas, callback hooks, workflow graphs, memory stores. NVIDIA’s NOOA (Object-Oriented Agents) asks a different question: what if an agent were just a Python class? Fields are state, methods are capabilities, docstrings are prompts, type annotations are contracts. If you know Python, you already know 90% of the framework.
The idea, concretely
from nooa import Agent
class SupportAgent(Agent, llm=llm):
"""You are a support agent."""
order_db: OrderDB
def refund_ok(self, order: Order) -> bool:
return order.delivered and order.days <= 30
async def triage(self, msg: str) -> Ticket:
"""Create a typed support ticket."""
...
There are exactly two kinds of methods here, and you can tell them apart at a glance:
- Real body (
refund_ok) — plain deterministic Python. It never touches the LLM. Unit-test it, debug it, refactor it with your IDE. ...body (triage) — a generation method. The signature is the contract (strin,Ticketout), the docstring is the task prompt, and the runtime implements the body with an LLM-driven agentic loop.
And the trick that replaces tool schemas entirely: code as action. The model doesn’t call tools through JSON schemas — it writes Python in a Jupyter-style REPL with access to self. Your methods and their type annotations are the callable interface. No separate tool definitions to keep in sync.
How this differs from traditional frameworks
| Traditional frameworks | NOOA | |
|---|---|---|
| Agent definition | Prompt string + tool schemas + callbacks + workflow graph | One Python class |
| Tools | Hand-written JSON schemas, kept in sync with code | Methods + type annotations; model writes Python against self |
| State | External store / context object you wire up | Typed fields; live objects passed by reference |
| Deterministic logic | Nodes in the workflow graph | Methods with real bodies — plain Python |
| Testing | Framework-specific harnesses | pytest, debuggers, type checkers — the usual Python toolchain |
| Model choice | Per-framework integrations | Model-agnostic via LiteLLM (Anthropic, OpenAI, Ollama, vLLM…) |
The deepest difference is philosophical: traditional frameworks treat the agent as a configuration to be assembled; NOOA treats it as software to be written. That means the skills you already have — testing, tracing, refactoring, version control — keep working.
When NOOA fits
- You live in Python. If your team thinks in classes and type annotations, NOOA has near-zero conceptual overhead.
- You need deterministic + agentic mixed. The real-body/
...-body split makes it natural to keep business rules in code and delegate judgment calls to the model — in the same class. - You want typed contracts. Methods declare
-> Ticketand the runtime validates the model’s output against it, with auto-retry on mismatch. Structured output isn’t a separate feature; it’s the default. - You run models locally. The LiteLLM registry includes Ollama and vLLM targets, so the same agent class runs against a local Qwen as against a frontier API.
- You care about observability. Every LLM call, code execution, and method invocation is traced by default, with a trace viewer (
nooa start-dev) that shows parent-child spans.
It’s research software (NVIDIA says so upfront), so expect rough edges — but the design is deliberately boring in the right places.
Architecture: what a ... call actually does
Three mechanics worth knowing:
- Prompt assembly. The runtime composes the docstring, the signature (types included), and the live
self— the model sees your actual object, not a serialized copy. - CodeAct execution. Generated Python runs in a REPL with
self, imports, and helpers in scope. Generated code passes AST checks and module deny-lists first — but NVIDIA is explicit that these are guardrails, not a boundary. Run code-executing agents in a container or VM, not on your laptop. - Typed I/O with auto-retry. The output must validate against the return annotation. If the model returns something that isn’t a
Ticket, it gets the type error back as feedback and tries again.
The framework ships as a core package plus optional sub-packages: nooa-cli (the nooa command, trace viewer, eval runner), nooa-acp (coding agent for Agent Client Protocol hosts), nooa-memory (long-term memory), and nooa-bench (benchmarks).
Try the anatomy yourself
Step through the class below — each part lights up with what the runtime does with it. The last step simulates a full run of the agentic loop:
class SupportAgent(Agent, llm=llm): """You are a support agent.""" order_db: OrderDB def refund_ok(self, order: Order) -> bool: return order.delivered and order.days <= 30 async def triage(self, msg: str) -> Ticket: """Create a typed support ticket.""" ...The agent is a class
You subclass Agent and pass a model. The class docstring becomes the system prompt — no separate prompt template file, no prompt registry. Rename the class or rewrite the docstring and the agent’s persona changes.
From scratch: your first agent in five minutes
uv init my-agent-project && cd my-agent-project
uv add nooa
Pick a model — anything LiteLLM supports:
from nooa.unifiedllm.registry import get_llm_client
llm = get_llm_client("ollama_chat/qwen3:1.7b",
api_base="http://localhost:11434") # local, no key
Write the agent. Note how little of this is framework API:
import asyncio
from nooa import Agent
class FeedbackAgent(Agent, llm=llm):
"""You are an agent specializing in analyzing customer feedback."""
async def analyze_feedback(self, text: str) -> str:
"""Analyze customer feedback for sentiment and key topics in one sentence."""
...
async def main():
agent = FeedbackAgent()
result = await agent.analyze_feedback("Great product, but shipping was slow")
print(result)
asyncio.run(main())
Rename analyze_feedback to analyze_feedback_briefly and the output changes — the method name, parameters, and docstring are the prompt. That’s the whole mental model.
To see what your agent is doing, start the trace viewer (needs the CLI extras):
uv add "nooa[cli]"
uv run nooa start-dev # trace viewer on http://localhost:5001
Every LLM call, code execution, and method invocation shows up as a span. If the viewer isn’t running, tracing silently disables itself — no config either way.
The takeaway
NOOA’s bet is that agents don’t need a new programming paradigm — they need Python taken seriously. A class gives you state (fields), capabilities (methods), prompts (docstrings), and contracts (types) with zero new concepts, and the ... convention cleanly separates what the model does from what your code does. If you’ve ever fought a framework’s tool-schema DSL or debugged a prompt buried three config layers deep, the appeal is immediate: it’s just an object, all the way down.
Sources: NOOA on GitHub (Apache 2.0); the NOOA paper on design principles and SWE-bench/Terminal-Bench results; NVIDIA’s developer blog post on agent harness capabilities. Code examples adapted from the repo’s quickstart.