176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from agentos.background import BackgroundExecutionManager
|
|
from agentos.cli import _handle_event
|
|
from agentos.config import RuntimeConfig
|
|
from agentos.model import DeterministicModelClient
|
|
from agentos.runtime import AgentRuntime
|
|
from agentos.types import ActionPlan, ExecutionOption, InputEvent, SandboxResult, utc_now
|
|
|
|
|
|
class FakeSandbox:
|
|
def run(self, **kwargs: Any) -> SandboxResult:
|
|
workspace = Path(kwargs["workspace"])
|
|
(workspace / "result.txt").write_text("done\n", encoding="utf-8")
|
|
now = utc_now()
|
|
return SandboxResult(
|
|
instance_id=kwargs["instance_id"],
|
|
linux_uid=kwargs["linux_uid"],
|
|
container_name="fake-container",
|
|
exit_code=0,
|
|
stdout="",
|
|
stderr="",
|
|
started_at=now,
|
|
finished_at=now,
|
|
)
|
|
|
|
|
|
class ConcurrentFakeSandbox(FakeSandbox):
|
|
def __init__(self) -> None:
|
|
self._lock = threading.Lock()
|
|
self.active = 0
|
|
self.max_active = 0
|
|
|
|
def run(self, **kwargs: Any) -> SandboxResult:
|
|
with self._lock:
|
|
self.active += 1
|
|
self.max_active = max(self.max_active, self.active)
|
|
try:
|
|
time.sleep(0.05)
|
|
return super().run(**kwargs)
|
|
finally:
|
|
with self._lock:
|
|
self.active -= 1
|
|
|
|
|
|
class FourOptionModel(DeterministicModelClient):
|
|
def propose_options(self, user_input, context):
|
|
del user_input, context
|
|
return [
|
|
ExecutionOption(
|
|
option_id=str(index),
|
|
title=f"方案 {index}",
|
|
approach=f"执行方案 {index}",
|
|
risk="high" if index == 1 else "low",
|
|
expected_changes=[f"option-{index}.txt"],
|
|
)
|
|
for index in range(1, 5)
|
|
]
|
|
|
|
def build_plan(self, user_input, option, context):
|
|
del user_input, context
|
|
filename = f"option-{option.option_id}.txt"
|
|
return ActionPlan(
|
|
summary=f"生成 {filename}",
|
|
script=f"touch {filename}",
|
|
success_criteria=[f"{filename} 存在"],
|
|
memory_notes=[],
|
|
)
|
|
|
|
|
|
class CliExecutionRoutingTests(unittest.TestCase):
|
|
def _runtime(self, directory: str) -> AgentRuntime:
|
|
config = RuntimeConfig(
|
|
state_dir=Path(directory),
|
|
model_command=("model",),
|
|
uid_range_start=282_000,
|
|
uid_range_size=16,
|
|
)
|
|
runtime = AgentRuntime(config, DeterministicModelClient())
|
|
runtime.sandbox = FakeSandbox() # type: ignore[assignment]
|
|
runtime.initialize()
|
|
return runtime
|
|
|
|
def test_major_operation_executes_all_options(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory, redirect_stdout(io.StringIO()):
|
|
runtime = self._runtime(directory)
|
|
result = _handle_event(
|
|
runtime,
|
|
InputEvent(event_id="major-input", text="执行重大架构迁移"),
|
|
choice=1,
|
|
decision="discard",
|
|
)
|
|
|
|
states = {row["state"] for row in runtime.memory.list_worldlines()}
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(states, {"discarded", "kept"})
|
|
self.assertEqual(len(runtime.memory.list_worldlines()), 2)
|
|
self.assertEqual(runtime.memory.list_memories(), [])
|
|
|
|
def test_routine_operation_executes_only_selected_option(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory, redirect_stdout(io.StringIO()):
|
|
runtime = self._runtime(directory)
|
|
result = _handle_event(
|
|
runtime,
|
|
InputEvent(event_id="routine-input", text="记录一段文本"),
|
|
choice=1,
|
|
decision="discard",
|
|
)
|
|
|
|
rows = runtime.memory.list_worldlines()
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(len(rows), 1)
|
|
self.assertEqual(rows[0]["state"], "discarded")
|
|
|
|
def test_major_operation_forks_every_option_and_runs_alternatives_in_parallel(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
config = RuntimeConfig(
|
|
state_dir=Path(directory),
|
|
model_command=("model",),
|
|
uid_range_start=282_100,
|
|
uid_range_size=16,
|
|
background_workers=4,
|
|
)
|
|
runtime = AgentRuntime(config, FourOptionModel())
|
|
sandbox = ConcurrentFakeSandbox()
|
|
runtime.sandbox = sandbox # type: ignore[assignment]
|
|
runtime.initialize()
|
|
notifications: list[str] = []
|
|
background = BackgroundExecutionManager(
|
|
runtime,
|
|
max_workers=4,
|
|
notifier=notifications.append,
|
|
)
|
|
output = io.StringIO()
|
|
try:
|
|
with redirect_stdout(output):
|
|
result = _handle_event(
|
|
runtime,
|
|
InputEvent(event_id="four-options", text="执行重大架构迁移"),
|
|
choice=3,
|
|
decision="discard",
|
|
background=background,
|
|
)
|
|
background.wait()
|
|
finally:
|
|
background.shutdown()
|
|
|
|
rows = runtime.memory.list_worldlines()
|
|
states_by_option = {
|
|
json.loads(row["option_json"])["option_id"]: row["state"] for row in rows
|
|
}
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(len(rows), 4)
|
|
self.assertEqual(states_by_option["3"], "discarded")
|
|
self.assertEqual(
|
|
{states_by_option[key] for key in ("1", "2", "4")},
|
|
{"kept"},
|
|
)
|
|
self.assertGreaterEqual(sandbox.max_active, 2)
|
|
self.assertEqual(len(notifications), 3)
|
|
self.assertEqual(output.getvalue().count("\n世界线:"), 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|