require content in directory artifact outputs

This commit is contained in:
pewdiepie-archdaemon
2026-09-18 20:17:19 +00:00
parent 27e53e857c
commit 0865e0e8da
2 changed files with 175 additions and 1 deletions
+49 -1
View File
@@ -1471,6 +1471,54 @@ def execution_targets_required_artifact(tool_name, arguments, required_artifacts
return any(str(path or '').rstrip('/') in serialized for path in required_artifacts) return any(str(path or '').rstrip('/') in serialized for path in required_artifacts)
def required_artifacts_have_content(required_artifacts):
"""Return true only when every required output contains material evidence.
A runner may declare a directory such as ``/tmp_workspace/results`` as its
output contract. Merely creating that directory is setup, not completion.
Directory outputs therefore require at least one non-empty regular file;
file outputs must themselves be non-empty. Required workspace artifacts
are local to the native runtime, so checking their post-mutation state is
stronger than inferring completion from a shell command string.
"""
targets = [Path(str(path or '').strip().rstrip('/')) for path in required_artifacts]
if not targets:
return False
for target in targets:
try:
if target.is_file():
if target.stat().st_size <= 0:
return False
continue
if target.is_dir():
if not any(
child.is_file() and child.stat().st_size > 0
for child in target.rglob('*')
):
return False
continue
except OSError:
return False
return False
return True
def successful_required_artifact_mutation(tool_name, arguments, required_artifacts):
"""Recognize completed outputs without letting an empty directory pass.
Typed tools can authoritatively report a successful exact file write even
when their workspace is mounted outside this process. Directory contracts
are different: mentioning or creating the directory only establishes a
container, so they require observable non-empty file content.
"""
targets = [str(path or '').strip().rstrip('/') for path in required_artifacts]
if required_artifacts_have_content(targets):
return True
if not targets or any(not Path(target).suffix for target in targets):
return False
return execution_targets_required_artifact(tool_name, arguments, targets)
def artifact_completion_tool_schemas(offered_schemas, required_artifacts): def artifact_completion_tool_schemas(offered_schemas, required_artifacts):
"""Bind the sole artifact writer to the runner-declared output file. """Bind the sole artifact writer to the runner-declared output file.
@@ -5581,7 +5629,7 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
successful_duplicate_counts.pop(prior_signature, None) successful_duplicate_counts.pop(prior_signature, None)
if canonical(block.tool_type) in {'edit_document', 'update_document'}: if canonical(block.tool_type) in {'edit_document', 'update_document'}:
successful_editor_writer = canonical(block.tool_type) successful_editor_writer = canonical(block.tool_type)
if execution_targets_required_artifact( if successful_required_artifact_mutation(
block.tool_type, args, required_artifacts, block.tool_type, args, required_artifacts,
): ):
successful_artifact_write = True successful_artifact_write = True
+126
View File
@@ -1,5 +1,6 @@
from types import SimpleNamespace from types import SimpleNamespace
from dataclasses import replace from dataclasses import replace
from pathlib import Path
import json import json
import jsonschema import jsonschema
import pytest import pytest
@@ -471,6 +472,131 @@ def test_runtime_required_artifacts_includes_runner_declared_directory():
) == ('/tmp_workspace/results',) ) == ('/tmp_workspace/results',)
def test_required_artifact_content_rejects_empty_directories(tmp_path):
import src.clean_agent_preview as module
output = tmp_path / 'results'
output.mkdir()
(output / 'nested-empty').mkdir()
assert not module.required_artifacts_have_content((str(output),))
def test_required_artifact_content_accepts_nonempty_files_and_nested_outputs(tmp_path):
import src.clean_agent_preview as module
output_file = tmp_path / 'output.html'
output_file.write_text('<h1>done</h1>')
output_dir = tmp_path / 'results'
nested = output_dir / 'part-01'
nested.mkdir(parents=True)
(nested / 'answer.json').write_text('{"done": true}')
assert module.required_artifacts_have_content((str(output_file),))
assert module.required_artifacts_have_content((str(output_dir),))
assert not module.required_artifacts_have_content((str(tmp_path / 'missing'),))
def test_required_artifact_mutation_trusts_exact_files_but_not_empty_directory_setup(tmp_path):
import src.clean_agent_preview as module
output_dir = tmp_path / 'results'
output_dir.mkdir()
assert not module.successful_required_artifact_mutation(
'bash', {'command': f'mkdir -p {output_dir}'}, (str(output_dir),),
)
output_file = tmp_path / 'output.html'
assert module.successful_required_artifact_mutation(
'write_file', {'path': str(output_file), 'content': '<h1>done</h1>'},
(str(output_file),),
)
@pytest.mark.asyncio
async def test_empty_required_directory_keeps_native_turn_running(monkeypatch, tmp_path):
import src.clean_agent_preview as module
output = tmp_path / 'results'
responses = iter([
{'choices': [{'delta': {'tool_calls': [{
'index': 0,
'id': 'call-mkdir',
'type': 'function',
'function': {
'name': 'bash',
'arguments': json.dumps({'command': f'mkdir -p {output}'}),
},
}]}}]},
{'choices': [{'delta': {'tool_calls': [{
'index': 0,
'id': 'call-write',
'type': 'function',
'function': {
'name': 'write_file',
'arguments': json.dumps({
'path': str(output / 'answer.txt'), 'content': 'done',
}),
},
}]}}]},
{'choices': [{'delta': {'content': 'Completed the requested output.'}}]},
])
requests = []
executed = []
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]'
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))
async def execute(block, **kwargs):
executed.append(block.tool_type)
if block.tool_type == 'bash':
output.mkdir()
else:
(output / 'answer.txt').write_text('done')
return block.tool_type, {'output': '(no output)', 'exit_code': 0}
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
monkeypatch.setattr(module, 'execute_tool_block', execute)
schemas = [
item for item in FUNCTION_TOOL_SCHEMAS
if item['function']['name'] in {'bash', 'write_file'}
]
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
raw = [chunk async for chunk in stream_preview(
endpoint_url='http://test', model='qwen-test',
messages=[{'role': 'user', 'content': 'Create the requested output files.'}],
headers={}, 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': [str(output)]},
}, max_rounds=8,
)]
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
assert executed == ['bash', 'write_file']
after_mkdir = next(
event for event in events
if event.get('type') == 'agent_step' and event.get('calls_used') == 1
)
assert after_mkdir['required_artifact_pending'] is True
assert (output / 'answer.txt').read_text() == 'done'
def test_artifact_completion_schema_binds_single_required_file_without_mutating_source(): def test_artifact_completion_schema_binds_single_required_file_without_mutating_source():
source = [{ source = [{
'type': 'function', 'type': 'function',