A tool-using agent can return a confident final answer after taking the wrong path. It can call the wrong command, repeat a tool, stop because of a guard, or create a file that does not run. If the application records only the final message, all of those executions look roughly the same.
That is the production problem with agent observability: the result matters, but so does the path that produced it.
Working rule: observe the loop while it runs, inspect its terminal state afterward, and verify important side effects independently.
A concrete coding-agent workflow
The runnable InstructorPHP example Coding Agent Creates and Repairs an Example gives us a useful test case.
The agent must:
- read an existing InstructorPHP example;
- create a materially different example in an isolated
/tmpworkspace; - run PHP syntax and execution checks;
- repair failures with an exact edit;
- finish only after the generated program prints
Example status: verified.
This is more than one model response. It is a loop with four tools—read,
bash, write, and edit—plus step, token, and execution-time guards.
If the only output were:
Done. The example was generated and verified.
we would still not know whether the agent read the reference, where it wrote the file, which commands it ran, whether an edit happened, or why the loop stopped.
What can go wrong
Agent failures occur at different layers. Each layer needs different evidence.
| Failure | What a final answer hides | Evidence you need |
|---|---|---|
| The agent calls the same tool repeatedly | The loop may sound productive until a limit stops it. | Step and continuation events. |
| A command or file operation fails | The model may summarize the failure incorrectly or omit it. | Tool start, completion, duration, and error events. |
| A guard stops execution | The agent may never reach a natural final answer. | Terminal status and stop reason. |
| The agent claims a file works | A generated artifact can still be missing or invalid. | A host-side postcondition check. |
| Debug output contains sensitive arguments | Prompt, path, command, or file content can leak into logs. | Explicit argument visibility, truncation, redaction, and retention policy. |
This is why “log the final response” is not an observability strategy.
Attach a console observer to the loop
For local development and CLI workflows, AgentEventConsoleObserver is the
smallest useful starting point:
use Cognesy\Agents\Events\Support\AgentEventConsoleObserver;
$observer = new AgentEventConsoleObserver(
useColors: true,
showTimestamps: true,
showContinuation: true,
showToolArgs: true,
);
$loop = (new DefinitionLoopFactory($capabilities))
->instantiateAgentLoop($definition)
->wiretap($observer->wiretap());
The observer listens to the agent event stream. It does not become part of the agent's decision policy. The loop still owns inference, tool execution, state transitions, and stop evaluation; the observer formats what happened.
The important options are operational choices:
| Option | Useful when | Caution |
|---|---|---|
showTimestamps |
You need to see ordering and time gaps. | Timestamps are not a replacement for durable trace correlation. |
showContinuation |
You need to know why the loop continued or stopped. | A continuation decision explains loop control, not business correctness. |
showToolArgs |
You are debugging paths, commands, and tool payloads. | Disable it or redact upstream when arguments may contain secrets or customer data. |
maxArgLength |
Large arguments would overwhelm the console. | Truncation limits volume; it is not redaction. |
Read the lifecycle, not just the lines
A shortened trace from the verified coding-agent run looked like this:
[EXEC] Execution started [messages=1, tools=4]
[STEP] Step 1 started [messages=1]
[TOOL] Calling read {path=.../examples/A01_Basics/Basic/run.php, offset=0, limit=300}
[TOOL] Tool read completed [duration=61ms]
[STEP] Step 1 completed [has_tool_calls, finish=tool_calls, tokens=1700]
[EVAL] Continuation evaluated [decision=CONTINUE, reason=none]
[TOOL] Calling write {path=/tmp/instructor-php/agent-test/.../run.php, content=...}
[TOOL] Calling bash {command=php -l ... && php ...}
[TOOL] Calling edit {old_string=const EXAMPLE_STATUS = 'draft';, new_string=const EXAMPLE_STATUS = 'verified';}
[TOOL] Calling bash {command=php /tmp/instructor-php/agent-test/.../run.php}
[EVAL] Continuation evaluated [decision=STOP, reason=none]
[DONE] Execution completed [status=completed, steps=6, tokens=...]
One verified run completed in six steps. A different provider response may take a different path, so the step count is evidence from that run, not a guarantee.
The labels answer separate questions:
| Label | Question it answers |
|---|---|
EXEC |
What resources and initial context did the execution start with? |
STEP |
Which inference/tool cycle is active, and how did it finish? |
TOOL |
Which capability was called, with what visible arguments, and did it succeed? |
EVAL |
Did loop policy decide to continue or stop, and for what reason? |
DONE / FAIL |
What terminal status, step count, usage, or error ended the execution? |
That separation is useful during incidents. A failed bash call is different
from a completed tool call followed by a guard stop. Both may produce no useful
final response, but they require different repairs.
Keep the terminal state
Live events explain the path. The returned AgentState is the authoritative
terminal result for application code:
$final = $loop->execute($state);
if ($final->status()->value !== 'completed') {
throw new RuntimeException(
"Agent stopped with status: {$final->status()->value}",
);
}
$response = $final->finalResponse()->toString();
Do not infer completion from the presence of text. Check the execution status. An agent may have emitted useful intermediate content and still finish as failed, stopped, or cancelled.
The distinction gives the application two clean responsibilities:
- event listeners handle live observation;
- terminal state handling decides what the application accepts after execution.
Verify the side effect outside the agent
Completed execution means the loop reached a completed state. It does not prove that an external side effect satisfies your business postcondition.
The coding example therefore checks the generated artifact after the loop:
$generatedExample = $workspace.'/run.php';
if (! is_file($generatedExample)) {
throw new RuntimeException("Agent did not create: {$generatedExample}");
}
$verificationCommand = 'php '.escapeshellarg($generatedExample);
$verificationOutput = BashTool::inDirectory($projectRoot)(
$verificationCommand,
);
if (! str_contains($verificationOutput, 'Example status: verified')) {
throw new RuntimeException(
"Generated example is not verified:\n{$verificationOutput}",
);
}
This verification is deliberately outside the agent's reasoning loop. The application does not accept “I ran it” as proof. It checks that the file exists, executes it, and tests the required output.
For another workflow, the postcondition might be:
- a database record exists with an expected state;
- a generated document passes schema validation;
- a deployment plan contains no destructive action;
- a customer-facing draft exists but has not been sent;
- a sandboxed command exits successfully.
The practical gain: events explain what the agent attempted; postconditions decide whether the application accepts the result.
Choose the observation surface by audience
The console observer is useful evidence, but it is not the whole production observability stack.
| Surface | Best use | Boundary |
|---|---|---|
AgentEventConsoleObserver |
Local development, examples, CLI troubleshooting | Human-readable and process-local; not durable monitoring. |
onEvent() / wiretap() |
Application logs, counters, audit records, custom reactions | Your listener owns filtering, redaction, storage, and failure handling. |
AgentEventBroadcaster |
SSE, WebSocket, Redis, or UI progress updates | Produces transport-friendly envelopes but does not provide the transport or persist state. |
AgentsTelemetryProjector |
Execution, step, tool, and subagent traces in a telemetry backend | Requires a telemetry hub, exporter, shared event dispatcher, and an explicit data policy. |
Terminal AgentState |
Final application decision and response handling | Describes the run; it does not validate external side effects for you. |
For interactive applications, AgentEventBroadcaster can expose stable envelope
types such as agent.status, agent.step.started, agent.tool.started,
agent.tool.completed, and agent.step.completed. Streamed text chunks require
streaming to be enabled in the underlying inference request; attaching a
broadcaster alone does not create them.
For remote traces, the repository includes runnable agent telemetry examples for
Logfire
and
Langfuse.
Those examples combine AgentsTelemetryProjector for agent lifecycle events,
PolyglotTelemetryProjector for nested LLM work, and
HttpClientTelemetryProjector for HTTP activity.
The broader distinction between events, logs, and traces is covered in Observability, Logging, and Telemetry.
A practical operating model
Start with the evidence required to answer an operational question:
- During development, attach
AgentEventConsoleObserverand keep continuation and tool visibility on. - At the application boundary, check terminal status instead of trusting the final message.
- For consequential tools, define a postcondition and verify it outside the agent loop.
- For user interfaces, broadcast selected lifecycle envelopes rather than parsing console text.
- For production operations, project the shared event stream into logs, metrics, or telemetry with correlation IDs and bounded retention.
- For sensitive workflows, disable raw tool arguments by default and add deliberate redaction before events leave the process.
The business value is not a more colorful terminal. It is shorter incident diagnosis, safer automation, and a reviewable record of how an agent reached a result.
When an agent can call tools with side effects, “what did it answer?” is only one question. You also need to know what it attempted, what the loop decided, how it ended, and whether the world actually changed in the way your application required.