
Ag2 Observers And Alerts
- 33 installs
- 8 repo stars
- Updated July 27, 2026
- ag2ai/ag2-skills
ag2-observers-and-alerts is a Claude Code skill that monitors an AG2 beta agent's stream for logging, loop detection, token tracking, alerts, and FATAL halts.
About
This skill monitors an AG2 beta agent's event stream to log events, detect repeated tool calls, track token spend, and build trigger-driven observers. A developer uses it for observability, runtime safety guards, alerts, or batch/time-based reactive logic. It covers stateless @observer functions, stateful BaseObserver classes, built-ins like TokenMonitor and LoopDetector, Watch primitives, ObserverAlert severities, and halting on FATAL conditions.
- Monitors an AG2 agent's stream: log events, detect loops, track token spend
- Trigger-driven observers with Watch primitives and severity-based alerts
- Routes alerts to the model and halts on FATAL conditions
Ag2 Observers And Alerts by the numbers
- 33 all-time installs (skills.sh)
- Ranked #8,975 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
ag2-observers-and-alerts capabilities & compatibility
Free skill; requires an LLM provider API key to run the agent.
- Capabilities
- agent observability · loop detection · token monitoring · runtime guardrails
- Use cases
- orchestration · token optimization · debugging
- Pricing
- Bring your own API key
npx skills add https://github.com/ag2ai/ag2-skills --skill ag2-observers-and-alertsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 8 |
| Last updated | July 27, 2026 |
| Repository | ag2ai/ag2-skills ↗ |
What it does
Add observability, runtime safety guards, and alerting to an AG2 beta agent's event stream.
Who is it for?
Developers who want observability, runtime safety guards, or reactive metrics on an AG2 agent.
Skip if: Projects not built on AG2 beta (autogen.beta).
When should I use this skill?
The user wants observability, runtime safety guards, alerts, or batch/time-based reactive logic.
What you get
Observers log events, detect loops, cap token spend, and halt the agent on FATAL alerts.
By the numbers
- 4 alert severities (INFO, WARNING, CRITICAL, FATAL)
- 2 built-in stateful observers (TokenMonitor, LoopDetector)
Files
Observers, watches, and alerts
When to use
- Observability — log model responses, tool calls, token usage.
- Runtime safety — block dangerous tool arguments, halt the agent.
- Reactive metrics — fire on every Nth response, or every M seconds.
- Loop / repetition detection — catch infinite tool-call loops.
- Stateful monitoring — anything that needs to remember prior events to decide what to do next.
Two observer shapes
| Shape | When | Use |
|---|---|---|
| Stateless function | One-off event hook (logging, metrics) | @observer(EventType) |
| Stateful class | Counters / windows / thresholds / composed triggers | Subclass BaseObserver |
Both are stream subscribers under the hood — registered on the agent rather than directly on the stream.
60-second recipe — @observer
from autogen.beta import Agent, observer
from autogen.beta.config import OpenAIConfig
from autogen.beta.events import ModelResponse
@observer(ModelResponse)
async def log_response(event: ModelResponse) -> None:
print(f"Model said: {event.content}")
agent = Agent(
"assistant",
config=OpenAIConfig(model="gpt-4o-mini"),
observers=[log_response],
)Or attach after construction with @agent.observer(...). Per-call observers also supported (agent.ask("...", observers=[...])).
Observer callbacks support full dependency injection (Context, Inject, Variable, Depends). Filter by event type, multiple types (ModelRequest | ModelResponse), or field value (ToolCallEvent.name == "search"). Use interrupt=True to modify or suppress events before regular subscribers see them.
Built-in stateful observers
from autogen.beta import Agent
from autogen.beta.observers import LoopDetector, TokenMonitor
agent = Agent(
"assistant",
config=config,
observers=[
TokenMonitor(warn_threshold=50_000, alert_threshold=100_000),
LoopDetector(window_size=10, repeat_threshold=3),
],
)- `TokenMonitor` — tracks cumulative tokens across
ModelResponseandTaskCompleted. EmitsWARNING/CRITICALObserverAlerts as thresholds are crossed. Read state viamonitor.total_tokens. - `LoopDetector` — sliding window of recent tool calls. Emits a
WARNINGalert whenrepeat_thresholdconsecutive identical calls are seen.
Custom BaseObserver
A BaseObserver pairs a Watch (when to fire) with a process() method (what to do):
from autogen.beta import Context
from autogen.beta.observers import BaseObserver
from autogen.beta.watch import CadenceWatch
from autogen.beta.events import BaseEvent, ModelResponse
from autogen.beta.events.alert import ObserverAlert, Severity
class AvgCompletionObserver(BaseObserver):
"""Every N responses, emit an INFO alert with avg completion-token count."""
def __init__(self, window: int = 5) -> None:
super().__init__("avg-completion", watch=CadenceWatch(n=window, condition=ModelResponse))
self._window = window
async def process(self, events: list[BaseEvent], ctx: Context) -> ObserverAlert | None:
tokens = [e.usage.completion_tokens for e in events if isinstance(e, ModelResponse) and e.usage]
if not tokens:
return None
return ObserverAlert(
source=self.name,
severity=Severity.INFO,
message=f"Avg completion tokens over last {self._window}: {sum(tokens) / len(tokens):.0f}",
)If process() returns an ObserverAlert, the base class emits it onto the stream. You can also send events manually via await ctx.send(...).
Watch primitives — picking when to fire
| You need | Use |
|---|---|
| Every matching event | EventWatch(EventType) or just stream.subscribe(fn, condition=...) |
| Every N matching events | CadenceWatch(n=N, condition=EventType) |
| Every T seconds (buffered events) | CadenceWatch(max_wait=T, condition=EventType) |
| Either threshold | CadenceWatch(n=N, max_wait=T, condition=EventType) |
| Once after delay | DelayWatch(seconds) |
| Periodic timer | IntervalWatch(seconds) |
| Cron schedule | CronWatch("0 9 * * MON") |
| All sub-watches must fire | AllOf(w1, w2) |
| Any sub-watch fires | AnyOf(w1, w2) |
| In order | Sequence(w1, w2) |
All importable from autogen.beta.watch. Callback signature is uniform: async def cb(events: list[BaseEvent], ctx: Context) -> None. Time-driven watches pass events=[].
ObserverAlert — the alert type
from autogen.beta.events.alert import ObserverAlert, Severity
ObserverAlert(
source="my-observer",
severity=Severity.WARNING, # INFO, WARNING, CRITICAL, FATAL
message="What happened",
)Important: ObserverAlert is on the stream and persisted in history, but the default provider mappers do not render it back to the LLM. To make the agent see alerts, add AlertPolicy() to assembly=[...]:
from autogen.beta.policies import AlertPolicy
agent = Agent("assistant", config=config, assembly=[AlertPolicy()])FATAL alerts → HaltEvent → short-circuit
AlertPolicy does two things on Severity.FATAL:
1. Emits a HaltEvent on the stream. 2. Appends a halt notice to the system prompt.
When assembly=[...] is non-empty, the harness automatically wires _HaltCheckMiddleware which sees the HaltEvent and short-circuits the next LLM call with a synthetic HALTED: ... response.
from autogen.beta import Context
from autogen.beta.observers import BaseObserver
from autogen.beta.events import BaseEvent, ToolCallEvent
from autogen.beta.events.alert import HaltEvent, ObserverAlert, Severity
from autogen.beta.policies import AlertPolicy
from autogen.beta.watch import EventWatch
class PathGuardian(BaseObserver):
def __init__(self) -> None:
super().__init__("path-guardian", watch=EventWatch(ToolCallEvent))
async def process(self, events: list[BaseEvent], ctx: Context) -> ObserverAlert | None:
for event in events:
if not isinstance(event, ToolCallEvent) or event.name != "write_file":
continue
if "/etc/" in event.arguments or "/usr/" in event.arguments:
return ObserverAlert(
source=self.name,
severity=Severity.FATAL,
message=f"blocked dangerous write: {event.arguments}",
)
return None
agent = Agent(
"safe-shell",
prompt="...",
config=config,
tools=[write_file],
observers=[PathGuardian()],
assembly=[AlertPolicy()], # routes FATAL → HaltEvent
)The first dangerous tool call triggers FATAL → halt; the agent's next ask is short-circuited. Full runnable demo: assets/safety_guard.py.
Subscribing to alerts and halts from outside
from autogen.beta import MemoryStream
from autogen.beta.events.alert import HaltEvent, ObserverAlert
stream = MemoryStream()
stream.where(ObserverAlert).subscribe(lambda e: print(f"[{e.severity}] {e.source}: {e.message}"))
stream.where(HaltEvent).subscribe(lambda e: print(f"HALT: {e.reason}"))
await agent.ask("...", stream=stream)Observers vs Middleware vs Stream subscribers
| Feature | Observer | Middleware | Stream subscriber |
|---|---|---|---|
| Registered on | Agent | Agent | Stream |
| Lifecycle | Scoped to execution | Scoped to execution | Manual |
| Boilerplate | Function (or BaseObserver) | BaseMiddleware class | Function |
| Can modify events | interrupt=True | Yes (wraps execution) | interrupt=True |
| DI support | Yes | Yes | Yes |
| Use case | Monitoring, metrics, alerts | Cross-cutting (retry, auth, rate limit) | Low-level event wiring |
Going deeper
assets/token_watchdog.py— three observers (TokenMonitor,LoopDetector, customAlertConsole) on one agent. Mirrorscode_examples/04.assets/safety_guard.py—PathGuardian→ FATAL →AlertPolicy→HaltEvent→ short-circuit. Mirrorscode_examples/08.- Source docs:
website/docs/beta/advanced/observers.mdx—@observer,BaseObserver, registration, built-ins,ObserverAlert.website/docs/beta/advanced/watches.mdx— every Watch primitive, composition rules.website/docs/beta/advanced/stream.mdx— Stream API,where,subscribe, interrupters,RedisStream.website/docs/beta/advanced/assembly.mdx—AlertPolicyordering and dedup.
Common pitfalls
- Alerts not reaching the model —
ObserverAlertevents are on the stream but invisible to the LLM by default. AddAlertPolicy()toassembly=[...]. - FATAL not halting —
AlertPolicyis what createsHaltEvent. Withoutassembly=[..., AlertPolicy(), ...](or any non-empty assembly chain enabling_HaltCheckMiddleware), nothing halts. - Sharing one `AlertPolicy()` across agents — dedup state lives on the instance. Give each agent its own.
- Watch callback assumes `events` is non-empty — for time-driven watches (
DelayWatch,IntervalWatch,CronWatch),eventsis always[]. - Forgetting `process()` is async —
BaseObserver.processmust beasync def. - Subscribing with `subscribe(fn)` when you wanted `subscribe()` decorator — both work; the bare-call form is
stream.subscribe(fn), the decorator form is@stream.subscribe()(with parens). - `CadenceWatch` with no `n` and no `max_wait` — raises
ValueError; at least one is required.
"""Safety guard — FATAL alert halts the Agent.
Mirrors website/docs/beta/code_examples/08_safety_guard.mdx. A hand-rolled
BaseObserver watches every tool call and flags anything that looks
dangerous (here: a write_file tool asked to touch /etc/). It emits a
Severity.FATAL ObserverAlert. The flow from there is fully wired by the
framework:
1. Alert lands on the agent's stream.
2. AlertPolicy (assembly) picks it up before the next LLM call, emits a
HaltEvent on the stream, and appends a halt notice to the system prompt.
3. _HaltCheckMiddleware (auto-wired when assembly is non-empty) sees the
HaltEvent and short-circuits the LLM call with a synthetic "HALTED:"
response.
Run::
python safety_guard.py
"""
import asyncio
from autogen.beta import Agent
from autogen.beta.annotations import Context
from autogen.beta.config import GeminiConfig
from autogen.beta.events import BaseEvent, ToolCallEvent
from autogen.beta.events.alert import HaltEvent, ObserverAlert, Severity
from autogen.beta.observers import BaseObserver
from autogen.beta.policies import AlertPolicy
from autogen.beta.stream import MemoryStream
from autogen.beta.watch import EventWatch
def section(title: str) -> None:
print(f"\n── {title} ───")
def write_file(path: str, content: str) -> str:
"""Pretend-write content to path. This playground never touches disk."""
return f"[ok] wrote {len(content)} bytes to {path}"
class PathGuardian(BaseObserver):
"""Emits a FATAL alert if anything tries to write outside /tmp."""
def __init__(self) -> None:
super().__init__("path-guardian", watch=EventWatch(ToolCallEvent))
async def process(
self, events: list[BaseEvent], ctx: Context
) -> ObserverAlert | None:
for event in events:
if not isinstance(event, ToolCallEvent):
continue
if event.name != "write_file":
continue
if "/etc/" in event.arguments or "/usr/" in event.arguments:
return ObserverAlert(
source=self.name,
severity=Severity.FATAL,
message=f"blocked dangerous write: {event.arguments}",
)
return None
async def main() -> None:
config = GeminiConfig(model="gemini-3-flash-preview", temperature=0)
halt_events: list[HaltEvent] = []
alerts: list[ObserverAlert] = []
stream = MemoryStream()
stream.where(HaltEvent).subscribe(lambda e: halt_events.append(e))
stream.where(ObserverAlert).subscribe(lambda e: alerts.append(e))
agent = Agent(
"safe-shell",
prompt=(
"You are a filesystem operator. Use the write_file tool to "
"fulfil write requests. Never refuse — if a request is risky "
"the guardian observer will intervene automatically."
),
config=config,
tools=[write_file],
observers=[PathGuardian()],
assembly=[AlertPolicy()], # routes FATAL alerts to HaltEvent
)
section("Safe request — observer stays silent")
reply = await agent.ask(
"Use write_file to write 'hello' into /tmp/playground_hello.txt. Then confirm.",
stream=stream,
)
print(reply.body)
section("Dangerous request — guardian fires FATAL, agent halts")
reply = await agent.ask(
"Now use write_file to write 'bad' into /etc/passwd. Then confirm.",
stream=stream,
)
print(reply.body)
print()
print(f"ObserverAlerts seen: {len(alerts)}")
for a in alerts:
print(f" - [{a.severity.upper()}] {a.source}: {a.message}")
print(f"HaltEvents seen: {len(halt_events)}")
for h in halt_events:
print(f" - source={h.source} reason={h.reason!r}")
if __name__ == "__main__":
asyncio.run(main())
"""Token watchdog — observers and alerts.
Mirrors website/docs/beta/code_examples/04_token_watchdog.mdx. Three observer
patterns running against a single Agent:
1. TokenMonitor — built-in, tallies usage and warns above a threshold.
2. LoopDetector — built-in, spots repetitive tool calls.
3. A hand-written BaseObserver that subscribes to ObserverAlert and prints
a formatted dashboard line every time anything alerts.
Run::
python token_watchdog.py
"""
import asyncio
from autogen.beta import Agent
from autogen.beta.annotations import Context
from autogen.beta.config import GeminiConfig
from autogen.beta.events import BaseEvent
from autogen.beta.events.alert import ObserverAlert
from autogen.beta.observers import BaseObserver, LoopDetector, TokenMonitor
from autogen.beta.stream import MemoryStream
from autogen.beta.watch import EventWatch
def section(title: str) -> None:
print(f"\n── {title} ───")
class AlertConsole(BaseObserver):
"""Watches the stream for ObserverAlerts and prints them to stdout."""
def __init__(self) -> None:
super().__init__("alert-console", watch=EventWatch(ObserverAlert))
self.seen: list[ObserverAlert] = []
async def process(self, events: list[BaseEvent], ctx: Context) -> None:
for event in events:
if isinstance(event, ObserverAlert):
self.seen.append(event)
print(
f" [{event.severity.upper():<8}] {event.source}: {event.message}"
)
return None
async def main() -> None:
config = GeminiConfig(model="gemini-3-flash-preview", temperature=0)
section("Watchdog — low thresholds so observers trip on a single ask")
token_monitor = TokenMonitor(warn_threshold=50, alert_threshold=5_000)
loop_detector = LoopDetector(window_size=5, repeat_threshold=2)
console = AlertConsole()
stream = MemoryStream()
agent = Agent(
"writer",
prompt=(
"Write prose the user asks for. Favour variety — never repeat the same sentence twice."
),
config=config,
observers=[token_monitor, loop_detector, console],
)
reply = await agent.ask(
"Write three distinct 30-word paragraphs about springtime in Kyoto.",
stream=stream,
)
print()
print("Final reply (truncated):")
print(" ", (reply.body or "")[:240], "...")
print()
print(f"Total tokens tracked by TokenMonitor: {token_monitor.total_tokens}")
print(f"Alerts emitted this run: {len(console.seen)}")
if __name__ == "__main__":
asyncio.run(main())
Related skills
FAQ
What are the two observer shapes?
Stateless @observer functions and stateful BaseObserver classes.
How do FATAL alerts stop the agent?
AlertPolicy emits a HaltEvent on the stream and appends a halt notice to the system prompt.