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:

  1. read an existing InstructorPHP example;
  2. create a materially different example in an isolated /tmp workspace;
  3. run PHP syntax and execution checks;
  4. repair failures with an exact edit;
  5. 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:

  1. During development, attach AgentEventConsoleObserver and keep continuation and tool visibility on.
  2. At the application boundary, check terminal status instead of trusting the final message.
  3. For consequential tools, define a postcondition and verify it outside the agent loop.
  4. For user interfaces, broadcast selected lifecycle envelopes rather than parsing console text.
  5. For production operations, project the shared event stream into logs, metrics, or telemetry with correlation IDs and bounded retention.
  6. 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.