Add short artifact handle (A1) so the model can edit by reference

Give each produced artifact a short virtual handle - A1, A2, ... - the n-th
artifact in the conversation or workflow run (case-insensitive, not stored). The
tools return it in their results, and the edit, rewrite, and input parameters
accept either the handle or the full id. A handle resolves only within the
caller's own conversation or run, and the resolved id is still checked against
that parent before any read, so it cannot reach another tenant's artifact. This
fixes the model creating a duplicate instead of a new version when asked to
edit an artifact.
This commit is contained in:
Alex committed 2026-06-24 15:35:06 +01:00
1 parent a06b348c34
commit e7f765b628
13 files changed
+655 -61

No files matched your search

+83
View File
@@ -7,6 +7,8 @@ run in the fast unit suite. The end-to-end persistence path is covered by
from __future__ import annotations
import uuid
from application.agents.tools.code_executor import (
CodeExecutorTool,
_infer_mime,
@@ -266,3 +268,84 @@ def test_session_kept_alive_on_positive_ttl(monkeypatch):
monkeypatch, manager, code="print(1)", ttl=30, capture_artifacts=False
)
assert manager.closed == []
# ---------------------------------------------------------------------------
# Input materialization: short-ref + uuid resolution (no live sandbox/DB)
# ---------------------------------------------------------------------------
_ART_ID = str(uuid.uuid4())
class _InputManager:
"""Records files staged into the workspace by _materialize_inputs."""
def __init__(self) -> None:
self.put_files: dict = {}
def put_file(self, session_id, dest_path, data):
self.put_files[dest_path] = data
def _patch_input_repo(monkeypatch, *, found_position: bool, conv: str):
"""Patch db_readonly + ArtifactsRepository so a ref/uuid resolves only within ``conv``."""
from application.agents.tools import code_executor as ce
class _Repo:
def __init__(self, conn):
pass
def artifact_id_at_position(self, n, *, conversation_id=None, workflow_run_id=None):
if not found_position or n != 1 or conversation_id != conv:
return None
return _ART_ID
def get_artifact_in_parent(self, artifact_id, *, conversation_id=None, workflow_run_id=None):
if conversation_id != conv:
return None
return {"id": artifact_id, "current_version": 1, "title": "seed.csv"}
def get_version(self, artifact_id, version):
return {"filename": "seed.csv", "storage_path": f"inputs/u/artifacts/{artifact_id}/v1/seed.csv"}
class _Conn:
def __enter__(self):
return object()
def __exit__(self, *exc):
return False
class _Storage:
def get_file(self, path):
import io
return io.BytesIO(b"col\n1\n")
monkeypatch.setattr(ce, "db_readonly", lambda: _Conn())
monkeypatch.setattr(ce, "ArtifactsRepository", _Repo)
monkeypatch.setattr(ce.StorageCreator, "get_storage", staticmethod(lambda: _Storage()))
def test_materialize_inputs_accepts_short_ref(monkeypatch):
_patch_input_repo(monkeypatch, found_position=True, conv="conv-1")
manager = _InputManager()
out = _tool()._materialize_inputs(manager, "conv-1", ["A1"])
assert "error" not in out
assert out["loaded"] == ["inputs/seed.csv"]
assert manager.put_files["inputs/seed.csv"] == b"col\n1\n"
def test_materialize_inputs_accepts_uuid(monkeypatch):
_patch_input_repo(monkeypatch, found_position=False, conv="conv-1")
manager = _InputManager()
out = _tool()._materialize_inputs(manager, "conv-1", [_ART_ID])
assert "error" not in out
assert out["loaded"] == ["inputs/seed.csv"]
def test_materialize_inputs_out_of_range_ref_is_clean_error(monkeypatch):
_patch_input_repo(monkeypatch, found_position=True, conv="conv-1")
manager = _InputManager()
out = _tool()._materialize_inputs(manager, "conv-1", ["A2"])
assert "A2" in out["error"]
assert "not found in this conversation/run" in out["error"]
assert manager.put_files == {}