Skip to content

Observability

Heartbit emits structured events throughout agent execution, giving you full visibility into LLM calls, tool usage, orchestration decisions, and safety guardrails.

Every significant action in the agent loop emits an AgentEvent — a tagged enum serialized as JSON with snake_case type discriminators. Events carry the agent name for identification in multi-agent runs.

EventFieldsDescription
run_startedagent, taskAgent loop begins
turn_startedagent, turn, max_turnsNew reasoning turn
run_completedagent, total_usage, tool_calls_madeSuccessful completion
run_failedagent, error, partial_usageFailure with partial token usage
EventFieldsDescription
llm_responseagent, turn, usage, stop_reason, tool_call_count, text, latency_ms, model, time_to_first_token_msLLM response with TTFT tracking
retry_attemptagent, attempt, max_retries, delay_ms, error_classRetry before sleep delay
model_escalatedagent, from_tier, to_tier, reasonCascade escalation between model tiers

The llm_response event includes wall-clock latency and time-to-first-token (streaming only, 0 for non-streaming). The model field is present when using cascading providers.

EventFieldsDescription
tool_call_startedagent, tool_name, tool_call_id, inputTool execution begins
tool_call_completedagent, tool_name, tool_call_id, is_error, duration_ms, outputTool execution finished
EventFieldsDescription
approval_requestedagent, turn, tool_namesHuman-in-the-loop prompt sent
approval_decisionagent, turn, approvedHuman approval response received
EventFieldsDescription
sub_agents_dispatchedagent, agentsSub-agents dispatched by orchestrator
sub_agent_completedagent, success, usageSub-agent finished
agent_spawnedagent, spawned_name, tools, taskDynamic agent created at runtime
task_routeddecision, reason, selected_agent, complexity_score, escalatedRouting decision by complexity analyzer
EventFieldsDescription
guardrail_deniedagent, hook, reason, tool_nameGuardrail blocked an operation
guardrail_warnedagent, hook, reason, tool_nameGuardrail warned but allowed
budget_exceededagent, used, limit, partial_usageToken budget exceeded

The hook field indicates which guardrail hook triggered: "post_llm", "pre_tool", or "post_tool". The tool_name field is present for tool-level hooks.

EventFieldsDescription
context_summarizedagent, turn, usageContext compacted at threshold
auto_compaction_triggeredagent, turn, success, usageOverflow recovery attempt
doom_loop_detectedagent, turn, consecutive_count, tool_namesStuck loop detected
session_prunedagent, turn, tool_results_pruned, bytes_saved, tool_results_totalOld tool results pruned
EventFieldsDescription
sensor_event_processedsensor_name, decision, priority, story_idSensor triage decision
story_updatedstory_id, subject, event_count, priorityStory correlation update

Wire event handling via the builder:

use heartbit::AgentRunner;
use heartbit::agent::AgentEvent;
let runner = AgentRunner::builder(provider)
.name("researcher")
.system_prompt("You are a researcher.")
.on_event(Arc::new(|event: AgentEvent| {
eprintln!("{}", serde_json::to_string(&event).unwrap());
}))
.build()?;

The callback type is dyn Fn(AgentEvent) + Send + Sync. Keep handlers fast to avoid blocking the agent loop.

Event payloads (LLM text, tool input/output) are truncated at 64KB (EVENT_MAX_PAYLOAD_BYTES = 65536). Truncated strings include a suffix like [truncated: 1234 bytes omitted]. Truncation respects UTF-8 character boundaries.

Use --verbose or -v to emit events as JSON to stderr:

Terminal window
heartbit run --config heartbit.toml --verbose "Analyze this codebase"

Each event is a single JSON line, suitable for piping to jq or log aggregators.

Control verbosity via the HEARTBIT_OBSERVABILITY environment variable or the [telemetry] config section. Priority order:

  1. HEARTBIT_OBSERVABILITY env var (highest)
  2. [telemetry] observability_mode in config TOML
  3. AgentRunnerBuilder::observability_mode() / OrchestratorBuilder::observability_mode()
  4. Default: production
ModeSpan DataMetricsPayloads
productionNames + durations onlyNoNo
analysisNames + durationsTokens, latencies, costs, stop reasonsNo
debugNames + durationsTokens, latencies, costs, stop reasonsFull (truncated to 4KB)
Terminal window
HEARTBIT_OBSERVABILITY=debug heartbit run --config heartbit.toml "debug this"

Or in config:

[telemetry]
observability_mode = "analysis"

Heartbit supports optional OTLP export for distributed tracing. Configure via the [telemetry] section:

[telemetry]
enabled = true
endpoint = "http://localhost:4317"
service_name = "heartbit-agent"
observability_mode = "analysis"

Span attributes follow the OpenTelemetry GenAI Semantic Conventions (v1.38.0), so OTel-compatible backends (Jaeger, Grafana Tempo, Honeycomb) render agent traces with standard attribute names.

The init_tracing_from_config() function wires telemetry for all CLI commands (run, chat, serve).