diff --git a/solstone/think/cogitate_contract.py b/solstone/think/cogitate_contract.py index f2826a9c5..914ca60bc 100644 --- a/solstone/think/cogitate_contract.py +++ b/solstone/think/cogitate_contract.py @@ -42,6 +42,9 @@ COGITATE_ACCESS_TIERS = ( "synthesis", "diagnostic", ) +TALENT_ACCESS_TIERS = tuple( + tier for tier in COGITATE_ACCESS_TIERS if tier != "diagnostic" +) # `code-agent` is a documented FUTURE tier — NOT part of the current cogitate # runtime (it needs write access, broad tools, and a repo cwd, deliberately out of @@ -110,6 +113,7 @@ __all__ = [ "COGITATE_ACCESS_TIERS", "COGITATE_READ_TOOL_NAMES", "FUTURE_ACCESS_TIERS", + "TALENT_ACCESS_TIERS", "TALENT_FINALIZATION_MODES", "capabilities_for_access_tier", "expects_emit_final", diff --git a/solstone/think/providers/openhands.py b/solstone/think/providers/openhands.py index 24ee320dd..3e056d1a2 100644 --- a/solstone/think/providers/openhands.py +++ b/solstone/think/providers/openhands.py @@ -18,6 +18,7 @@ import os import re import shutil import sys +import tempfile import threading import traceback import uuid @@ -1604,6 +1605,7 @@ async def run_cogitate( llm: Any | None = None usage_start: dict[str, int] | None = None + persistence_tmpdir: tempfile.TemporaryDirectory[str] | None = None try: with _openhands_import_policy(): from openhands.sdk import Conversation @@ -1678,8 +1680,14 @@ async def run_cogitate( system_prompt=system_instruction, ) - persistence_dir = journal / ".cache" / "cogitate-history" / session_id - persistence_dir.mkdir(parents=True, exist_ok=True) + if diagnostic: + persistence_tmpdir = tempfile.TemporaryDirectory( + prefix="solstone-cogitate-diagnostic-" + ) + persistence_dir = Path(persistence_tmpdir.name) + else: + persistence_dir = journal / ".cache" / "cogitate-history" / session_id + persistence_dir.mkdir(parents=True, exist_ok=True) translator = _OpenHandsTranslator( callback=callback, llm=llm, @@ -1899,6 +1907,9 @@ async def run_cogitate( callback.emit(error_event) setattr(exc, "_evented", True) raise + finally: + if persistence_tmpdir is not None: + persistence_tmpdir.cleanup() def run_generate( diff --git a/solstone/think/talent.py b/solstone/think/talent.py index c870433ac..a53c4eb66 100644 --- a/solstone/think/talent.py +++ b/solstone/think/talent.py @@ -30,7 +30,7 @@ from typing import Any, Callable import frontmatter from jsonschema import Draft202012Validator, SchemaError -from solstone.think.cogitate_contract import COGITATE_ACCESS_TIERS +from solstone.think.cogitate_contract import TALENT_ACCESS_TIERS from solstone.think.facets import get_facets # Import core prompt utilities from solstone.think.prompts @@ -92,11 +92,11 @@ def _validate_access_tier(raw: Any, talent_type: Any, key: str) -> str | None: if talent_type == "cogitate": if raw is None: return "normal" - if raw in COGITATE_ACCESS_TIERS: + if raw in TALENT_ACCESS_TIERS: return raw raise ValueError( f"Prompt '{key}' has invalid 'access_tier' value '{raw}' " - f"(must be one of {COGITATE_ACCESS_TIERS})" + f"(must be one of {TALENT_ACCESS_TIERS})" ) if raw is not None: diff --git a/tests/test_cogitate_contract.py b/tests/test_cogitate_contract.py index 1560285d8..555a6e288 100644 --- a/tests/test_cogitate_contract.py +++ b/tests/test_cogitate_contract.py @@ -12,6 +12,7 @@ from solstone.think.cogitate_contract import ( COGITATE_READ_TOOL_NAMES, COGITATE_RUNTIME_PREAMBLE, FUTURE_ACCESS_TIERS, + TALENT_ACCESS_TIERS, TALENT_FINALIZATION_MODES, expects_emit_final, ) @@ -81,6 +82,12 @@ def test_cogitate_vocabulary_lock(): "grep_search", ) assert FUTURE_ACCESS_TIERS == ("code-agent",) + assert TALENT_ACCESS_TIERS == ( + "normal", + "system-read", + "outbound", + "synthesis", + ) assert TALENT_FINALIZATION_MODES == ("emit_final", "FinishTool", "quiet") assert "repair" not in COGITATE_ACCESS_TIERS assert "repair" not in FUTURE_ACCESS_TIERS diff --git a/tests/test_cogitate_diagnostic.py b/tests/test_cogitate_diagnostic.py index 0388dfc05..f1baa58c4 100644 --- a/tests/test_cogitate_diagnostic.py +++ b/tests/test_cogitate_diagnostic.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio +from pathlib import Path from types import SimpleNamespace from solstone.think.cogitate_contract import ( @@ -94,15 +95,24 @@ def test_diagnostic_run_registers_only_emit_final_and_writes_no_files( _install_emit_final_arun(fake_openhands, "diagnostic ok") config = _run_config(monkeypatch, tmp_path) events: list[dict] = [] + created_temp_dirs: list[Path] = [] - before_files = {path.relative_to(tmp_path) for path in tmp_path.rglob("*")} + real_tempdir = openhands.tempfile.TemporaryDirectory + + def tracking_tempdir(*args, **kwargs): + kwargs.setdefault("dir", tmp_path.parent) + tempdir = real_tempdir(*args, **kwargs) + created_temp_dirs.append(Path(tempdir.name)) + return tempdir + + monkeypatch.setattr(openhands.tempfile, "TemporaryDirectory", tracking_tempdir) + + before_tree = {path.relative_to(tmp_path) for path in tmp_path.rglob("*")} result = asyncio.run(openhands.run_cogitate(config, events.append)) conversation = fake_openhands.Conversation.instances[0] agent_tool_names = {tool.name for tool in conversation.agent.tools} - after_files = { - path.relative_to(tmp_path) for path in tmp_path.rglob("*") if path.is_file() - } + after_tree = {path.relative_to(tmp_path) for path in tmp_path.rglob("*")} assert result == "diagnostic ok" assert agent_tool_names == {"emit_final"} @@ -119,7 +129,9 @@ def test_diagnostic_run_registers_only_emit_final_and_writes_no_files( assert "grep_search" not in conversation.agent.system_prompt assert "through the `sol` tool" not in conversation.agent.system_prompt assert "sol call" not in conversation.agent.system_prompt - assert before_files == set() - assert after_files == set() + assert before_tree == set() + assert after_tree == set() + assert created_temp_dirs + assert not any(path.exists() for path in created_temp_dirs) assert [event["event"] for event in events] == ["finish"] assert events[0]["result"] == "diagnostic ok" diff --git a/tests/test_talent.py b/tests/test_talent.py index fef4f7e4e..f25a4076a 100644 --- a/tests/test_talent.py +++ b/tests/test_talent.py @@ -123,6 +123,14 @@ def test_validate_access_tier_rejects_unknown_tier(): _validate_access_tier("repair", "cogitate", "test-agent") +def test_validate_access_tier_rejects_internal_diagnostic_tier(): + with pytest.raises( + ValueError, + match="Prompt 'test-agent' has invalid 'access_tier' value 'diagnostic'", + ): + _validate_access_tier("diagnostic", "cogitate", "test-agent") + + def test_validate_access_tier_rejects_generate_with_access_tier(): with pytest.raises( ValueError,