docs(workflow): correct doc comments that describe fields and behaviour that do not exist - #1711
sushant-me wants to merge 7 commits into
Conversation
…ur that do not exist Fixes google#1541 (items 1-5; item 6 needs a separate decision). Several doc comments in workflow/ and agent/workflowagent/ described things a reader could not detect were wrong without reading the implementation, and downstream documentation had copied some of them verbatim. - NodeState.PendingRequest does not exist. Every occurrence was inside a comment. The real carriers are NodeState.Interrupts (the long-running tool call IDs) and the unexported interruptSchemas map. Rewritten at all eight sites. - RunStateSessionKey does not exist. It was named once, in a comment. - RunState is not persisted in session.State. The runtime path is ReconstructRunState, which rebuilds it by scanning session event history (persistence.go), and a grep for session.State writes in non-test workflow code returns comments only. Corrected in state.go, workflow.go, agent/workflowagent/workflow.go, including the Name() accessor and the detectResume doc. - The workflow name is not a state key. Run prefixes the node path it records with it, except for a root wrapper; RunNode and Resume add no prefix. - Resume step 2 claimed a duplicate call "becomes a no-op". It is not: a re-entry node (RerunOnResume) is set to NodePending and re-runs, and a handoff node yields ErrNothingToResume. Both cases are now stated. - JoinNode's "configuration error" is not enforced. validateFanIn skips every *JoinNode. The comment now says so, and describes the real symptom (silent, completion-order dependent, no error and no hang) rather than claiming the run is rejected. - DefaultRetryConfig said "retry every error"; defaultShouldRetry excludes ErrInputValidation. Also corrected two adjacent inaccuracies in text this change did not otherwise touch: NodeState.Interrupts claimed "Non-empty iff Status == NodeWaiting" (a partial resume sets NodePending with non-empty Interrupts, and the WaitForOutput park sets NodeWaiting without clearing it), and a test comment named PendingRequest. Testing Plan: - go build -mod=readonly: exit 0. - go test -race -mod=readonly -count=1 -shuffle=on ./workflow/... ./agent/workflowagent/...: both ok. - gofmt -l: clean. go mod tidy -diff: prints nothing. - golangci-lint is not installed locally at the CI-pinned v2.3.1, so it was not run; this change is comment-only and has no lint surface beyond gofmt. - The diff contains no non-comment added or removed line (checked with git diff -U0 filtered for non-comment changes). - No behaviour change for anyone on the current release: only comments changed. The claims were checked against the source in three rounds of independent review, which is what caught the two versions of the JoinNode wording that over-claimed the barrier mechanism, an earlier claim that a duplicate Resume always yields ErrNothingToResume, and an earlier fix that would have stated a false invariant about Interrupts.
wolo-lab
left a comment
There was a problem hiding this comment.
Most of these corrections check out against the code, and removing the session.State and PendingRequest references is a clear improvement. Two places still describe behavior the code doesn't have, and since accuracy is the point of this PR, they should be fixed before merge. One is below, and the other is inline on resume.go.
The workflow name doesn't separate history during reconstruction. The new New doc says the prefixed node path "is what ReconstructRunState matches on when it attributes history back to nodes", and advises distinct names so paths "remain distinguishable" (workflow/workflow.go#L225-L236). Reconstruction attributes an event through eventNodeName, which returns the first path segment that matches one of this workflow's node names and ignores the workflow-name segment entirely. wfA@1/asker@1, wfB@1/asker@1 and asker@1 all resolve to asker. What actually keeps two runs apart is the invocation-ID filter (persistence.go#L72-L78), and with an empty invocationID two workflows sharing a node name collide whatever their names are. The accurate statement is that Run prefixes the path with the name and reconstruction is scoped by invocation ID. The "Give a workflow a name whenever more than one can run in the same session" sentence carries the same claim, and so does the name field comment at workflow.go#L153-L156.
| // re-entry case. | ||
| // | ||
| // 3. Routes the response to the asker's successors as if the | ||
| // asker had emitted it as its output (handoff mode). The |
There was a problem hiding this comment.
Step 3 still says the asker "does NOT re-execute", which is true only for the handoff case and now contradicts the re-entry bullet in step 2 directly above it. Folding step 3 into the handoff bullet would remove the conflict.
The comment inside the function at resume.go#L111-L116 has the same problem. It still says the gating "keeps Resume idempotent: a duplicate turn ... reschedules nothing", which the new step 2 correctly says is false for re-entry nodes. A duplicate resume of a re-entry node re-runs it and its successor a second time, with no error.
| // re-entry node, where some interrupts are resolved and others are | ||
| // not and the node runs again to re-interrupt for the rest. A node | ||
| // parked by WaitForOutput also reaches NodeWaiting, but that park | ||
| // does not clear this field. |
There was a problem hiding this comment.
"Does not clear this field" reads as if Interrupts may still hold IDs for a WaitForOutput park. The WaitForOutput park sets no interrupt ID at all (scheduler.go#L850-L855), so the useful fact for a reader is the opposite: Status == NodeWaiting can come with an empty Interrupts.
| // consumed by the first call, so no waiting node matches and | ||
| // Second resume with the same payload: the handoff completed the | ||
| // asker and cleared its interrupts on the first call, so no | ||
| // waiting node matches and |
There was a problem hiding this comment.
Nothing from the first call carries over to the second one, since RunState is rebuilt from event history on every turn. The duplicate is rejected because the response is already in history (so it doesn't count as answered this turn) and the successor is already completed. The rewrap also leaves "waiting node matches and" on a line of its own.
hitl_test.go#L76-L79 still says the test "proves resume goes through session.State", which is the same stale reference this PR removes elsewhere.
| // long-running interrupt (the nodeRun collects these from | ||
| // Event.LongRunningToolIDs), the node transitions to NodeWaiting | ||
| // instead of NodeCompleted, the interrupt IDs are recorded on | ||
| // NodeState.Interrupts, and successors are not scheduled. The scheduler's main loop terminates naturally when |
There was a problem hiding this comment.
Non-blocking: this reflowed line runs to about 110 columns, while the rest of the block wraps at about 66.
Addresses the review on google#1711. The field and New doc comments said the workflow name namespaces a workflow's nodes and that the prefixed path is what ReconstructRunState matches on, advising distinct names so histories stay distinguishable. eventNodeName takes the first path segment naming one of the workflow's nodes and ignores the workflow-name segment, so wfA@1/asker@1, wfB@1/asker@1 and asker@1 all resolve to asker. History is scoped to a run by the invocation-ID filter, not by the name. Also corrects resume.go step 3, which said the asker never re-executes while step 2 directly above describes the re-entry case where it does, and state.go, which said a WaitForOutput park 'does not clear' Interrupts when that park creates no interrupt ID at all. Comments only - no behaviour change.
Addresses the review on google#1711. The field and New doc comments said the workflow name namespaces a workflow's nodes and that the prefixed path is what ReconstructRunState matches on, advising distinct names so histories stay distinguishable. eventNodeName takes the first path segment naming one of the workflow's nodes and ignores the workflow-name segment, so wfA@1/asker@1, wfB@1/asker@1 and asker@1 all resolve to asker. History is scoped to a run by the invocation-ID filter, not by the name. Also corrects resume.go step 3, which said the asker never re-executes while step 2 directly above describes the re-entry case where it does, and state.go, which said a WaitForOutput park 'does not clear' Interrupts when that park creates no interrupt ID at all. Comments only - no behaviour change.
Addresses the review on google#1711. The field and New doc comments said the workflow name namespaces a workflow's nodes and that the prefixed path is what ReconstructRunState matches on, advising distinct names so histories stay distinguishable. eventNodeName takes the first path segment naming one of the workflow's nodes and ignores the workflow-name segment, so wfA@1/asker@1, wfB@1/asker@1 and asker@1 all resolve to asker. History is scoped to a run by the invocation-ID filter, not by the name. Also corrects resume.go step 3, which said the asker never re-executes while step 2 directly above describes the re-entry case where it does, and state.go, which said a WaitForOutput park 'does not clear' Interrupts when that park creates no interrupt ID at all. Comments only - no behaviour change.
|
Thanks — both points were right, and the Fixed in
Still to do, and I wanted to check rather than guess:
Comments only, no behaviour change. Say the word if you would rather I fold the two remaining ones into the same revision. |
…t left behind The edit that corrected the New doc anchored mid-sentence, so the original opening survived above the correction and the doc contradicted itself. Removed. Comments only.
scheduler.go: the 'human-input waiting branch' paragraph had one line running to ~108 columns while the block wraps at ~66. Re-wrapped, no wording change. hitl_test.go: the duplicate-resume comment said the first call 'completed the asker and cleared its interrupts', implying state carried over. RunState is rebuilt from event history every turn, so nothing carries over; the duplicate is rejected because the response it replays has already been consumed. Comments only - no behaviour change.
scheduler.go: the 'human-input waiting branch' paragraph had one line running to ~108 columns while the block wraps at ~66. Re-wrapped, no wording change. hitl_test.go: the duplicate-resume comment said the first call 'completed the asker and cleared its interrupts', implying state carried over. RunState is rebuilt from event history every turn, so nothing carries over; the duplicate is rejected because the response it replays has already been consumed. Comments only - no behaviour change.
|
Both remaining points are done — the review is now fully addressed.
All six items from your review, comments only, no behaviour change:
One correction worth flagging: my first attempt at the No rush on the re-review — thanks for the detail in the original, it made the mechanism clear. |
Fixes #1541 (items 1–5). Item 6 is deliberately not touched — as the issue says, it needs a decision rather than a drive-by edit.
What changed
Only doc comments.
git diff -U0filtered for non-comment lines is empty, so nothing executable is modified.PendingRequestis not a field. Every occurrence in non-test code was inside a comment. The real carriers areNodeState.Interruptsand the unexportedinterruptSchemasmap. Corrected at all eight sites the issue lists. One more was in a test comment inagent/workflowagent/hitl_test.go.RunStateSessionKeydoes not exist. It was named once, in a comment.Run state is not persisted in
session.State. The only runtime path isagent/workflowagent/workflow.go:148→ReconstructRunState, which rebuilds from session event history, andpersistence.go:59says so explicitly. A grep forsession.Statewrites in non-test workflow code returns comments and nothing else. Corrected instate.go,workflow.go(including theName()accessor) andagent/workflowagent/workflow.go(including thedetectResumedoc).The workflow name is not a state key.
Runprefixes the node path it records with it, except for a root wrapper;RunNodeandResumeadd no prefix —w.Name()is read in exactly one place.Resumestep 2 was wrong in a way that matters. A duplicate is not a no-op. A re-entry node (RerunOnResume, the default forDynamicNode) is set toNodePendingand re-runs; a handoff node yieldsErrNothingToResume. Both cases are now stated, because a reader who trusted the old sentence wrote no handling for either.JoinNode—validateFanInskips every*JoinNode(validation.go:377), so a conditional predecessor into a join is never rejected. The comment now says that, and describes the symptom rather than claiming rejection.DefaultRetryConfigsaid "retry every error";defaultShouldRetryexcludesErrInputValidation.Two adjacent inaccuracies in text this change did not otherwise touch were also corrected, since they are the same class and sit in the same doc comments:
NodeState.Interruptsclaimed "Non-empty iff Status == NodeWaiting", and a test comment namedPendingRequest.Note on the earlier attempt
I opened this once as #1710 and closed it within minutes: the branch had been cut while
another branch was checked out, so it inherited an unrelated commit, and
git add -Aswept in an untracked scratch directory. It carried
tool/mcptoolset/and anauditprobe/directory that have nothing to do with this issue. #1710 is closed.This PR is rebuilt from
origin/main(9026ee8) and contains one commit touching onlythe nine files below. I have left the closed PR up rather than deleting it so the
mistake is visible.
Testing Plan
golangci-lintis not installed locally at the CI-pinned v2.3.1, so it was not run. The change is comment-only and has no lint surface beyondgofmt, but I would rather say that than imply a clean lint run.Behaviour change for someone already on the current release: none. Comments only.
I did not run anything against a live model or API — none is involved.
On the wording
The comments about
JoinNodeand aboutResume's duplicate handling went through three rounds of independent review, and the first two attempts at theJoinNodeparagraph were wrong: I claimed a route-skipped predecessor "never releases the barrier", then that it "still completes" and contributes a nil entry. Both are false — the outcome depends on the graph and on completion order. The committed wording deliberately states only what I could reproduce: that it is unvalidated, that the failure is silent, that the join and downstream can be skipped, and that the run still reports success. I have not described the barrier mechanism, because I could not state it correctly.The same process caught a version of the
Resumecomment that claimed a duplicate always yieldsErrNothingToResume(re-entry nodes re-run), and a version of theInterruptsfix that would have introduced a false invariant.If you would rather the
JoinNodeparagraph carry the precise mechanism, it needs someone who can verify both orderings — the reproductions I used are described above and I am happy to add them as a table-driven test if that is wanted.