Skip to content

aimu.events

Run events: a frozen-dataclass event union plus one Callable[[RunEvent], None] sink. Attach a sink to a client, an Agent, an OrchestratorAgent (assemble(..., events=...)), or a workflow's from_client(...) to see what a run actually did. See how-to: observe a run.

Sink and dispatch

aimu.events.EventSink module-attribute

EventSink = Callable[[RunEvent], None]

One callable taking one :class:RunEvent. Its return value is ignored and an exception it raises is logged, not propagated (see :func:emit). It must be thread-safe: with concurrent_tool_calls=True tool events arrive from concurrent workers in nondeterministic order.

aimu.events.emit

emit(sink: Optional[EventSink], event: RunEvent) -> None

Deliver event to sink, logging rather than propagating its failure.

Observation must not change what it observes: a sink that raises would otherwise break a run that was working. Same contract as the SubagentObserver display hook.

A sink must be thread-safe. With concurrent_tool_calls=True the tool-loop engine dispatches a turn's tool calls from a ThreadPoolExecutor (sync) or an asyncio.TaskGroup (async), so :class:ToolCalled / :class:ToolDenied are emitted concurrently and their order is nondeterministic -- a sink that appends to a plain list, writes a file, or accumulates per-tool state needs its own lock. Turn and run events are emitted from the calling thread and stay ordered.

aimu.events.log_events

log_events(target: Logger, level: int = logging.INFO) -> EventSink

A sink that writes one line per event.

The shortest path to the comprehension payoff -- attach it and watch what actually happened -- and the sink the docs lead with.

Events

aimu.events.RunEvent dataclass

RunEvent(agent: Optional[str] = None, iteration: int = 0)

Base for every event.

agent and iteration mirror :class:~aimu.models.StreamChunk's fields of the same name, so one sink can attribute events correctly inside a nested workflow.

aimu.events.RunStarted dataclass

RunStarted(agent: Optional[str] = None, iteration: int = 0, task: str = '')

Bases: RunEvent

A Runner (an agent or a workflow) began a run.

Fires once at the top of run(), before the first model turn, and is paired with exactly one :class:RunFinished. task is the task string as the caller passed it.

aimu.events.ModelTurnStarted dataclass

ModelTurnStarted(agent: Optional[str] = None, iteration: int = 0, model: str = '', message_count: int = 0, tool_names: tuple[str, ...] = ())

Bases: RunEvent

One model request is about to be issued.

Fires inside chat() / generate(), before the provider is called, and is paired with exactly one :class:ModelTurnFinished even when the request raises.

message_count is how many messages this request carries (including the user turn it is about to append, and the system message when this turn seeds one); it is 1 for a stateless generate(), which sends only the prompt. tool_names are the tools advertised on this turn, not the ones called.

aimu.events.RequestPrepared dataclass

RequestPrepared(agent: Optional[str] = None, iteration: int = 0, provider: str = '', model: str = '', payload: Any = None)

Bases: RunEvent

The payload as it goes to the provider, after every adaptation AIMU applies.

This is the event that makes "when a model surprises you, the surprise should be the model's" checkable: between a caller's chat() and the wire sit the four-tier generate_kwargs merge, the GENERATE_KWARG_SUPPORT renames and drops, thinking resolution, strip_inert_keys, and provider format adaptation. Without this, none of it is visible at runtime.

The payload is the request, unredacted -- it contains whatever the caller put in the conversation. Redacting here would reintroduce the hiding this exists to remove; a sink that ships events off the machine is the right place to filter.

aimu.events.ModelTurnFinished dataclass

ModelTurnFinished(agent: Optional[str] = None, iteration: int = 0, model: str = '', text: Optional[str] = None, usage: Optional[dict] = None, duration_s: float = 0.0, error: Optional[BaseException] = None)

Bases: RunEvent

The model request that a :class:ModelTurnStarted announced has ended.

Fires for every started turn, including one that failed: error is then the exception (ContextOverflowError, ModelConnectionError, a provider 4xx, ...) and text / usage are None. A sink pairing started/finished can therefore close its span unconditionally.

On a streamed turn this fires when the stream is actually drained (or abandoned), not when it was created, so usage -- which only populates at the end -- is real and duration_s covers the whole stream. text is the concatenated GENERATING content on a streamed turn, and the returned string on a non-streamed one.

Not emitted at all on the schema= structured-output path: chat() / generate() return before :class:ModelTurnStarted is emitted when schema is set, since that path makes exactly one call and reports itself a different way (see Agent._structured_run_events / :class:RunFinished). There is therefore no started turn to leave unpaired there either.

aimu.events.ToolCalled dataclass

ToolCalled(agent: Optional[str] = None, iteration: int = 0, name: str = '', arguments: dict = dict(), result: Optional[str] = None, error: Optional[str] = None, duration_s: float = 0.0)

Bases: RunEvent

A tool the model asked for was dispatched.

Fires after the call returns, for every outcome the model gets to see: a normal result, a tool that raised (error set, result carrying the message handed back to the model), invalid arguments, and a name the model invented that matches no tool. It does not fire for a call a tool_approval policy refused -- that is :class:ToolDenied.

arguments is what the model passed, before AIMU's coercion. With concurrent_tool_calls=True these are emitted from worker threads (sync) or concurrent tasks (async): see :func:emit on sink thread-safety and ordering.

aimu.events.ToolDenied dataclass

ToolDenied(agent: Optional[str] = None, iteration: int = 0, name: str = '', arguments: dict = dict())

Bases: RunEvent

A tool_approval policy refused this call.

Fires instead of :class:ToolCalled, before the tool runs: nothing was executed. The model still sees a tool message saying the call was not approved.

aimu.events.ContextCompacted dataclass

ContextCompacted(agent: Optional[str] = None, iteration: int = 0, dropped: list = list(), before_tokens: int = 0, after_tokens: int = 0)

Bases: RunEvent

Conversation history was rewritten by an Agent's compaction= callable.

Fires once per compaction, after the rewrite, before the next model turn.

dropped is the removed messages, not a count, so a caller who wants the discarded turns still has them. They are copies, so a later in-place edit of client.messages (the provenance tag an agent loop adds, say) cannot mutate an already-emitted event.

before_tokens / after_tokens are AIMU's own default token estimate (see :func:aimu.context.count_tokens), not a measurement of whatever the compaction callable itself counted to decide what to drop. A callable that used a real tokenizer, a word count, or any other budget will disagree with these numbers -- that is stated rather than hidden, since AIMU cannot see inside an opaque callable to know what it actually counted. Treat them as rough orientation, not a claim about the number that drove the decision.

aimu.events.RunFinished dataclass

RunFinished(agent: Optional[str] = None, iteration: int = 0, result: Optional[str] = None, error: Optional[BaseException] = None)

Bases: RunEvent

A Runner finished, successfully or not.

Fires in a finally, so it is emitted even when the run raised: error is the exception in that case and result is None.

result is also None on a streamed run (run(stream=True)): the chunks go to the caller as they arrive and the runner never assembles a final string, so there is no result to report. Read the text off the GENERATING chunks instead.