Based Mdels

Agent Loop docs

agentloop.observer

An observer is an object that receives callbacks as the agent loop runs. The loop calls it before and after model requests and local tool calls, when a request is retried, and when a conversation ends. These callbacks let an application record what happened while the conversation is still in progress.

Pass an observer to Agent or run_conversation() with observer=.... For example, an observer can count model responses, log tool activity, or send timing data to a tracing or metrics system. The ResponseCounter below shows the smallest useful case: it increments a counter each time a model response arrives. Subclass NullObserver to implement only the callbacks your application needs.

LoopObserver

LoopObserver is the callback interface accepted by the observer= parameter. It is a Python Protocol: it describes the methods an observer can provide and the values each method receives. You normally create an observer by subclassing NullObserver and overriding the callbacks you need, as ResponseCounter does below.

conversation() provides a context around the whole run. The on_* methods receive individual events inside that context. The last six hooks in the table are optional at runtime; if a custom observer omits them, the loop skips them. NullObserver supplies no-op implementations of every hook.

MethodCalled when
conversation() -> AbstractContextManager[None]Enters a context covering one complete conversation
on_request(request: ModelRequest)Immediately before a provider call
on_response(response: ModelResponse)After a successful provider call
on_retry(attempt: int, error: Exception)Before retrying a provider failure; attempt is the failed attempt number
on_tool_start(call: ToolCallPart)Before a local tool call is handled, including a call to an unknown tool
on_tool_end(call: ToolCallPart, result: ToolResultPart)After that call produces a result, including an error result
on_conversation_start(context: ConversationContext)Once, after conversation() is entered, with the IDs and limits; optional
on_conversation_end(summary: ConversationSummary)Once per conversation, on success or failure, inside conversation(); optional
on_budget_warning(kind: BudgetKind, used: float, limit: float)A configured share of the iteration or deadline limit has been used; optional
on_stall(tool_name: str, count: int)A tool repeated the same call to the configured threshold; optional
on_model_error(error: Exception)A model call failed after its final attempt, before the exception leaves the loop; optional
on_text_delta(text: str)Reserved for streaming; unused by the current loop

An observer can use conversation() to create and clean up state for one conversation. on_response can run more than once in a conversation when the model calls tools; each successful model call produces a response event. on_conversation_end runs once for the whole conversation.

on_model_error includes failures that are not retryable, so it can fire without any on_retry event.

NullObserver

Implements every protocol method as a no-op and returns nullcontext() from conversation(). Subclass it when only some events matter.

agentloop supports Anthropic, OpenAI, OpenRouter, and Modal/vLLM. This example uses OpenAI. Install its support and set the provider credentials:

python -m pip install 'based-models-agentloop[openai-compat]'
export LLM_PROVIDER=openai
export OPENAI_API_KEY='your-key'

Count the successful model responses in one agent run:

import os

from agentloop import Agent, NullObserver
from agentloop.provider import ModelResponse


class ResponseCounter(NullObserver):
    def __init__(self) -> None:
        self.count = 0

    def on_response(self, response: ModelResponse) -> None:
        self.count += 1


observer = ResponseCounter()
with Agent.from_env(os.environ, tools=[], observer=observer) as agent:
    answer = agent.run("How many days are in a week? Reply with one digit.")

print(answer)
print(observer.count)

Output:

7
1

on_response runs after the provider returns. The counter records one response for this conversation.

ConsoleObserver

Subclasses NullObserver. on_response() prints nonblank text parts; on_tool_start() and on_tool_end() print tool activity; on_retry() prints retry details. Because it prints the final answer, do not also print Agent.run() unless duplicate output is wanted.

MultiObserver

MultiObserver(*observers)

Calls each observer in argument order. conversation() starts them in argument order and closes them in reverse order. A failed on_* callback in one observer does not prevent the others from receiving the event. An exception from a conversation() context still propagates.

GuardedObserver

GuardedObserver(inner)

Uses the wrapped observer's conversation() context directly, so exceptions from that context propagate. All on_* methods catch and log exceptions from the wrapped observer so reporting failures do not interrupt agent work.

guarded

guarded(observer: LoopObserver | None) -> LoopObserver

Returns NullObserver for None. It leaves an existing GuardedObserver or exact NullObserver unchanged and wraps other observers. Calling it more than once does not add more wrappers.