Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ jobs:
- name: Require the native provider and verify adapter boundaries offline
run: |
uv run --no-sync python -c 'from amplifier_module_provider_openai.native import NativeResponsesProvider'
uv run --no-sync pytest -q tests/test_native_provider_adapter.py tests/test_native_provider_budget_compatibility.py tests/test_native_job_attribution.py tests/test_native_adapter_fallback.py tests/test_image_budget.py
uv run --no-sync pytest -q tests/test_native_provider_adapter.py tests/test_native_history_options.py tests/test_native_provider_budget_compatibility.py tests/test_native_job_attribution.py tests/test_native_adapter_fallback.py tests/test_image_budget.py
7 changes: 4 additions & 3 deletions adapters/cli/amplifier_loop_live_cli/bundle_astra.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def wrap(cls, original):

def _convert_messages(self, messages, **kwargs):
from amplifier_loop_live_cli.computer_results import failed_computer_history,validate_computer_outputs
messages,_=failed_computer_history(messages)
messages,_=failed_computer_history(
messages, ordinary_names=kwargs.get("ordinary_computer_names", frozenset()))
return validate_computer_outputs(super()._convert_messages(messages,**kwargs))

async def complete(self, request, **kwargs):
Expand All @@ -47,9 +48,9 @@ def wrap(cls, original):
return super().wrap(original, owner_getter=LIVE_OWNER.get, trace=trace,
trace_context=trace_native_context)

def _prepare_native_messages(self, messages):
def _prepare_native_messages(self, messages, *, ordinary_names=frozenset()):
from .computer_results import failed_computer_history
return failed_computer_history(messages)[0]
return failed_computer_history(messages, ordinary_names=ordinary_names)[0]

def _validate_native_items(self, items):
from .computer_results import validate_computer_outputs
Expand Down
8 changes: 5 additions & 3 deletions adapters/cli/amplifier_loop_live_cli/computer_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,25 @@ def __init__(self, message):
super().__init__(self.public_message, provider="openai", retryable=False)


def failed_computer_history(messages):
def failed_computer_history(messages, *, ordinary_names=frozenset()):
"""Represent an old failure as evidence only after a subsequent user message.

The saved transcript is untouched. A failed native call cannot receive a
made-up screenshot; the next user turn starts a new Responses lineage with
an explicit failure record carrying the original identity and result.
"""
# The provider owns native eligibility. An ordinary tool may share the
# computer name and actions-shaped arguments without returning screenshots.
native = set()
for message in messages:
for call in message.get("tool_calls") or []:
name=call.get("tool") or call.get("name")
args=call.get("arguments") or call.get("input") or {}
if name=="computer" and isinstance(args,dict) and isinstance(args.get("actions"),list):
if name=="computer" and name not in ordinary_names and isinstance(args,dict) and isinstance(args.get("actions"),list):
native.add(call.get("id"))
content=message.get("content")
for block in content if isinstance(content,list) else []:
if isinstance(block,dict) and block.get("type")=="tool_call" and block.get("name")=="computer" and isinstance((block.get("input") or {}).get("actions"),list):
if isinstance(block,dict) and block.get("type")=="tool_call" and block.get("name")=="computer" and block.get("name") not in ordinary_names and isinstance((block.get("input") or {}).get("actions"),list):
native.add(block.get("id"))
last_user=max((i for i,m in enumerate(messages) if m.get("role")=="user" and not (m.get("metadata") or {}).get("ephemeral")),default=-1)
failures={}
Expand Down
65 changes: 65 additions & 0 deletions tests/test_native_history_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Provider-owned ordinary-tool classification survives the optional CLI adapter."""
import copy
import json
import sys
from pathlib import Path

import pytest

pytest.importorskip('amplifier_module_provider_openai.native')
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'adapters/cli'))
from amplifier_loop_live_cli.bundle_astra import BundleAstraProvider, SafeOpenAIProvider
from amplifier_loop_live_cli.computer_results import ComputerResultError, failed_computer_history
from amplifier_module_provider_openai import OpenAIProvider


def history(*, block, later_user):
call = {'id': 'ordinary-call', 'name': 'computer', 'arguments': {'actions': []}}
assistant = {'role': 'assistant', 'content': '', 'tool_calls': [call]}
if block:
assistant = {'role': 'assistant', 'content': [
{'type': 'tool_call', 'id': call['id'], 'name': call['name'], 'input': call['arguments']}]}
messages = [{'role': 'user', 'content': 'Use the ordinary tool'}, assistant,
{'role': 'tool', 'tool_call_id': call['id'],
'content': json.dumps({'success': False, 'error': 'ordinary tool failure'})}]
if later_user:
messages.append({'role': 'user', 'content': 'Explain that result'})
return messages


@pytest.mark.parametrize('provider_type', [BundleAstraProvider, SafeOpenAIProvider])
@pytest.mark.parametrize('block', [False, True])
@pytest.mark.parametrize('later_user', [False, True])
def test_actual_provider_serializer_preserves_ordinary_computer_result(provider_type, block, later_user):
messages = history(block=block, later_user=later_user)
original = copy.deepcopy(messages)
provider = provider_type.wrap(OpenAIProvider(api_key='fixture', config={'default_model': 'gpt-6-astra'}))
converted = provider._convert_messages(messages, ordinary_computer_names=frozenset({'computer'}))
output = [item for item in converted if item.get('type') == 'function_call_output']
assert len(output) == 1 and output[0]['call_id'] == 'ordinary-call'
assert 'ordinary tool failure' in str(output[0]['output'])
assert not any(item.get('type') == 'computer_call_output' for item in converted)
assert messages == original


@pytest.mark.parametrize('block', [False, True])
def test_native_failure_still_fails_closed_in_current_turn(block):
with pytest.raises(ComputerResultError, match='ordinary tool failure'):
failed_computer_history(history(block=block, later_user=False))


@pytest.mark.parametrize('block', [False, True])
def test_later_native_failure_is_reference_only_and_does_not_edit_transcript(block):
messages = history(block=block, later_user=True)
original = copy.deepcopy(messages)
projected, changed = failed_computer_history(messages)
assert changed and messages == original
assert any('Do not replay the prior action automatically' in str(m.get('content')) for m in projected)
assert not any(m.get('role') == 'tool' for m in projected)

@pytest.mark.parametrize('block', [False, True])
@pytest.mark.parametrize('later_user', [False, True])
def test_native_projection_accepts_provider_owned_ordinary_names(block, later_user):
messages = history(block=block, later_user=later_user)
provider = BundleAstraProvider.wrap(OpenAIProvider(api_key='fixture', config={'default_model': 'gpt-6-astra'}))
assert provider._prepare_native_messages(messages, ordinary_names=frozenset({'computer'})) is messages
Loading