Problem
CLIProvider.run returns the first provider whose command exits 0 with non-empty stdout (cli_provider.py:172-177). It never checks whether the output can satisfy the assessment contract. A provider that answers with prose, a fenced block, or a partial object is recorded as ok and ends the provider loop.
run_screening then fails to parse, retries once with the contract-violation prefix (screening.py:618-625), and that retry re-enters the same loop — so it reaches the same provider first and can fail the same way. Two ok attempts later, the run lands on fallback_reason="assessment-contract-exhausted".
The effect is that one provider returning unusable output prevents every later provider in the chain, including the local endpoint, from ever being asked. Observed in a real run: the chain recorded two ok attempts for the first provider and never reached the local endpoint, even though the local endpoint produces contract-valid output for the same prompt.
Why it matters
The chain exists so a weaker provider can be replaced by a stronger one. A non-empty response that cannot be parsed is a failed attempt, not a successful one, but the adapter cannot tell them apart because it applies no contract check at all.
The document-loss consequence is now blocked separately: a fallback publish is withheld when the record already holds a real assessment. That guard limits the damage; it does not restore the skipped providers.
Suggested direction
Give the adapter a caller-supplied acceptance predicate (the same parse the application layer runs), and treat a rejected response as a failed attempt: record a distinct status such as unparseable_output and continue to the next provider. Keep the raw text of the last rejection for telemetry.
The predicate must come from the caller. The adapter layer must not import the assessment parser.
Acceptance
- A provider whose stdout is non-empty but contract-invalid does not end the provider loop; the next provider runs.
- The attempt chain distinguishes that outcome from
ok and from empty_output.
- A run whose first provider always answers with contract-invalid output still publishes when a later provider answers correctly.
- No change when every provider answers correctly.
Problem
CLIProvider.runreturns the first provider whose command exits 0 with non-empty stdout (cli_provider.py:172-177). It never checks whether the output can satisfy the assessment contract. A provider that answers with prose, a fenced block, or a partial object is recorded asokand ends the provider loop.run_screeningthen fails to parse, retries once with the contract-violation prefix (screening.py:618-625), and that retry re-enters the same loop — so it reaches the same provider first and can fail the same way. Twookattempts later, the run lands onfallback_reason="assessment-contract-exhausted".The effect is that one provider returning unusable output prevents every later provider in the chain, including the local endpoint, from ever being asked. Observed in a real run: the chain recorded two
okattempts for the first provider and never reached the local endpoint, even though the local endpoint produces contract-valid output for the same prompt.Why it matters
The chain exists so a weaker provider can be replaced by a stronger one. A non-empty response that cannot be parsed is a failed attempt, not a successful one, but the adapter cannot tell them apart because it applies no contract check at all.
The document-loss consequence is now blocked separately: a fallback publish is withheld when the record already holds a real assessment. That guard limits the damage; it does not restore the skipped providers.
Suggested direction
Give the adapter a caller-supplied acceptance predicate (the same parse the application layer runs), and treat a rejected response as a failed attempt: record a distinct status such as
unparseable_outputand continue to the next provider. Keep the raw text of the last rejection for telemetry.The predicate must come from the caller. The adapter layer must not import the assessment parser.
Acceptance
okand fromempty_output.