litellm/tests/unit/harness/handlers/test_deepagents_handler.py
ishaan-berri 0f6a06a6b9
feat: add litellm.agent() to run claude code, codex, opencode and deep agents through the ai gateway (#43885)
* feat(harness): add litellm/__init__.py

* feat(harness): add litellm/constants.py

* feat(harness): add litellm/harness/__init__.py

* feat(harness): add litellm/harness/adapters/__init__.py

* feat(harness): add litellm/harness/adapters/base.py

* feat(harness): add litellm/harness/adapters/claude_code.py

* feat(harness): add litellm/harness/adapters/codex.py

* feat(harness): add litellm/harness/adapters/opencode.py

* feat(harness): add litellm/harness/endpoint.py

* feat(harness): add litellm/harness/errors.py

* feat(harness): add litellm/harness/options.py

* feat(harness): add litellm/harness/runtime.py

* feat(harness): add litellm/harness/sandbox/__init__.py

* feat(harness): add litellm/harness/sandbox/base.py

* feat(harness): add litellm/harness/sandbox/docker.py

* feat(harness): add litellm/harness/sandbox/local.py

* feat(harness): add litellm/harness/sandbox/snapshot.py

* feat(harness): add litellm/harness/sync.py

* feat(harness): add litellm/harness/types.py

* feat(harness): add litellm/sandbox/__init__.py

* feat(harness): add README.md

* feat(harness): add tests/harness_e2e/__init__.py

* feat(harness): add tests/harness_e2e/conftest.py

* feat(harness): add tests/harness_e2e/test_harness_e2e.py

* feat(harness): add tests/test_litellm/harness/__init__.py

* feat(harness): add tests/test_litellm/harness/adapters/__init__.py

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/claude_code/api_error.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/claude_code/max_turns.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/claude_code/resume_turn.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/claude_code/structured_output.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/claude_code/success_tools.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/codex/reasoning.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/codex/structured_output.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/codex/turn_failed.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/codex/turn1_bash.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/codex/turn2_resume_apply_patch.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/opencode/api_error.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/opencode/endpoint_requests.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/opencode/readonly_denied_bash.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/opencode/turn1_write_read.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/fixtures/opencode/turn2_session_skill.jsonl

* feat(harness): add tests/test_litellm/harness/adapters/test_claude_code.py

* feat(harness): add tests/test_litellm/harness/adapters/test_codex.py

* feat(harness): add tests/test_litellm/harness/adapters/test_opencode.py

* feat(harness): add tests/test_litellm/harness/core_fakes.py

* feat(harness): add tests/test_litellm/harness/sandbox/__init__.py

* feat(harness): add tests/test_litellm/harness/sandbox/test_docker.py

* feat(harness): add tests/test_litellm/harness/sandbox/test_local.py

* feat(harness): add tests/test_litellm/harness/sandbox/test_snapshot.py

* feat(harness): add tests/test_litellm/harness/test_endpoint.py

* feat(harness): add tests/test_litellm/harness/test_init.py

* feat(harness): add tests/test_litellm/harness/test_runtime.py

* feat(harness): add tests/test_litellm/harness/test_sync.py

* feat(harness): add tests/test_litellm/harness/test_types.py

* test(harness): use word recall in stream e2e test

* refactor(harness): update litellm/__init__.py

* refactor(harness): update litellm/constants.py

* refactor(harness): update litellm/harness/__init__.py

* refactor(harness): remove litellm/harness/adapters/__init__.py

* refactor(harness): update litellm/harness/context.py

* refactor(harness): update litellm/harness/endpoint.py

* refactor(harness): update litellm/harness/handlers/__init__.py

* refactor(harness): update litellm/harness/handlers/base.py

* refactor(harness): update litellm/harness/handlers/cli_handler.py

* refactor(harness): update litellm/harness/handlers/deepagents_handler.py

* refactor(harness): update litellm/harness/runtime.py

* refactor(harness): update litellm/harness/sandbox/docker.py

* refactor(harness): update litellm/harness/sandbox/local.py

* refactor(harness): update litellm/harness/sync.py

* refactor(harness): update litellm/harness/types.py

* refactor(harness): update litellm/llms/base_llm/harness/__init__.py

* refactor(harness): update litellm/llms/base_llm/harness/transformation.py

* refactor(harness): update litellm/llms/base_llm/harness/utils.py

* refactor(harness): update litellm/llms/claude_code/__init__.py

* refactor(harness): update litellm/llms/claude_code/harness/__init__.py

* refactor(harness): update litellm/llms/claude_code/harness/transformation.py

* refactor(harness): update litellm/llms/codex/__init__.py

* refactor(harness): update litellm/llms/codex/harness/__init__.py

* refactor(harness): update litellm/llms/codex/harness/transformation.py

* refactor(harness): update litellm/llms/deepagents/__init__.py

* refactor(harness): update litellm/llms/deepagents/harness/__init__.py

* refactor(harness): update litellm/llms/deepagents/harness/sandbox_backend.py

* refactor(harness): update litellm/llms/deepagents/harness/transformation.py

* refactor(harness): update litellm/llms/opencode/__init__.py

* refactor(harness): update litellm/llms/opencode/harness/__init__.py

* refactor(harness): update litellm/llms/opencode/harness/transformation.py

* refactor(harness): update litellm/utils.py

* refactor(harness): update README.md

* refactor(harness): update tests/harness_e2e/conftest.py

* refactor(harness): update tests/harness_e2e/test_harness_e2e.py

* refactor(harness): remove tests/test_litellm/harness/__init__.py

* refactor(harness): remove tests/test_litellm/harness/adapters/__init__.py

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/claude_code/api_error.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/claude_code/max_turns.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/claude_code/resume_turn.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/claude_code/structured_output.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/claude_code/success_tools.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/codex/reasoning.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/codex/structured_output.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/codex/turn_failed.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/codex/turn1_bash.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/codex/turn2_resume_apply_patch.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/opencode/api_error.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/opencode/endpoint_requests.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/opencode/readonly_denied_bash.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/opencode/turn1_write_read.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/fixtures/opencode/turn2_session_skill.jsonl

* refactor(harness): remove tests/test_litellm/harness/adapters/test_claude_code.py

* refactor(harness): remove tests/test_litellm/harness/adapters/test_codex.py

* refactor(harness): remove tests/test_litellm/harness/adapters/test_opencode.py

* refactor(harness): remove tests/test_litellm/harness/core_fakes.py

* refactor(harness): remove tests/test_litellm/harness/sandbox/__init__.py

* refactor(harness): remove tests/test_litellm/harness/sandbox/test_docker.py

* refactor(harness): remove tests/test_litellm/harness/sandbox/test_local.py

* refactor(harness): remove tests/test_litellm/harness/sandbox/test_snapshot.py

* refactor(harness): remove tests/test_litellm/harness/test_endpoint.py

* refactor(harness): remove tests/test_litellm/harness/test_init.py

* refactor(harness): remove tests/test_litellm/harness/test_runtime.py

* refactor(harness): remove tests/test_litellm/harness/test_sync.py

* refactor(harness): remove tests/test_litellm/harness/test_types.py

* refactor(harness): update tests/unit/harness/__init__.py

* refactor(harness): update tests/unit/harness/core_fakes.py

* refactor(harness): update tests/unit/harness/handlers/__init__.py

* refactor(harness): update tests/unit/harness/handlers/test_deepagents_handler.py

* refactor(harness): update tests/unit/harness/sandbox/__init__.py

* refactor(harness): update tests/unit/harness/sandbox/test_docker.py

* refactor(harness): update tests/unit/harness/sandbox/test_local.py

* refactor(harness): update tests/unit/harness/sandbox/test_snapshot.py

* refactor(harness): update tests/unit/harness/test_endpoint.py

* refactor(harness): update tests/unit/harness/test_init.py

* refactor(harness): update tests/unit/harness/test_runtime.py

* refactor(harness): update tests/unit/harness/test_sync.py

* refactor(harness): update tests/unit/harness/test_types.py

* refactor(harness): update tests/unit/llms/claude_code/__init__.py

* refactor(harness): update tests/unit/llms/claude_code/harness/__init__.py

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/api_error.jsonl

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/max_turns.jsonl

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/resume_turn.jsonl

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/structured_output.jsonl

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/success_tools.jsonl

* refactor(harness): update tests/unit/llms/claude_code/harness/test_transformation.py

* refactor(harness): update tests/unit/llms/codex/__init__.py

* refactor(harness): update tests/unit/llms/codex/harness/__init__.py

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/reasoning.jsonl

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/structured_output.jsonl

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/turn1_bash.jsonl

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/turn2_resume_apply_patch.jsonl

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/turn_failed.jsonl

* refactor(harness): update tests/unit/llms/codex/harness/test_transformation.py

* refactor(harness): update tests/unit/llms/deepagents/__init__.py

* refactor(harness): update tests/unit/llms/deepagents/harness/__init__.py

* refactor(harness): update tests/unit/llms/deepagents/harness/test_transformation.py

* refactor(harness): update tests/unit/llms/opencode/__init__.py

* refactor(harness): update tests/unit/llms/opencode/harness/__init__.py

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/api_error.jsonl

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/endpoint_requests.jsonl

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/readonly_denied_bash.jsonl

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/turn1_write_read.jsonl

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/turn2_session_skill.jsonl

* refactor(harness): update tests/unit/llms/opencode/harness/test_transformation.py

* ci: allowlist tests/harness_e2e, which needs live runtimes and a gateway

* fix(harness): bound the turn event queue

* fix(harness): bound the turn event queue with backpressure

* style: sort imports in utils

* ci: exclude agent-harness config folders from provider docs check

* refactor(harness): update tests/harness_e2e/conftest.py

* refactor(harness): update tests/harness_e2e/test_harness_e2e.py

* refactor(harness): update tests/unit/harness/core_fakes.py

* refactor(harness): update tests/unit/harness/handlers/test_deepagents_handler.py

* refactor(harness): update tests/unit/harness/test_runtime.py

* refactor(harness): update tests/unit/harness/test_sync.py

* refactor(harness): update tests/unit/llms/deepagents/harness/test_transformation.py

* refactor(harness): update tests/unit/llms/opencode/harness/test_transformation.py

* refactor(harness): update litellm/harness/endpoint.py

* refactor(harness): update litellm/harness/handlers/deepagents_handler.py

* refactor(harness): update litellm/harness/options.py

* refactor(harness): update litellm/harness/runtime.py

* refactor(harness): update litellm/harness/sandbox/snapshot.py

* refactor(harness): update litellm/llms/base_llm/harness/transformation.py

* refactor(harness): update litellm/llms/base_llm/harness/utils.py

* refactor(harness): update litellm/llms/claude_code/harness/transformation.py

* refactor(harness): update litellm/llms/codex/harness/transformation.py

* refactor(harness): update litellm/llms/deepagents/harness/sandbox_backend.py

* refactor(harness): update litellm/llms/deepagents/harness/transformation.py

* refactor(harness): update litellm/llms/opencode/harness/transformation.py

* refactor(harness): update litellm/sandbox/__init__.py

* refactor(harness): update tests/unit/llms/claude_code/harness/test_transformation.py

* refactor(harness): update tests/unit/llms/deepagents/harness/test_sandbox_backend_symlinks.py

* refactor(harness): update tests/unit/llms/opencode/harness/test_transformation.py

* refactor(harness): update litellm/llms/base_llm/harness/utils.py

* refactor(harness): update litellm/llms/codex/harness/transformation.py

* refactor(harness): update tests/code_coverage_tests/recursive_detector.py

* refactor(harness): update tests/unit/llms/base_llm/harness/__init__.py

* refactor(harness): update tests/unit/llms/claude_code/harness/fixtures/__init__.py

* refactor(harness): update tests/unit/llms/codex/harness/fixtures/__init__.py

* refactor(harness): update tests/unit/llms/opencode/harness/fixtures/__init__.py

* refactor(harness): update litellm/harness/__init__.py

* refactor(harness): update litellm/harness/context.py

* refactor(harness): update litellm/harness/endpoint.py

* refactor(harness): update litellm/harness/handlers/__init__.py

* refactor(harness): update litellm/harness/handlers/base.py

* refactor(harness): update litellm/harness/handlers/cli_handler.py

* refactor(harness): update litellm/harness/handlers/deepagents_handler.py

* refactor(harness): update litellm/harness/runtime.py

* refactor(harness): update litellm/harness/sandbox/__init__.py

* refactor(harness): update litellm/harness/sandbox/base.py

* refactor(harness): update litellm/harness/sandbox/docker.py

* refactor(harness): update litellm/harness/sandbox/local.py

* refactor(harness): update litellm/harness/sandbox/snapshot.py

* refactor(harness): update litellm/harness/sync.py

* refactor(harness): update litellm/harness/types.py

* refactor(harness): update litellm/llms/base_llm/harness/transformation.py

* refactor(harness): update litellm/llms/base_llm/harness/utils.py

* refactor(harness): update litellm/llms/claude_code/harness/transformation.py

* refactor(harness): update litellm/llms/codex/harness/transformation.py

* refactor(harness): update litellm/llms/deepagents/harness/sandbox_backend.py

* refactor(harness): update litellm/llms/deepagents/harness/transformation.py

* refactor(harness): update litellm/llms/opencode/harness/transformation.py

* refactor(harness): update litellm/types/llms/custom_http.py

* refactor(harness): update tests/unit/harness/test_endpoint.py

* refactor(harness): update litellm/harness/runtime.py

* refactor(harness): update litellm/llms/deepagents/harness/sandbox_backend.py

* refactor(harness): update tests/unit/harness/test_init.py

* refactor(harness): update tests/unit/harness/test_runtime.py

* refactor(harness): update tests/unit/llms/deepagents/harness/test_sandbox_backend_symlinks.py
2026-10-01 22:27:49 +00:00

378 lines
13 KiB
Python

import asyncio
import builtins
import os
import sys
from pathlib import Path
from typing import Any
import pytest
from pydantic import BaseModel
from litellm.harness.context import GatewayTarget, SessionContext
from litellm.harness.errors import HarnessError, HarnessInstallFailed
from litellm.harness.handlers import deepagents_handler as dh
from litellm.harness.sandbox.local import LocalSandbox
from litellm.harness.types import Approval, Harness, Text, ToolCall, ToolResult
from litellm.llms.deepagents.harness.transformation import DeepAgentsHarnessConfig
pytest.importorskip("deepagents")
pytest.importorskip("langchain_litellm")
from langchain_core.language_models.fake_chat_models import ( # noqa: E402
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage # noqa: E402
from litellm.llms.deepagents.harness.sandbox_backend import ( # noqa: E402
SandboxBackend,
message_cost,
)
USAGE = {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}
class FakeToolModel(FakeMessagesListChatModel):
"""Canned responses; records the tool names bound on each call."""
bound: list = []
def bind_tools(self, tools: Any, **kwargs: Any) -> "FakeToolModel":
names = [getattr(t, "name", None) or t.get("name") for t in tools]
self.bound.append(sorted(n for n in names if n))
return self
def tool_call(name: str, args: dict, call_id: str) -> AIMessage:
return AIMessage(
content="",
tool_calls=[{"name": name, "args": args, "id": call_id}],
usage_metadata=USAGE,
)
def final(text: str) -> AIMessage:
return AIMessage(content=text, usage_metadata=USAGE)
@pytest.fixture
def fake_model(monkeypatch: pytest.MonkeyPatch):
def install(responses: list) -> FakeToolModel:
model = FakeToolModel(responses=responses, bound=[])
monkeypatch.setattr(dh, "build_chat_model", lambda ctx, deps: model)
return model
return install
def make_ctx(tmp_path: Path, **kwargs: Any) -> SessionContext:
base: dict[str, Any] = {
"harness": Harness.DEEPAGENTS,
"sandbox": LocalSandbox(tmp_path),
"session_id": f"s-{os.urandom(4).hex()}",
"model": "gpt-4o-mini",
}
return SessionContext(**{**base, **kwargs})
def make_handler() -> dh.DeepAgentsHandler:
return dh.DeepAgentsHandler(DeepAgentsHarnessConfig())
async def started(ctx: SessionContext) -> dh.DeepAgentsHandler:
handler = make_handler()
await handler.start(ctx)
return handler
async def run_turn(
handler: dh.DeepAgentsHandler,
ctx: SessionContext,
prompt: str,
approve: bool = True,
) -> list:
events = []
async for event in handler.turn(ctx, prompt):
events.append(event)
if isinstance(event, Approval):
event.allow() if approve else event.deny("no")
return events
async def test_write_then_read_events_and_file(tmp_path: Path, fake_model) -> None:
fake_model(
[
tool_call("write_file", {"file_path": "/hello.txt", "content": "hi"}, "c1"),
tool_call("read_file", {"file_path": "/hello.txt"}, "c2"),
final("done"),
]
)
ctx = make_ctx(tmp_path)
handler = await started(ctx)
events = await run_turn(handler, ctx, "write hello.txt with hi")
calls = [e for e in events if isinstance(e, ToolCall)]
results = [e for e in events if isinstance(e, ToolResult)]
assert [(c.name, c.native_name, c.builtin) for c in calls] == [
("write", "write_file", True),
("read", "read_file", True),
]
assert [r.id for r in results] == ["c1", "c2"]
assert "hi" in results[1].output
assert not any(r.is_error for r in results)
assert "done" in "".join(e.delta for e in events if isinstance(e, Text))
assert (tmp_path / "hello.txt").read_text() == "hi"
assert ctx.final_text == "done"
assert (ctx.input_tokens, ctx.output_tokens, ctx.calls) == (30, 15, 3)
assert ctx.cost > 0
history = await handler.history(ctx)
assert history[0] == {"role": "user", "content": "write hello.txt with hi"}
assert history[-1]["content"] == "done"
async def test_read_only_hides_write_tools(tmp_path: Path, fake_model) -> None:
model = fake_model(
[
tool_call("write_file", {"file_path": "/x.txt", "content": "no"}, "c1"),
final("ok"),
]
)
ctx = make_ctx(tmp_path, permissions="read-only")
handler = await started(ctx)
events = await run_turn(handler, ctx, "try to write")
first = model.bound[0]
assert "read_file" in first and "ls" in first
assert not {"write_file", "edit_file", "execute", "delete"} & set(first)
result = next(e for e in events if isinstance(e, ToolResult))
assert result.is_error
assert not (tmp_path / "x.txt").exists()
async def test_disable_tools_uses_normalized_names(tmp_path: Path, fake_model) -> None:
model = fake_model([final("ok")])
ctx = make_ctx(tmp_path, disable_tools=["bash", "grep"])
handler = await started(ctx)
await run_turn(handler, ctx, "hi")
assert "execute" not in model.bound[0] and "grep" not in model.bound[0]
assert "write_file" in model.bound[0]
class Answer(BaseModel):
city: str
async def test_structured_output(tmp_path: Path, fake_model) -> None:
fake_model([tool_call("Answer", {"city": "Paris"}, "c1")])
ctx = make_ctx(tmp_path, output=Answer)
handler = await started(ctx)
events = await run_turn(handler, ctx, "capital of France?")
assert Answer.model_validate_json(ctx.output_json or "") == Answer(city="Paris")
assert not any(isinstance(e, ToolCall) for e in events)
async def test_custom_tool(tmp_path: Path, fake_model) -> None:
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
fake_model([tool_call("add", {"a": 2, "b": 3}, "c1"), final("5")])
ctx = make_ctx(tmp_path, tools=[add])
handler = await started(ctx)
events = await run_turn(handler, ctx, "2+3")
call = next(e for e in events if isinstance(e, ToolCall))
assert (call.name, call.builtin) == ("add", False)
assert next(e for e in events if isinstance(e, ToolResult)).output == "5"
@pytest.mark.parametrize("approve", [True, False])
async def test_ask_permissions_emit_approval(
tmp_path: Path, fake_model, approve: bool
) -> None:
fake_model(
[
tool_call("write_file", {"file_path": "/a.txt", "content": "x"}, "c1"),
final("end"),
]
)
ctx = make_ctx(tmp_path, permissions="ask")
handler = await started(ctx)
events = await run_turn(handler, ctx, "write a", approve=approve)
approval = next(e for e in events if isinstance(e, Approval))
assert approval.tool == "write"
assert approval.input["file_path"] == "/a.txt"
assert (tmp_path / "a.txt").exists() is approve
assert ctx.final_text == "end"
async def test_edit_and_execute_through_sandbox(tmp_path: Path, fake_model) -> None:
(tmp_path / "f.txt").write_text("one two\n")
fake_model(
[
tool_call(
"edit_file",
{"file_path": "/f.txt", "old_string": "two", "new_string": "three"},
"c1",
),
tool_call("execute", {"command": "cat f.txt"}, "c2"),
final("ok"),
]
)
ctx = make_ctx(tmp_path)
handler = await started(ctx)
events = await run_turn(handler, ctx, "edit")
calls = [e.name for e in events if isinstance(e, ToolCall)]
assert calls == ["edit", "bash"]
results = [e for e in events if isinstance(e, ToolResult)]
assert "one three" in results[1].output
assert (tmp_path / "f.txt").read_text() == "one three\n"
async def test_resume_keeps_thread(tmp_path: Path, fake_model) -> None:
fake_model([final("first"), final("second")])
ctx = make_ctx(tmp_path)
handler = await started(ctx)
await run_turn(handler, ctx, "one")
native = handler.native_session_id()
assert native == ctx.session_id
other = make_handler()
ctx2 = make_ctx(tmp_path)
await other.start(ctx2)
await other.resume(ctx2, native or "")
await run_turn(other, ctx2, "two")
history = await other.history(ctx2)
assert [m["content"] for m in history if m["role"] == "user"] == ["one", "two"]
async def test_skills_copied_and_loaded(tmp_path: Path, fake_model) -> None:
skill = tmp_path / "src-skills" / "greeter"
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text(
"---\nname: greeter\ndescription: Says hi\n---\nSay hi.\n"
)
work = tmp_path / "work"
work.mkdir()
fake_model([final("ok")])
ctx = make_ctx(work, skills=[str(skill)])
handler = await started(ctx)
await run_turn(handler, ctx, "hi")
assert (work / ".deepagents" / "skills" / "greeter" / "SKILL.md").exists()
async def test_turn_and_history_before_start_and_after_stop(
tmp_path: Path, fake_model
) -> None:
fake_model([final("ok")])
ctx = make_ctx(tmp_path)
handler = make_handler()
with pytest.raises(HarnessError, match="not started"):
await run_turn(handler, ctx, "hi")
await handler.start(ctx)
await handler.stop(ctx)
with pytest.raises(HarnessError, match="not started"):
await handler.history(ctx)
async def test_start_validates_model(tmp_path: Path, fake_model) -> None:
fake_model([final("ok")])
with pytest.raises(ValueError, match="needs model="):
await started(make_ctx(tmp_path, model=None))
def test_build_chat_model_uses_chat_model_kwargs(tmp_path: Path) -> None:
deps = dh.load_deps()
gw = GatewayTarget(api_base="https://gw.example.com", api_key="sk-virtual")
model = dh.build_chat_model(make_ctx(tmp_path, gateway=gw), deps)
assert isinstance(model, deps.chat_litellm)
assert model.model == "litellm_proxy/gpt-4o-mini"
def test_shared_checkpointer_is_process_wide() -> None:
deps = dh.load_deps()
assert dh.shared_checkpointer(deps) is dh.shared_checkpointer(deps)
async def test_sandbox_backend_fs_ops(tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.py").write_text("print('hello')\n")
(tmp_path / "b.txt").write_text("hello world\n")
backend = SandboxBackend(LocalSandbox(tmp_path), loop=asyncio.get_running_loop())
ls = await backend.als("/")
assert {e["path"] for e in ls.entries or []} == {"/b.txt", "/src/"}
assert (await backend.als("/missing")).error
globbed = await backend.aglob("*.py")
assert [m["path"] for m in globbed.matches or []] == ["/src/a.py"]
grep = await backend.agrep("hello", glob="*.txt")
assert [(m["path"], m["line"]) for m in grep.matches or []] == [("/b.txt", 1)]
read = await backend.aread("/b.txt")
assert read.file_data and read.file_data["content"] == "hello world\n"
assert (await backend.aread("/nope.txt")).error
assert (await backend.aread("/../etc/passwd")).error
edit = await backend.aedit("/b.txt", "hello", "bye")
assert edit.occurrences == 1
assert (await backend.aedit("/b.txt", "zzz", "q")).error
assert (await backend.adelete("/src")).path == "/src"
assert not (tmp_path / "src").exists()
assert (
backend.to_real(str(tmp_path / "b.txt"))
== str(LocalSandbox(tmp_path).workdir) + "/b.txt"
)
sync_ls = await asyncio.to_thread(backend.ls, "/")
assert [e["path"] for e in sync_ls.entries or []] == ["/b.txt"]
read_only = SandboxBackend(
LocalSandbox(tmp_path),
loop=asyncio.get_running_loop(),
writable=False,
allow_execute=False,
)
assert (await read_only.awrite("/c.txt", "x")).error
assert (await read_only.aexecute("ls")).exit_code == 1
assert not (tmp_path / "c.txt").exists()
def test_message_cost_prefers_reported_and_never_raises() -> None:
reported = AIMessage(content="", response_metadata={"response_cost": 0.5})
assert message_cost(reported, "gpt-4o-mini", 1, 1) == 0.5
assert message_cost(AIMessage(content=""), "not-a-real-model-xyz", 10, 10) == 0.0
assert message_cost(AIMessage(content=""), "gpt-4o-mini", 1000, 1000) > 0
def test_missing_deps_raise_install_hint(monkeypatch: pytest.MonkeyPatch) -> None:
real_import = builtins.__import__
def fake_import(name: str, *args: Any, **kwargs: Any) -> Any:
if name.startswith("deepagents"):
raise ImportError("No module named 'deepagents'")
return real_import(name, *args, **kwargs)
for mod in [m for m in sys.modules if m.startswith("deepagents")]:
monkeypatch.delitem(sys.modules, mod)
monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(
HarnessInstallFailed, match="pip install deepagents langchain-litellm"
):
dh.load_deps()
LIVE_BASE = os.environ.get("LITELLM_PROXY_API_BASE", "")
LIVE_KEY = os.environ.get("LITELLM_PROXY_API_KEY", "")
@pytest.mark.skipif(
not (LIVE_BASE and LIVE_KEY), reason="LITELLM_PROXY_API_BASE / KEY not set"
)
async def test_live_gateway_write_file(tmp_path: Path) -> None:
model = os.environ.get("HARNESS_DEEPAGENTS_LIVE_MODEL", "claude-haiku-4-5-20251001")
ctx = make_ctx(
tmp_path,
model=model,
gateway=GatewayTarget(api_base=LIVE_BASE, api_key=LIVE_KEY),
max_turns=6,
)
handler = await started(ctx)
events = await run_turn(handler, ctx, "write hello.txt with hi")
assert any(isinstance(e, ToolCall) and e.name == "write" for e in events)
assert (tmp_path / "hello.txt").read_text().strip() == "hi"
assert ctx.calls >= 1 and ctx.input_tokens > 0