nvidia-nemo/labs-oo-agents · Archived

nooa-channels

Reactive input for NOOA agents — Channel/QueueManager for queued and event-mode input, race() turn dispatch, spawn() background jobs with JobHandle, and the bundled producers (monitor a shell command, cron ticks, one-shot timers, file tails).

First seen Aug 13, 2026

Installation

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

Summary

  • Reactive input for NOOA agents — Channel/QueueManager for queued and event-mode input, race() turn dispatch, spawn() background jobs with JobHandle, and the bundled producers (monitor a shell command, cron ticks, one-shot timers, file tails).
  • Use when an agent must react to external input mid-run — user messages, CI output, timers, job completions — or when building an interactive/long-running agent loop.

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 6,371 B
  • docs SUMMARY.md 435 B

History

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

SKILL.md

Channels: Reactive Agent Input

Channel + QueueManager (nooa.runtime.channels) are the producer side of agent input: outside events flow in through named channels; the agent's dispatch loop races them and reacts. Peer of EventManager/ContextManager, but NOT auto-created — you construct it.

from nooa.runtime.channels import QueueManager
from nooa.runtime.producers import monitor, cron

class Watcher(Agent, llm=llm):
    def __init__(self, **kw):
        super().__init__(**kw)
        qm = QueueManager(agent=self, event_manager=self.event_manager)
        self.qm = qm
        self.user_messages_in = qm.queue("user_messages")   # producer side (put)
        self.user_messages = self.user_messages_in.reader   # LLM-facing side (get/snapshot only)
        self.notifications = qm.event("notifications")      # fire-and-forget → prompt

    async def run(self) -> None:                            # pure-Python dispatcher
        self.qm.spawn(monitor("make test"), channel="user_messages", buffer=100)
        while True:
            items = await self.qm.race()                    # block until any channel produces
            if items:                                       # [(channel_name, item)] — queue winner
                await self.handle(items[0])
            # [] means an event-mode put woke us — it's already rendered
            # into the next turn's prompt; just proceed to a turn.

    async def handle(self, item: tuple[str, object]) -> str:
        """React to the new input."""
        ...

Two channel modes

Mode Factory put() does Consumption
queue qm.queue(name) append to a deque, wake one waiter race() in the dispatcher, or agent code drains mid-turn with await self.<chan>.get()
event qm.event(name) add a QueueOutput event to event_manager — value renders inline in the next turn's prompt none — no buffer, no get()
  • Use queue for inputs the agent must consume one at a time (user messages, job results); event for notifications the agent should merely notice ("build finished", status pings).
  • QueueOutput renders source/valuetype/valuepreview to the model; the full value is hidden (repr=False) but reachable from code via event_manager.get(tag).value.
  • Expose the read side to the LLM as channel.reader — a get(timeout=5.0)/status() wrapper without put (readers time out with QueueReadTimeoutError instead of blocking a cell forever).
  • channel.snapshot() peeks without consuming; qm.status() renders a composite status block for all channels (pin it as a dynamic context block so the model sees pending input).

race() — the dispatch primitive

  • Returns a length-1 list [(name, item)] for a queue-mode winner, or [] when an event-mode put woke it (events are already in the prompt; the empty list tells you no queue item was consumed). The list shape is the contract.
  • If multiple puts land on adjacent ticks, the first channel in registration order wins; the other drained items are restored to the head of their channels — nothing is dropped. on_get hooks fire only for the winner.
  • Raises ValueError when no channels are registered (dispatchers treat this as "exit cleanly"); propagates CancelledError after cleaning up its drain tasks.
  • Thread-safe wakeups: producers may put() from another thread (e.g. a UI thread); the manager wakes race() via the owning event loop.

spawn() — background jobs feeding channels

from nooa.runtime.producers import monitor, after, cron, tail, run_job

handle = qm.spawn(monitor("pytest -q"), channel="ci", buffer=100)  # stream stdout lines
qm.spawn(after(300), channel="wakeup")                              # one-shot timer → None
qm.spawn(cron(60), channel="ticks")                                 # yields 1, 2, 3, ... per minute
qm.spawn(tail("app.log"), channel="logs")                           # new lines as they appear
qm.spawn(run_job(some_coro(), job_id="batch-7"), channel="jobs")    # {"job_id": ..., "result": ...}
  • A coroutine job puts its single result on completion; an async generator puts each yielded value.
  • Lifecycle events go to the agent's event log: StreamEnd(channelname=...) when a job finishes, JobError(channelname, errortype, errormessage) on failure — JobError is also put on the data channel so race() wakes the agent to react.
  • JobHandle: .state (running/done/cancelled/failed), await handle.cancel() (awaits generator finally cleanup), .values (buffered outputs — buffer=True unbounded, buffer=N ring of last N, default off).
  • monitor() runs the command in its own process group and kills the whole group on cancel — no orphaned children; stderr is merged into stdout.

Pitfalls

  • QueueManager is hidden from the LLM by default; expose deliberately (spec(self, "queue_manager", hidden=False)) or, better, expose only the reader attributes and keep put/spawn on the Python side.
  • Registration order is priority order for race() — register the highest-priority channel (usually user messages) first.
  • Don't await channel.get() (producer object) from LLM code — give the LLM the .reader, whose get(timeout=...) can't deadlock a cell.
  • The dispatcher belongs in a pure-Python orchestrator method (run() above) — see "Orchestrators are pure Python" in nooa-agent-authoring.
  • spawn() requires the channel to already exist (ValueError otherwise).

Related skills

  • nooa-agent-authoring — the orchestrator pattern the dispatch loop lives in.
  • nooa-context-and-stateQueueOutput/StreamEnd/JobError are events; query them like any others.
  • nooa-middleware-hookson() observers if you only need to react to recorded events, not consume input.