Skip to main content

Runner Events

PlanRunner.run_stream() turns a Plan into a stream of dataclass events. The runner is intentionally transport-neutral: it does not know about HTTP, sessions, retries in your gateway, SSE, database rows, or UI state. The product adapter provides a call_tool(tool_name, args) function and decides where each event should go.

Use runner events when you need the execution path to be observable:

  • UI progress for a running plan
  • logs that can explain which step failed
  • Quality Lab results with per-stage latency
  • trace-learning records that can be scrubbed and promoted later
  • structured failure responses instead of uncaught executor errors

Minimal Usage

from dataclasses import asdict

from graph_tool_call.plan import PlanRunner

def call_tool(tool_name: str, args: dict):
return executor.execute(tool_name, args)

runner = PlanRunner(call_tool)

for event in runner.run_stream(
plan,
input_context={"siteNo": "10001"},
trace_metadata={"collection_id": "orders", "case_id": "order-detail-001"},
):
send_sse(asdict(event))

input_context supplies values for both ${input.foo} and ${user_input.foo} bindings. trace_metadata is copied onto every emitted event, so keep it compact and scrubbed.

Event Lifecycle

The default runner mode is linear and aborts on the first hard failure.

plan.started
step.started
step.completed
step.started
step.completed
plan.completed

When a binding or tool call fails in on_error="abort" mode:

plan.started
step.started
step.failed
plan.aborted

Recovery modes add events without changing the base contract:

EventEmitted WhenWhat The Adapter Should Do
step.retryinga retryable step failed and another attempt will runshow retry state, keep the run open
step.skippedrecover mode skipped an unconsumed failed stepshow degraded progress, keep the run open
plan.repaireda repairer created a replacement planreplace the visible plan id/path if needed
binding.repairedstale ${sN.path} binding was redirectedstore as low-confidence repair evidence
args.coercedargs were type-cast or enum-folded before executionexpose the coercion in diagnostics

The v1 runner does not perform fan-out, conditionals, or automatic replanning unless on_error="recover" is configured with a PlanRepairer.

Event Types

TypeMain FieldsSuccess Path
plan.startedplan_id, goal, step_countfirst event
step.startedstep_id, tool, args_resolved, index, totalbefore each tool call
step.completedduration_ms, output_preview, output_sizeafter a successful tool call
plan.completedoutput, total_duration_ms, trace_stepsfinal success event
step.failederror, duration_msfailed step before abort
plan.abortedfailed_step, error, trace_stepsfinal failure event

Every event can carry:

FieldMeaning
typestable event type for routing
stagecurrently runner
plan_idcurrent plan id
step_idcurrent step id when applicable
tooltool name when applicable
graph_tool_call_versionengine version
trace_metadataadapter-provided safe metadata

Payload Examples

{
"type": "step.started",
"stage": "runner",
"plan_id": "plan-42",
"step_id": "s1",
"tool": "getOrderDetail",
"args_resolved": {"orderNo": "ORD-1001"},
"index": 1,
"total": 2,
"graph_tool_call_version": "0.31.0",
"trace_metadata": {"collection_id": "orders-api", "quality_case_id": "case-001"}
}

Adapter Contract

The adapter should keep the runner boundary narrow:

Runner InputAdapter Owns
Planretrieving or synthesizing the plan
input_contextuser-entered fields, extracted entities, collection defaults
trace_metadatacollection id, case id, safe auth readiness, request id
call_toolHTTP execution, auth headers, host allowlists, mutation policy

Do not put raw tokens, cookies, emails, phone numbers, raw user ids, or full request/response bodies in trace_metadata. If a product needs audit-grade payload storage, keep that in a separate secured system and store only a reference id in runner events.

SSE And UI Mapping

For product UIs, forward events as a stable envelope:

{
"type": "runner.event",
"run_id": "ql-run-20260725-001",
"event": {"type": "step.completed", "step_id": "s1"}
}

Recommended UI grouping:

UI StateEvent Condition
Runningplan.started, step.started, step.retrying
Partial progressstep.completed, step.skipped, args.coerced
Needs attentionbinding.repaired, plan.repaired
Successplan.completed
Failedstep.failed, plan.aborted

Persisting Events

For long-term storage, keep the fields that explain behavior without exposing payloads:

  • event type
  • plan id and step id
  • tool name
  • duration
  • failure reason
  • retry or repair state
  • scrubbed output preview
  • trace metadata

Quality Lab and trace learning should use the same event stream, then derive a compact record from it. This keeps debugging, evaluation, and learning grounded in one source of truth.

Troubleshooting

SymptomLikely CauseCheck
plan.aborted with kind=bindinga required producer value is missinginspect PlanStep.args bindings and upstream output_preview
step.failed with kind=tooladapter executor raisedinspect HTTP status, auth readiness, and executor logs
no step.completed eventsfirst step failed before outputcheck step.failed.error and failed_step
args.coerced missingvalidate_args is off or tools map is absentconfigure PlanRunner(..., tools=..., validate_args="coerce")
retries never happenstep is not retryable or no RetryPolicy is configuredset PlanStep.retryable=True or RetryPolicy(retry_all=True)