Skip to content

feat(graph): read the final candidate from the last text producer - #365

Merged
binaryaaron merged 2 commits into
aagonzales/graph-sdk/feat/mainfrom
aagonzales/feature/graph-sdk-final-candidate
Nov 4, 2026
Merged

binaryaaron merged 2 commits into
aagonzales/graph-sdk/feat/mainfrom
aagonzales/feature/graph-sdk-final-candidate

Conversation

@binaryaaron

Copy link
Copy Markdown
Collaborator

A target's final candidate is now the output of the workflow's last text-producing operation. Before, the result used the latest text from any operation, so when a later step failed, an earlier step's text could be returned as the final candidate. An earlier output can still be read on request, by name.

What changes

  • Final candidate. TargetOutcome.available, ProtectionResult.content() and ProtectionResult.protected read only the last text-producing operation's output. That is the same operation the covering verifiers are bound to. If it did not succeed, the target has no candidate and keeps its own reason.
  • Reading one operation's output. ProtectionResult.value(target, port, producer=name) returns that operation's output for the target. Without producer, value keeps its meaning: the latest value of the port from any operation.
  • Release causes. These need no new type: TargetOutcome.withheld_by and the member outcomes already explain group and dependency withholding. New spec checks pin this.
  • Unchanged. Routes and user output: each route has one text-producing operation. anonymizer.graph.__all__ is unchanged (24).

Checks

  • make check passes.
  • pytest gives 1593 passed.
  • The spec programs give 143 PASS and 8 RETIRED. SP-23 adds five checks. E23.1 (B raises after A) fails before this change and passes after; the others pin behaviour that already held.
  • Replacing the prepared final_text is rejected by the existing seal.
  • Ruled source lines: +6.

Stacks on the checks-and-reasons PR.

🤖 Generated with Claude Code

binaryaaron and others added 2 commits November 3, 2026 09:19
content(), protected and TargetOutcome.available now read the output of
the last operation that produces text. When that operation did not
succeed, the target has no final candidate and its outcome gives the
reason; earlier text is no longer substituted. value() gains a
producer= argument to read one operation's own output.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
@binaryaaron
binaryaaron requested a review from a team as a code owner November 3, 2026 17:10
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects result-reading behavior in Graph SDK workflows.

The PR appears safe to merge, with a non-blocking concern about reads from operations with repeated names.

Findings

  1. P2 Repeated names hide later outputs ▶

Summary

The PR makes final-candidate reads use the last operation that produces text, rather than falling back to earlier output.

  • Results use the last text-producing step as final, while named reads keep earlier output available.

Reviews (1) · Last reviewed commit: "style(tests): format final-candidate spe..." · Reviewed by Greptile

operation named ``producer`` only; None if never produced."""
outputs = self.state.rows[self._index(target)].outputs
if producer is not None:
return outputs.get(self.prepared.names.index(producer), {}).get(port)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Repeated names hide later outputs

value(..., producer=name) silently selects the first operation with that name. Workflow.flatten() allows repeated names, so running two operations called redact makes the second output impossible to select through this API. Reject ambiguous names or give callers a way to select each operation instead of silently returning the first output.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we reject duplicate flattened operation names during preparation? With two operations named redact, value(target, producer="redact") returns the first output and offers no way to select the second. content() and protected still select the correct final output, so this seems non-blocking, but explicit inspection should be unambiguous.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes—reject duplicate flattened operation names during prepare, before building the plan. That makes producer=name unambiguous without changing its API; nested workflows can still reuse local names when their flattened, prefixed names differ. A stable rejection reason such as duplicate_operation_name would also make the failure explicit.

Copy link
Copy Markdown
Collaborator

The final-producer change and #354 address two different places where stale text can survive. Selecting the last text-producing operation still leaves the repair loop inside that operation responsible for returning its latest candidate. Can we keep a multi-repair case through the Rewrite route when these changes come together? That would cover the RFC’s requirement that the final assessment applies to the candidate actually returned.

Base automatically changed from aagonzales/feature/graph-sdk-checks-and-reasons to aagonzales/graph-sdk/feat/main November 4, 2026 17:38
@binaryaaron
binaryaaron merged commit 490a57a into aagonzales/graph-sdk/feat/main Nov 4, 2026
7 checks passed
@binaryaaron
binaryaaron deleted the aagonzales/feature/graph-sdk-final-candidate branch November 4, 2026 17:38

Copy link
Copy Markdown
Collaborator

The November 4 GB300 rerun covers the two fixes separately now. At 20f2b230, a failed final producer leaves the candidate absent. On #371 at 0fed56fb, the repair controls retain the second repair when it passes and the third when iterations are exhausted, with seed columns preserved. The remaining check is a multi-repair Rewrite run on the combined SDK head, confirming that the returned text and final assessment refer to the same candidate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants