109 lines
4.0 KiB
Python
109 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import shutil
|
|
import unittest
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from opc.core.config import OPCConfig
|
|
from opc.core.models import Task, TaskStatus
|
|
from opc.database.store import OPCStore
|
|
from opc.layer3_agent.runtime_v2.runtime import NativeRuntimeV2
|
|
from opc.layer4_tools.registry import ToolRegistry
|
|
from opc.layer5_memory.memory_manager import MemoryManager
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _workspace_tempdir() -> Path:
|
|
base = Path.cwd() / ".tmp-test" / f"durable-memory-{uuid.uuid4().hex}"
|
|
base.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
yield base
|
|
finally:
|
|
shutil.rmtree(base, ignore_errors=True)
|
|
|
|
|
|
class _StubExtractionLLM:
|
|
def __init__(self) -> None:
|
|
self.config = type("Cfg", (), {"max_tokens": 4096})()
|
|
|
|
def prepare_user_message_content(self, content: str, attachment_refs=None):
|
|
_ = attachment_refs
|
|
return content
|
|
|
|
def get_tool_definitions(self, tools):
|
|
return tools
|
|
|
|
def is_context_overflow_error(self, error: Exception) -> bool:
|
|
_ = error
|
|
return False
|
|
|
|
async def chat_stream(self, messages, tools=None):
|
|
_ = (messages, tools)
|
|
yield type("Evt", (), {"event_type": "message_start", "payload": {}, "model": "stub"})()
|
|
yield type("Evt", (), {"event_type": "assistant_delta", "payload": {"text": "Repository convention captured."}, "model": "stub"})()
|
|
yield type("Evt", (), {"event_type": "message_stop", "payload": {"finish_reason": "stop"}, "model": "stub"})()
|
|
|
|
async def simple_chat(self, prompt: str, system: str | None = None, task_type: str | None = None) -> str:
|
|
_ = (prompt, task_type)
|
|
if system and "extracting durable reusable memory" in system:
|
|
return json.dumps(
|
|
{
|
|
"project_memory": [
|
|
{
|
|
"title": "Search Conventions",
|
|
"content": "- Prefer rg for repo-wide search before editing files.",
|
|
}
|
|
],
|
|
"global_memory": [],
|
|
"reason": "Stable repo runtime preference.",
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
raise AssertionError(f"Unexpected system prompt: {system}")
|
|
|
|
|
|
class RuntimeDurableMemoryExtractionTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_runtime_does_not_auto_extract_durable_memory_on_completed_turn(self) -> None:
|
|
with _workspace_tempdir() as root:
|
|
store = OPCStore(root / "tasks.db")
|
|
await store.initialize()
|
|
memory = MemoryManager(root, "proj1", store=store)
|
|
config = OPCConfig()
|
|
config.system.native_runtime.durable_memory_extract_min_messages = 2
|
|
runtime = NativeRuntimeV2(
|
|
llm=_StubExtractionLLM(),
|
|
tool_registry=ToolRegistry(),
|
|
memory_manager=memory,
|
|
config=config,
|
|
)
|
|
|
|
task = Task(
|
|
id="task-durable",
|
|
title="Capture convention",
|
|
description="Inspect the repository and remember reusable conventions.",
|
|
session_id="sess-durable",
|
|
project_id="proj1",
|
|
assigned_to="executor",
|
|
)
|
|
|
|
result = await runtime.run(
|
|
system_prompt="You are a test runtime.",
|
|
user_message="Please remember that we prefer rg for search.",
|
|
task=task,
|
|
)
|
|
|
|
self.assertEqual(result.status, TaskStatus.DONE)
|
|
self.assertNotIn("durable_memory_extraction", result.artifacts)
|
|
project_memory = memory.load_project_memory_markdown("proj1")
|
|
self.assertEqual(project_memory, "")
|
|
session = await store.get_session("sess-durable")
|
|
self.assertFalse(str(session.metadata.get("durable_memory_cursor_message_id", "")).strip())
|
|
await store.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|