fix(sdk): resolve all condition leaf selectors into control event input - #269
Open
jasmine-ab-tea wants to merge 3 commits into
Open
jasmine-ab-tea wants to merge 3 commits into
jasmine-ab-tea wants to merge 3 commits into
Conversation
Composite controls (and/or/not) previously collapsed control-execution
event input down to a single representative leaf's value via
_composite_metadata(), hiding the other selector values that fed the
decision. Reconstruct the full {selector_path: value} map by resolving
every leaf in the condition tree (via observability_identity's
all_selector_paths and select_data) against the original step, so the
emitted event/span input reflects everything the control actually
evaluated.
Co-Authored-By: Claude Code <noreply@anthropic.com>
jasmine-ab-tea
force-pushed
the
fix/control-input-all-selector-values
branch
from
September 24, 2026 23:23
be350e4 to
9de0cd0
Compare
The untyped _apply_filters helper makes page_stmt infer as Any, which cascades to Any for rows and leaves mypy unable to infer a concrete element type for the list() call. Co-Authored-By: Claude Code <noreply@anthropic.com>
identity_metadata is typed as dict[str, object], so .get(...) yields object, which mypy correctly flags as not iterable in the dict comprehension below. Co-Authored-By: Claude Code <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
lan17
approved these changes
Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Agent Control controls can be simple (one selector + one evaluator) or composite trees built from
and/or/notcombinators, each combinator wrapping multiple{selector, evaluator}leaves. When a composite control's execution is reconstructed into aControlExecutionEvent(and from there into the Console's control span), the existing_composite_metadata()logic in the engine picks one representative leaf to stand in formatched/action/condition_trace— that collapsing behavior is correct and unchanged.The bug: the reconstructed event's
inputfield inherited that same collapse. It was populated from a single leaf'sselected_data_preview/engine_selected_data_preview(or, for simple controls, just whatever scalar the one selector resolved to). For a composite control, that meant the Console only ever showed the value for whichever leaf happened to be chosen as "representative" — every other selector path that the control actually evaluated (e.g. the other side of anand, or the untriggered branch of anor) was invisible ininput, even though it was part of the decision.The fix
_build_events_for_matches()(sdks/python/src/agent_control/evaluation_events.py) now walks the control's full condition tree viaobservability_identity().all_selector_paths(built fromConditionNode.iter_leaf_parts()— every leaf, in tree order, already deduplicated) and resolves each path against the real step payload withselect_data().inputbecomes{selector_path: resolved_value, ...}for every leaf, instead of one scalar. Falls back to the old preview-scalar behavior only when the control definition can't be resolved fromcontrol_lookup(e.g. genuinely missing).This is entirely client-side SDK reconstruction —
build_control_execution_events()runs in-process the same way regardless of whether a given control executed withexecution="server"orexecution="sdk", so the fix applies uniformly without touching the engine, the server, or orbit's ingestion/schema.Before / after (real devstack traces)
Before — a composite control's span
input(demo-steer-large-transfer-2fa, anand(amount >= $10k, not verified_2fa)control) showed only the one representative leaf, collapsed to a bare scalar:15000There's no way to tell from this that
verified_2fawas even part of the decision.After — the same kind of composite control (
demo-deny-risky-transfer-composite, anor(destination sanctioned, fraud_score high)control) now shows every leaf that was actually evaluated:{ "input.destination_country": "United Kingdom", "input.fraud_score": 0.1 }(Screenshots from Console attached below/in the PR thread — before: scalar
inputondemo-steer-large-transfer-2fa; after: full resolved map ondemo-deny-risky-transfer-composite.)Test plan
test_observability_updates.py: updated the stale single-scalar-fallback test to reflect the new resolved-map behavior, added coverage for theandcomposite (assertsinputhas both leaf values), theorcomposite, and the control-lookup-miss fallback path (control not incontrol_lookupstill falls back to the debug preview scalar).ace-demo'srun_demo.pyagainst a real Agent Control server, then fetched the resulting spans back via the Galileo API.and+notcomposite (demo-steer-large-transfer-2fa, matched):input = {"input.amount": 15000.0, "input.verified_2fa": false}orcomposite (demo-deny-risky-transfer-composite, non-match):input = {"input.destination_country": "United Kingdom", "input.fraud_score": 0.1}🤖 Generated with Claude Code