The short version: NVIDIA's NOOA — labs-OO-Agents — lets you write an AI agent as one plain Python class. The whole mental model fits on a sticky note: methods are the actions the agent can take, fields are its state, docstrings are the prompt the model sees, and type annotations are contracts the runtime enforces. You install it with pip install nooa, subclass Agent, write your capabilities as typed methods, and run it with await. Below is the full build, copy-paste ready, plus the one part that matters for long-running agents: the SQLite memory that lets NOOA drop the context-compaction step.

Install#

NOOA is Apache 2.0 and targets Python 3.12–3.13. It's published as an early research preview (v0.0.x, marked alpha on PyPI), so pin the version before you build anything real on it.

pip install nooa
# or, with uv:
uv add nooa
# add optional capabilities (CLI + the SQLite memory store):
uv add "nooa[cli,memory]"

The mental model: an agent is an object#

Most frameworks make you learn a new noun — a graph, a chain, a crew, a flow. NOOA's bet is that you already know the right one: a class. Everything an agent needs maps onto a construct Python already has.

That's the entire framework surface. There's no orchestration DSL to memorize because the control flow is just Python.

Step 1 — Subclass Agent#

The class docstring is your system prompt. Fields declared with type annotations are the agent's state.

from nooa import Agent

class RefundAgent(Agent):
    """You are a refund support agent. Resolve refund requests
    accurately and never exceed the daily refund cap."""

    daily_cap: float = 500.0     # field = state
    spent_today: float = 0.0     # field = state

Step 2 — Write generation methods (the model fills these in)#

A generation method is an async def whose body is just .... You don't implement it — the model does, at runtime. Its signature and docstring become the prompt, and its return type annotation is a contract the runtime validates the output against. You describe the capability; NOOA gets the model to produce a correctly-typed result.

async def triage(self, message: str, order: Order) -> Ticket:
    """Read the customer message and the order, and return a typed
    support ticket classifying the request and its urgency."""
    ...   # the model implements this; `Ticket` is enforced on the way out

Because the return type is Ticket (say, a dataclass or Pydantic model), you get a validated object back — not a wall of text you have to parse and pray over. That single guarantee removes most of the glue code a hand-rolled tool-calling loop accumulates.

Step 3 — Write deterministic methods (you control these)#

The steps that must be exact — money, eligibility, writes — are normal Python methods with real bodies. The model never decides them; it can only call them.

def is_refund_eligible(self, order: Order) -> bool:
    """Deterministic rule — no model judgement involved."""
    return order.delivered and order.days_since_delivery <= 30

def record_refund(self, amount: float) -> None:
    if self.spent_today + amount > self.daily_cap:
        raise ValueError("daily refund cap exceeded")
    self.spent_today += amount

This split is the reason NOOA is worth the ceremony: the model reasons, your code enforces. A generation method can propose a refund; a deterministic method is what actually moves the money, with a hard cap the model cannot argue its way past. For anything irreversible, pair that with an explicit human sign-off gate.

Step 4 — Run it#

Instantiate the class and await a method like any async object. NOOA traces every model call and method invocation as it goes, so the run is auditable by default.

import asyncio

async def main():
    agent = RefundAgent()
    order = load_order("A-10432")
    ticket = await agent.triage("Where is my refund?", order)

    if agent.is_refund_eligible(order):
        agent.record_refund(order.total)     # deterministic, capped
    print(ticket, agent.spent_today)

asyncio.run(main())

Notice what you can do with this that you can't do with a prompt blob: set a breakpoint on record_refund, assert on agent.spent_today in a pytest, and diff the whole agent in code review. State lives on the object, so there's nothing hidden to reconstruct after an incident — the property covered in how to wire an agent's three memory tiers.

Step 5 — The memory that lets you drop compaction#

Here's the part that matters for long-running agents. Install the nooa[memory] extra and NOOA keeps the agent's world in a typed, relational SQLite store, then passes live objects by reference into methods. The runtime hands the model a handle to an object rather than serializing the entire object graph back into the prompt on every turn.

The consequence is direct: the context window stops filling up with re-stringified state, so — per NVIDIA — you can skip the summarize-and-truncate compaction pipeline that most long agents bolt on. If you've wrestled with context editing vs. compaction for long-running agents, NOOA's answer is to make it a non-problem at the data layer instead of a tuning knob. Treat "it replaces compaction entirely" as a claim to measure on your workload, not a given — but the mechanism is sound, and it's the most interesting idea in the framework.

When NOT to reach for this#

Be honest about the trade. NOOA is a research preview, not a 1.0 — pin the version and don't bet a launch-critical path on an unpinned nooa. And the object-oriented discipline is overhead you don't need if your goal is a throwaway demo; an orchestration framework gets you to "it works on stage" faster. NOOA earns its keep on the other side of that line — when you'll have to test, trace, and defend what the agent did, which is exactly when a graph of opaque nodes becomes a liability. For the full framework-choice decision, see NOOA vs. LangGraph: when your agent should be a class, not a graph.

The bottom line#

NOOA's whole pitch is that you already know how to build reliable software, and an agent shouldn't be an exception. Write the capabilities as typed methods, keep the risky ones deterministic, let the model fill in the reasoning ones, and hold state on the object where you can see it. You get an agent you can unit-test and a memory model that scales past the context window — for the price of writing real code instead of a prompt.