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
7 changes: 7 additions & 0 deletions dana/core/agent/star_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,16 +304,23 @@ def set_session_id(self, session_id: str, reload_timeline: bool = False) -> None
if not reload_timeline:
# Pure relabel — caller owns the timeline; do not flush/rebuild.
self._session_id = session_id
self._invalidate_system_prompt_cache()
return

timeline = getattr(self, "_timeline", None)
if timeline is not None and getattr(timeline, "_repository", None) is not None and timeline.timeline:
timeline.save(self._session_id)

self._session_id = session_id
self._invalidate_system_prompt_cache()
self._timeline = self._build_timeline()
self._timeline.rehydrate()

def _invalidate_system_prompt_cache(self) -> None:
runtime = getattr(self, "_runtime", None)
if runtime is not None and hasattr(runtime, "invalidate_system_prompt_cache"):
runtime.invalidate_system_prompt_cache()

def resume(self, session_id: str) -> None:
"""Resume a persisted session by id, reloading its timeline from disk.

Expand Down
14 changes: 10 additions & 4 deletions dana/core/prompt/environment_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,19 @@ def git_recent_commits(self) -> str:

@property
def scratchpad_directory(self) -> str:
# Deferred to avoid circular import at module level
from dana.core.agent.tool_result_dump import resolve_session_folder_for_agent

session_folder = resolve_session_folder_for_agent(self._agent)
if session_folder is not None:
tmp_path = session_folder / "scratchpad"
tmp_path.mkdir(parents=True, exist_ok=True)
return str(tmp_path.absolute())

# Deferred to avoid circular import at module level.
from dana.config.storage_config import FileStorageConfig

workspace_folder = Path(FileStorageConfig().workspace_folder)

relative_prompt_path = Path(self._relative_path)
_session_id = getattr(self._agent, "_session_id", str(uuid4()))
tmp_path = workspace_folder / relative_prompt_path.parent / "tmp" / _session_id / "scratchpad"
tmp_path = workspace_folder / str(self._agent.object_id) / "sessions" / _session_id / "scratchpad"
tmp_path.mkdir(parents=True, exist_ok=True)
return str(tmp_path.absolute())
4 changes: 4 additions & 0 deletions dana/core/runtime/codec/codec_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ def _build_system_prompt(self, agent: STARAgent) -> str:
prompt_api = self._get_prompt_api(agent)
return prompt_api.system_prompt

def invalidate_system_prompt_cache(self) -> None:
if self._prompt_api is not None:
self._prompt_api._system_prompt = None

def call_llm(
self,
messages: list[LLMMessage],
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/core/star/test_set_session_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
from __future__ import annotations

from datetime import datetime
from pathlib import Path
from unittest.mock import Mock

import pytest

from dana.config.storage_config import FileStorageConfig
from dana.core.agent.star_agent import STARAgent
from dana.core.prompt.environment_info import EnvironmentInfo
from dana.core.timeline.compressed_timeline import CompressedTimeline
from dana.core.timeline.timeline import TimelineEntry, TimelineEntryType
from dana.repositories.local_file_repository import LocalTimelineRepository
Expand Down Expand Up @@ -146,6 +149,42 @@ def test_same_session_id_is_fast_path(self, tmp_path):
assert agent._timeline is original_timeline
assert [e.content for e in agent._timeline.timeline] == ["untouched"]

def test_session_scratchpad_lives_under_session_folder(self, tmp_path):
agent = _make_agent(tmp_path, session_id="A")

scratchpad = Path(EnvironmentInfo(agent, "NativeToolsCodec/agent-1/prompts").scratchpad_directory)

assert scratchpad == tmp_path / "agent-1" / "sessions" / "A" / "scratchpad"
assert scratchpad.is_dir()

def test_relabel_invalidates_system_prompt_cache(self, tmp_path):
agent = _make_agent(tmp_path, session_id="A")
agent._runtime.invalidate_system_prompt_cache = Mock()

agent.set_session_id("B", reload_timeline=False)

agent._runtime.invalidate_system_prompt_cache.assert_called_once_with()

def test_reload_invalidates_system_prompt_cache(self, tmp_path):
agent = _make_agent(tmp_path, session_id="A")
agent._runtime.invalidate_system_prompt_cache = Mock()

agent.set_session_id("B", reload_timeline=True)

agent._runtime.invalidate_system_prompt_cache.assert_called_once_with()

def test_cached_system_prompt_rerenders_after_session_change(self, tmp_path):
agent = _make_agent(tmp_path, session_id="A")
prompt_api = agent._runtime._get_prompt_api(agent)
prompt_api.load = lambda: "scratch={{scratchpad_directory}}"

first_prompt = agent._runtime._build_system_prompt(agent)
agent.set_session_id("B", reload_timeline=False)
second_prompt = agent._runtime._build_system_prompt(agent)

assert str(tmp_path / "agent-1" / "sessions" / "A" / "scratchpad") in first_prompt
assert str(tmp_path / "agent-1" / "sessions" / "B" / "scratchpad") in second_prompt


class TestRehydrate:
"""CompressedTimeline.rehydrate guards."""
Expand Down
Loading