Use decision traces when an agent proposes an action and a human or downstream system can edit, override, approve, or bind that action later.
AgentGuard keeps this narrow:
- no new transport
- no new storage dependency
- no graph or embedding logic
- just structured events through the normal
TraceContext.event()pipeline
AgentGuard emits five stable decision event names:
decision.proposeddecision.editeddecision.overriddendecision.approveddecision.bound
Each event is a normal AgentGuard event with the decision schema stored in
event.data.
Every emitted decision event includes these keys in data:
decision_idworkflow_idtrace_idobject_typeobject_idactor_typeactor_idevent_typeproposalfinaldiffreasoncommenttimestampbinding_stateoutcome
trace_id is duplicated into the decision payload on purpose so downstream
systems can query decision events without reconstructing context from the outer
trace envelope.
binding_state is always a non-empty string so hosted decision-history
validators can index the event without custom parsing. The helper defaults are:
decision.proposed->proposeddecision.edited->editeddecision.overridden->overriddendecision.approved->approveddecision.bound-> the explicit caller-provided state, such asapplied,merged, orfailed
from agentguard import JsonlFileSink, Tracer, decision_flow
tracer = Tracer(
sink=JsonlFileSink(".agentguard/traces.jsonl"),
service="approval-flow",
)
with tracer.trace("agent.run") as run:
with decision_flow(
run,
workflow_id="deploy-approval",
object_type="deployment",
object_id="deploy-042",
actor_type="agent",
actor_id="release-bot",
) as decision:
decision.proposed({"action": "deploy", "environment": "staging"})
decision.edited(
{"action": "deploy", "environment": "production"},
actor_type="human",
actor_id="reviewer-123",
reason="Customer approved direct rollout",
comment="Promoting immediately",
)
decision.approved(actor_type="human", actor_id="reviewer-123")
decision.bound(
actor_type="system",
actor_id="deploy-api",
binding_state="applied",
outcome="success",
)This works with:
JsonlFileSinkStdoutSinkHttpSink- any custom sink that already consumes AgentGuard events
If you do not want the stateful wrapper, emit the events directly:
from agentguard import log_decision_proposed, log_decision_overridden
with tracer.trace("agent.run") as span:
proposed = log_decision_proposed(
span,
workflow_id="ticket-review",
object_type="ticket",
object_id="ticket-7",
actor_type="agent",
actor_id="triage-bot",
proposal={"action": "close_ticket"},
)
log_decision_overridden(
span,
decision_id=proposed["decision_id"],
workflow_id="ticket-review",
object_type="ticket",
object_id="ticket-7",
actor_type="human",
actor_id="support-lead",
proposal=proposed["proposal"],
final={"action": "escalate_ticket"},
reason="Customer is still blocked",
comment="Escalating instead of closing",
)decision.edited and decision.overridden compute a unified diff by default:
- strings diff as text
- dicts and lists diff as normalized JSON
- if you already have a domain-specific diff, pass it explicitly with
diff=...
The event always retains both:
proposal: the original proposalfinal: the human-modified or overridden form
No migration is required.
Decision traces are ordinary AgentGuard events:
- existing trace files stay valid
- existing sinks keep working
- existing dashboards or downstream processors can opt into the new event names
- apps can adopt the new helpers incrementally inside one approval workflow at a time
See examples/decision_trace_workflow.py
for a full local example.
For local JSONL traces, you can extract normalized decision payloads with:
agentguard decisions .agentguard/traces.jsonl
agentguard decisions .agentguard/traces.jsonl --workflow-id deploy-approval --jsonFor retained traces accessed through the hosted read API, the AgentGuard MCP
server now exposes a matching get_trace_decisions tool so coding agents can
inspect the same normalized decision.* payloads without custom trace parsing.