mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 07:12:20 +01:00
recover mixed artifact tool payloads
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
0.20.12
|
0.20.13
|
||||||
|
|||||||
@@ -1603,6 +1603,11 @@ def required_artifact_completion_tool_choice(required_artifacts, offered_schemas
|
|||||||
targets = [str(path or '').strip().rstrip('/') for path in required_artifacts]
|
targets = [str(path or '').strip().rstrip('/') for path in required_artifacts]
|
||||||
if not targets:
|
if not targets:
|
||||||
return None
|
return None
|
||||||
|
# A required directory commonly contains several files and may need a
|
||||||
|
# programmatic extractor. Requiring *a* tool call preserves forward
|
||||||
|
# progress without trapping the model in one enormous write_file payload.
|
||||||
|
if len(targets) == 1 and not Path(targets[0]).suffix:
|
||||||
|
return 'required'
|
||||||
offered = {
|
offered = {
|
||||||
canonical((schema.get('function') or {}).get('name')):
|
canonical((schema.get('function') or {}).get('name')):
|
||||||
(schema.get('function') or {}).get('name')
|
(schema.get('function') or {}).get('name')
|
||||||
@@ -1688,6 +1693,51 @@ def expand_concatenated_write_calls(calls, *, max_calls=16):
|
|||||||
if len(items) > max_calls:
|
if len(items) > max_calls:
|
||||||
raise ValueError('too many concatenated write calls')
|
raise ValueError('too many concatenated write calls')
|
||||||
except (TypeError, ValueError, json.JSONDecodeError):
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
# Qwen's native parser can leave one complete, grounded writer
|
||||||
|
# object followed by textual <tool_call> blocks in the same
|
||||||
|
# argument. Execute only that leading writer. The trailing calls
|
||||||
|
# may depend on reads that have not run yet, so they stay inert and
|
||||||
|
# the next model round can propose them normally with fresh
|
||||||
|
# evidence.
|
||||||
|
try:
|
||||||
|
leading, end = decoder.raw_decode(raw)
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
leading, end = None, 0
|
||||||
|
tail = raw[end:].strip() if end else ''
|
||||||
|
markup_openers = tail.count('<tool_call>')
|
||||||
|
markup_closers = tail.count('</tool_call>')
|
||||||
|
bounded_markup = (
|
||||||
|
tail.startswith('<tool_call>')
|
||||||
|
and 1 <= markup_openers <= 32
|
||||||
|
# A provider may truncate the last textual call at its output
|
||||||
|
# limit. It remains inert; only the complete leading JSON
|
||||||
|
# writer is recovered.
|
||||||
|
and markup_closers in {markup_openers, markup_openers - 1}
|
||||||
|
and not strip_tool_blocks(
|
||||||
|
tail,
|
||||||
|
skip_fenced=True,
|
||||||
|
additional_tool_names=('bash', 'python', 'read_file', 'write_file'),
|
||||||
|
).strip()
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
bounded_markup
|
||||||
|
and isinstance(leading, dict)
|
||||||
|
and set(leading) == {'path', 'content'}
|
||||||
|
and isinstance(leading['path'], str)
|
||||||
|
and bool(leading['path'].strip())
|
||||||
|
and isinstance(leading['content'], str)
|
||||||
|
):
|
||||||
|
base_id = str(call.get('id') or 'call_write')
|
||||||
|
expanded.append({
|
||||||
|
'id': f'{base_id}_0',
|
||||||
|
'type': 'function',
|
||||||
|
'function': {
|
||||||
|
'name': function.get('name', 'write_file'),
|
||||||
|
'arguments': json.dumps(leading, ensure_ascii=False),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
recovered_count += 1
|
||||||
|
continue
|
||||||
expanded.append(call)
|
expanded.append(call)
|
||||||
continue
|
continue
|
||||||
if not (2 <= len(items) <= max_calls) or not all(
|
if not (2 <= len(items) <= max_calls) or not all(
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ def test_required_binary_artifact_forces_python_instead_of_text_writer():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_required_directory_artifact_forces_writer_with_descendant_schema():
|
def test_required_directory_artifact_allows_any_offered_mutation_tool():
|
||||||
offered = [
|
offered = [
|
||||||
{'type': 'function', 'function': {'name': 'bash'}},
|
{'type': 'function', 'function': {'name': 'bash'}},
|
||||||
{'type': 'function', 'function': {'name': 'write_file'}},
|
{'type': 'function', 'function': {'name': 'write_file'}},
|
||||||
@@ -692,7 +692,7 @@ def test_required_directory_artifact_forces_writer_with_descendant_schema():
|
|||||||
|
|
||||||
assert required_artifact_completion_tool_choice(
|
assert required_artifact_completion_tool_choice(
|
||||||
('/tmp_workspace/results',), offered,
|
('/tmp_workspace/results',), offered,
|
||||||
) == {'type': 'function', 'function': {'name': 'write_file'}}
|
) == 'required'
|
||||||
|
|
||||||
|
|
||||||
def test_action_promise_response_rejects_future_work_but_not_real_answers():
|
def test_action_promise_response_rejects_future_work_but_not_real_answers():
|
||||||
@@ -1035,6 +1035,38 @@ def test_concatenated_writer_recovery_fails_closed_for_ambiguous_or_large_batche
|
|||||||
assert expand_concatenated_write_calls([too_large]) == ([too_large], 0)
|
assert expand_concatenated_write_calls([too_large]) == ([too_large], 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixed_writer_argument_recovers_only_grounded_leading_json_write():
|
||||||
|
from src.clean_agent_preview import expand_concatenated_write_calls
|
||||||
|
|
||||||
|
first = {'path': '/workspace/results/1.tex', 'content': 'grounded table'}
|
||||||
|
trailing = '''
|
||||||
|
<tool_call>
|
||||||
|
<function=bash>
|
||||||
|
<parameter=command>sed -n '2,3p' /workspace/paper.tex</parameter>
|
||||||
|
</function>
|
||||||
|
</tool_call>
|
||||||
|
<tool_call>
|
||||||
|
<function=write_file>
|
||||||
|
<parameter=path>/workspace/results/2.tex</parameter>
|
||||||
|
<parameter=content>ungrounded table</parameter>
|
||||||
|
</function>
|
||||||
|
</tool_call>
|
||||||
|
'''
|
||||||
|
source = {
|
||||||
|
'id': 'mixed', 'type': 'function', 'function': {
|
||||||
|
'name': 'write_file',
|
||||||
|
'arguments': json.dumps(first) + trailing,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
recovered, count = expand_concatenated_write_calls([source])
|
||||||
|
|
||||||
|
assert count == 1
|
||||||
|
assert len(recovered) == 1
|
||||||
|
assert recovered[0]['id'] == 'mixed_0'
|
||||||
|
assert json.loads(recovered[0]['function']['arguments']) == first
|
||||||
|
|
||||||
|
|
||||||
def test_malformed_writer_handoff_uses_descendant_file_not_required_directory():
|
def test_malformed_writer_handoff_uses_descendant_file_not_required_directory():
|
||||||
from src.clean_agent_preview import malformed_write_handoff_target
|
from src.clean_agent_preview import malformed_write_handoff_target
|
||||||
|
|
||||||
@@ -5527,7 +5559,7 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon
|
|||||||
]
|
]
|
||||||
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy())
|
||||||
raw = [chunk async for chunk in stream_preview(
|
raw = [chunk async for chunk in stream_preview(
|
||||||
endpoint_url="http://test", model="deepseek-flash",
|
endpoint_url="http://test", model="test",
|
||||||
messages=[{"role": "user", "content": "Research and create the requested output."}],
|
messages=[{"role": "user", "content": "Research and create the requested output."}],
|
||||||
headers={}, turn_contract=contract, session_id="test", owner="test",
|
headers={}, turn_contract=contract, session_id="test", owner="test",
|
||||||
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace",
|
||||||
@@ -5562,16 +5594,16 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon
|
|||||||
)
|
)
|
||||||
assert "write_file" in request_contract["offered_tools"]
|
assert "write_file" in request_contract["offered_tools"]
|
||||||
assert "web_search" not in request_contract["offered_tools"]
|
assert "web_search" not in request_contract["offered_tools"]
|
||||||
assert request_contract["tool_choice"] is None
|
assert request_contract["tool_choice"] == "required"
|
||||||
reserved_names = [tool["function"]["name"] for tool in requests[12]["tools"]]
|
reserved_names = [tool["function"]["name"] for tool in requests[12]["tools"]]
|
||||||
assert reserved_names == ["write_file"]
|
assert reserved_names == ["bash", "python", "write_file"]
|
||||||
assert "tool_choice" not in requests[12]
|
assert requests[12]["tool_choice"] == "required"
|
||||||
directory_path = requests[12]["tools"][0]["function"]["parameters"]["properties"]["path"]
|
directory_path = requests[12]["tools"][2]["function"]["parameters"]["properties"]["path"]
|
||||||
assert re.search(directory_path["pattern"], "/tmp_workspace/results/out.md")
|
assert re.search(directory_path["pattern"], "/tmp_workspace/results/out.md")
|
||||||
assert not re.search(directory_path["pattern"], "/tmp_workspace/results")
|
assert not re.search(directory_path["pattern"], "/tmp_workspace/results")
|
||||||
assert "create one or more files inside" in requests[12]["messages"][-1]["content"]
|
assert "create one or more files inside" in requests[12]["messages"][-1]["content"]
|
||||||
assert "Do not pass the directory itself as a file path" in requests[12]["messages"][-1]["content"]
|
assert "Do not pass the directory itself as a file path" in requests[12]["messages"][-1]["content"]
|
||||||
assert "bash" not in executed
|
assert "bash" in executed
|
||||||
assert any(
|
assert any(
|
||||||
event.get("type") == "tool_output" and event.get("tool") == "write_file"
|
event.get("type") == "tool_output" and event.get("tool") == "write_file"
|
||||||
and not event.get("error") for event in events
|
and not event.get("error") for event in events
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
def test_original_harness_version_is_canonical_semver() -> None:
|
def test_original_harness_version_is_canonical_semver() -> None:
|
||||||
version = (ROOT / "HARNESS_VERSION").read_text(encoding="utf-8").strip()
|
version = (ROOT / "HARNESS_VERSION").read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
assert version == "0.20.12"
|
assert version == "0.20.13"
|
||||||
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user