mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-03 11:42:23 +01:00
recover required artifacts after tool suppression
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
0.20.9
|
0.20.10
|
||||||
|
|||||||
@@ -6357,6 +6357,44 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
|
|||||||
break
|
break
|
||||||
if terminal_suppression_violation:
|
if terminal_suppression_violation:
|
||||||
missing_artifacts = missing_workspace_artifacts(latest_user, workspace)
|
missing_artifacts = missing_workspace_artifacts(latest_user, workspace)
|
||||||
|
remaining_artifact_tools = artifact_completion_tool_schemas(
|
||||||
|
[
|
||||||
|
schema for schema in offered
|
||||||
|
if canonical(schema['function']['name'])
|
||||||
|
not in permanently_suppressed_tools
|
||||||
|
],
|
||||||
|
required_artifacts,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
missing_artifacts
|
||||||
|
and remaining_artifact_tools
|
||||||
|
and not suppression_completion_attempted
|
||||||
|
and round_number < round_limit
|
||||||
|
):
|
||||||
|
suppression_completion_attempted = True
|
||||||
|
artifact_write_phase = True
|
||||||
|
force_no_tools_next_round = False
|
||||||
|
recovery = (
|
||||||
|
'Completion recovery: the repeated evidence tool is disabled. '
|
||||||
|
'Do not call it again. Use the evidence already returned and an '
|
||||||
|
'available workspace writer to create and verify the missing '
|
||||||
|
'artifact(s): ' + ', '.join(missing_artifacts) + '. '
|
||||||
|
'Do not perform more research before writing.'
|
||||||
|
)
|
||||||
|
if history and history[-1].get('_harness_control'):
|
||||||
|
history[-1]['content'] = (
|
||||||
|
str(history[-1].get('content') or '') + ' ' + recovery
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
history.append({
|
||||||
|
'role': 'user', '_harness_control': True, 'content': recovery,
|
||||||
|
})
|
||||||
|
yield event({
|
||||||
|
'type': 'completion_recovery',
|
||||||
|
'reason': 'suppressed_tool_artifact_recovery',
|
||||||
|
'missing_artifacts': list(missing_artifacts),
|
||||||
|
})
|
||||||
|
continue
|
||||||
if (
|
if (
|
||||||
not missing_artifacts
|
not missing_artifacts
|
||||||
and not suppression_completion_attempted
|
and not suppression_completion_attempted
|
||||||
|
|||||||
@@ -5964,6 +5964,108 @@ async def test_native_stream_terminates_after_calling_a_permanently_suppressed_t
|
|||||||
assert metrics['clean_v3_turn'][-1]['content'] == 'Final answer from existing evidence.'
|
assert metrics['clean_v3_turn'][-1]['content'] == 'Final answer from existing evidence.'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_suppressed_evidence_tool_recovers_missing_artifact_with_writer(
|
||||||
|
monkeypatch, tmp_path,
|
||||||
|
):
|
||||||
|
import src.clean_agent_preview as module
|
||||||
|
|
||||||
|
output = tmp_path / 'results' / 'report.md'
|
||||||
|
output_alias = '/workspace/results/report.md'
|
||||||
|
search_arguments = json.dumps({'query': 'arxiv cs.CV 2026-02-25'})
|
||||||
|
responses = iter([
|
||||||
|
{'choices': [{'delta': {'tool_calls': [{
|
||||||
|
'index': 0, 'id': f'search-{index}', 'function': {
|
||||||
|
'name': 'web_search', 'arguments': search_arguments,
|
||||||
|
},
|
||||||
|
}]}}]}
|
||||||
|
for index in range(1, 4)
|
||||||
|
] + [
|
||||||
|
{'choices': [{'delta': {'tool_calls': [{
|
||||||
|
'index': 0, 'id': 'write-1', 'function': {
|
||||||
|
'name': 'write_file', 'arguments': json.dumps({
|
||||||
|
'path': output_alias, 'content': '# Verified report\nEvidence retained.\n',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}]}}]},
|
||||||
|
{'choices': [{'delta': {'content': 'Created and verified the report.'}}]},
|
||||||
|
])
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
def __init__(self, payload): self.payload = payload
|
||||||
|
async def __aenter__(self): return self
|
||||||
|
async def __aexit__(self, *args): pass
|
||||||
|
def raise_for_status(self): pass
|
||||||
|
async def aiter_lines(self):
|
||||||
|
yield 'data: ' + json.dumps(self.payload)
|
||||||
|
yield 'data: [DONE]'
|
||||||
|
|
||||||
|
requests = []
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
async def __aenter__(self): return self
|
||||||
|
async def __aexit__(self, *args): pass
|
||||||
|
def stream(self, *args, **kwargs):
|
||||||
|
requests.append(kwargs['json'])
|
||||||
|
return Response(next(responses))
|
||||||
|
|
||||||
|
executions = []
|
||||||
|
|
||||||
|
async def execute(block, **kwargs):
|
||||||
|
executions.append(block.tool_type)
|
||||||
|
if block.tool_type == 'write_file':
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.write_text('# Verified report\nEvidence retained.\n')
|
||||||
|
return 'write_file', {'output': f'wrote {output}', 'exit_code': 0}
|
||||||
|
return 'web_search', {'output': 'useful arxiv evidence', 'exit_code': 0}
|
||||||
|
|
||||||
|
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||||||
|
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||||||
|
monkeypatch.setattr(module, 'NATIVE_ARTIFACT_RESEARCH_LIMIT', 100)
|
||||||
|
schemas = [
|
||||||
|
next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == name)
|
||||||
|
for name in ('web_search', 'write_file')
|
||||||
|
]
|
||||||
|
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||||||
|
raw = [chunk async for chunk in stream_preview(
|
||||||
|
endpoint_url='http://test', model='test', headers={},
|
||||||
|
messages=[{'role': 'user', 'content': (
|
||||||
|
'Find cs.CV papers submitted on 2026-02-25 and create '
|
||||||
|
f'{output_alias} from the returned evidence.'
|
||||||
|
)}],
|
||||||
|
turn_contract=contract, session_id='test', owner='test',
|
||||||
|
disabled_tools=set(), tool_policy=ToolPolicy(), workspace=str(tmp_path),
|
||||||
|
client_runtime_context={
|
||||||
|
'surface': 'odysseus-native', 'terminal_agent': True,
|
||||||
|
'unattended_mode': True,
|
||||||
|
'completion_requirements': {'required_artifacts': [output_alias]},
|
||||||
|
}, max_rounds=8,
|
||||||
|
)]
|
||||||
|
|
||||||
|
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
|
||||||
|
assert executions == ['web_search', 'write_file'], [
|
||||||
|
(event.get('type'), event.get('reason'), event.get('tool')) for event in events
|
||||||
|
]
|
||||||
|
recovery_events = [
|
||||||
|
event for event in events
|
||||||
|
if event.get('reason') == 'suppressed_tool_artifact_recovery'
|
||||||
|
]
|
||||||
|
assert recovery_events, [
|
||||||
|
(event.get('type'), event.get('reason'), event.get('tool'), event.get('output'))
|
||||||
|
for event in events
|
||||||
|
]
|
||||||
|
recovery = recovery_events[0]
|
||||||
|
assert recovery['missing_artifacts'] == [output_alias]
|
||||||
|
assert any(
|
||||||
|
tool['function']['name'] == 'write_file'
|
||||||
|
for tool in requests[4].get('tools', [])
|
||||||
|
)
|
||||||
|
final = [event for event in events if event.get('type') == 'final_response']
|
||||||
|
assert all('repeated a tool call' not in event.get('content', '') for event in final)
|
||||||
|
assert output.read_text().startswith('# Verified report')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_native_stream_stops_reexecuting_an_identical_failed_call(monkeypatch):
|
async def test_native_stream_stops_reexecuting_an_identical_failed_call(monkeypatch):
|
||||||
import src.clean_agent_preview as module
|
import src.clean_agent_preview as module
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
def test_original_harness_version_is_canonical_semver() -> None:
|
def test_original_harness_version_is_canonical_semver() -> None:
|
||||||
version = (ROOT / "HARNESS_VERSION").read_text(encoding="utf-8").strip()
|
version = (ROOT / "HARNESS_VERSION").read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
assert version == "0.20.9"
|
assert version == "0.20.10"
|
||||||
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user