nvidia-nemo/labs-oo-agents · Archived

nooa-middleware-hooks

Intercept and observe NOOA execution — middleware via event_manager.intercept() (guardrails, input/output transforms, blocking), event observers via event_manager.on() (react to Task/Error/LLMComplete/turn events), and the InstrumentationHooks protocol for observability backends.

First seen Aug 13, 2026

Installation

$ npx skills add nvidia-nemo/labs-oo-agents --skill nooa-middleware-hooks

Summary

  • Intercept and observe NOOA execution — middleware via event_manager.intercept() (guardrails, input/output transforms, blocking), event observers via event_manager.on() (react to Task/Error/LLMComplete/turn events), and the InstrumentationHooks protocol for observability backends.
  • Use when adding guardrails, redacting or rewriting prompts, blocking or faking an LLM call or code execution, rate-limiting agent methods, subscribing to lifecycle events, or wiring custom telemetry.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from nvidia-nemo/labs-oo-agents · top by installs.

npx skills add nvidia-nemo/labs-oo-agents

Browse all from nvidia-nemo/labs-oo-agents

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1.5K
License LICENSE
Default branch main
Open issues 44
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Compatibilitynooa package

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,144 B
  • docs SUMMARY.md 511 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Middleware and Hooks

Three interception surfaces, one decision rule:

Surface Registration Can it change behavior? Errors Scope
Middleware agent.event_manager.intercept(kind, fn) YES — transform inputs/outputs, short-circuit, block propagate (a guardrail raising aborts the call) per-agent
Observers agent.eventmanager.on(eventtype, fn) no — fire-and-forget after the event is recorded isolated per-agent
InstrumentationHooks set_hooks(obj) (nooa.runtime.hooks) no — observational timing/tracing pairs swallowed + logged, overhead metered one global slot per async context

Rule of thumb: enforcing or transforming → intercept(); reacting → on(); building an observability backend → you probably want a tracing exporter (nooa-capturing-traces), not raw hooks — the tracing system already occupies the hooks slot.

Middleware (intercept)

Each middleware is async def mw(ctx, nxt) -> ctx — a typed context object and a nxt callable running the rest of the chain. Three kinds (nooa.runtime.middleware):

Kind Wraps Context (ctx) mutables in result out
"agent_call" one whole agent-method call (all turns) args, kwargs (+ agent, method_name) ctx.result
"llm_call" one LLM round-trip inside runtime.generate() messages (the rendered prompt), params (tools, outputmodel, maxtokens, ...) ctx.response (LLMResponse)
"execute_python" one CodeAct cell in runtime.execute_code() code, params (timeout, restrictions, ...) ctx.result (ExecutionResult)
from nooa.runtime.middleware import LLMCallContext, LLMCallNext

async def redact_secrets(ctx: LLMCallContext, nxt: LLMCallNext) -> LLMCallContext:
    for m in ctx.messages:                          # transform inputs
        if isinstance(m.get("content"), str):
            m["content"] = m["content"].replace(SECRET, "[redacted]")
    ctx = await nxt(ctx)                            # run the rest of the chain + the real call
    return ctx                                      # (could also inspect/patch ctx.response here)

unsubscribe = agent.event_manager.intercept("llm_call", redact_secrets)
...
unsubscribe()                                       # intercept() returns a remover

Verified semantics:

  • Order: registration order = execution order; first registered is outermost. Nesting across kinds: agentcall → per-turn llmcall → per-cell execute_python.
  • Short-circuiting (don't call nxt) is allowed for guardrails/caching, but you MUST set the output slot (ctx.result / ctx.response) — the runtime raises RuntimeError if middleware returns without it. To fake an LLM turn, construct an LLMResponse (content, toolcalls=[], finishreason="stop", assistantmessage={...}, rawresponse=None).
  • Blocking: raise from the middleware — the exception propagates to the caller exactly like a failure of the wrapped operation (for llm_call, CodeAct counts it against its session error budget).
  • Exceptions are NOT swallowed — middleware is control flow, unlike hooks.
  • Per-agent: registered on that agent's EventManager; subagents have their own.
  • execute_python has a re-entry guard: code the middleware itself triggers (e.g. it calls agent methods) skips the middleware, while nested generation methods called by executed code re-enter it for their own cells.
  • On a context-window error the runtime archives events, rebuilds messages, and retries — so llm_call middleware can run more than once per logical turn; keep it idempotent.
  • The tracing onmessagesbuilt hook fires inside the innermost core, so traces show the post-middleware messages.

Worked production example: src/nooa/nemorelaymiddleware.py installs all three kinds to route calls through NeMo Relay (guardrails/ATIF); nemorelayscope(agent, name) wraps install/uninstall. Runnable: examples/quickstart/13nemorelay.py. Test patterns (mutate/short-circuit/ordering): tests/testeventmiddleware.py.

Observers (on)

Fire-and-forget callbacks after an event is recorded — they cannot alter it.

unsub = agent.event_manager.on("Error", lambda e: log.warning("agent error: %s", e.content))
agent.event_manager.on("*", audit)                 # wildcard: every event
  • Useful runtime-only events (never rendered to the model): BeforeTurn / AfterTurn (per generation turn; AfterTurn.isfinal marks method completion) and LLMComplete (tokens, cost, modelname, toolcalls, reasoning metadata per round-trip — emitted precisely so you don't need intercept("llmcall") just to read LLM metrics).
  • Model-visible events (Task, Message, Error, PythonOutput, ...) are observable the same way — see nooa-context-and-state for the full list.
  • The summarizers are the house pattern: subscribe to AfterTurn to schedule work, apply it at the next BeforeTurn (agents/summarization.py:159-160).

InstrumentationHooks (set_hooks)

A Protocol of paired callbacks (nooa.runtime.hooks): before/afteragentcall, before/aftergeneration, before/aftercodeexecution, before/aftermethodinvocation, before/aftertoolexecution, plus the point-in-time onmessagesbuilt. Each before may return a context object that is handed to its after (span/timing state without globals).

from nooa.runtime.hooks import set_hooks

class TimingHooks:
    def before_agent_call(self, agent, method_name, args, kwargs, call_id, parent_call_id, **kw):
        return {"t0": time.perf_counter()}
    def after_agent_call(self, agent, method_name, result, exception, context, **kw):
        print(f"{method_name}: {time.perf_counter() - context['t0']:.2f}s")
    # unimplemented methods are fine — calls are defensive

set_hooks(TimingHooks())   # set_hooks(None) removes
  • Hook exceptions never affect the agent (logged at WARNING); time spent in hooks is metered as tracing overhead.
  • One slot, contextvar-scoped — and tracing uses it. enabletracing() installs OpenInferenceHooks via this same sethooks (tracing/init.py:136-148); calling set_hooks(MyHooks()) afterwards silently replaces tracing (and vice versa). If you need both, delegate to the previous hooks object from yours, or prefer a tracing exporter / on() observers.
  • afteragentcall can receive context=None when agent_call middleware short-circuits before the before-hook ran — handle it.

Pitfalls

  • Don't use hooks for app logic (they're swallowed-exception observational); don't use middleware for metrics you can get from LLMComplete (you'd pay complexity for nothing).
  • AgentCallContext.result uses a not-set sentinel — a short-circuiting agent_call middleware that "returns None" on purpose must still assign ctx.result = None.
  • Middleware lives on the instance's event manager: install in init (after super().init()) or on the constructed agent, not on the class.
  • Keep llm_call middleware fast — it's on the critical path of every turn, and runs again on context-window retries.

Related skills

  • nooa-context-and-state — the event model that on() observes; EventQuery filtering.
  • nooa-capturing-traces — the tracing system that occupies the hooks slot; exporters are usually the right telemetry surface.
  • nooa-codeact-advanced — what execute_python middleware wraps (validator pipeline, restrictions, cells).