Repository navigation
feat(harness): state, carry and switch the scope choices Search settles - #88
Merged
Merged
Conversation
SearchReading keeps each part's resolved.scope_choices, persisted with the turn and rendered as one ledger line per choice for the planner and recall. The model-facing part summary is unchanged. Parsed searches send the user's own message (from a contextvar set at turn start and on resume) and the conversation's stated choices as carried entries, which data-mcp forwards to the Search API only while search_sends_conversation_context is on.
Send the newest 4,000 characters of the user message, so appended amendments survive, and the 16 newest carried entries, matching the Search API limits.
The digest prints one line per scope choice (axis, active option, provenance and the other options a client can ask for), and the structured answer carries the same record. The region eval accepts a summed region with its own regional and unspecified buckets, and the count-and-prices eval no longer expects a gross or net question where the convention table settles the basis.
fetch and the plan execute node keep a compact record of what was served (members and residuals per role, measure basis, components) on the reading. finish() appends one "Counted as:" line built from the stored scope choices and their search labels, corrected where the served figures depart, beside the loose-match notes, so it reaches the stored turn and the next turn's history. The planner's ledger text is unchanged.
propose_plan writes, in code, one decision per scope choice on each pinned source that has another available or partial option, citing it from the requirements that pin the source, user-resolved for explicit or carried choices and planner default otherwise. Decisions past the plan cap are left to the basis line. The decision keeps each option's availability, so the card and a later switch read from the plan alone, and the Studio wire carries the choice id, the active option and the options with reader reasons.
…nner model A switch answer on a plan pause is checked against the pending card (a stale revision is refused 409, an option not offered or unavailable 422) and routed to a deterministic node. The node posts each affected source's base identity with its scope entries and the switch to the Search API through data-mcp, which registers the compiled definition, then revises the plan through propose_plan's own chain with no model call and offers it again with what moved. A switch writes no approval, and a refusal re-offers the unchanged plan with a note.
…search the user's own words A fetch narrowed to some of a region's places drops the region clause; a plan reading a windowed copy states the line of the reading it was copied from. A switch label stays in the turn record but never reaches search as the user's message. The scope record is read leniently, so a field the Search API adds never drops the choices. data-mcp types each carried entry and logs whether it forwards the conversation context.
A switch or an approve naming a plan revision the user no longer sees is refused 409 plan_revision_stale with the pending plan card, stored with the pause exactly as the stream sent it, so a client replaces its card instead of acting on a plan it has not seen. Approve without a revision behaves as before.
A share, ratio, sum, comparison or join reading several sources has its inputs put on one basis before the plan is recorded, unless the user stated different bases on them. The target is the basis the user stated, else the narrowest input's; other inputs are recompiled through the same Search resubmit a plan-card switch uses, with no model call, and stored as readings. A basis the planner chose this way is marked on its spec block, so it never carries into later searches as the user's choice. An input that cannot be counted on the target refuses the plan as operation_unsupported, naming the mismatch.
…of history and label switched inputs by their source The translator reads a pinned identity from the pin's own spec block, so a source a switch or basis alignment compiled during the request is fetched as pinned. The turn record stores the basis line apart from the answer, so the next turn's model never sees it to copy. A step reading a dataset governed by a basis or sector choice names its input as Search counts the served source.
…ne basis An input of a multi-input calculation that the planner left unpinned is searched in code with the words the translator would send, pinned to what Search serves and stored as a reading, so a basis alignment or a switch moves it with the rest. Code-appended reply notes are stored apart from the answer, like the basis line, so the next turn's model never reads them.
…sks them A definite search part whose window spans several years, whose unit is money in current prices and whose prices the user left unstated carries open_prices, the options of its prices question with search's reading first. The orchestrator asks prices from that field, so the question no longer depends on another counting choice being open beside it.
…frames get_data states each component of a composite frame by counts and year spans, and the change of measure at 2018 in a spliced one; component member lists are bounded like resolved ones. The basis line names an inclusive frame as bilateral plus imputed multilateral with its years, and a headline frame as net to 2017 and grant equivalents from 2018. A calculation puts its inputs on one provider scope as it does one basis, and refuses a sum of inputs the user stated on different providers. An analysis verb given a composite frame points to the summed get_data read, where dfo-apis adds the components once.
…nswer After a fast-path answer, Studio suggests up to four chips, one for each available option of the scope choices the answer read. A chip turn resubmits the stored base with the chosen option through the same path a plan-card switch uses, stores the reading, fetches the compiled spec and answers with the basis line, with no search or plan call. A stale or unavailable chip is refused before the turn starts. Chips shown, applied, kept and reversed are recorded as telemetry events.
The fetch arguments that shape a frame are stored on the reading, and a chip turn fetches the switched spec with them, so a chip changes only the chosen reading. An answer that named its places offers no chip on a choice that binds places, since the refetch would not change them.
…anner's A spec block marks the scope axes the planner's own words or alignment settled. Those choices never carry into later searches as the user's, never count as a basis the user stated, and show on the plan card as the planner's. A calculation input that search cannot settle to one dataset refuses the plan, since one basis cannot be guaranteed.
# Conflicts: # services/harness/src/harness/graph/switch.py # services/harness/src/harness/orchestrator_tools.py # services/harness/src/harness/planner_seam.py # services/harness/src/harness/scope_choices.py # services/harness/src/harness/tool_contracts.py # services/harness/src/harness/turn_records.py # services/harness/src/planner/agent.py # services/harness/src/submission/service.py # services/harness/tests/planner/test_agent.py # services/harness/tests/test_graph.py # services/harness/tests/test_orchestrator_tools.py
# Conflicts: # services/harness/src/harness/planner_seam.py # services/harness/src/harness/scope_choices.py # services/harness/src/planner/agent.py # services/harness/src/submission/service.py
…ames a message or a chip A reading keeps the arguments of each fetch that returned data, and a chip fetches the switched spec once per entry, so an answer that read Ghana and then Kenya is switched for both. A turn request carrying neither a message nor a chip is refused at the boundary.
After a switch or a basis alignment, the basis line names the part by the label Search gives the served pin, so the name never states a basis the clause contradicts. A part still on its first pin keeps the words it was asked in.
# Conflicts: # services/harness/src/harness/turn_records.py # services/harness/tests/test_turn_records.py
# Conflicts: # services/harness/src/planner/agent.py # services/harness/src/planner/tool_results.py # services/harness/src/submission/service.py # services/harness/src/translator/discovery.py # services/harness/tests/planner/test_agent.py # services/harness/tests/test_plan_display.py
…mpile each alignment once The search digest and its structured content state each entry Search dropped: the option asked for, its provenance, the option served and the cause in words, so an MCP client learns why an explicit request was not applied. The planner compiles each source and option set once, however many requirements ask for it.
A chip names the turn whose answer offered it and the reading it switches, and is refused as stale once a later turn has answered or a later reading holds its choice, so an old chip never switches a reading the conversation has moved past.
# Conflicts: # services/harness/src/planner/agent.py
A realignment names one option per axis an input moves on, so an input counted gross and inclusive can become net and bilateral in one resolve_definition resubmit carrying both entries. The compiled pin is keyed by the origin and that option set, and inputs sharing both compile once.
…ested calculations as one group A retryable Search refusal for an unpinned input is answered as retryable and never held. Calculations sharing an input are aligned as one connected group, a basis the user chose in an earlier turn counts as stated, the decision cap counts only decisions that survive a re-pin, and the input search and compile take the time the entry has left. A scope record from a newer Search keeps every part it can read. The plan switch resubmits through resolve_definition with the groups every resubmit posts, and data_mcp.scope holds the one definition of scope entries, axes and reason wording.
# Conflicts: # services/harness/src/harness/scope_choices.py # services/harness/src/planner/agent.py # services/harness/src/submission/service.py
…over disjoint donors A chip resubmits with the groups its reading was resolved with, is refused stale when seed finds its choice on another reading, is offered only on the newest reading holding its choice, and fetches through the failure handling every agent fetch gets. A sum over different stated provider scopes is refused only when its inputs share a donor or one counts every provider. A composite refusal names the side whose components hold the refused StatVars, and common.composite holds the one reader of trace components and year spans.
# Conflicts: # services/harness/src/planner/agent.py
jm-rivera
marked this pull request as ready for review
October 27, 2026 10:49
jm-rivera
marked this pull request as draft
October 27, 2026 10:50
jm-rivera
marked this pull request as ready for review
October 27, 2026 10:50
… scope decision fields The planner-entry eval takes the Resolver protocol from harness.orchestrator_tools, so its recorded and recording resolvers accept and replay the search context the real one sends, and its probe passes revise through. The revision base format lists DecisionScope and ScopeOptionState.
This was referenced Oct 29, 2026
Closed
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.
The harness now keeps the scope choices Search reports for each part (
ResolvedSpec.scope_choices, mirrored inharness/scope_choices.py) on every stored reading, states them in the answer, and lets the user switch them on the plan card. Every answer closes with a code-written "Counted as:" line built from what the fetched frame served (turn_records.basis_line), stored apart from the model's text so the next turn's model never reads or copies it. Later searches carry the user's own choices ascarried_entries, with the user's words, through data-mcp'ssearch_meta(org.one.dfo/search_context), gated by the data-mcp settingsearch_sends_conversation_context, which defaults off.The plan card shows each scope choice as a decision with its options and the reason an option is unavailable (
PlanDecision.choice_id,active_option_id,options). A switch (InterruptDecision.SWITCH, nodegraph/switch.py) resubmits the stored base with the switched entry through Search, stores the new reading, revises the plan in code with no model call, and re-offers the card with what moved. A stale switch or approve is refused 409plan_revision_stalecarrying the current card, and approve now acceptsplan_revision;docs/frontend-interface.mdsection 4.2 has the wire.A calculation reading several inputs (share, quotient, sum, comparison, join) pins every input and puts them on one measure basis before the plan records: a basis the user stated wins, else the narrowest input's. Inputs are recompiled through the same resubmit a switch uses; an input that cannot move refuses the plan. Choices the planner's own words or alignment settled are marked on the spec block (
_planner_axes) and never carried or credited as the user's. Over several years with prices unstated, the search result statesopen_pricesin code, and the orchestrator asks prices from that field, since a settled basis no longer opens a gross-or-net question beside it.Reviewers should challenge
submission.service._aligned_bases, the switch node, andplanner.plan.identity_on_record, which is now the one lookup of a pin's identity for admission and the translator.Merge with the dfo-apis scope-choices PR, which serves the record and the resubmit entries.
search_sends_conversation_contextis turned on in dfo-infra once dfo-apis is deployed. The planner_entry recordings need re-recording against the deployed Search.Testing: targeted harness and data-mcp test files pass locally; the full suite runs on CI. Live, with Search from the dfo-apis branch: the share plan switched to net moves both inputs (14.0%, $8.29bn of $59.2bn), a sector switch moves only health, a stale switch gets the current card, "total health aid from the US to Africa in 2024" answers $5.81bn and "now Ghana" $78.5m with the sector carried, and both live region and prices evals pass 3 of 3.