mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 07:52:20 +02:00
fix: close Wave 1.1 completion-gate audit findings
- Headless consumers (task scheduler, background follow-up) now treat a completion-gate final_response as the authoritative answer instead of collecting deltas only. A gated replacement no longer leaves scheduled output empty, which used to trigger an extra, ungated grace-summary model call. - The scheduler closes the agent stream with contextlib.aclosing, so the approval-pause break unwinds the gate's journal and teacher-takeover context in its own task. Chained runs no longer inherit a stale parent_run_id, and later finalization no longer raises ContextVar reset errors. - On provider error, the completion gate applies the live answer's statement filter to persisted round_texts. Diagnostics and the failure note survive; claims rejected by the gate cannot reappear on reload.
This commit is contained in:
@@ -219,7 +219,9 @@ async def test_failed_terminal_diagnostics_survive_answer_replacement(terminal_k
|
||||
chunks = [chunk async for chunk in stream([])]
|
||||
terminal = next(data['data'] for event, data in _frames(chunks)
|
||||
if event == 'message' and data.get('type') == terminal_kind)
|
||||
assert terminal['round_texts'] == diagnostics
|
||||
# Diagnostics and the failure note survive; the rejected claim does not,
|
||||
# because round_texts are rendered again when the turn is reloaded.
|
||||
assert terminal['round_texts'] == ['Earlier tool failure and retry', '[Agent stopped: HTTP 504]']
|
||||
assert terminal['round_models'] == ['first-model', 'failed-model']
|
||||
assert terminal['failure'] == {'status': 504, 'message': 'Model request failed'}
|
||||
assert terminal['failed'] is True
|
||||
@@ -229,6 +231,42 @@ async def test_failed_terminal_diagnostics_survive_answer_replacement(terminal_k
|
||||
assert _labels(chunks).index(terminal_kind) < _labels(chunks).index('error')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provider_failure_round_texts_cannot_replay_removed_claim_after_reload():
|
||||
claim = 'I created report.md and all tests passed.'
|
||||
note = '[Agent stopped: Model request failed (HTTP 504)]'
|
||||
|
||||
@with_completion_gate
|
||||
async def stream(messages):
|
||||
yield _event({'type': 'tool_start', 'tool': 'read_file'})
|
||||
yield _event({'delta': 'Inspected the layout. ' + claim})
|
||||
yield ERROR
|
||||
yield _event({'type': 'agent_terminal', 'data': {
|
||||
'failed': True, 'failure': {'status': 504, 'message': 'Model request failed'},
|
||||
'tool_events': [{'round': 1, 'tool': 'read_file'}],
|
||||
'round_texts': ['Inspected the layout. ' + claim, 'Retrying the build.\n\n' + note],
|
||||
}})
|
||||
yield DONE
|
||||
|
||||
chunks = [chunk async for chunk in stream([{'role': 'user', 'content': 'create report.md and run the tests'}])]
|
||||
assert _labels(chunks) == [
|
||||
'tool_start', 'final_response', 'completion_decision', 'agent_terminal', 'error',
|
||||
], _labels(chunks)
|
||||
assert DONE not in chunks
|
||||
live = next(data['content'] for event, data in _frames(chunks)
|
||||
if event == 'message' and data.get('type') == 'final_response')
|
||||
terminal = next(data['data'] for event, data in _frames(chunks)
|
||||
if event == 'message' and data.get('type') == 'agent_terminal')
|
||||
# The chat route persists this metadata and the renderer rebuilds one bubble
|
||||
# per round from it, so every persisted round is presentation.
|
||||
persisted = terminal['round_texts']
|
||||
assert persisted == ['Inspected the layout.', 'Retrying the build.\n\n' + note]
|
||||
for text in [live, *persisted]:
|
||||
assert 'tests passed' not in text and 'created report.md' not in text
|
||||
assert 'Inspected the layout.' in live
|
||||
assert terminal['completion_decision']['status'] == 'failed'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_boundary_is_independent_of_network_chunking():
|
||||
@with_completion_gate
|
||||
|
||||
Reference in New Issue
Block a user