mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-13 19:52:20 +02:00
2612 lines
116 KiB
Python
2612 lines
116 KiB
Python
from types import SimpleNamespace
|
||
import json
|
||
import jsonschema
|
||
import pytest
|
||
|
||
from src.clean_agent_preview import conversation, readonly_call, preview_call_allowed, evaluate_preview_call, authorized_write_families, compact_schemas, normalize_preview_function_args, private_browser_dom_batch, stream_preview, denied_response, requests_mutation, claims_completion, recent_successful_write_families, scope_preview_contract, multimodal_image_count, attachment_reference_count, active_document_context_message, active_email_context_message, targets_active_editor, active_editor_whole_draft_request, active_editor_suggestion_request, scope_active_editor_contract, native_execution_limits, runtime_required_artifacts, document_suggestions_event, required_read_tool_choice, sealed_read_arguments
|
||
|
||
|
||
def test_compact_notes_preserves_create_vs_edit_and_replacement_semantics():
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
compact = compact_schemas([schema])[0]['function']
|
||
assert 'add creates a new note' in compact['description']
|
||
assert 'update with id' in compact['description']
|
||
assert 'replaces the whole checklist' in compact['parameters']['properties']['checklist_items']['description']
|
||
|
||
|
||
def test_compact_notes_exposes_optional_explicit_checked_state():
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
for candidate in (schema, compact_schemas([schema])[0]):
|
||
parameters = candidate['function']['parameters']
|
||
assert parameters['properties']['done']['type'] == 'boolean'
|
||
assert 'omit to toggle' in parameters['properties']['done']['description']
|
||
assert 'done' not in parameters['required']
|
||
|
||
|
||
def test_compact_skills_distinguishes_field_edits_from_raw_text_patches():
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_skills')
|
||
compact = compact_schemas([schema])[0]['function']
|
||
props = compact['parameters']['properties']
|
||
assert props['procedure']['type'] == 'array'
|
||
assert 'add/edit' in props['procedure']['description']
|
||
assert 'not a flag' in props['procedure']['description']
|
||
assert 'exactly once' in props['old_string']['description']
|
||
assert 'full SKILL.md' in props['old_string']['description']
|
||
|
||
|
||
def test_compact_skills_preserves_reference_read_contract():
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_skills')
|
||
params = compact_schemas([schema])[0]['function']['parameters']
|
||
props = params['properties']
|
||
assert 'view = SKILL.md' in props['action']['description']
|
||
assert 'view_ref = supporting file' in props['action']['description']
|
||
assert 'not a file path' in props['name']['description']
|
||
assert 'view_ref only' in props['path']['description']
|
||
assert params['required'] == ['action'] # listing still needs no name/path
|
||
|
||
|
||
def test_interactive_ocr_uses_upload_references_not_arbitrary_workspace_reads():
|
||
owned_ref = evaluate_preview_call('extract_text', {'path': 'odysseus://attachment/fixture-upload'}, 'OCR this image')
|
||
assert owned_ref.allowed
|
||
assert owned_ref.effects == ('read_private',)
|
||
assert not evaluate_preview_call('extract_text', {'path': '/etc/passwd'}, 'OCR this image').allowed
|
||
assert not evaluate_preview_call('extract_text', {'path': '/workspace/image.png'}, 'OCR this image').allowed
|
||
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS, function_call_to_tool_block
|
||
from src.tool_policy import ToolPolicy
|
||
from src.turn_contract import resolve_full_inventory_contract
|
||
|
||
|
||
def test_native_execution_limits_allow_multi_artifact_work_without_unbounded_rounds():
|
||
assert native_execution_limits(64) == (64, 32)
|
||
assert native_execution_limits(1000) == (64, 32)
|
||
assert native_execution_limits("invalid") == (8, 32)
|
||
|
||
|
||
@pytest.mark.parametrize('prompt,expected', [
|
||
('Read /workspace/fixtures/paper.pdf and create /workspace/results.csv and /workspace/chart.png',
|
||
('/workspace/results.csv', '/workspace/chart.png')),
|
||
('Create /workspace/results.csv using /workspace/source.csv', ('/workspace/results.csv',)),
|
||
('Create /workspace/results.csv. Read /workspace/source.csv and /workspace/other.csv',
|
||
('/workspace/results.csv',)),
|
||
('Read /workspace/source.csv and /workspace/other.csv', ()),
|
||
('Create /workspace/results.v2.csv and /workspace/chart.v2.png',
|
||
('/workspace/results.v2.csv', '/workspace/chart.v2.png')),
|
||
])
|
||
def test_all_requested_outputs_survive_filename_periods(prompt, expected):
|
||
from src.clean_agent_preview import declared_workspace_artifacts
|
||
assert declared_workspace_artifacts(prompt) == expected
|
||
|
||
|
||
def test_notes_terminal_response_preserves_links_from_wrapped_executor_output():
|
||
from src.clean_agent_preview import notes_terminal_response
|
||
|
||
raw = json.dumps({
|
||
'results': '- [note-123] **Fixture note**',
|
||
'exit_code': 0,
|
||
})
|
||
assert notes_terminal_response(raw) == (
|
||
'Here are your notes (1):\n📝 [Fixture note](#note-note-123)'
|
||
)
|
||
|
||
|
||
def test_successful_document_suggestion_has_one_browser_owned_event():
|
||
suggestions = [{'find': 'wordy', 'replace': 'concise', 'reason': 'clarity'}]
|
||
assert document_suggestions_event({'doc_id': 'doc-1', 'suggestions': suggestions}) == {
|
||
'type': 'doc_suggestions', 'doc_id': 'doc-1', 'suggestions': suggestions,
|
||
}
|
||
assert document_suggestions_event(
|
||
{'doc_id': 'doc-1', 'suggestions': suggestions}, failed=True,
|
||
) is None
|
||
assert document_suggestions_event({'error': 'no match'}, failed=True) is None
|
||
|
||
|
||
def test_runtime_required_artifacts_includes_runner_declared_directory():
|
||
assert runtime_required_artifacts(
|
||
'Create the requested output.',
|
||
{'completion_requirements': {'required_artifacts': ['/tmp_workspace/results/']}},
|
||
) == ('/tmp_workspace/results',)
|
||
|
||
|
||
def test_prompt_input_paths_are_not_misclassified_as_required_artifacts():
|
||
assert runtime_required_artifacts(
|
||
'Use read_file to read /workspace/sample.txt. Read only.', {}
|
||
) == ()
|
||
assert runtime_required_artifacts(
|
||
'Use OCR to extract text from /workspace/receipt.png. Read only.', {}
|
||
) == ()
|
||
|
||
|
||
def test_prompt_output_path_is_tracked_without_its_source_path():
|
||
assert runtime_required_artifacts(
|
||
'Create an HTML report at /workspace/output.html from /workspace/input.csv.',
|
||
{},
|
||
) == ('/workspace/output.html',)
|
||
|
||
|
||
def test_compact_writer_advertises_parallel_independent_file_calls():
|
||
writer = next(
|
||
schema for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] == 'write_file'
|
||
)
|
||
assert 'multiple write_file calls in the same response' in writer['function']['description']
|
||
|
||
|
||
def test_read_only_gate_blocks_mutation_and_network_shell():
|
||
assert readonly_call('manage_notes', {'action': 'list'})
|
||
assert readonly_call('manage_notes', {'action': 'view', 'id': 'abc'})
|
||
assert not readonly_call('manage_notes', {'action': 'delete', 'id': 'abc'})
|
||
assert not readonly_call('bash', {'command': 'curl https://example.com'})
|
||
assert not readonly_call('mcp__email__send_email', {})
|
||
|
||
|
||
def test_preview_allows_safe_personal_writes_only():
|
||
assert preview_call_allowed('manage_notes', {'action': 'add', 'title': 'x'}, 'add a note')
|
||
assert preview_call_allowed('manage_tasks', {'action': 'create', 'task': 'x'}, 'add a task')
|
||
assert preview_call_allowed('manage_calendar', {'action': 'create_event', 'summary': 'x'}, 'add a calendar event')
|
||
assert preview_call_allowed('manage_memory', {'action': 'edit', 'id': 'x'}, 'edit my memory')
|
||
assert preview_call_allowed('create_document', {'title': 'x'}, 'make a document')
|
||
assert preview_call_allowed('manage_notes', {'action': 'delete', 'id': 'x'}, 'delete a note')
|
||
assert not preview_call_allowed('manage_tasks', {'action': 'run', 'id': 'x'}, 'run task')
|
||
assert not preview_call_allowed('send_email', {'to': 'x@example.com'}, 'send email')
|
||
assert not preview_call_allowed('bash', {'command': 'true'}, 'run shell')
|
||
|
||
|
||
@pytest.mark.parametrize("tool,args", [
|
||
('manage_research', {'action': 'list'}),
|
||
('manage_research', {'action': 'read', 'id': 'report-1'}),
|
||
('list_sessions', {}),
|
||
('manage_contact', {'action': 'list'}),
|
||
('manage_contact', {'action': 'search', 'query': 'Alex'}),
|
||
])
|
||
def test_supplemental_private_inventory_reads_are_preview_safe(tool, args):
|
||
decision = evaluate_preview_call(tool, args, 'list my saved data')
|
||
assert decision.allowed and decision.reason == 'allowed'
|
||
|
||
|
||
@pytest.mark.parametrize("tool,args", [
|
||
('manage_research', {'action': 'delete', 'id': 'report-1'}),
|
||
('manage_contact', {'action': 'add', 'name': 'Alex', 'email': 'a@example.com'}),
|
||
])
|
||
def test_supplemental_private_inventory_mutations_remain_blocked(tool, args):
|
||
decision = evaluate_preview_call(tool, args, 'list my saved data')
|
||
assert not decision.allowed
|
||
|
||
|
||
@pytest.mark.parametrize('sentinel', ['', 'all', 'all sessions', 'all_sessions', 'no_filter', '*'])
|
||
def test_preview_unfiltered_session_sentinels_do_not_become_literal_title_filters(sentinel):
|
||
tool, args = normalize_preview_function_args('list_sessions', {'filter': sentinel})
|
||
assert tool == 'list_sessions'
|
||
assert args == {}
|
||
|
||
|
||
def test_preview_preserves_real_session_title_filter():
|
||
tool, args = normalize_preview_function_args('list_sessions', {'filter': 'audit'})
|
||
assert tool == 'list_sessions'
|
||
assert args == {'filter': 'audit'}
|
||
|
||
|
||
def test_server_sealed_read_forces_exact_first_tool_then_releases_choice():
|
||
from src.turn_contract import RequiredReadOperation, resolve_turn_contract
|
||
|
||
contract = resolve_turn_contract(
|
||
capabilities={'contacts'}, schemas=FUNCTION_TOOL_SCHEMAS,
|
||
policy=ToolPolicy(),
|
||
required_read_operation=RequiredReadOperation(
|
||
'manage_contact', {'action': 'list'}, max_items=3,
|
||
),
|
||
)
|
||
offered = compact_schemas(contract.schemas())
|
||
assert required_read_tool_choice(contract, offered) == {
|
||
'type': 'function', 'function': {'name': 'manage_contact'},
|
||
}
|
||
assert required_read_tool_choice(contract, offered, calls=1) is None
|
||
assert sealed_read_arguments(
|
||
contract, 'manage_contact', {'action': 'delete', 'id': 'wrong'}
|
||
) == {'action': 'list'}
|
||
assert sealed_read_arguments(
|
||
contract, 'manage_contact', {'action': 'delete'}, calls=1
|
||
) == {'action': 'delete'}
|
||
|
||
|
||
def test_preview_allows_only_safe_panel_open_ui_control():
|
||
assert preview_call_allowed(
|
||
'ui_control', {'action': 'open_panel', 'name': 'gallery'}, 'open gallery'
|
||
)
|
||
assert preview_call_allowed(
|
||
'ui_control', {'action': 'open_panel', 'name': 'settings'}, 'open settings'
|
||
)
|
||
assert not preview_call_allowed(
|
||
'ui_control', {'action': 'switch_model', 'name': 'other'}, 'switch models'
|
||
)
|
||
assert not preview_call_allowed(
|
||
'ui_control', {'action': 'toggle', 'name': 'web', 'value': 'off'}, 'turn off web'
|
||
)
|
||
|
||
|
||
def test_bash_requires_explicit_turn_enablement():
|
||
args = {'command': "printf '%s\\n' ODY_SHELL_FILES_READONLY; cat /etc/hostname"}
|
||
assert not preview_call_allowed('bash', args, 'run this command')
|
||
assert preview_call_allowed('bash', args, 'run this command', allow_execute_code=True)
|
||
|
||
|
||
def test_native_workspace_tools_require_validated_native_scope():
|
||
samples = {
|
||
'inspect_media': {'path': '/workspace/fixture.webm'},
|
||
'extract_text': {'path': '/workspace/fixture.png'},
|
||
'read_file': {'path': '/workspace/input.txt'},
|
||
'ls': {'path': '/workspace'},
|
||
'write_file': {'path': '/workspace/output.html', 'content': '<html></html>'},
|
||
'python': {'code': '2 + 2'},
|
||
}
|
||
for name, args in samples.items():
|
||
assert not preview_call_allowed(
|
||
name, args, 'work in the supplied workspace',
|
||
allow_execute_code=True,
|
||
)
|
||
assert preview_call_allowed(
|
||
name, args, 'work in the supplied workspace',
|
||
allow_execute_code=True,
|
||
allow_native_workspace=True,
|
||
)
|
||
|
||
|
||
def test_compact_native_file_schemas_preserve_argument_semantics():
|
||
schemas = {
|
||
schema['function']['name']: schema['function']
|
||
for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
}
|
||
read_properties = schemas['read_file']['parameters']['properties']
|
||
write_properties = schemas['write_file']['parameters']['properties']
|
||
assert 'line 4 means offset=4' in read_properties['offset']['description']
|
||
assert 'final newline' in write_properties['content']['description']
|
||
assert '/workspace refers to its root' in schemas['python']['description']
|
||
|
||
|
||
def test_explicit_final_newline_is_preserved_for_exact_file_content():
|
||
tool, args = normalize_preview_function_args(
|
||
'write_file',
|
||
{'path': '/workspace/result.txt', 'content': 'DONE'},
|
||
user_text=(
|
||
'Write /workspace/result.txt containing exactly DONE followed by a newline.'
|
||
),
|
||
)
|
||
assert tool == 'write_file'
|
||
assert args['content'] == 'DONE\n'
|
||
|
||
|
||
def test_file_content_is_not_changed_without_explicit_newline_request():
|
||
_, args = normalize_preview_function_args(
|
||
'write_file',
|
||
{'path': '/workspace/result.txt', 'content': 'DONE'},
|
||
user_text='Write /workspace/result.txt containing DONE.',
|
||
)
|
||
assert args['content'] == 'DONE'
|
||
|
||
|
||
def test_unrequested_invalid_transcript_precision_is_dropped():
|
||
_, args = normalize_preview_function_args(
|
||
'transcribe_media',
|
||
{'path': '/workspace/audio.wav', 'timestamp_precision': 100},
|
||
user_text='Return timestamped segments.',
|
||
)
|
||
assert args == {'path': '/workspace/audio.wav'}
|
||
|
||
|
||
def test_explicit_transcript_precision_remains_strictly_validated():
|
||
_, args = normalize_preview_function_args(
|
||
'transcribe_media',
|
||
{'path': '/workspace/audio.wav', 'timestamp_precision': 100},
|
||
user_text='Use timestamp precision of 2 decimal places.',
|
||
)
|
||
assert args['timestamp_precision'] == 100
|
||
|
||
|
||
def test_visual_tool_result_is_uniformly_bounded_to_endpoint_limit():
|
||
from src.clean_agent_preview import bounded_visual_result_blocks
|
||
|
||
blocks = bounded_visual_result_blocks({
|
||
'images': [
|
||
{'mimeType': 'image/jpeg', 'data': str(index)}
|
||
for index in range(5)
|
||
],
|
||
}, max_images=3)
|
||
assert [block['image_url']['url'] for block in blocks] == [
|
||
'data:image/jpeg;base64,0',
|
||
'data:image/jpeg;base64,2',
|
||
'data:image/jpeg;base64,4',
|
||
]
|
||
|
||
|
||
def test_brokered_web_fetch_and_deliberately_offered_private_browser_are_preview_safe_reads():
|
||
assert preview_call_allowed(
|
||
'web_fetch', {'url': 'https://www.reuters.com/example'}, 'tell me more'
|
||
)
|
||
assert preview_call_allowed(
|
||
'private_browser', {'action': 'open', 'url': 'https://example.com'}, 'open this'
|
||
)
|
||
|
||
|
||
def test_preview_policy_decisions_have_stable_sanitized_reasons():
|
||
allowed = evaluate_preview_call('web_fetch', {'url': 'https://example.com'}, 'tell me more')
|
||
assert allowed.allowed and allowed.reason == 'allowed'
|
||
assert allowed.audit() == {
|
||
'allowed': True, 'reason': 'allowed', 'tool': 'web_fetch',
|
||
'family': 'search_browser',
|
||
'effects': ['brokered_network_read', 'network_egress'],
|
||
}
|
||
denied = evaluate_preview_call('manage_notes', {'action': 'delete', 'id': 'x'}, 'delete it')
|
||
assert not denied.allowed and denied.reason == 'write_family_not_authorized'
|
||
assert 'delete it' not in json.dumps(denied.audit())
|
||
|
||
|
||
def test_explicit_calendar_event_delete_is_allowed_without_weakening_other_deletes():
|
||
allowed = evaluate_preview_call(
|
||
'manage_calendar',
|
||
{'action': 'delete_event', 'event_id': 'event-1'},
|
||
'remove the happy horizon event',
|
||
)
|
||
assert allowed.allowed and allowed.reason == 'allowed'
|
||
assert not preview_call_allowed(
|
||
'manage_calendar',
|
||
{'action': 'delete_event', 'event_id': 'event-1'},
|
||
'do it',
|
||
)
|
||
assert preview_call_allowed(
|
||
'manage_notes', {'action': 'delete', 'id': 'note-1'}, 'delete the note'
|
||
)
|
||
|
||
|
||
def test_explicit_followup_mutation_uses_the_immutable_turn_family():
|
||
args = {'action': 'delete_event', 'event_id': 'event-1'}
|
||
denied = evaluate_preview_call(
|
||
'manage_calendar', args, 'delete the amazon delivery',
|
||
)
|
||
assert not denied.allowed and denied.reason == 'write_family_not_authorized'
|
||
|
||
allowed = evaluate_preview_call(
|
||
'manage_calendar', args, 'delete the amazon delivery',
|
||
turn_authorized_families={'calendar'},
|
||
)
|
||
assert allowed.allowed and allowed.reason == 'allowed'
|
||
|
||
|
||
def test_explicit_followup_forget_uses_the_immutable_memory_family():
|
||
allowed = evaluate_preview_call(
|
||
'manage_memory', {'action': 'delete', 'memory_id': 'memory-1'},
|
||
'Forget that memory.', turn_authorized_families={'memory'},
|
||
)
|
||
assert allowed.allowed and allowed.reason == 'allowed'
|
||
|
||
|
||
def test_skill_update_alias_normalizes_to_edit_before_policy():
|
||
tool, args = normalize_preview_function_args(
|
||
'manage_skills', {'action': 'update', 'name': 'example-skill', 'description': 'new'},
|
||
)
|
||
assert tool == 'manage_skills'
|
||
assert args['action'] == 'edit'
|
||
|
||
|
||
def test_private_browser_open_normalizes_to_atomic_snapshot_batch():
|
||
tool, args = normalize_preview_function_args(
|
||
'private_browser',
|
||
{'action': 'open', 'url': 'https://example.com', 'timeout_ms': 12000},
|
||
)
|
||
|
||
assert tool == 'private_browser'
|
||
assert args == {
|
||
'action': 'batch',
|
||
'commands': [['open', 'https://example.com'], ['snapshot']],
|
||
'timeout_ms': 12000,
|
||
}
|
||
|
||
|
||
def test_private_browser_local_artifact_open_is_not_rewritten():
|
||
tool, args = normalize_preview_function_args(
|
||
'private_browser',
|
||
{'action': 'open', 'url': 'file:///workspace/output.html'},
|
||
)
|
||
|
||
assert tool == 'private_browser'
|
||
assert args == {'action': 'open', 'url': 'file:///workspace/output.html'}
|
||
|
||
|
||
def test_private_browser_dom_batch_only_matches_automatic_open_snapshot():
|
||
assert private_browser_dom_batch({
|
||
'action': 'batch',
|
||
'commands': [['open', 'https://example.com'], ['snapshot']],
|
||
})
|
||
assert not private_browser_dom_batch({
|
||
'action': 'batch',
|
||
'commands': [['open', 'https://example.com'], ['snapshot'], ['screenshot']],
|
||
})
|
||
|
||
|
||
def test_every_compactly_offered_preview_tool_has_valid_policy_permitted_call():
|
||
samples = {
|
||
'extract_text': ({'path': 'odysseus://attachment/fixture.png'}, 'OCR this image'),
|
||
'bash': ({'command': 'pwd'}, 'run this shell command'),
|
||
'create_document': ({'title': 'x', 'content': 'y'}, 'create a document'),
|
||
'edit_document': ({'edits': [{'find': 'x', 'replace': 'y'}]}, 'edit my document'),
|
||
'list_cached_models': ({}, 'list cached models'),
|
||
'list_cookbook_servers': ({}, 'list cookbook servers'),
|
||
'list_downloads': ({}, 'list downloads'),
|
||
'list_email_accounts': ({}, 'list my email accounts'),
|
||
'list_emails': ({'limit': 3}, 'list my emails'),
|
||
'list_models': ({}, 'list models'),
|
||
'list_serve_presets': ({}, 'list serve presets'),
|
||
'list_served_models': ({}, 'list served models'),
|
||
'manage_calendar': ({'action': 'list_events'}, 'list my calendar events'),
|
||
'manage_contact': ({'action': 'list'}, 'list my contacts'),
|
||
'manage_documents': ({'action': 'list'}, 'list my documents'),
|
||
'manage_memory': ({'action': 'list'}, 'list my memories'),
|
||
'manage_notes': ({'action': 'list'}, 'list my notes'),
|
||
'manage_research': ({'action': 'list'}, 'list my saved research reports'),
|
||
'manage_skills': ({'action': 'list'}, 'list my skills'),
|
||
'manage_tasks': ({'action': 'list'}, 'list my tasks'),
|
||
'list_sessions': ({}, 'list my chat sessions'),
|
||
'pdf_extract': ({'url': 'https://example.com/x.pdf', 'query': 'metric'}, 'read this pdf'),
|
||
'private_browser': ({'action': 'batch', 'commands': [['open', 'https://example.com'], ['snapshot']]}, 'use the private browser'),
|
||
'read_email': ({'uid': '1'}, 'read my email'),
|
||
'search_chats': ({'query': 'project'}, 'search my chats'),
|
||
'search_emails': ({'query': 'project'}, 'search my emails'),
|
||
'search_hf_models': ({'query': 'Qwen'}, 'search Hugging Face models'),
|
||
'suggest_document': ({'suggestions': [{'find': 'x', 'replace': 'y', 'reason': 'clarity'}]}, 'suggest edits to my document'),
|
||
'update_document': ({'content': 'updated'}, 'update my document'),
|
||
'ui_control': ({'action': 'open_panel', 'name': 'gallery'}, 'open gallery'),
|
||
'trigger_research': ({'topic': 'AI info'}, 'research AI info'),
|
||
'web_fetch': ({'url': 'https://example.com'}, 'read this page'),
|
||
'web_search': ({'query': 'current AI news'}, 'search the web'),
|
||
'youtube_tool': ({'action': 'metadata', 'video_url': 'https://youtube.com/watch?v=x'}, 'read YouTube metadata'),
|
||
}
|
||
schemas = {
|
||
schema['function']['name']: schema
|
||
for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] in samples
|
||
}
|
||
assert set(samples) == set(schemas)
|
||
from src.clean_agent_preview import PREVIEW_TOOLS
|
||
assert set(samples) == set(PREVIEW_TOOLS)
|
||
for name, (args, prompt) in samples.items():
|
||
jsonschema.validate(args, schemas[name]['function']['parameters'])
|
||
decision = evaluate_preview_call(
|
||
name, args, prompt, allow_execute_code=(name == 'bash'),
|
||
turn_authorized_families={'research'} if name == 'trigger_research' else frozenset(),
|
||
)
|
||
assert decision.allowed, f'{name}: {decision.reason}'
|
||
|
||
|
||
def test_pdf_extract_accepts_task_local_path_and_normalizes_it_for_execution():
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item['function']['name'] == 'pdf_extract'
|
||
)
|
||
arguments = {
|
||
'path': '/workspace/fixtures/paper.pdf',
|
||
'query': 'references arXiv preprints',
|
||
}
|
||
jsonschema.validate(arguments, schema['function']['parameters'])
|
||
block = function_call_to_tool_block('pdf_extract', json.dumps(arguments))
|
||
assert block is not None
|
||
assert json.loads(block.content) == {
|
||
'url': '/workspace/fixtures/paper.pdf',
|
||
'query': 'references arXiv preprints',
|
||
}
|
||
|
||
|
||
def test_pdf_extract_rejects_a_guessed_filename_with_actionable_discovery():
|
||
from src.tool_schemas import normalized_native_function_argument_error
|
||
|
||
error = normalized_native_function_argument_error(
|
||
'pdf_extract', {'url': 'example-paper.pdf', 'query': 'Table 2'}
|
||
)
|
||
|
||
assert 'public http(s) PDF URL' in error
|
||
assert 'use web_search' in error
|
||
assert normalized_native_function_argument_error(
|
||
'pdf_extract',
|
||
{'url': 'https://example.com/paper.pdf', 'query': 'Table 2'},
|
||
) is None
|
||
assert normalized_native_function_argument_error(
|
||
'pdf_extract',
|
||
{'url': '/workspace/fixtures/paper.pdf', 'query': 'Table 2'},
|
||
) is None
|
||
|
||
|
||
def test_write_authority_prevents_cross_family_substitution():
|
||
assert authorized_write_families('send an emil') == {'email'}
|
||
assert not preview_call_allowed('manage_tasks', {'action': 'create', 'name': 'send mail'}, 'send an email')
|
||
assert preview_call_allowed('manage_notes', {'action': 'add', 'title': 'Sweden'}, 'add todo go to Sweden tomorrow')
|
||
assert not preview_call_allowed('manage_notes', {'action': 'add', 'title': 'x'}, 'do it')
|
||
|
||
|
||
def test_immediate_successful_write_allows_same_family_revision_only():
|
||
turn = [
|
||
{'role': 'assistant', 'tool_calls': [{'id': 'c1', 'function': {
|
||
'name': 'manage_calendar',
|
||
'arguments': '{"action":"create_event","summary":"Meeting with Elon","dtstart":"2026-09-09T10:00:00Z"}',
|
||
}}]},
|
||
{'role': 'tool', 'tool_call_id': 'c1', 'content': '{"uid":"event-1","exit_code":0}'},
|
||
{'role': 'assistant', 'content': 'Meeting added.'},
|
||
]
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': 'add meeting elon at 10am'},
|
||
{'role': 'assistant', 'content': 'Meeting added.', 'metadata': {'clean_v3_turn': turn}},
|
||
])
|
||
inherited = recent_successful_write_families(session)
|
||
assert inherited == {'calendar'}
|
||
assert preview_call_allowed(
|
||
'manage_calendar', {'action': 'update_event', 'event_id': 'event-1'},
|
||
'no tomorrow 10am and alon', contextual_write_families=inherited,
|
||
)
|
||
assert not preview_call_allowed(
|
||
'manage_calendar', {'action': 'create_event', 'summary': 'another'},
|
||
'do it again', contextual_write_families=inherited,
|
||
)
|
||
assert not preview_call_allowed(
|
||
'manage_notes', {'action': 'update', 'id': 'note-1'},
|
||
'change it', contextual_write_families=inherited,
|
||
)
|
||
|
||
|
||
def test_active_editor_authorizes_revision_but_not_replacement_creation():
|
||
context = frozenset({'documents'})
|
||
assert preview_call_allowed(
|
||
'update_document', {'content': 'Hello'}, 'write the email',
|
||
contextual_write_families=context,
|
||
)
|
||
assert not preview_call_allowed(
|
||
'create_document', {'title': 'Replacement', 'content': 'Hello'},
|
||
'write the email', contextual_write_families=context,
|
||
)
|
||
|
||
|
||
def test_open_email_reply_is_scoped_to_one_whole_draft_writer():
|
||
document = SimpleNamespace(
|
||
title='New Email', language='email',
|
||
current_content='To: a@example.com\nSubject: Hello\n---\nOriginal body',
|
||
)
|
||
assert active_editor_whole_draft_request(document, 'Write reply this email')
|
||
contract = resolve_full_inventory_contract(
|
||
schemas=FUNCTION_TOOL_SCHEMAS, policy=ToolPolicy(),
|
||
)
|
||
scoped = scope_active_editor_contract(contract, whole_draft=True)
|
||
offered = {name.removeprefix('mcp__email__') for name in scoped.offered}
|
||
assert 'update_document' in offered
|
||
assert not {'create_document', 'manage_documents', 'edit_document', 'suggest_document'} & offered
|
||
|
||
|
||
def test_active_review_is_scoped_to_suggestions_without_applying_changes():
|
||
document = SimpleNamespace(
|
||
title='Draft', language='markdown', current_content='A wordy sentence.',
|
||
)
|
||
prompt = 'Add another inline suggestion. Do not apply either suggestion.'
|
||
assert active_editor_suggestion_request(document, prompt)
|
||
contract = resolve_full_inventory_contract(
|
||
schemas=FUNCTION_TOOL_SCHEMAS, policy=ToolPolicy(),
|
||
)
|
||
scoped = scope_active_editor_contract(contract, suggestion_only=True)
|
||
assert {name.removeprefix('mcp__email__') for name in scoped.offered} == {'suggest_document'}
|
||
|
||
|
||
def test_failed_write_does_not_authorize_contextual_revision():
|
||
session = SimpleNamespace(history=[{'role': 'assistant', 'content': 'Failed.', 'metadata': {
|
||
'clean_v3_turn': [
|
||
{'role': 'assistant', 'tool_calls': [{'id': 'c1', 'function': {
|
||
'name': 'manage_calendar', 'arguments': '{"action":"create_event"}',
|
||
}}]},
|
||
{'role': 'tool', 'tool_call_id': 'c1', 'content': '{"error":"blocked","exit_code":1}'},
|
||
],
|
||
}}])
|
||
assert not recent_successful_write_families(session)
|
||
|
||
|
||
def test_denial_never_claims_action_succeeded():
|
||
text = denied_response().casefold()
|
||
assert 'no changes were made' in text
|
||
assert 'deleted' not in text and 'created' not in text and 'sent' not in text
|
||
|
||
|
||
@pytest.mark.parametrize('text', [
|
||
'Delete all my notes.', 'add a calendar event tomorrow', 'remember that I like tea',
|
||
'create a document called Atlas', 'send an email to Alex', 'write the email for me',
|
||
])
|
||
def test_mutation_request_detection_is_family_aware(text):
|
||
assert requests_mutation(text)
|
||
|
||
|
||
@pytest.mark.parametrize('text', [
|
||
'Show my notes.', 'What is on my calendar?', 'Search the web for Sweden.', 'Explain tasks.',
|
||
'List my notes. Read-only; do not change data or send messages.',
|
||
'List my scheduled tasks. Do not change data or send messages.',
|
||
"Show my notes without changing or deleting anything.",
|
||
])
|
||
def test_reads_and_general_questions_are_not_mutations(text):
|
||
assert not requests_mutation(text)
|
||
|
||
|
||
def test_definition_note_and_sports_set_are_not_a_notes_mutation():
|
||
text = (
|
||
'How many set points did the player fail to convert? '
|
||
'Note: a set point is one point away from winning the set.'
|
||
)
|
||
|
||
assert authorized_write_families(text) == frozenset()
|
||
assert not requests_mutation(text)
|
||
|
||
|
||
def test_plain_note_request_still_authorizes_note_mutation():
|
||
assert authorized_write_families('Set my note title to Travel') == {'notes'}
|
||
assert requests_mutation('Set my note title to Travel')
|
||
|
||
|
||
def test_negated_mutation_does_not_hide_later_positive_instruction():
|
||
assert requests_mutation("Don't delete my note; update its title instead.")
|
||
|
||
|
||
def test_completion_claim_distinguishes_success_from_denial_or_question():
|
||
assert claims_completion('All notes have been deleted.')
|
||
assert claims_completion("Done — I've added the note.")
|
||
assert not claims_completion("I can't delete those. No changes were made.")
|
||
assert not claims_completion('What title should be added?')
|
||
|
||
|
||
def test_history_preserves_native_call_result_group_and_current_user():
|
||
saved = [{'role': 'assistant', 'tool_calls': [{'id': 'c1', 'function': {'name': 'manage_notes', 'arguments': '{"action":"list"}'}}]},
|
||
{'role': 'tool', 'tool_call_id': 'c1', 'content': 'notes'},
|
||
{'role': 'assistant', 'content': 'Here are notes.'}]
|
||
session = SimpleNamespace(history=[{'role': 'user', 'content': 'notes'},
|
||
{'role': 'assistant', 'content': 'Here are notes.', 'metadata': {'clean_v3_turn': saved}}])
|
||
result = conversation(session, [{'role': 'user', 'content': 'second one?'}])
|
||
assert result[1:4] == saved
|
||
assert result[-1] == {'role': 'user', 'content': 'second one?'}
|
||
|
||
|
||
def test_history_drops_large_older_turn_without_losing_recent_note_evidence():
|
||
saved = [
|
||
{'role': 'assistant', 'tool_calls': [{'id': 'notes-call', 'function': {
|
||
'name': 'manage_notes', 'arguments': '{"action":"list"}'}}]},
|
||
{'role': 'tool', 'tool_call_id': 'notes-call', 'content': '[fixture-id] Today'},
|
||
{'role': 'assistant', 'content': 'Today'},
|
||
]
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': 'Calendar'},
|
||
{'role': 'assistant', 'content': 'x' * 23000},
|
||
{'role': 'user', 'content': 'Notes'},
|
||
{'role': 'assistant', 'content': 'Today', 'metadata': {'clean_v3_turn': saved}},
|
||
])
|
||
result = conversation(session, [{'role': 'user', 'content': 'Delete that note'}])
|
||
assert result[0]['content'] == 'Notes'
|
||
assert result[1:4] == saved
|
||
|
||
|
||
def test_history_character_limit_drops_whole_oversized_reference_turn():
|
||
# Character-budget limitation: availability of the tool alone does not
|
||
# guarantee that an oversized prior result remains in the model context.
|
||
saved = [
|
||
{'role': 'assistant', 'tool_calls': [{'id': 'large-call', 'function': {
|
||
'name': 'manage_notes', 'arguments': '{"action":"list"}'}}]},
|
||
{'role': 'tool', 'tool_call_id': 'large-call', 'content': 'x' * 23000},
|
||
]
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': 'Notes'},
|
||
{'role': 'assistant', 'content': 'Notes', 'metadata': {'clean_v3_turn': saved}},
|
||
])
|
||
result = conversation(session, [{'role': 'user', 'content': 'Read the second one'}])
|
||
assert result == [{'role': 'user', 'content': 'Read the second one'}]
|
||
|
||
|
||
def test_history_rehydrates_most_recent_owner_checked_image_for_followup(monkeypatch, tmp_path):
|
||
image_path = tmp_path / 'fixture.png'
|
||
image_path.write_bytes(b'PNG-test-bytes')
|
||
|
||
class Uploads:
|
||
def resolve_upload(self, upload_id, owner=None, allow_admin=True):
|
||
assert upload_id == 'a' * 32 + '.png'
|
||
assert owner == 'alice'
|
||
assert allow_admin is False
|
||
return {'path': str(image_path), 'mime': 'image/png', 'name': 'fixture.png'}
|
||
|
||
def is_image_file(self, name, mime):
|
||
return mime == 'image/png'
|
||
|
||
monkeypatch.setattr('src.tool_utils.get_upload_handler', lambda: Uploads())
|
||
upload_id = 'a' * 32 + '.png'
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': 'What is shown?', 'metadata': {
|
||
'attachments': [{'id': upload_id, 'name': 'fixture.png', 'mime': 'image/png'}],
|
||
}},
|
||
{'role': 'assistant', 'content': 'A test image.', 'metadata': {
|
||
'clean_v3_turn': [{'role': 'assistant', 'content': 'A test image.'}],
|
||
}},
|
||
])
|
||
result = conversation(session, [{'role': 'user', 'content': 'What color was it?'}], owner='alice')
|
||
image_turn = result[0]
|
||
assert image_turn['role'] == 'user'
|
||
assert image_turn['content'][0] == {'type': 'text', 'text': 'What is shown?'}
|
||
assert image_turn['content'][1]['image_url']['url'].startswith('data:image/png;base64,')
|
||
assert any(block.get('type') == 'text' and f'odysseus://attachment/{upload_id}' in block.get('text', '')
|
||
for block in image_turn['content'])
|
||
assert result[-1] == {'role': 'user', 'content': 'What color was it?'}
|
||
|
||
|
||
def test_history_trims_after_removing_raw_image_bytes(monkeypatch, tmp_path):
|
||
image_path = tmp_path / 'fixture.png'
|
||
image_path.write_bytes(b'PNG-test-bytes')
|
||
|
||
class Uploads:
|
||
def resolve_upload(self, upload_id, owner=None, allow_admin=True):
|
||
return {'path': str(image_path), 'mime': 'image/png', 'name': 'fixture.png'}
|
||
|
||
def is_image_file(self, name, mime):
|
||
return mime == 'image/png'
|
||
|
||
monkeypatch.setattr('src.tool_utils.get_upload_handler', lambda: Uploads())
|
||
upload_id = 'b' * 32 + '.png'
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': [
|
||
{'type': 'text', 'text': 'Inspect this dashboard.'},
|
||
{'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,' + 'x' * 30000}},
|
||
], 'metadata': {'attachments': [{'id': upload_id}]}},
|
||
{'role': 'assistant', 'content': 'Q3 is highest.'},
|
||
{'role': 'user', 'content': 'Save that in a note.', 'metadata': {}},
|
||
])
|
||
diagnostics = {}
|
||
result = conversation(
|
||
session, [{'role': 'user', 'content': 'Save that in a note.'}],
|
||
owner='alice', diagnostics=diagnostics,
|
||
)
|
||
assert result[0]['content'][1]['type'] == 'image_url'
|
||
assert diagnostics['image_rehydration'] == 'rehydrated'
|
||
|
||
|
||
def test_history_never_rehydrates_image_without_an_authenticated_owner(monkeypatch):
|
||
monkeypatch.setattr('src.tool_utils.get_upload_handler', lambda: (_ for _ in ()).throw(AssertionError('must not resolve')))
|
||
session = SimpleNamespace(history=[
|
||
{'role': 'user', 'content': 'Image marker', 'metadata': {'attachments': [{'id': 'a' * 32 + '.png'}]}},
|
||
{'role': 'assistant', 'content': 'Seen.'},
|
||
])
|
||
assert conversation(session, [{'role': 'user', 'content': 'Again?'}])[0]['content'] == 'Image marker'
|
||
|
||
|
||
def test_multimodal_image_count_never_exposes_payloads():
|
||
messages = [
|
||
{'role': 'user', 'content': [
|
||
{'type': 'text', 'text': 'look'},
|
||
{'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,secret'}},
|
||
]},
|
||
{'role': 'assistant', 'content': 'seen'},
|
||
]
|
||
assert multimodal_image_count(messages) == 1
|
||
|
||
|
||
def test_attachment_reference_count_uses_metadata_only():
|
||
session = SimpleNamespace(history=[
|
||
SimpleNamespace(role='user', content='one', metadata={'attachments': [{'id': 'a'}, {'id': 'b'}]}),
|
||
SimpleNamespace(role='assistant', content='seen', metadata={}),
|
||
])
|
||
assert attachment_reference_count(session) == 2
|
||
|
||
|
||
def test_plain_system_wrappers_are_not_conversation_messages():
|
||
assert conversation(None, [{'role': 'system', 'content': 'legacy wrapper'}, {'role': 'user', 'content': 'hi'}]) == [{'role': 'user', 'content': 'hi'}]
|
||
|
||
|
||
def test_open_empty_email_draft_is_still_visible_context():
|
||
message = active_document_context_message(SimpleNamespace(
|
||
id='draft-1', title='Reply to Jordan', language='email', current_content='',
|
||
))
|
||
assert message['role'] == 'user'
|
||
assert message['metadata']['trusted'] is False
|
||
assert 'Open editor kind: email draft' in message['content']
|
||
assert 'Title: Reply to Jordan' in message['content']
|
||
assert 'Content (currently empty)' in message['content']
|
||
|
||
|
||
def test_open_document_context_includes_current_text():
|
||
message = active_document_context_message(SimpleNamespace(
|
||
id='doc-1', title='Trip', language='markdown', current_content='Visit Uppsala.',
|
||
))
|
||
assert 'Open editor kind: document' in message['content']
|
||
assert 'Visit Uppsala.' in message['content']
|
||
|
||
|
||
def test_open_email_reader_is_visible_as_typed_untrusted_context():
|
||
message = active_email_context_message({
|
||
'uid': 'fixture-uid', 'folder': 'INBOX', 'account': 'fixture-account',
|
||
'subject': 'Status', 'from': 'Jordan', 'body_preview': 'Can we meet tomorrow?',
|
||
})
|
||
assert message['role'] == 'user'
|
||
assert message['metadata']['trusted'] is False
|
||
assert 'Open email reader' in message['content']
|
||
assert 'Subject: Status' in message['content']
|
||
assert 'Can we meet tomorrow?' in message['content']
|
||
|
||
|
||
def test_active_editor_targeting_distinguishes_edit_from_new_document():
|
||
draft = SimpleNamespace(title='Reply', language='email', current_content='')
|
||
assert targets_active_editor(draft, 'Write reply')
|
||
assert targets_active_editor(draft, 'Draft a reply')
|
||
assert targets_active_editor(draft, "Write a friendly email saying I'll reply tomorrow")
|
||
assert targets_active_editor(draft, 'Make it friendlier')
|
||
assert not targets_active_editor(draft, 'Write a note')
|
||
assert not targets_active_editor(draft, 'Write a JavaScript function')
|
||
assert not targets_active_editor(draft, 'Create a new separate document about Sweden')
|
||
|
||
|
||
def test_active_editor_contract_excludes_other_tool_families():
|
||
schemas = [s for s in FUNCTION_TOOL_SCHEMAS
|
||
if s['function']['name'] in {
|
||
'create_document', 'update_document', 'manage_documents',
|
||
'manage_notes', 'ui_control',
|
||
}]
|
||
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||
scoped = scope_active_editor_contract(contract)
|
||
assert 'create_document' not in scoped.offered
|
||
assert 'ui_control' not in scoped.offered
|
||
assert 'manage_documents' not in scoped.offered
|
||
assert scoped.offered == {'update_document'}
|
||
assert {'create_document', 'ui_control', 'manage_documents'} <= scoped.executable
|
||
|
||
|
||
def test_empty_active_editor_contract_offers_only_whole_document_update_from_document_family():
|
||
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {
|
||
'create_document', 'edit_document', 'suggest_document', 'update_document',
|
||
'manage_documents', 'manage_notes', 'ui_control',
|
||
}]
|
||
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||
scoped = scope_active_editor_contract(contract, empty=True)
|
||
document_tools = {
|
||
name for name in scoped.offered
|
||
if name in {'create_document', 'edit_document', 'suggest_document',
|
||
'update_document', 'manage_documents'}
|
||
}
|
||
assert document_tools == {'update_document'}
|
||
assert 'manage_notes' not in scoped.offered
|
||
|
||
|
||
def test_v3_schema_preserves_names_and_action_enum():
|
||
notes = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
compact = compact_schemas([notes])[0]
|
||
assert compact['function']['name'] == 'manage_notes'
|
||
assert compact['function']['parameters']['properties']['action']['enum'] == notes['function']['parameters']['properties']['action']['enum']
|
||
assert compact['function']['description']
|
||
|
||
|
||
@pytest.mark.parametrize('name', ['read_email', 'mcp__email__read_email'])
|
||
def test_v3_live_email_schema_preserves_identifier_semantics(name):
|
||
import asyncio
|
||
import copy
|
||
from mcp_servers import email_server
|
||
|
||
live = next(tool for tool in asyncio.run(email_server.list_tools()) if tool.name == 'read_email')
|
||
schema = {'type': 'function', 'function': {
|
||
'name': name, 'description': live.description,
|
||
'parameters': copy.deepcopy(live.inputSchema),
|
||
}}
|
||
original = copy.deepcopy(schema)
|
||
function = compact_schemas([schema])[0]['function']
|
||
properties = function['parameters']['properties']
|
||
|
||
assert function['name'] == name
|
||
assert schema == original
|
||
assert set(properties) == set(live.inputSchema['properties'])
|
||
assert 'UID' in properties['uid'].get('description', '')
|
||
assert 'search_emails' in properties['uid']['description']
|
||
assert 'within its account and folder' in properties['uid']['description']
|
||
assert 'Folder from the selected result' in properties['folder'].get('description', '')
|
||
assert 'RFC Message-ID header' in properties['message_id'].get('description', '')
|
||
assert 'not a UID' in properties['message_id']['description']
|
||
assert 'uid or message_id' in function['description']
|
||
|
||
|
||
def test_v3_media_schema_preserves_temporal_sampling_semantics():
|
||
media = next(
|
||
schema for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] == 'inspect_media'
|
||
)
|
||
function = media['function']
|
||
properties = function['parameters']['properties']
|
||
|
||
assert 'does not locate or count events' in function['description']
|
||
assert 'does not search' in properties['query']['description']
|
||
assert 'overview' in properties['sampling']['description']
|
||
assert 'up to 24' in properties['frames']['description']
|
||
assert properties['frames']['maximum'] == 24
|
||
assert 'midpoint' in properties['segments']['description']
|
||
|
||
|
||
def test_v3_media_overview_defaults_to_three_lossless_sheets():
|
||
import src.clean_agent_preview as module
|
||
|
||
tool, arguments = module.normalize_preview_function_args(
|
||
'inspect_media',
|
||
{'path': '/workspace/video.mp4', 'sampling': 'overview'},
|
||
)
|
||
|
||
assert tool == 'inspect_media'
|
||
assert arguments['frames'] == 24
|
||
|
||
|
||
def test_v3_transcription_schema_rejects_media_output_semantics_in_prose():
|
||
transcript = next(
|
||
schema for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] == 'transcribe_media'
|
||
)['function']
|
||
|
||
assert 'does not inspect pixels or create media clips' in transcript['description']
|
||
assert '.jsonl' in transcript['parameters']['properties']['output_path']['description']
|
||
|
||
|
||
def test_compact_browser_distinguishes_element_refs_from_keyboard_keys():
|
||
original = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'private_browser')
|
||
browser = compact_schemas([original])[0]['function']
|
||
assert 'fill/click/press' not in browser['description']
|
||
assert 'key' in browser['description'] and 'Enter' in browser['description']
|
||
assert 'focused' in browser['parameters']['properties']['key']['description']
|
||
assert set(browser['parameters']['properties']) == set(original['function']['parameters']['properties'])
|
||
|
||
|
||
def test_v3_browser_batch_schema_matches_executor_sequence_contract():
|
||
browser = next(
|
||
schema for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] == 'private_browser'
|
||
)['function']
|
||
commands = browser['parameters']['properties']['commands']
|
||
|
||
assert commands['items']['type'] == 'array'
|
||
assert commands['items']['items'] == {'type': 'string'}
|
||
assert '[["open"' in commands['description']
|
||
assert 'snapshot' in browser['description']
|
||
assert 'does not search the site' in browser['description']
|
||
|
||
|
||
def test_v3_browser_target_fields_preserve_selector_semantics():
|
||
browser = next(schema for schema in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if schema['function']['name'] == 'private_browser')['function']
|
||
for name in ('target', 'selector'):
|
||
description = browser['parameters']['properties'][name].get('description', '')
|
||
assert '@e2' in description
|
||
assert 'CSS selector' in description
|
||
assert 'not visible text' in description
|
||
|
||
|
||
def test_long_skill_index_retains_readable_names_instead_of_truncated_json():
|
||
from src.clean_agent_preview import preview_tool_result_text
|
||
index = '- **first-skill**: Description\n- **second-skill**: Description\n' + 'Long description ' * 800
|
||
text = preview_tool_result_text({'results': index, 'exit_code': 0}, 'manage_skills', {'action': 'list'})
|
||
assert text.startswith('- **first-skill**: Description\n- **second-skill**: Description\n')
|
||
assert '[Tool result truncated' in text
|
||
# Structured document results retain IDs and other payload fields.
|
||
result = {'results': 'Saved', 'doc_id': 'doc-test'}
|
||
assert json.loads(preview_tool_result_text(result, 'create_document', {})) == result
|
||
|
||
|
||
def test_v3_schema_uses_configured_versioned_contract_root(tmp_path, monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
|
||
contract_root = tmp_path / 'contract'
|
||
contract_root.mkdir()
|
||
(contract_root / 'eval_alltools_unseen_compare.py').write_text(
|
||
"def tools_for_mode(tools, mode):\n"
|
||
" assert mode == 'compact_contract_v5'\n"
|
||
" tools[0]['function']['description'] = 'versioned-contract-loaded'\n"
|
||
" return tools\n",
|
||
encoding='utf-8',
|
||
)
|
||
monkeypatch.setenv('ODYSSEUS_TOOL_CONTRACT_ROOT', str(contract_root))
|
||
module.contract_builder.cache_clear()
|
||
try:
|
||
notes = next(
|
||
s for s in FUNCTION_TOOL_SCHEMAS
|
||
if s['function']['name'] == 'manage_notes'
|
||
)
|
||
compact = module.compact_schemas([notes])[0]
|
||
assert compact['function']['description'].startswith('versioned-contract-loaded')
|
||
finally:
|
||
module.contract_builder.cache_clear()
|
||
|
||
|
||
def test_v3_document_edit_schema_has_one_unambiguous_structured_form():
|
||
edit = next(s for s in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if s['function']['name'] == 'edit_document')
|
||
parameters = edit['function']['parameters']
|
||
assert parameters['required'] == ['edits']
|
||
assert set(parameters['properties']) == {'edits'}
|
||
|
||
|
||
def test_v3_ui_schema_advertises_only_policy_executable_panel_open():
|
||
ui = next(s for s in compact_schemas(FUNCTION_TOOL_SCHEMAS)
|
||
if s['function']['name'] == 'ui_control')
|
||
parameters = ui['function']['parameters']
|
||
assert parameters['required'] == ['action', 'name']
|
||
assert parameters['properties']['action']['enum'] == ['open_panel']
|
||
assert parameters['properties']['name']['enum'] == [
|
||
'documents', 'gallery', 'calendar', 'email', 'sessions', 'notes',
|
||
'brain', 'skills', 'settings', 'theme', 'cookbook',
|
||
]
|
||
assert set(parameters['properties']) == {'action', 'name'}
|
||
|
||
|
||
def test_preview_contract_exposes_no_fallback_family_when_required_tool_is_unavailable():
|
||
notes = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
preview = resolve_full_inventory_contract(schemas=[notes], policy=ToolPolicy())
|
||
routed = SimpleNamespace(unavailable=frozenset({'web_search'}), offered=frozenset())
|
||
scoped = scope_preview_contract(preview, routed, {'search_browser'})
|
||
assert scoped.offered == frozenset()
|
||
assert scoped.schemas() == []
|
||
assert scoped.unavailable == {'web_search', 'capability:search_browser'}
|
||
assert scoped.active_capabilities == {'search_browser'}
|
||
|
||
|
||
def test_preview_contract_detects_active_family_removed_by_preview_permissions():
|
||
notes = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
preview = resolve_full_inventory_contract(schemas=[notes], policy=ToolPolicy())
|
||
scoped = scope_preview_contract(
|
||
preview, SimpleNamespace(unavailable=frozenset(), offered=frozenset()), {'search_browser'}
|
||
)
|
||
assert scoped.offered == frozenset()
|
||
assert scoped.unavailable == {'capability:search_browser'}
|
||
|
||
|
||
def test_preview_contract_keeps_only_routed_family_from_trained_inventory():
|
||
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {
|
||
'manage_notes', 'manage_calendar', 'web_search',
|
||
}]
|
||
preview = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||
routed = SimpleNamespace(
|
||
unavailable=frozenset(), offered=frozenset({'manage_notes'}),
|
||
required=frozenset({'manage_notes'}), capabilities=frozenset({'notes'}),
|
||
required_read_operation=None,
|
||
)
|
||
scoped = scope_preview_contract(
|
||
preview, routed, {'notes'}
|
||
)
|
||
assert scoped.offered == {'manage_notes'}
|
||
assert scoped.required == {'manage_notes'}
|
||
assert scoped.capabilities == {'notes'}
|
||
assert {s['function']['name'] for s in scoped.schemas()} == {'manage_notes'}
|
||
assert scoped.active_capabilities == {'notes'}
|
||
|
||
|
||
def test_preview_contract_keeps_available_family_when_an_independent_family_is_unavailable():
|
||
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {
|
||
'write_file', 'web_search',
|
||
}]
|
||
preview = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||
routed = SimpleNamespace(
|
||
unavailable=frozenset({'manage_calendar'}),
|
||
offered=frozenset({'write_file'}),
|
||
required=frozenset(),
|
||
capabilities=frozenset({'calendar', 'shell_files'}),
|
||
required_read_operation=None,
|
||
)
|
||
|
||
scoped = scope_preview_contract(
|
||
preview, routed, {'calendar', 'shell_files'}
|
||
)
|
||
|
||
assert scoped.offered == {'write_file'}
|
||
assert {s['function']['name'] for s in scoped.schemas()} == {'write_file'}
|
||
assert scoped.unavailable == {'manage_calendar', 'capability:calendar'}
|
||
assert scoped.active_capabilities == {'calendar', 'shell_files'}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_stream_emits_incremental_text_and_persistable_history(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
class Response:
|
||
async def __aenter__(self): return self
|
||
async def __aexit__(self, *args): pass
|
||
def raise_for_status(self): pass
|
||
async def aiter_lines(self):
|
||
for text in ['Hello', ' there']:
|
||
yield 'data: ' + json.dumps({'choices': [{'delta': {'content': text}}]})
|
||
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'])
|
||
assert kwargs['json']['temperature'] == 0
|
||
assert kwargs['json']['chat_template_kwargs']['enable_thinking'] is False
|
||
return Response()
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
contract = resolve_full_inventory_contract(schemas=[], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(endpoint_url='http://test', model='test', messages=[{'role': 'user', 'content': 'hi'}], headers={}, turn_contract=contract, session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
assert [e['delta'] for e in events if 'delta' in e] == ['Hello', ' there']
|
||
assert requests[0]['messages'][0]['content'].startswith('You are Odysseus.')
|
||
metrics = next(e['data'] for e in events if e.get('type') == 'metrics')
|
||
assert metrics['clean_v3_turn'] == [{'role': 'assistant', 'content': 'Hello there'}]
|
||
assert raw[-1] == 'data: [DONE]\n\n'
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_calendar_list_structured_result_owns_linked_terminal_render(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
|
||
class Response:
|
||
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({'choices': [{'delta': {'tool_calls': [{
|
||
'index': 0, 'id': 'calendar-1', 'function': {
|
||
'name': 'manage_calendar',
|
||
'arguments': '{"action":"list_events","start":"2026-09-14","end":"2026-09-21"}',
|
||
},
|
||
}]}}]})
|
||
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()
|
||
|
||
linked = (
|
||
'Found 1 event(s) between 2026-09-14 and 2026-09-21:\n'
|
||
'- 2026-09-15T11:00:00 -> 2026-09-15T12:30:00: '
|
||
'[Interior meeting](#event-event-123) (Personal)'
|
||
)
|
||
|
||
async def execute(block, **kwargs):
|
||
return 'manage_calendar', {'response': linked, 'exit_code': 0}
|
||
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_calendar')
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test',
|
||
messages=[{'role': 'user', 'content': 'whats my events this week'}], headers={},
|
||
turn_contract=contract, session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
|
||
assert len(requests) == 1
|
||
rendered = next(event['delta'] for event in events if 'delta' in event)
|
||
assert '[Interior meeting](#event-event-123) — Sep 15, 11:00 AM–12:30 PM' in rendered
|
||
assert '2026-09-15T11:00:00' not in rendered
|
||
metrics = next(event['data'] for event in events if event.get('type') == 'metrics')
|
||
assert metrics['clean_v3_turn'][-1] == {'role': 'assistant', 'content': rendered}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_whole_email_draft_is_protocol_bound_to_update_document(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
responses = iter([
|
||
{
|
||
'choices': [{'delta': {'tool_calls': [{
|
||
'index': 0, 'id': 'write-1', 'function': {
|
||
'name': 'update_document',
|
||
'arguments': '{"content":"To: alex@example.com\\nSubject: Re: Meeting\\n---\\nTomorrow works."}',
|
||
},
|
||
}]}}],
|
||
'usage': {'prompt_tokens': 321, 'completion_tokens': 20},
|
||
},
|
||
{
|
||
'choices': [{'delta': {'content': 'Draft ready.'}}],
|
||
'usage': {'prompt_tokens': 400, 'completion_tokens': 9},
|
||
},
|
||
])
|
||
|
||
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):
|
||
assert block.tool_type == 'update_document'
|
||
return 'update_document', {
|
||
'output': 'Document updated', 'exit_code': 0,
|
||
'doc_id': 'draft-1', 'title': 'Meeting', 'language': 'email',
|
||
'content': 'To: alex@example.com\nSubject: Re: Meeting\n---\nTomorrow works.',
|
||
'version': 2,
|
||
}
|
||
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {
|
||
'update_document', 'edit_document', 'suggest_document', 'ui_control',
|
||
}]
|
||
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||
draft = SimpleNamespace(
|
||
id='draft-1', title='Meeting', language='email',
|
||
current_content='To: alex@example.com\nSubject: Re: Meeting\n---\nCan we meet tomorrow?',
|
||
)
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test',
|
||
messages=[{'role': 'user', 'content': 'Write reply to this email'}], headers={},
|
||
turn_contract=contract, session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), active_document=draft)]
|
||
|
||
assert [tool['function']['name'] for tool in requests[0]['tools']] == ['update_document']
|
||
assert requests[0]['tool_choice'] == {
|
||
'type': 'function', 'function': {'name': 'update_document'},
|
||
}
|
||
assert 'tools' not in requests[1]
|
||
assert 'tool_choice' not in requests[1]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
event_types = [event.get('type') for event in events]
|
||
assert event_types.index('doc_update') < event_types.index('tool_output')
|
||
doc_update = next(event for event in events if event.get('type') == 'doc_update')
|
||
assert doc_update == {
|
||
'type': 'doc_update', 'doc_id': 'draft-1', 'title': 'Meeting',
|
||
'language': 'email',
|
||
'content': 'To: alex@example.com\nSubject: Re: Meeting\n---\nTomorrow works.',
|
||
'version': 2,
|
||
}
|
||
tool_output = next(event for event in events if event.get('type') == 'tool_output')
|
||
assert tool_output['doc_id'] == 'draft-1'
|
||
assert tool_output['document_content'].endswith('Tomorrow works.')
|
||
metrics = next(e['data'] for e in events if e.get('type') == 'metrics')
|
||
assert metrics['injected_tokens'] == 321
|
||
assert metrics['tool_schema_count'] == 1
|
||
assert metrics['agent_rounds'] == 2
|
||
assert metrics['tool_calls'] == 1
|
||
assert metrics['tokens_per_second'] >= 0
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_stream_forwards_successful_panel_open_to_ui(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
responses = iter([
|
||
{'choices': [{'delta': {'tool_calls': [{'index': 0, 'id': 'ui-1', 'function': {
|
||
'name': 'ui_control',
|
||
'arguments': '{"action":"open_panel","name":"gallery"}',
|
||
}}]}}]},
|
||
{'choices': [{'delta': {'content': 'Opened the gallery.'}}]},
|
||
])
|
||
|
||
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): return Response(next(responses))
|
||
|
||
async def execute(block, **kwargs):
|
||
assert block.tool_type == 'ui_control'
|
||
assert block.content == 'open_panel gallery'
|
||
return 'ui_control', {
|
||
'ui_event': 'open_panel', 'panel': 'gallery',
|
||
'results': 'Opening gallery panel', 'exit_code': 0,
|
||
}
|
||
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'ui_control')
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test',
|
||
messages=[{'role': 'user', 'content': 'Open gallery'}], headers={},
|
||
turn_contract=contract, session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
assert any(e.get('type') == 'tool_start' and e.get('tool') == 'ui_control' for e in events)
|
||
assert any(
|
||
e.get('type') == 'ui_control'
|
||
and (e.get('data') or {}).get('ui_event') == 'open_panel'
|
||
and (e.get('data') or {}).get('panel') == 'gallery'
|
||
for e in events
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_stream_replaces_unsupported_write_completion(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
class Response:
|
||
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({'choices': [{'delta': {'content': 'All notes have been deleted.'}}]})
|
||
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): return Response()
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
contract = resolve_full_inventory_contract(schemas=[], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(endpoint_url='http://test', model='test', messages=[{'role': 'user', 'content': 'Delete all my notes.'}], headers={}, turn_contract=contract, session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
final = next(e['content'] for e in events if e.get('type') == 'final_response')
|
||
assert final == denied_response()
|
||
metrics = next(e['data'] for e in events if e.get('type') == 'metrics')
|
||
assert metrics['clean_v3_turn'] == [{'role': 'assistant', 'content': denied_response()}]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_workspace_write_allows_evidence_backed_completion(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
responses = iter([
|
||
{'choices': [{'delta': {'tool_calls': [{
|
||
'index': 0, 'id': 'write', 'function': {
|
||
'name': 'write_file',
|
||
'arguments': json.dumps({
|
||
'path': '/workspace/output.txt', 'content': 'verified result',
|
||
}),
|
||
},
|
||
}]}}]},
|
||
{'choices': [{'delta': {'content': 'Saved the workspace artifact.'}}]},
|
||
])
|
||
|
||
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):
|
||
assert block.tool_type == 'write_file'
|
||
return 'write_file', {'output': 'Wrote output.txt', 'exit_code': 0}
|
||
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'write_file')
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test',
|
||
messages=[{'role': 'user', 'content': 'Create a workspace artifact.'}], headers={},
|
||
turn_contract=contract, session_id='test', owner='test', disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), workspace='/tmp/workspace',
|
||
client_runtime_context={
|
||
'surface': 'odysseus-native', 'terminal_agent': True,
|
||
'unattended_mode': True,
|
||
}, max_rounds=2,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
|
||
assert not [event for event in events if event.get('type') == 'final_response']
|
||
assert any(
|
||
event.get('delta') == 'Saved the workspace artifact.' for event in events
|
||
)
|
||
native_system = requests[0]['messages'][0]['content']
|
||
assert 'batch independent known URLs' in native_system
|
||
assert 'create required artifacts incrementally' in native_system
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_policy_denied_batch_executes_nothing(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
class Response:
|
||
async def __aenter__(self): return self
|
||
async def __aexit__(self, *args): pass
|
||
def raise_for_status(self): pass
|
||
async def aiter_lines(self):
|
||
calls = [
|
||
{'index': 0, 'id': 'read', 'function': {'name': 'manage_notes', 'arguments': '{"action":"list"}'}},
|
||
{'index': 1, 'id': 'delete', 'function': {'name': 'manage_notes', 'arguments': '{"action":"delete","id":"x"}'}},
|
||
]
|
||
yield 'data: ' + json.dumps({'choices': [{'delta': {'tool_calls': calls}}]})
|
||
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): return Response()
|
||
async def must_not_execute(*args, **kwargs):
|
||
raise AssertionError('a partially permitted batch must not execute')
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', must_not_execute)
|
||
notes = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'manage_notes')
|
||
contract = resolve_full_inventory_contract(schemas=[notes], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(endpoint_url='http://test', model='test', messages=[{'role': 'user', 'content': 'List my notes.'}], headers={}, turn_contract=contract, session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
assert not [e for e in events if e.get('type') in {'tool_start', 'tool_output'}]
|
||
assert next(e['content'] for e in events if e.get('type') == 'final_response') == denied_response()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_bash_arguments_use_canonical_execution_content(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
command = "printf '%s\\n' ODY_SHELL_FILES_READONLY; cat /etc/hostname"
|
||
responses = iter([
|
||
{'choices': [{'delta': {'tool_calls': [{'index': 0, 'id': 'shell', 'function': {'name': 'bash', 'arguments': json.dumps({'command': command})}}]}}]},
|
||
{'choices': [{'delta': {'content': 'Marker verified.'}}]},
|
||
])
|
||
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): return Response(next(responses))
|
||
captured = []
|
||
async def execute(block, **kwargs):
|
||
captured.append(block)
|
||
return 'bash', {'output': 'ODY_SHELL_FILES_READONLY\nkierkegaard', 'exit_code': 0}
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
bash = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'bash')
|
||
contract = resolve_full_inventory_contract(schemas=[bash], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(endpoint_url='http://test', model='test', messages=[{'role': 'user', 'content': 'Run the displayed read-only shell command.'}], headers={}, turn_contract=contract, session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert len(captured) == 1
|
||
assert captured[0].tool_type == 'bash'
|
||
assert captured[0].content == command
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_normalizes_single_clip_exports_before_validation(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"exports": json.dumps([{
|
||
"start": "10",
|
||
"end": "16",
|
||
"output_path": "/workspace/clip.mp4",
|
||
}]),
|
||
})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0,
|
||
"id": "clip-1",
|
||
"function": {"name": "inspect_media", "arguments": arguments},
|
||
}]}}]},
|
||
{"choices": [{"delta": {"content": "Created the requested clip."}}]},
|
||
])
|
||
|
||
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): return Response(next(responses))
|
||
|
||
captured = []
|
||
|
||
async def execute(block, **kwargs):
|
||
captured.append(block)
|
||
return "inspect_media", {"output": "clip created", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Create the requested short video clip."}],
|
||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=2,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(captured) == 1
|
||
assert json.loads(captured[0].content) == {
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"start": "10",
|
||
"end": "16",
|
||
"output_path": "/workspace/clip.mp4",
|
||
}
|
||
assert not [event for event in events if event.get("type") == "tool_output" and event.get("error")]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_explains_how_to_recover_from_timestamp_free_export(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
malformed = json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"exports": [{"output_path": "/workspace/diagram.png", "type": "diagram"}],
|
||
})
|
||
corrected = json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"query": "inspect the room layout",
|
||
})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": "bad-export",
|
||
"function": {"name": "inspect_media", "arguments": malformed},
|
||
}]}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": "inspect-first",
|
||
"function": {"name": "inspect_media", "arguments": corrected},
|
||
}]}}]},
|
||
{"choices": [{"delta": {"content": "I inspected the video before authoring."}}]},
|
||
])
|
||
|
||
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): return Response(next(responses))
|
||
|
||
captured = []
|
||
|
||
async def execute(block, **kwargs):
|
||
captured.append(block)
|
||
return "inspect_media", {"output": "visual evidence", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Inspect a video and create a diagram."}],
|
||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=3,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
errors = [
|
||
event["output"] for event in events
|
||
if event.get("type") == "tool_output" and event.get("error")
|
||
]
|
||
assert len(captured) == 1
|
||
assert json.loads(captured[0].content) == json.loads(corrected)
|
||
assert len(errors) == 1
|
||
assert "explicit timestamp plus output_path" in errors[0]
|
||
assert "write_file or python" in errors[0]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_terminates_on_first_post_budget_tool_call(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": "first",
|
||
"function": {
|
||
"name": "inspect_media",
|
||
"arguments": json.dumps({"path": "/workspace/fixtures/video.mp4"}),
|
||
},
|
||
}]}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": "over-budget",
|
||
"function": {
|
||
"name": "inspect_media",
|
||
"arguments": json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"query": "different evidence",
|
||
}),
|
||
},
|
||
}]}}]},
|
||
])
|
||
|
||
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): return Response(next(responses))
|
||
|
||
executions = []
|
||
|
||
async def execute(block, **kwargs):
|
||
executions.append(block)
|
||
return "inspect_media", {"output": "visual evidence", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module, "NATIVE_TOOL_CALL_LIMIT", 1)
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Inspect the video."}], headers={},
|
||
turn_contract=contract, session_id="test", owner="test", disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=20,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 1
|
||
budget_errors = [
|
||
event for event in events
|
||
if event.get("type") == "tool_output"
|
||
and "budget exhausted" in event.get("output", "")
|
||
]
|
||
assert len(budget_errors) == 1
|
||
final = [event for event in events if event.get("type") == "final_response"]
|
||
assert len(final) == 1
|
||
assert "budget was exhausted" in final[0]["content"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_model_choice_budget_preserves_evidence_for_final_answer_without_extra_execution(monkeypatch):
|
||
from dataclasses import replace
|
||
import src.clean_agent_preview as module
|
||
def call(i):
|
||
return {'index': i, 'id': f'call-{i}', 'function': {
|
||
'name': 'web_fetch', 'arguments': json.dumps({'url': f'https://example.org/{i}'})}}
|
||
packets = iter([
|
||
{'choices': [{'delta': {'tool_calls': [call(i) for i in range(6)]}}]},
|
||
{'choices': [{'delta': {'tool_calls': [call(7)]}}]},
|
||
{'choices': [{'delta': {'content': 'Found six source pages; further details are unverified.'}}]},
|
||
])
|
||
requests, executions = [], []
|
||
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(packets))
|
||
async def execute(block, **kwargs):
|
||
executions.append(block)
|
||
return 'web_fetch', {'output': f'Source page {len(executions)}', 'exit_code': 0}
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'web_fetch')
|
||
contract = replace(resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy()),
|
||
routing_experiment='recent_model_choice')
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', messages=[{'role':'user','content':'Read these public pages.'}],
|
||
headers={}, turn_contract=contract, session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert len(executions) == 6
|
||
assert len(requests) == 3
|
||
assert 'tools' not in requests[-1]
|
||
assert any(m.get('role') == 'tool' and 'Source page' in m.get('content','') for m in requests[-1]['messages'])
|
||
assert any('tool_budget_final_synthesis' in chunk for chunk in raw)
|
||
assert any('Found six source pages' in chunk for chunk in raw)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_reserves_remaining_budget_for_required_artifact(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
|
||
payloads = [
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": f"search-{index}",
|
||
"function": {"name": "web_search", "arguments": json.dumps({"query": f"topic {index}"})},
|
||
}]}}]}
|
||
for index in range(module.NATIVE_ARTIFACT_RESEARCH_LIMIT)
|
||
]
|
||
payloads.extend([
|
||
{"choices": [{"delta": {"tool_calls": [{
|
||
"index": 0, "id": "write",
|
||
"function": {"name": "write_file", "arguments": json.dumps({
|
||
"path": "/tmp_workspace/results/out.md", "content": "evidence",
|
||
})},
|
||
}]}}]},
|
||
{"choices": [{"delta": {"content": "Saved."}}]},
|
||
])
|
||
responses = iter(payloads)
|
||
requests = []
|
||
|
||
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):
|
||
return block.tool_type, {"output": "ok", "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 {"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",
|
||
messages=[{"role": "user", "content": "Research and create the requested output."}],
|
||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
"completion_requirements": {"required_artifacts": ["/tmp_workspace/results"]},
|
||
}, max_rounds=32,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
recovery = next(
|
||
event for event in events
|
||
if event.get("type") == "completion_recovery"
|
||
and event.get("reason") == "artifact_write_budget_reserved"
|
||
)
|
||
assert recovery["calls_used"] == module.NATIVE_ARTIFACT_RESEARCH_LIMIT
|
||
assert [tool["function"]["name"] for tool in requests[12]["tools"]] == ["write_file"]
|
||
assert requests[12]["tool_choice"] == {
|
||
"type": "function", "function": {"name": "write_file"},
|
||
}
|
||
assert any(
|
||
event.get("type") == "tool_output" and event.get("tool") == "write_file"
|
||
and not event.get("error") for event in events
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_detailed_video_answer_requires_second_focused_inspection(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "broad", "function": {
|
||
"name": "inspect_media",
|
||
"arguments": json.dumps({"path": "/workspace/video.mp4", "frames": 1}),
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "There were three events."}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "focused", "function": {
|
||
"name": "inspect_media",
|
||
"arguments": json.dumps({
|
||
"path": "/workspace/video.mp4",
|
||
"start": "00:00:10", "end": "00:00:20", "frames": 8,
|
||
}),
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "There were two verified events."}}]},
|
||
])
|
||
|
||
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)
|
||
return "inspect_media", {
|
||
"output": "Video duration: 00:01:00.000\nTimestamped visual evidence.",
|
||
"exit_code": 0,
|
||
}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "How many events occur in this video?"}],
|
||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=4,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 2
|
||
assert len(requests) == 4
|
||
recovery = next(event for event in events if event.get('type') == 'completion_recovery')
|
||
assert recovery['reason'] == 'detailed_video_requires_focused_inspection'
|
||
final = next(event for event in events if event.get('type') == 'final_response')
|
||
assert final['content'] == 'There were two verified events.'
|
||
metrics = next(event['data'] for event in events if event.get('type') == 'metrics')
|
||
assert metrics['clean_v3_turn'][-1]['content'] == 'There were two verified events.'
|
||
assert not any(
|
||
message.get('content') == 'There were three events.'
|
||
for message in metrics['clean_v3_turn']
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_exact_native_read_gets_tool_free_synthesis_round(monkeypatch):
|
||
from dataclasses import replace
|
||
import src.clean_agent_preview as module
|
||
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "read-1", "function": {
|
||
"name": "read_file",
|
||
"arguments": json.dumps({"path": "/workspace/sample.txt", "limit": 3}),
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "Read the requested three lines."}}]},
|
||
])
|
||
|
||
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)
|
||
return "read_file", {"output": "alpha\nbeta\ngamma", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(item for item in FUNCTION_TOOL_SCHEMAS if item["function"]["name"] == "read_file")
|
||
contract = replace(
|
||
resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy()),
|
||
required=frozenset({"read_file"}),
|
||
capabilities=frozenset({"shell_files"}),
|
||
active_capabilities=frozenset({"shell_files"}),
|
||
)
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Use read_file to read the first three lines of /workspace/sample.txt."}],
|
||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=4,
|
||
)]
|
||
|
||
assert len(executions) == 1
|
||
assert len(requests) == 2
|
||
assert 'tools' in requests[0]
|
||
assert 'tools' not in requests[1]
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert any(event.get('delta') == 'Read the requested three lines.' for event in events)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@pytest.mark.parametrize('recovery_kind', ['none', 'budget', 'duplicate'])
|
||
async def test_context_overflow_retries_model_request_without_replaying_tool(monkeypatch, recovery_kind):
|
||
from dataclasses import replace
|
||
import httpx
|
||
import src.clean_agent_preview as module
|
||
requests, executions = [], []
|
||
overflow_request = 2 if recovery_kind == 'none' else 3
|
||
if recovery_kind == 'budget':
|
||
monkeypatch.setattr(module, 'INTERACTIVE_TOOL_CALL_LIMIT', 1)
|
||
async def handle(request):
|
||
payload = json.loads(request.content)
|
||
requests.append(payload)
|
||
if len(requests) == overflow_request:
|
||
return httpx.Response(400, json={'error': {'message':
|
||
"This model's maximum context length is 4096 tokens. However, "
|
||
"you requested 768 output tokens and your prompt contains at least "
|
||
"3900 input tokens, for a total of at least 4668 tokens."}})
|
||
delta = ({'tool_calls': [{'index': 0, 'id': 'page-read', 'function': {
|
||
'name': 'private_browser', 'arguments': json.dumps({
|
||
'action': 'open', 'url': 'https://example.org'})}}]}
|
||
if len(requests) == 1 else {'content': 'The page was read.'})
|
||
if recovery_kind != 'none' and len(requests) == 2:
|
||
args = ({'action': 'click', 'target': '@e2'} if recovery_kind == 'budget'
|
||
else {'action': 'find', 'text': 'heading'})
|
||
delta = {'tool_calls': [{'index': 0, 'id': 'blocked-click', 'function': {
|
||
'name': 'private_browser', 'arguments': json.dumps(args)}}]}
|
||
if recovery_kind == 'duplicate' and len(requests) == 1:
|
||
delta['tool_calls'][0]['function']['arguments'] = json.dumps({'action': 'find', 'text': 'heading'})
|
||
return httpx.Response(200, text='data: ' + json.dumps({'choices': [{'delta': delta}]})
|
||
+ '\n\ndata: [DONE]\n\n')
|
||
original_client = httpx.AsyncClient
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', lambda **kwargs: original_client(
|
||
**kwargs, transport=httpx.MockTransport(handle)))
|
||
async def execute(block, **kwargs):
|
||
executions.append(block)
|
||
return 'page', {'output': 'heading current page\n' + '木' * 7900, 'exit_code': 0}
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'private_browser')
|
||
contract = replace(resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy()),
|
||
routing_experiment='recent_model_choice')
|
||
prompt = 'Open https://example.org and report the page heading.'
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', headers={}, turn_contract=contract,
|
||
messages=[{'role': 'user', 'content': prompt}], session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert any('The page was read.' in chunk for chunk in raw)
|
||
assert len(executions) == 1
|
||
assert len(requests) == overflow_request + 1
|
||
before, after = requests[-2:]
|
||
assert after['max_tokens'] == before['max_tokens']
|
||
assert len(json.dumps(after)) < len(json.dumps(before))
|
||
assert {'role': 'user', 'content': prompt} in after['messages']
|
||
assert after.get('tools') == before.get('tools')
|
||
calls = {call['id'] for msg in after['messages'] for call in msg.get('tool_calls', [])}
|
||
assert all(msg['tool_call_id'] in calls for msg in after['messages'] if msg['role'] == 'tool')
|
||
assert not any('_harness_control' in msg for request in requests for msg in request['messages'])
|
||
if recovery_kind == 'budget':
|
||
assert any('budget exhausted' in str(msg.get('content')) for msg in after['messages'])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@pytest.mark.parametrize('status,context_error,expected_requests', [
|
||
(400, True, 3), (413, True, 3), (400, False, 1), (401, True, 1),
|
||
])
|
||
async def test_context_recovery_is_bounded_and_not_used_for_other_errors(
|
||
monkeypatch, status, context_error, expected_requests):
|
||
import httpx
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
async def handle(request):
|
||
requests.append(request)
|
||
message = ("This model's maximum context length is 16384 tokens. However, "
|
||
"your messages resulted in 17000 tokens."
|
||
if context_error else 'Invalid tool schema')
|
||
return httpx.Response(status, json={'error': {'message': message}})
|
||
original_client = httpx.AsyncClient
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', lambda **kwargs: original_client(
|
||
**kwargs, transport=httpx.MockTransport(handle)))
|
||
contract = resolve_full_inventory_contract(schemas=[], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', headers={}, turn_contract=contract,
|
||
messages=[{'role': 'user', 'content': 'Hello'}], session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert len(requests) == expected_requests
|
||
assert not any('"type": "tool_start"' in chunk for chunk in raw)
|
||
assert any('encountered an error' in chunk for chunk in raw)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_started_model_stream_is_never_retried(monkeypatch):
|
||
import httpx
|
||
import src.clean_agent_preview as module
|
||
requests = []
|
||
class BrokenStream(httpx.AsyncByteStream):
|
||
async def __aiter__(self):
|
||
yield b'data: {"choices":[{"delta":{"content":"Partial answer"}}]}\n\n'
|
||
raise httpx.ReadError('stream disconnected')
|
||
async def handle(request):
|
||
requests.append(request)
|
||
return httpx.Response(200, stream=BrokenStream())
|
||
original_client = httpx.AsyncClient
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', lambda **kwargs: original_client(
|
||
**kwargs, transport=httpx.MockTransport(handle)))
|
||
contract = resolve_full_inventory_contract(schemas=[], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', headers={}, turn_contract=contract,
|
||
messages=[{'role': 'user', 'content': 'Hello'}], session_id='test', owner='test',
|
||
disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert len(requests) == 1
|
||
assert sum('Partial answer' in chunk for chunk in raw) == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@pytest.mark.parametrize('transition,transition_result,can_repeat', [
|
||
({'action': 'open', 'url': 'https://example.org/next'}, {}, True),
|
||
({'action': 'snapshot'}, {}, True),
|
||
({'action': 'click', 'target': '@e2'}, {'error': 'Element changed', 'exit_code': 1}, True),
|
||
({'action': 'read'}, {}, False),
|
||
({'action': 'open', 'url': 'https://example.org/next'},
|
||
{'blocked': True, 'error': 'Denied', 'exit_code': 1}, False),
|
||
])
|
||
async def test_browser_can_repeat_observation_after_navigation(
|
||
monkeypatch, transition, transition_result, can_repeat):
|
||
from dataclasses import replace
|
||
import src.clean_agent_preview as module
|
||
actions = [{'action': 'find', 'text': 'heading'},
|
||
transition,
|
||
{'action': 'find', 'text': 'heading'}]
|
||
packets = iter([{'choices': [{'delta': {'tool_calls': [{
|
||
'index': 0, 'id': f'browser-{i}', 'function': {'name': 'private_browser',
|
||
'arguments': json.dumps(args)}}]}}]} for i, args in enumerate(actions)]
|
||
+ [{'choices': [{'delta': {'content': 'New page heading.'}}]}])
|
||
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): return Response(next(packets))
|
||
executions = []
|
||
async def execute(block, **kwargs):
|
||
executions.append(json.loads(block.content))
|
||
return 'browser fixture', {
|
||
'output': f'Page evidence {len(executions)}', 'exit_code': 0,
|
||
**(transition_result if len(executions) == 2 else {}),
|
||
}
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'private_browser')
|
||
contract = replace(resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy()),
|
||
routing_experiment='recent_model_choice')
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', headers={}, turn_contract=contract,
|
||
messages=[{'role': 'user', 'content': 'Inspect the heading, open the next page, then inspect its heading.'}],
|
||
session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
assert executions == (actions if can_repeat else actions[:2])
|
||
if not transition_result.get('blocked'):
|
||
assert any('already returned evidence' in chunk for chunk in raw) != can_repeat
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
@pytest.mark.parametrize('recovers', [True, False])
|
||
async def test_empty_search_retry_preserves_evidence_dedupe_and_execution_budget(monkeypatch, recovers):
|
||
from dataclasses import replace
|
||
import src.clean_agent_preview as module
|
||
import src.search as search
|
||
from src.agent_tools.web_tools import WebSearchTool
|
||
arguments = json.dumps({'query': 'documentation domains'})
|
||
packets = iter([
|
||
{'choices': [{'delta': {'tool_calls': [{'index': 0, 'id': f'search-{i}',
|
||
'function': {'name': 'web_search', 'arguments': arguments}}]}}]}
|
||
for i in range(3 if recovers else 7)
|
||
] + [{'choices': [{'delta': {'content': 'Used the available source.'}}]}])
|
||
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): return Response(next(packets))
|
||
queries = []
|
||
def provider(query, **kwargs):
|
||
queries.append(query)
|
||
if len(queries) == 1 or not recovers:
|
||
return 'No search results found.', []
|
||
return 'Example domains are for documentation.', [
|
||
{'title': 'Example Domains', 'url': 'https://www.iana.org/help/example-domains'}]
|
||
async def execute(block, **kwargs):
|
||
return 'web_search', await WebSearchTool().execute(arguments, {})
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
monkeypatch.setattr(search, 'comprehensive_web_search', provider)
|
||
schema = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'web_search')
|
||
contract = replace(resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy()),
|
||
routing_experiment='recent_model_choice')
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test', headers={}, turn_contract=contract,
|
||
messages=[{'role': 'user', 'content': 'Search for documentation domains.'}],
|
||
session_id='test', owner='test', disabled_tools=set(), tool_policy=ToolPolicy())]
|
||
events = [json.loads(chunk[6:]) for chunk in raw if '[DONE]' not in chunk]
|
||
outputs = [event for event in events if event.get('type') == 'tool_output']
|
||
assert len(queries) == (2 if recovers else 6)
|
||
assert outputs[0]['evidence_status'] == 'empty'
|
||
if recovers:
|
||
assert outputs[1]['evidence_status'] == 'available'
|
||
assert 'iana.org' in outputs[1]['output']
|
||
assert outputs[2]['error'] and 'already returned evidence' in outputs[2]['output']
|
||
else:
|
||
assert all(output['evidence_status'] == 'empty' for output in outputs[:6])
|
||
assert outputs[-1]['error'] and 'budget exhausted' in outputs[-1]['output']
|
||
assert not any('already returned evidence' in output['output'] for output in outputs)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_does_not_reexecute_an_identical_successful_call(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({"path": "/workspace/fixtures/video.mp4"})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "inspect-1", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "inspect-2", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "Used the existing visual evidence."}}]},
|
||
])
|
||
|
||
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)
|
||
return "inspect_media", {"output": "four useful frames", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Inspect the video."}], headers={},
|
||
turn_contract=contract, session_id="test", owner="test", disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=3,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 1
|
||
duplicate = [
|
||
event for event in events
|
||
if event.get("type") == "tool_output" and event.get("error")
|
||
]
|
||
assert len(duplicate) == 1
|
||
assert "already returned evidence" in duplicate[0]["output"]
|
||
assert 'tools' not in requests[2]
|
||
assert requests[2]['messages'][-1]['role'] == 'user'
|
||
assert 'finish from the evidence' in requests[2]['messages'][-1]['content'].lower()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_permanently_withholds_tool_when_model_ignores_duplicate_correction(
|
||
monkeypatch,
|
||
):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({"path": "/workspace/fixtures/video.mp4"})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "inspect-1", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "inspect-2", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "inspect-3", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "Finished from the existing evidence."}}]},
|
||
])
|
||
|
||
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)
|
||
return "inspect_media", {"output": "visual evidence", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Inspect the video."}], headers={},
|
||
turn_contract=contract, session_id="test", owner="test", disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=4,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 1
|
||
duplicate_errors = [
|
||
event for event in events
|
||
if event.get("type") == "tool_output"
|
||
and "already returned evidence" in event.get("output", "")
|
||
]
|
||
assert len(duplicate_errors) == 2
|
||
assert 'tools' not in requests[2]
|
||
assert 'tools' not in requests[3]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_terminates_after_calling_a_permanently_suppressed_tool(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({"path": "/workspace/fixtures/video.mp4"})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": f"inspect-{index}", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]}
|
||
for index in range(1, 5)
|
||
] + [{"choices": [{"delta": {"content": "Final answer from existing evidence."}}]}])
|
||
|
||
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)
|
||
return "inspect_media", {"output": "visual evidence", "exit_code": 0}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": "Inspect the video."}], headers={},
|
||
turn_contract=contract, session_id="test", owner="test", disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=20,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 1
|
||
assert len(requests) == 5
|
||
assert 'tools' not in requests[4]
|
||
assert 'best concise final answer' in requests[4]['messages'][-1]['content'].lower()
|
||
final = [event for event in events if event.get("type") == "final_response"]
|
||
assert final == []
|
||
metrics = next(event['data'] for event in events if event.get('type') == 'metrics')
|
||
assert metrics['clean_v3_turn'][-1]['content'] == 'Final answer from existing evidence.'
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_stops_reexecuting_an_identical_failed_call(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"exports": [{
|
||
"timestamp": "00:00:00",
|
||
"output_path": "/workspace/clip.mp4",
|
||
}],
|
||
})
|
||
responses = iter([
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": f"clip-{index}", "function": {
|
||
"name": "inspect_media", "arguments": arguments,
|
||
}}]}}]}
|
||
for index in range(1, 5)
|
||
] + [{"choices": [{"delta": {"content": "Unable to create the clip."}}]}])
|
||
|
||
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)
|
||
return "inspect_media", {
|
||
"error": "a video clip export requires explicit start and end",
|
||
"exit_code": 1,
|
||
}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": (
|
||
"Inspect the video and save /workspace/clip.mp4."
|
||
)}], headers={}, turn_contract=contract, session_id="test", owner="test",
|
||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||
client_runtime_context={
|
||
"surface": "odysseus-native", "terminal_agent": True,
|
||
"unattended_mode": True,
|
||
}, max_rounds=5,
|
||
)]
|
||
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert len(executions) == 2
|
||
assert any(event.get('type') == 'tool_loop_recovery' for event in events)
|
||
assert any(s['function']['name'] == 'inspect_media' for s in requests[3]['tools'])
|
||
assert len(requests) == 5
|
||
assert any(
|
||
event.get('type') == 'tool_output'
|
||
and 'already failed twice' in event.get('output', '')
|
||
and event.get('execution_attempted') is False
|
||
for event in events
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_native_stream_recovers_when_declared_workspace_artifact_is_missing(
|
||
monkeypatch, tmp_path,
|
||
):
|
||
import src.clean_agent_preview as module
|
||
responses = iter([
|
||
{"choices": [{"delta": {"content": "The collision occurs at 00:00:15."}}]},
|
||
{"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "clip-1", "function": {
|
||
"name": "inspect_media",
|
||
"arguments": json.dumps({
|
||
"path": "/workspace/fixtures/video.mp4",
|
||
"start": "00:00:12", "end": "00:00:18",
|
||
"output_path": "/workspace/clip.mp4",
|
||
}),
|
||
}}]}}]},
|
||
{"choices": [{"delta": {"content": "Saved /workspace/clip.mp4."}}]},
|
||
])
|
||
requests = []
|
||
|
||
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):
|
||
(tmp_path / "clip.mp4").write_bytes(b"video")
|
||
return "inspect_media", {
|
||
"output": "Created clip: /workspace/clip.mp4", "exit_code": 0,
|
||
"output_path": "/workspace/clip.mp4",
|
||
}
|
||
|
||
monkeypatch.setattr(module.httpx, "AsyncClient", Client)
|
||
monkeypatch.setattr(module, "execute_tool_block", execute)
|
||
schema = next(
|
||
item for item in FUNCTION_TOOL_SCHEMAS
|
||
if item["function"]["name"] == "inspect_media"
|
||
)
|
||
contract = resolve_full_inventory_contract(schemas=[schema], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url="http://test", model="test",
|
||
messages=[{"role": "user", "content": (
|
||
"Inspect /workspace/fixtures/video.mp4 and save the collision clip "
|
||
"to /workspace/clip.mp4."
|
||
)}], 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,
|
||
}, max_rounds=4,
|
||
)]
|
||
|
||
assert (tmp_path / "clip.mp4").read_bytes() == b"video"
|
||
assert len(requests) == 3
|
||
assert all(
|
||
message['role'] != 'system'
|
||
for message in requests[1]['messages'][1:]
|
||
)
|
||
assert requests[1]['messages'][-1]['role'] == 'user'
|
||
assert any(
|
||
event.get("type") == "completion_recovery"
|
||
for event in (json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk)
|
||
)
|
||
events = [json.loads(chunk[6:]) for chunk in raw if "[DONE]" not in chunk]
|
||
assert not any(
|
||
event.get("type") == "final_response"
|
||
and event.get("content") == denied_response()
|
||
for event in events
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_preview_propagates_active_document_and_truthfully_marks_tool_error(monkeypatch):
|
||
import src.clean_agent_preview as module
|
||
arguments = json.dumps({'edits': [{'find': 'alpha', 'replace': 'beta'}]})
|
||
responses = iter([
|
||
{'choices': [{'delta': {'tool_calls': [{'index': 0, 'id': 'edit', 'function': {
|
||
'name': 'edit_document', 'arguments': arguments,
|
||
}}]}}]},
|
||
{'choices': [{'delta': {'content': 'Could not edit.'}}]},
|
||
])
|
||
|
||
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): return Response(next(responses))
|
||
|
||
captured = {}
|
||
async def execute(block, **kwargs):
|
||
captured.update(kwargs)
|
||
return 'edit_document', {'error': 'FIND did not match'}
|
||
|
||
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
||
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
||
edit = next(s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] == 'edit_document')
|
||
contract = resolve_full_inventory_contract(schemas=[edit], policy=ToolPolicy())
|
||
raw = [chunk async for chunk in stream_preview(
|
||
endpoint_url='http://test', model='test',
|
||
messages=[{'role': 'user', 'content': 'Edit my document.'}], headers={},
|
||
turn_contract=contract, session_id='test', owner='test', disabled_tools=set(),
|
||
tool_policy=ToolPolicy(), active_document=SimpleNamespace(
|
||
id='doc-123', title='Fixture', language='text', current_content='alpha',
|
||
))]
|
||
events = [json.loads(s[6:]) for s in raw if '[DONE]' not in s]
|
||
output = next(e for e in events if e.get('type') == 'tool_output')
|
||
assert captured['active_document_id'] == 'doc-123'
|
||
assert output['error'] is True
|
||
assert output['exit_code'] == 1
|