feat(goal): a steer is a gated obligation, not advisory text - #535
Merged
Conversation
An operator's redirect to a running goal now lives on the goal's desired state, is delivered to the run on every turn it survives, and refuses the run's completion claim until the run has said what it did about it. Steering that only appends text to the prompt is steering that gets ignored: the recorded case is five messages in thirty minutes, all delivered, all ignored, and a commit thirty-eight seconds after the last one. So the primitive is not delivery, it is that the run cannot report success while an obligation is outstanding. Spec.Steers carries the redirects and Status.Steers records what the run was given and the account that discharged it. A redirect the run has been handed cannot be dropped or reworded, the way an invariant cannot: the party under an obligation is the one party who must not be able to edit it. Issuing another is always allowed, which is how a wrong one is corrected. Discharge goes through goal.SteerJudge, asked at the completion claim with the run's own account of finishing. A redirect the judge does not name stays outstanding, and the goal settles un-done with the redirect and the account quoted. evidence.ModelSteerJudge is the shipped judge: it rules on one redirect at a time, refuses an acceptance that quotes nothing from the account, and records each judgement on the run's spine. A goal steered with no judge wired stalls as SteerJudgeMissing and is released when one is wired. `flynn steer <run-id> "<instruction>"` is the operator surface. It writes to the durable record, so it redirects a run this process is not driving, and it refuses a run that has already finished. Signed-off-by: Ion Alpha <contact@ionalpha.io>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
The paths a redirect takes when something around it fails were untested: a record that cannot be read or written, a judge that cannot be asked or recorded, a redirect set the reconciler would refuse at admission, and a finding about a redirect the status has not taken on yet. Each of these decides whether an operator's instruction stays outstanding or is silently discharged, so each is now pinned by a test that says which way it must fall. Signed-off-by: Ion Alpha <contact@ionalpha.io>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
An operator can now redirect a goal that is already running, and the redirect is an obligation rather than a suggestion: it lives on the goal's desired state, is delivered to the run on every turn it survives, and refuses the run's completion claim until the run has said what it did about it.
flynn steer <run-id> "<instruction>"is the operator surface.goal.Spec.Steerscarries the redirects,goal.Status.Steersrecords what the run was given and the account that discharged it,goal.SteerJudgerules on the account, andevidence.ModelSteerJudgeis the shipped judge.Why
Steering that is only injected into the prompt is steering that gets ignored. The clearest record of it is a timeline rather than an argument: a human sent five messages in thirty minutes, each folded into the prompt as advisory prose, and the loop finished its build, passed its own review ten seconds after being asked to read the guidance, and committed thirty-eight seconds later. Every message arrived. None of them changed a decision, because nothing downstream of the prompt ever asked whether they had been addressed.
So the primitive is not "deliver the text", it is that the run cannot report success while an obligation is outstanding. Three rules are what make it one.
It is delivered on every turn it survives, not once. A message folded into the transcript when it arrived is one the pruner, the compactor and a reseed are all free to drop before the turn that finally claims completion, so it is rendered from the durable record each turn instead.
It is discharged by an account, and the account is judged. When the run claims it is finished, its own statement of what it did is held against every redirect outstanding, and a judge decides whether that statement addresses each one. A redirect the judge does not name stays outstanding: silence is a refusal, so a judge that answers half the question leaves the other half unpaid. A goal steered on a host with no judge wired stalls as
SteerJudgeMissingand is released when one is wired, which is the same ruleWithInvariantAuditfollows.It cannot be withdrawn by the run. Once a reconcile has taken a redirect on, dropping it or rewording it is refused as a terminal spec fault, for the reason an invariant's is: the one party who must not be able to edit an obligation is the party under it, and the spec is not a surface only the operator can reach. Issuing another is always allowed, which is how a wrong one is corrected.
A steer is deliberately not an amendment. Rewriting the objective changes what done means, and the grader then rules on the amended text, so the correction disappears into the definition of success. A redirect leaves the objective and the stop condition untouched and is checked separately.
How to verify
go test ./goal/... ./mission/... ./evidence/... ./cmd/flynn/ -run 'Steer|Redirect|Account|Acknowledg'End to end: start a goal,
flynn steer <run-id> "you are writing to sessions; write to events instead"from another terminal, and the next turn carries the redirect and the rule it will be held to. A run that finishes without saying what it did about it settles stalled asSteerUnaddressed, with the instruction and the run's own account of finishing both quoted in the message.The tests carry the argument: the completion claim is refused while a redirect is unanswered (
goal/steerrun_test.go), the redirect survives a turn whose transcript never carried it (mission/steer_test.go), an acceptance quoting nothing from the account discharges nothing (evidence/steerjudge_test.go), and a withdrawn or reworded redirect is refused at admission (goal/steer_test.go).Notes for reviewers
The judgement is asked at the completion claim rather than after each step, which is the opposite placement to the invariant audit and comes from the same argument. A term of the run is checked before the stop evaluator so a breach cannot be traded against having finished; a redirect is checked at the claim because the claim is where the run states what it did, and that statement is the thing being judged.
A goal that reported completion and is steered afterwards is judged by the same rule against the account it already gave, so it settles saying the redirect was not addressed.
flynn steerrefuses a finished run up front rather than letting that happen, since holding a finished account against an instruction that did not exist when it was written stops a run for something nobody could have acted on.Taking on a new redirect clears the non-convergence count. That count stops a run that is being told nothing new, and an operator redirect is the run being told something new by the party the count was standing in for.
Left out on purpose: the kill switch from the same piece of work (a stop that takes effect at tool dispatch rather than at a step boundary, with a self-test proving it engages). It shares nothing with this beyond the operator holding both, and it belongs in its own change.