Execution Tracing¶
Agents can feel like black boxes — you know what went in and what came out, but not what happened in between. AgentTrace records the complete execution trajectory: every observation, every LLM decision, every tool call.
If your agent uses Phase Annotation (self.snapshot()), the trace organizes steps by phase. If not, all steps are still recorded — they just appear as flat "steps" without phase grouping. Either way, you get full visibility.
In this tutorial, we'll run a form-filling agent in amphiflow mode with tracing enabled, then inspect the trace to understand exactly what happened — including how a workflow failure triggered an agent fallback.
Initialize¶
import os
model_name = os.environ.get("MODEL_NAME")
api_key = os.environ.get("API_KEY")
api_base = os.environ.get("BASE_URL")
from bridgic.llms.openai import OpenAILlm, OpenAIConfiguration
llm = OpenAILlm(
api_key=api_key,
api_base=api_base,
timeout=120,
configuration=OpenAIConfiguration(
model=model_name,
temperature=0.0,
max_tokens=16384,
),
)
from bridgic.core.agentic.tool_specs import FunctionToolSpec
state = {"captcha_shown": False, "form_submitted": False}
async def fill_field(field_name: str, value: str) -> str:
"""Fill a form field with a value"""
return f"Filled '{field_name}' with '{value}'"
async def click_button(button_name: str) -> str:
"""Click a button on the page"""
if button_name == "submit" and not state["captcha_shown"]:
state["captcha_shown"] = True
raise RuntimeError("CAPTCHA detected! Submit blocked.")
state["form_submitted"] = True
return f"Clicked '{button_name}' — success"
async def solve_captcha() -> str:
"""Solve the CAPTCHA challenge"""
return "CAPTCHA solved successfully"
fill_field_tool = FunctionToolSpec.from_raw(fill_field)
click_button_tool = FunctionToolSpec.from_raw(click_button)
solve_captcha_tool = FunctionToolSpec.from_raw(solve_captcha)
We have three tools:
fill_field— fills a form field with a given value.click_button— clicks a button. The first submit attempt raises aRuntimeErrorto simulate a CAPTCHA challenge.solve_captcha— solves the CAPTCHA challenge.
The state dictionary simulates a real-world scenario: the first time we try to submit the form, a CAPTCHA appears. After solving it, subsequent submissions succeed.
Enabling Execution Tracing¶
Enable tracing by passing trace_running=True to arun(). The framework records every step into a structured AgentTrace object.
Let's build a form-filling agent in amphiflow mode: the workflow defines the happy path (fill fields → submit), and the agent handles errors when the workflow fails. We won't add any phase annotations here — the trace still captures everything.
from bridgic.amphibious import (
AmphibiousAutoma, CognitiveContext, CognitiveWorker,
think_unit, ActionCall, RunMode,
)
class TracedFormFiller(AmphibiousAutoma[CognitiveContext]):
fixer = think_unit(
CognitiveWorker.inline(
"Diagnose the problem and fix it so the form can be submitted. "
),
max_attempts=5,
)
async def on_agent(self, ctx: CognitiveContext):
await self.fixer
async def on_workflow(self, ctx: CognitiveContext):
yield ActionCall("fill_field", field_name="alice", value="alice")
yield ActionCall("fill_field", field_name="email", value="alice@example.com")
yield ActionCall("click_button", button_name="submit")
# Reset state
state["captcha_shown"] = False
state["form_submitted"] = False
agent = TracedFormFiller(llm=llm, verbose=True)
result = await agent.arun(
goal="Fill and submit the registration form with username='alice' and email='alice@example.com'",
tools=[fill_field_tool, click_button_tool, solve_captcha_tool],
mode=RunMode.AMPHIFLOW,
trace_running=True,
)
print(result)
The agent ran in amphiflow mode: the workflow filled two form fields successfully, then the submit step hit the CAPTCHA error. The framework automatically activated on_agent(), which diagnosed the problem, solved the CAPTCHA, and re-submitted the form.
All of this was recorded in the trace. Let's look inside.
Accessing the Trace¶
After arun() completes, the trace is available on the agent. Call build() to get a structured dictionary of the entire execution trajectory.
trace = agent._agent_trace.build()
print("Trace keys:", list(trace.keys()))
print("Number of phases:", len(trace["phases"]))
print("Orphan steps:", len(trace["orphan_steps"]))
Trace keys: ['phases', 'orphan_steps', 'metadata'] Number of phases: 0 Orphan steps: 4
The trace dictionary has three sections:
phases— steps grouped by Phase Annotation (self.snapshot()blocks). If you don't use phase annotations, this list is empty — that's fine.orphan_steps— steps recorded outside any phase annotation. When there are no phases, all steps appear here.metadata— optional metadata attached during save.
Inspecting the Trace Steps¶
Each step records what the LLM said (step_content) and what tools it called (tool_calls). Let's print all steps — whether they're inside phases or recorded as orphan steps.
def print_step(idx, step):
"""Print a single trace step (works with both TraceStep objects and dicts)."""
content = step.step_content if hasattr(step, "step_content") else step.get("step_content", "")
output_type = step.output_type if hasattr(step, "output_type") else step.get("output_type", "?")
tool_calls = step.tool_calls if hasattr(step, "tool_calls") else step.get("tool_calls", [])
content_preview = (content[:80] + "...") if content and len(content) > 80 else (content or "(no content)")
print(f"\n Step {idx} [{output_type}]: {content_preview}")
for tc in tool_calls:
name = tc.tool_name if hasattr(tc, "tool_name") else tc.get("tool_name", "?")
args = tc.tool_arguments if hasattr(tc, "tool_arguments") else tc.get("tool_arguments", {})
success = tc.success if hasattr(tc, "success") else tc.get("success", True)
error = tc.error if hasattr(tc, "error") else tc.get("error")
status = "OK" if success else f"FAILED: {error}"
print(f" -> {name}({args}) [{status}]")
# Print phases (if any)
for i, phase in enumerate(trace["phases"]):
print(f"\n{'='*60}")
print(f"Phase {i+1} [{phase['phase_type']}]: {phase['goal']}")
for j, step in enumerate(phase["steps"]):
print_step(j + 1, step)
# Print orphan steps (steps outside any phase)
if trace["orphan_steps"]:
print(f"\n{'='*60}")
print(f"Steps (no phase annotation): {len(trace['orphan_steps'])} steps")
for j, step in enumerate(trace["orphan_steps"]):
print_step(j + 1, step)
============================================================
Steps (no phase annotation): 4 steps
Step 1 [tool_calls]: (no content)
-> fill_field({'field_name': 'alice', 'value': 'alice'}) [OK]
Step 2 [tool_calls]: (no content)
-> fill_field({'field_name': 'email', 'value': 'alice@example.com'}) [OK]
Step 3 [tool_calls]: The form submission failed due to two issues: 1) The username field was incorrec...
-> fill_field({'field_name': 'username', 'value': 'alice'}) [OK]
-> solve_captcha({}) [OK]
-> click_button({'button_name': 'submit'}) [OK]
Step 4 [content_only]: The form submission was initially blocked due to an incorrect field name ('alice...
Since we didn't use any phase annotations (self.snapshot), all steps appear under "orphan steps". The trace still captures everything:
- The workflow steps —
fill_fieldcalls that succeeded - The failed
click_buttoncall (withsuccess=Falseand the error message) - The agent fallback steps —
solve_captchaand the successful re-submit
If we had used phase annotations in on_agent(), those steps would be grouped under named phases instead. But either way, the full execution trajectory is recorded.
Saving and Loading Traces¶
Traces can be saved to JSON files for later analysis or sharing. save() writes the trace to disk; AgentTrace.load() reads it back as a plain dictionary.
# Save trace to a JSON file
agent._agent_trace.save("form_filler_trace.json")
print("Trace saved to form_filler_trace.json")
# Load it back as a plain dictionary
from bridgic.amphibious import AgentTrace
loaded = AgentTrace.load("form_filler_trace.json")
print(f"Loaded trace — phases: {len(loaded['phases'])}, orphan steps: {len(loaded['orphan_steps'])}")
Saved traces are plain JSON — you can open them in any text editor, load them into pandas for analysis, or feed them into a visualization tool.
Note that AgentTrace.load() returns a plain dictionary (not an AgentTrace instance), so loaded traces use dict access (step["step_content"]) rather than attribute access (step.step_content).
What have we learnt?¶
In this tutorial, we learned how to see inside the agent's decision-making process:
- Enable tracing with
arun(..., trace_running=True)to record every observation, decision, and tool call. - Access the trace via
agent._agent_trace.build()for a structured dictionary withphases,orphan_steps, andmetadata. - Steps are always recorded. If you use Phase Annotation (
self.snapshot()), steps are grouped by phase. If not, they all appear inorphan_steps— the trace is still complete. - Each step records the LLM's reasoning (
step_content), tool calls with arguments and results, and success/failure status. - Save/load traces with
.save("path.json")andAgentTrace.load("path.json")for offline analysis.
Execution tracing turns agent behavior from opaque to transparent, making it practical to debug, optimize, and audit complex agentic systems.