@@ -84,53 +84,59 @@ async with agent_control.AgentControlClient() as client:
8484The existing ` evaluate_controls ` helper remains available for callers that
8585prefer its field-based convenience arguments.
8686
87- ## Building trace/session steps
87+ ## Building a step with children
8888
89- Trace- and session-level controls evaluate the aggregate of several
90- already-executed child steps, so ` Step.children ` has to be populated by the
91- caller. Building that tree by hand means hand-rolling a side-channel record
92- for every leaf call and converting it into ` Step ` objects afterward:
89+ A control's scope can target any step type, including one that aggregates
90+ several already-executed child steps - ` Step.children ` just has to be
91+ populated by the caller. Building that tree by hand means hand-rolling a
92+ side-channel record for every leaf call and converting it into ` Step `
93+ objects afterward:
9394
9495``` python
9596# Before: a hand-built dict record next to the real return value
9697def execute_policy_search (query ):
9798 docs = search_policy_documents(query)
98- record = {" type" : " retriever " , " query" : query, " docs" : docs}
99+ record = {" type" : " lookup " , " query" : query, " docs" : docs}
99100 return StepExecution(value = docs, record = record)
100101
101- spans = []
102+ children = []
102103result = execute_policy_search(query)
103- spans .append(result.record)
104+ children .append(result.record)
104105...
105- trace_step = build_agent_control_step({" type" : " trace " , " spans " : spans , ... })
106+ workflow_step = build_agent_control_step({" type" : " workflow " , " children " : children , ... })
106107```
107108
108109` agent_control.record_step() ` builds the same tree incrementally, in ` Step `
109110vocabulary, with no intermediate dict and no converter:
110111
111112``` python
112- with agent_control.record_step(" trace " , " banking_trace " , input = {" request" : req}) as trace :
113- with trace .child(" retriever " , " policy_lookup" , input = query) as span:
113+ with agent_control.record_step(" workflow " , " banking_workflow " , input = {" request" : req}) as workflow :
114+ with workflow .child(" lookup " , " policy_lookup" , input = query) as span:
114115 span.output = search_policy_documents(query)
115116
116- account = trace .call(lookup_account, account_id = " acct-1001" , step_type = " tool" )
117- plan = trace .call(run_banking_model, req, step_type = " llm" , tools = TOOL_DEFINITIONS )
118- trace .output = {" status" : " planned" , " message" : plan[" content" ]}
117+ account = workflow .call(lookup_account, account_id = " acct-1001" , step_type = " tool" )
118+ plan = workflow .call(run_banking_model, req, step_type = " llm" , tools = TOOL_DEFINITIONS )
119+ workflow .output = {" status" : " planned" , " message" : plan[" content" ]}
119120
120- result = await trace .evaluate(stage = " post" )
121+ result = await workflow .evaluate(stage = " post" )
121122```
122123
123- ` trace .call(...)` /` await trace .acall(...) ` run the function and record it as
124- a child using the same capture logic as ` @control() ` (input from bound
124+ ` workflow .call(...)` /` await workflow .acall(...) ` run the function and record
125+ it as a child using the same capture logic as ` @control() ` (input from bound
125126arguments, output from the return value), then return the real result -
126127removing the need for a separate ` StepExecution ` -style wrapper. A failed
127128call is still recorded (with the error in ` context ` ) before the exception is
128- re-raised. ` trace.child(...) ` nests another recorder the same way, so
129- sessions nest traces with ` session.child("trace", ...) ` .
130-
131- ` trace.build() ` produces the frozen ` Step ` ; ` trace.evaluate(...) ` builds it
132- and evaluates it in one call via ` evaluate_step() ` - the same function
133- ` evaluate_controls() ` uses internally once its ` Step ` is built.
129+ re-raised. ` workflow.child(...) ` nests another recorder the same way, so a
130+ parent step can nest children of any depth, e.g. `session.child("trace",
131+ ...)` for callers that model multi-turn sessions of traces.
132+
133+ ` workflow.build() ` produces the frozen ` Step ` , with ` children=[] ` instead of
134+ omitted (` None ` ) for a step type passed via ` container_types ` even when no
135+ children were recorded - e.g. `record_step("workflow", ..., container_types=
136+ {"workflow"})` for a workflow that legitimately ran with zero children.
137+ ` workflow.evaluate(...) ` builds the step and evaluates it in one call via
138+ ` evaluate_step() ` - the same function ` evaluate_controls() ` uses internally
139+ once its ` Step ` is built.
134140
135141## Sharing an OpenTelemetry provider with Google ADK
136142
0 commit comments