mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-05 12:42:20 +01:00
constrain directory completion to child files
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
0.20.7
|
0.20.8
|
||||||
|
|||||||
+22
-10
@@ -1525,14 +1525,31 @@ def artifact_completion_tool_schemas(offered_schemas, required_artifacts):
|
|||||||
This applies only after native execution enters its reserved artifact-write
|
This applies only after native execution enters its reserved artifact-write
|
||||||
phase. A JSON-Schema ``const`` gives the provider the exact destination
|
phase. A JSON-Schema ``const`` gives the provider the exact destination
|
||||||
instead of relying on it to recover the path from a long conversation.
|
instead of relying on it to recover the path from a long conversation.
|
||||||
Multiple outputs and directory targets stay unconstrained because choosing
|
Multiple outputs stay unconstrained. A sole directory target constrains the
|
||||||
one of those paths requires model intent.
|
writer to a non-empty descendant path while leaving the filename to the
|
||||||
|
model.
|
||||||
"""
|
"""
|
||||||
targets = [str(path or '').strip().rstrip('/') for path in required_artifacts]
|
targets = [str(path or '').strip().rstrip('/') for path in required_artifacts]
|
||||||
targets = [path for path in targets if path and Path(path).suffix]
|
targets = [path for path in targets if path]
|
||||||
if len(targets) != 1 or len(tuple(required_artifacts or ())) != 1:
|
if len(targets) != 1 or len(tuple(required_artifacts or ())) != 1:
|
||||||
return offered_schemas
|
return offered_schemas
|
||||||
target = targets[0]
|
target = targets[0]
|
||||||
|
if not Path(target).suffix:
|
||||||
|
bound = copy.deepcopy(offered_schemas)
|
||||||
|
for schema in bound:
|
||||||
|
function = schema.get('function') or {}
|
||||||
|
if canonical(function.get('name')) != 'write_file':
|
||||||
|
continue
|
||||||
|
properties = (function.get('parameters') or {}).get('properties') or {}
|
||||||
|
path_schema = properties.get('path')
|
||||||
|
if not isinstance(path_schema, dict):
|
||||||
|
continue
|
||||||
|
path_schema['pattern'] = '^' + re.escape(target + '/') + '.+'
|
||||||
|
path_schema['description'] = (
|
||||||
|
f'Write a new file inside the required directory {target}; '
|
||||||
|
'do not use the directory path itself.'
|
||||||
|
)
|
||||||
|
return bound
|
||||||
if Path(target).suffix.lower() in _NON_TEXT_ARTIFACT_SUFFIXES:
|
if Path(target).suffix.lower() in _NON_TEXT_ARTIFACT_SUFFIXES:
|
||||||
# ``write_file`` deliberately accepts UTF-8 text only. Keeping it in
|
# ``write_file`` deliberately accepts UTF-8 text only. Keeping it in
|
||||||
# a binary artifact completion round lets a forced writer choice trap
|
# a binary artifact completion round lets a forced writer choice trap
|
||||||
@@ -1571,14 +1588,9 @@ def artifact_completion_tool_schemas(offered_schemas, required_artifacts):
|
|||||||
|
|
||||||
|
|
||||||
def required_artifact_completion_tool_choice(required_artifacts, offered_schemas):
|
def required_artifact_completion_tool_choice(required_artifacts, offered_schemas):
|
||||||
"""Choose a writer only when the contract names an exact output file.
|
"""Force the compatible writer during reserved artifact completion."""
|
||||||
|
|
||||||
A directory contract needs the model to choose one or more child filenames.
|
|
||||||
Forcing ``write_file`` there encourages an impossible write to the directory
|
|
||||||
path itself, so leave tool choice unconstrained for that case.
|
|
||||||
"""
|
|
||||||
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 or any(not Path(target).suffix for target in targets):
|
if not targets:
|
||||||
return None
|
return None
|
||||||
offered = {
|
offered = {
|
||||||
canonical((schema.get('function') or {}).get('name')):
|
canonical((schema.get('function') or {}).get('name')):
|
||||||
|
|||||||
@@ -624,7 +624,7 @@ def test_artifact_completion_schema_binds_single_required_file_without_mutating_
|
|||||||
assert 'const' not in source[0]['function']['parameters']['properties']['path']
|
assert 'const' not in source[0]['function']['parameters']['properties']['path']
|
||||||
|
|
||||||
|
|
||||||
def test_artifact_completion_schema_does_not_bind_directory_or_multiple_outputs():
|
def test_artifact_completion_schema_binds_directory_descendant_but_not_multiple_outputs():
|
||||||
source = [{
|
source = [{
|
||||||
'type': 'function',
|
'type': 'function',
|
||||||
'function': {
|
'function': {
|
||||||
@@ -636,9 +636,14 @@ def test_artifact_completion_schema_does_not_bind_directory_or_multiple_outputs(
|
|||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
assert artifact_completion_tool_schemas(
|
directory_bound = artifact_completion_tool_schemas(
|
||||||
source, ('/workspace/results/',),
|
source, ('/workspace/results/',),
|
||||||
) == source
|
)
|
||||||
|
directory_path = directory_bound[0]['function']['parameters']['properties']['path']
|
||||||
|
assert re.search(directory_path['pattern'], '/workspace/results/output.md')
|
||||||
|
assert not re.search(directory_path['pattern'], '/workspace/results')
|
||||||
|
assert 'inside the required directory' in directory_path['description']
|
||||||
|
assert 'pattern' not in source[0]['function']['parameters']['properties']['path']
|
||||||
assert artifact_completion_tool_schemas(
|
assert artifact_completion_tool_schemas(
|
||||||
source, ('/workspace/a.txt', '/workspace/b.txt'),
|
source, ('/workspace/a.txt', '/workspace/b.txt'),
|
||||||
) == source
|
) == source
|
||||||
@@ -679,7 +684,7 @@ def test_required_binary_artifact_forces_python_instead_of_text_writer():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_required_directory_artifact_does_not_force_writer_to_directory_path():
|
def test_required_directory_artifact_forces_writer_with_descendant_schema():
|
||||||
offered = [
|
offered = [
|
||||||
{'type': 'function', 'function': {'name': 'bash'}},
|
{'type': 'function', 'function': {'name': 'bash'}},
|
||||||
{'type': 'function', 'function': {'name': 'write_file'}},
|
{'type': 'function', 'function': {'name': 'write_file'}},
|
||||||
@@ -687,7 +692,7 @@ def test_required_directory_artifact_does_not_force_writer_to_directory_path():
|
|||||||
|
|
||||||
assert required_artifact_completion_tool_choice(
|
assert required_artifact_completion_tool_choice(
|
||||||
('/tmp_workspace/results',), offered,
|
('/tmp_workspace/results',), offered,
|
||||||
) is None
|
) == {'type': 'function', 'function': {'name': 'write_file'}}
|
||||||
|
|
||||||
|
|
||||||
def test_action_promise_response_rejects_future_work_but_not_real_answers():
|
def test_action_promise_response_rejects_future_work_but_not_real_answers():
|
||||||
@@ -5328,11 +5333,14 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon
|
|||||||
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"] is None
|
||||||
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 == ["bash", "python", "write_file"]
|
assert reserved_names == ["write_file"]
|
||||||
assert "tool_choice" not in requests[12]
|
assert "tool_choice" not in requests[12]
|
||||||
|
directory_path = requests[12]["tools"][0]["function"]["parameters"]["properties"]["path"]
|
||||||
|
assert re.search(directory_path["pattern"], "/tmp_workspace/results/out.md")
|
||||||
|
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" in executed
|
assert "bash" not 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.7"
|
assert version == "0.20.8"
|
||||||
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
assert re.fullmatch(r"\d+\.\d+\.\d+", version)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user