Observability - agbossjs/agboss GitHub Wiki
Observability
Observers see a run's events: workflow, agent, router, tool, model, knowledge base, guardrail, PII
policy, action, channel and judge start / end, and errors. They are registered, not discovered: only
classes listed in @Workflow({ observers: [...] }) are called. Tracing to Langfuse is built in
(Tracing); observers are for everything else (metrics, your own tracer, audit).
The hooks
Each hook is its own interface with a typed payload (root entry agboss):
| Hook interfaces | Payload |
|---|---|
OnWorkflowStart / OnWorkflowEnd |
AppState / the ExecutionOutput and AppState |
OnWorkflowPause / OnWorkflowResume |
WorkflowPauseEvent (pause) / WorkflowResumeEvent (decision) |
OnAgentStart / OnAgentEnd |
AgentStartEvent / AgentEndEvent |
OnRouterStart / OnRouterEnd |
RouterStartEvent / RouterEndEvent |
OnToolStart / OnToolEnd |
ToolStartEvent (tool, agent, arguments) / ToolEndEvent |
OnModelStart / OnModelEnd |
ModelStartEvent / ModelEndEvent (model, usage, cost) |
OnRagStart / OnRagEnd |
RagStartEvent / RagEndEvent |
OnActionStart / OnActionEnd |
ActionStartEvent / ActionEndEvent |
OnGuardrailStart / OnGuardrailEnd |
GuardrailStartEvent / GuardrailEndEvent |
OnPiiPolicyStart / OnPiiPolicyEnd |
PiiPolicyStartEvent / PiiPolicyEndEvent |
OnChannelStart / OnChannelEnd |
ChannelStartEvent / ChannelEndEvent |
OnJudgeStart / OnJudgeEnd |
JudgeStartEvent / JudgeEndEvent |
OnError |
the Error and AppState |
OnRunRejected |
RunRejectedEvent (operation, threadId?, code, message) |
Every event carries the run's state (AppState: runId, threadId, activeNode, …) for
correlation: one runId for the whole run (its resumes included) and the conversation's real
threadId.
onWorkflowStart fires once per run, at execute — a resume does not start the run again, so the
count of starts is the count of runs. A run that pauses (an approval on a channel) emits
onWorkflowPause (event.pause: what it waits for) instead of onWorkflowEnd; app.resume
emits onWorkflowResume (event.decision). onWorkflowEnd / onError fire when the run finishes
or fails, also after a resume. Cancelling a paused run (app.cancel) emits onError with
WorkflowCancelledError. A run with one approval: start → pause → resume → end.
Refused requests
A request the app refuses before a run starts or is located emits onRunRejected and is still
thrown to the caller. RunRejectedEvent carries the operation ("execute", "resume" or
"cancel"), the threadId the request named (absent for an execute without one), the error's
stable code, a message without the input values, and state (the thread only). It never carries
a runId: no run started, and another owner's run is not revealed.
| Refusal | Operation | code |
|---|---|---|
a thread of another owner (ThreadOwnerError) |
execute, resume, cancel |
run.thread-owner |
nothing paused on the thread (NotPausedError) |
resume |
run.not-paused |
an unknown thread (UnknownThreadError) |
execute |
run.unknown-thread |
| a class that is not a start of the workflow | execute |
workflow.not-a-start |
an invalid start input (DtoValidationError) |
execute |
dto.invalid |
For an invalid start input the message names the DTO and its fields (invalid Voucher at code),
never their values; the thrown error keeps the full message. A resume refused after a deploy
(IncompatibleResumeError) is not a refused request: the paused run is known, so it is an onError
with that run's runId and thread, and the run stays paused — resumable by an app whose
onIncompatibleResume policy accepts it. A run whose day's budget is already spent does start
(onWorkflowStart, its own runId) and fails at once with onError (BudgetExceededError).
import { Injectable, type OnRunRejected, type RunRejectedEvent } from "agboss";
@Injectable()
export class Audit implements OnRunRejected {
onRunRejected(event: RunRejectedEvent) {
console.warn(`${event.operation} refused on ${event.threadId ?? "a new thread"}: ${event.code}`);
}
}
Registering an observer
- A class decorated with
@Injectable()(itsdepsinjected like any component). - It implements the hooks it needs.
- It is listed in
@Workflow({ observers }).
The container creates observers at app start. Assembly rejects an observer with no hook
([observer.no-hooks]) or a misspelled one ([observer.unknown-hook], did you mean onToolEnd?). An
observer that throws is reported as a process warning and never fails the run.
Example: a tracer
import {
Injectable,
Workflow,
WorkflowSettings,
chain,
type AgentEndEvent,
type AgentStartEvent,
type AppState,
type ModelEndEvent,
type OnAgentEnd,
type OnAgentStart,
type OnError,
type OnModelEnd,
type WorkflowDefinition,
} from "agboss";
@Injectable()
export class ConsoleTracer implements OnAgentStart, OnAgentEnd, OnModelEnd, OnError {
onAgentStart(event: AgentStartEvent) {
console.log(`[trace] agent ${event.name} started in run ${event.state.runId}`);
}
onAgentEnd(event: AgentEndEvent) {
console.log(`[trace] agent ${event.name} finished in run ${event.state.runId}`);
}
onModelEnd(event: ModelEndEvent) {
console.log(`[trace] ${event.callerName} → ${event.model}: ${String(event.usage.totalTokens)} tokens`);
}
onError(error: Error, state: AppState) {
console.error(`[trace] run ${state.runId} failed`, error);
}
}
@Workflow({
name: "support",
version: "1.0.0",
flow: [chain(ChatWorkflowStart, SupportAgent, ChatWorkflowFinish)],
defaults: {
models: { temperature: 0 },
router: { kind: "jev", model: "typesafe/jev-1.13" },
history: { limit: 5 },
},
observers: [ConsoleTracer],
})
export class Support implements WorkflowDefinition {
settings() {
return WorkflowSettings.builder().build();
}
}