From 7295ab2334a5d255d1fa07c1b898b3108faec94c Mon Sep 17 00:00:00 2001 From: Andrei Savu Date: Mon, 16 Feb 2026 23:01:08 -0800 Subject: [PATCH 1/3] Add multi-adapter support (claude / codex / opencode) Allow selecting the execution adapter via TRIVIA_ADAPTER env var (or `make agent ADAPTER=codex`). This demonstrates WINK's portability across harnesses. Default remains claude for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 8 +- Makefile | 6 +- integration-tests/conftest.py | 23 +++- integration-tests/test_evals.py | 1 + pyproject.toml | 2 +- src/trivia_agent/adapters.py | 159 +++++++++++++++++++------- src/trivia_agent/agent_loop.py | 21 ++-- tests/trivia_agent/test_adapters.py | 89 +++++++++++--- tests/trivia_agent/test_agent_loop.py | 90 ++++++++++++++- 9 files changed, 323 insertions(+), 76 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7b4c94f..8c69cd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,9 @@ make check # Start Redis and run the agent make redis -make agent +make agent # Default: claude adapter +make agent ADAPTER=codex # Use Codex adapter +make agent ADAPTER=opencode # Use OpenCode adapter # Ask about secrets make dispatch QUESTION="What is the secret number?" @@ -42,6 +44,10 @@ make dispatch QUESTION="What is the secret word?" # Run evals make dispatch-eval QUESTION="What is the secret number?" EXPECTED="42" + +# Integration tests with different adapters +make integration-test # Default: claude +make integration-test ADAPTER=codex # Test with Codex ``` ## Repository Structure diff --git a/Makefile b/Makefile index 4f72ba1..b764bf4 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ help: @echo "" @echo "Run:" @echo " make agent Start the Secret Trivia agent worker" + @echo " make agent ADAPTER=codex Start with Codex adapter" + @echo " make agent ADAPTER=opencode Start with OpenCode adapter" @echo " make dispatch Submit a test question (set QUESTION=...)" @echo " make dispatch-eval Submit an eval case with experiment metadata" @echo "" @@ -66,9 +68,11 @@ EXPECTED ?= "" EXPERIMENT ?= cli-eval OWNER ?= DESCRIPTION ?= +ADAPTER ?= claude agent: REDIS_URL=$(REDIS_URL) \ + TRIVIA_ADAPTER=$(ADAPTER) \ TRIVIA_DEBUG_BUNDLES_DIR=$(TRIVIA_DEBUG_BUNDLES_DIR) \ TRIVIA_PROMPT_OVERRIDES_DIR=$(TRIVIA_PROMPT_OVERRIDES_DIR) \ uv run trivia-agent @@ -103,7 +107,7 @@ test: uv run pytest tests -v integration-test: - uv run pytest integration-tests/ -v --timeout=300 --no-cov + TRIVIA_ADAPTER=$(ADAPTER) uv run pytest integration-tests/ -v --timeout=300 --no-cov # ============================================================================= # Cleanup diff --git a/integration-tests/conftest.py b/integration-tests/conftest.py index 4d8c1d3..7174173 100644 --- a/integration-tests/conftest.py +++ b/integration-tests/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import shutil import pytest @@ -19,10 +20,22 @@ def pytest_collection_modifyitems( config: pytest.Config, items: list[pytest.Item], ) -> None: - """Skip integration tests if ANTHROPIC_API_KEY is not set.""" - if not os.environ.get("ANTHROPIC_API_KEY"): - skip_marker = pytest.mark.skip( - reason="ANTHROPIC_API_KEY not set - skipping integration tests" - ) + """Skip integration tests based on adapter prerequisites.""" + adapter = os.environ.get("TRIVIA_ADAPTER", "claude") + + skip_reason: str | None = None + + if adapter == "claude": + if not os.environ.get("ANTHROPIC_API_KEY"): + skip_reason = "ANTHROPIC_API_KEY not set - skipping integration tests" + elif adapter == "codex": + if shutil.which("codex") is None: + skip_reason = "codex binary not found on PATH - skipping integration tests" + elif adapter == "opencode": + if shutil.which("opencode") is None: + skip_reason = "opencode binary not found on PATH - skipping integration tests" + + if skip_reason: + skip_marker = pytest.mark.skip(reason=skip_reason) for item in items: item.add_marker(skip_marker) diff --git a/integration-tests/test_evals.py b/integration-tests/test_evals.py index e0c8f3b..0bd54a9 100644 --- a/integration-tests/test_evals.py +++ b/integration-tests/test_evals.py @@ -66,6 +66,7 @@ def agent(redis_url: str) -> Generator[subprocess.Popen[bytes], None, None]: """Start the trivia agent.""" env = os.environ.copy() env["REDIS_URL"] = redis_url + env["TRIVIA_ADAPTER"] = os.environ.get("TRIVIA_ADAPTER", "claude") # Enable debug bundle generation project_root = Path(__file__).parent.parent diff --git a/pyproject.toml b/pyproject.toml index bf6e242..d2a9b0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Secret Trivia Agent - a WINK starter demonstrating background age requires-python = ">=3.12" dependencies = [ "redis", - "weakincentives[claude-agent-sdk,redis,skills]", + "weakincentives[claude-agent-sdk,acp,redis,skills]", ] [project.scripts] diff --git a/src/trivia_agent/adapters.py b/src/trivia_agent/adapters.py index 009fbb1..256ae27 100644 --- a/src/trivia_agent/adapters.py +++ b/src/trivia_agent/adapters.py @@ -7,19 +7,22 @@ Key components: - ``SimpleTaskCompletionChecker``: Pass-through completion checker (used by prompt) - ``create_adapter``: Factory function to build configured adapters + - ``AdapterChoice``: Literal type for selecting the adapter backend + - ``resolve_adapter_choice``: Reads TRIVIA_ADAPTER env var -The default model is Claude Sonnet, accessed via the "sonnet" alias. +The default adapter is ``claude`` (Claude Agent SDK), accessed via the "sonnet" alias. Usage: - >>> from trivia_agent.adapters import create_adapter - >>> adapter = create_adapter() - >>> # Pass adapter to AgentLoop.create() or EvalLoop.create() + >>> from trivia_agent.adapters import create_adapter, resolve_adapter_choice + >>> adapter_choice = resolve_adapter_choice(os.environ) + >>> adapter = create_adapter(adapter_choice, isolation=isolation, cwd=cwd) """ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, cast +from weakincentives.adapters import ProviderAdapter from weakincentives.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter from weakincentives.adapters.claude_agent_sdk.config import ClaudeAgentSDKClientConfig from weakincentives.prompt import TaskCompletionContext, TaskCompletionResult @@ -27,11 +30,18 @@ from trivia_agent.models import TriviaResponse if TYPE_CHECKING: + from collections.abc import Mapping + from weakincentives.adapters.claude_agent_sdk.isolation import IsolationConfig # Use Claude Sonnet (alias) DEFAULT_MODEL = "sonnet" +# Adapter selection +AdapterChoice = Literal["claude", "codex", "opencode"] +ADAPTER_ENV = "TRIVIA_ADAPTER" +_VALID_ADAPTERS: set[str] = {"claude", "codex", "opencode"} + class SimpleTaskCompletionChecker: """Task completion checker that unconditionally marks tasks as complete. @@ -76,53 +86,116 @@ def check(self, context: TaskCompletionContext) -> TaskCompletionResult: return TaskCompletionResult.ok() -def create_adapter( - *, - isolation: IsolationConfig | None = None, - cwd: str | None = None, -) -> ClaudeAgentSDKAdapter[TriviaResponse]: - """Create and configure a Claude Agent SDK adapter for the trivia agent. +def resolve_adapter_choice(env: Mapping[str, str]) -> AdapterChoice: + """Read the adapter choice from the environment. - Factory function that assembles all components needed to run the trivia - agent: model selection, isolation configuration, and working directory - setup. The returned adapter is ready to be passed to a WINK AgentLoop - or EvalLoop. - - The adapter is configured with: - - Model: Claude Sonnet (via the "sonnet" alias) - - Response type: TriviaResponse (structured output schema) - - Note: Task completion checking is declared on the PromptTemplate (see - agent_loop.py), not on the adapter config. + Reads the ``TRIVIA_ADAPTER`` environment variable and validates it against + the supported adapter values (``claude``, ``codex``, ``opencode``). + Defaults to ``claude`` if the variable is not set or is empty. Args: - isolation: Optional isolation configuration that controls the agent's - execution environment. When provided, specifies: - - Skills to load (e.g., secret-trivia skill with answers) - - Sandbox settings restricting file/network access - If None, the agent runs without isolation constraints. - cwd: Optional working directory path for the agent. The agent will - execute with this directory as its current working directory. - If None, uses the default working directory. + env: A mapping of environment variable names to values (typically os.environ). Returns: - ClaudeAgentSDKAdapter[TriviaResponse]: A fully configured adapter - instance typed to produce TriviaResponse structured output. - Pass this adapter to ``AgentLoop.create()`` or ``EvalLoop.create()`` - to run the trivia agent. + The validated adapter choice. - Example: - >>> from trivia_agent.isolation import create_isolation_config - >>> isolation = create_isolation_config() - >>> adapter = create_adapter(isolation=isolation, cwd="/path/to/workspace") - >>> # Use adapter with AgentLoop - >>> loop = AgentLoop.create(adapter=adapter, sections=[...]) + Raises: + ValueError: If the env var value is not one of the supported adapters. """ + raw = env.get(ADAPTER_ENV, "").strip() + if not raw: + return "claude" + if raw not in _VALID_ADAPTERS: + msg = f"Invalid {ADAPTER_ENV}={raw!r}. Must be one of: claude, codex, opencode" + raise ValueError(msg) + return cast("AdapterChoice", raw) + + +def _create_claude_adapter( + *, + isolation: IsolationConfig | None, + cwd: str | None, +) -> ProviderAdapter[TriviaResponse]: + """Create a Claude Agent SDK adapter.""" client_config = ClaudeAgentSDKClientConfig( isolation=isolation, cwd=cwd, ) - return ClaudeAgentSDKAdapter[TriviaResponse]( - model=DEFAULT_MODEL, - client_config=client_config, + return cast( + "ProviderAdapter[TriviaResponse]", + ClaudeAgentSDKAdapter[TriviaResponse]( + model=DEFAULT_MODEL, + client_config=client_config, + ), + ) + + +def _create_codex_adapter( + *, + cwd: str | None, +) -> ProviderAdapter[TriviaResponse]: + """Create a Codex App Server adapter.""" + from weakincentives.adapters.codex_app_server import ( + CodexAppServerAdapter, + CodexAppServerClientConfig, + ) + + client_config = CodexAppServerClientConfig( + cwd=cwd, + approval_policy="never", + ) + return cast( + "ProviderAdapter[TriviaResponse]", + CodexAppServerAdapter[TriviaResponse](client_config=client_config), ) + + +def _create_opencode_adapter( + *, + cwd: str | None, +) -> ProviderAdapter[TriviaResponse]: + """Create an OpenCode ACP adapter.""" + from weakincentives.adapters.opencode_acp import ( + OpenCodeACPAdapter, + OpenCodeACPClientConfig, + ) + + client_config = OpenCodeACPClientConfig(cwd=cwd) + return cast( + "ProviderAdapter[TriviaResponse]", + OpenCodeACPAdapter[TriviaResponse](client_config=client_config), + ) + + +def create_adapter( + adapter: AdapterChoice = "claude", + *, + isolation: IsolationConfig | None = None, + cwd: str | None = None, +) -> ProviderAdapter[TriviaResponse]: + """Create and configure a provider adapter for the trivia agent. + + Factory function that assembles all components needed to run the trivia + agent with the selected backend. The returned adapter is ready to be + passed to a WINK AgentLoop or EvalLoop. + + Supported adapters: + - ``claude``: Claude Agent SDK (default) - uses Claude Sonnet model + - ``codex``: Codex App Server - uses OpenAI Codex + - ``opencode``: OpenCode ACP - uses ACP protocol + + Args: + adapter: Which adapter backend to use. Defaults to ``"claude"``. + isolation: Optional isolation configuration for the Claude adapter. + Only used when ``adapter="claude"``. Ignored for other adapters. + cwd: Optional working directory path for the agent. + + Returns: + ProviderAdapter[TriviaResponse]: A fully configured adapter instance. + """ + if adapter == "claude": + return _create_claude_adapter(isolation=isolation, cwd=cwd) + if adapter == "codex": + return _create_codex_adapter(cwd=cwd) + # opencode + return _create_opencode_adapter(cwd=cwd) diff --git a/src/trivia_agent/agent_loop.py b/src/trivia_agent/agent_loop.py index 922f2eb..232f42a 100644 --- a/src/trivia_agent/agent_loop.py +++ b/src/trivia_agent/agent_loop.py @@ -37,7 +37,11 @@ from weakincentives.runtime.mailbox import Mailbox from weakincentives.skills import SkillMount -from trivia_agent.adapters import SimpleTaskCompletionChecker, create_adapter +from trivia_agent.adapters import ( + SimpleTaskCompletionChecker, + create_adapter, + resolve_adapter_choice, +) from trivia_agent.config import load_redis_settings from trivia_agent.feedback import build_feedback_providers from trivia_agent.isolation import API_KEY_ENV, has_auth, resolve_isolation_config, resolve_skills @@ -476,8 +480,11 @@ def main( assert settings is not None # for type checker - # Validate authentication early - this is the most common first-run error - if rt.adapter is None and not has_auth(os.environ): + # Determine which adapter backend to use + adapter_choice = resolve_adapter_choice(os.environ) + + # Validate authentication early - only needed for the claude adapter + if rt.adapter is None and adapter_choice == "claude" and not has_auth(os.environ): err.write(f"Missing {API_KEY_ENV}. Set it with: export {API_KEY_ENV}=your-api-key\n") return 1 @@ -486,15 +493,15 @@ def main( # if it detects it's inside another Claude Code session. os.environ.pop("CLAUDECODE", None) - # Resolve isolation config (sandbox, API key — no longer includes skills) - isolation = resolve_isolation_config(os.environ) + # Resolve isolation config (sandbox, API key — only needed for claude adapter) + isolation = resolve_isolation_config(os.environ) if adapter_choice == "claude" else None # Discover skills for section-level mounting skills = resolve_skills(os.environ) # Use injected or create real dependencies try: - adapter = rt.adapter or create_adapter(isolation=isolation) + adapter = rt.adapter or create_adapter(adapter_choice, isolation=isolation) except Exception as e: err.write(f"Failed to create adapter: {e}\n") return 1 @@ -536,7 +543,7 @@ def main( ) # Run both loops - out.write("Starting trivia agent worker...\n") + out.write(f"Starting trivia agent worker (adapter={adapter_choice})...\n") group = LoopGroup(loops=[loop, eval_loop]) # type: ignore[list-item] group.run() diff --git a/tests/trivia_agent/test_adapters.py b/tests/trivia_agent/test_adapters.py index 32ab092..2c2b664 100644 --- a/tests/trivia_agent/test_adapters.py +++ b/tests/trivia_agent/test_adapters.py @@ -2,10 +2,19 @@ from unittest.mock import MagicMock +import pytest +from weakincentives.adapters import ProviderAdapter from weakincentives.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter +from weakincentives.adapters.codex_app_server import CodexAppServerAdapter +from weakincentives.adapters.opencode_acp import OpenCodeACPAdapter from weakincentives.prompt import TaskCompletionResult -from trivia_agent.adapters import SimpleTaskCompletionChecker, create_adapter +from trivia_agent.adapters import ( + ADAPTER_ENV, + SimpleTaskCompletionChecker, + create_adapter, + resolve_adapter_choice, +) class TestSimpleTaskCompletionChecker: @@ -27,28 +36,74 @@ def test_check_ignores_context(self) -> None: assert result.feedback is None +class TestResolveAdapterChoice: + """Tests for resolve_adapter_choice function.""" + + def test_defaults_to_claude(self) -> None: + """Test that missing env var defaults to 'claude'.""" + assert resolve_adapter_choice({}) == "claude" + + def test_empty_string_defaults_to_claude(self) -> None: + """Test that empty string defaults to 'claude'.""" + assert resolve_adapter_choice({ADAPTER_ENV: ""}) == "claude" + + def test_whitespace_defaults_to_claude(self) -> None: + """Test that whitespace-only string defaults to 'claude'.""" + assert resolve_adapter_choice({ADAPTER_ENV: " "}) == "claude" + + def test_reads_claude(self) -> None: + """Test reading 'claude' from env.""" + assert resolve_adapter_choice({ADAPTER_ENV: "claude"}) == "claude" + + def test_reads_codex(self) -> None: + """Test reading 'codex' from env.""" + assert resolve_adapter_choice({ADAPTER_ENV: "codex"}) == "codex" + + def test_reads_opencode(self) -> None: + """Test reading 'opencode' from env.""" + assert resolve_adapter_choice({ADAPTER_ENV: "opencode"}) == "opencode" + + def test_rejects_invalid_value(self) -> None: + """Test that invalid adapter values raise ValueError.""" + with pytest.raises(ValueError, match="Invalid TRIVIA_ADAPTER='bogus'"): + resolve_adapter_choice({ADAPTER_ENV: "bogus"}) + + class TestCreateAdapter: """Tests for create_adapter function.""" - def test_returns_claude_agent_sdk_adapter(self) -> None: - """Test that create_adapter returns a ClaudeAgentSDKAdapter.""" + def test_returns_claude_adapter_by_default(self) -> None: + """Test that create_adapter returns a Claude adapter by default.""" adapter = create_adapter() assert isinstance(adapter, ClaudeAgentSDKAdapter) - def test_adapter_uses_default_model(self) -> None: - """Test that adapter uses default model configuration.""" - adapter = create_adapter() - # The adapter should be created without errors - assert adapter is not None + def test_returns_claude_adapter_explicitly(self) -> None: + """Test that create_adapter('claude') returns a Claude adapter.""" + adapter = create_adapter("claude") + assert isinstance(adapter, ClaudeAgentSDKAdapter) - def test_adapter_has_client_config(self) -> None: - """Test that adapter is configured with client config.""" - adapter = create_adapter() - # Verify adapter was created with client config (no task_completion_checker — - # that's now on the PromptTemplate) - assert adapter._client_config is not None + def test_returns_codex_adapter(self) -> None: + """Test that create_adapter('codex') returns a Codex adapter.""" + adapter = create_adapter("codex") + assert isinstance(adapter, CodexAppServerAdapter) - def test_adapter_no_task_completion_on_client_config(self) -> None: + def test_returns_opencode_adapter(self) -> None: + """Test that create_adapter('opencode') returns an OpenCode adapter.""" + adapter = create_adapter("opencode") + assert isinstance(adapter, OpenCodeACPAdapter) + + def test_all_adapters_are_provider_adapters(self) -> None: + """Test that all adapter types satisfy the ProviderAdapter protocol.""" + for choice in ("claude", "codex", "opencode"): + adapter = create_adapter(choice) # type: ignore[arg-type] + assert isinstance(adapter, ProviderAdapter) + + def test_claude_adapter_has_client_config(self) -> None: + """Test that claude adapter is configured with client config.""" + adapter = create_adapter("claude") + assert adapter._client_config is not None # type: ignore[union-attr] + + def test_claude_adapter_no_task_completion_on_client_config(self) -> None: """Test that task completion checker is not on client config (moved to prompt).""" - adapter = create_adapter() - assert not hasattr(adapter._client_config, "task_completion_checker") + adapter = create_adapter("claude") + assert not hasattr(adapter._client_config, "task_completion_checker") # type: ignore[union-attr] diff --git a/tests/trivia_agent/test_agent_loop.py b/tests/trivia_agent/test_agent_loop.py index 85bb15f..9bed993 100644 --- a/tests/trivia_agent/test_agent_loop.py +++ b/tests/trivia_agent/test_agent_loop.py @@ -346,7 +346,7 @@ def test_successful_startup( result = main(runtime=runtime) assert result == 0 - assert "Starting trivia agent worker" in out.getvalue() + assert "Starting trivia agent worker (adapter=claude)" in out.getvalue() mock_instance.run.assert_called_once() def test_creates_real_dependencies_when_not_injected( @@ -521,3 +521,91 @@ def test_mailbox_creation_failure( assert result == 1 assert "Failed to connect to Redis" in err.getvalue() assert "Connection refused" in err.getvalue() + + def test_codex_adapter_skips_auth_check( + self, + fake_mailboxes: TriviaMailboxes, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Test that codex adapter does not require ANTHROPIC_API_KEY.""" + out = io.StringIO() + err = io.StringIO() + mock_adapter: ProviderAdapter[TriviaResponse] = MagicMock() + + runtime = TriviaRuntime( + adapter=mock_adapter, + mailboxes=fake_mailboxes, + out=out, + err=err, + ) + + monkeypatch.setenv("REDIS_URL", "redis://localhost:6379") + monkeypatch.setenv("TRIVIA_ADAPTER", "codex") + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("CLAUDE_CODE_USE_BEDROCK", raising=False) + + with patch("trivia_agent.agent_loop.LoopGroup") as mock_loop_group: + mock_instance = MagicMock() + mock_loop_group.return_value = mock_instance + result = main(runtime=runtime) + + assert result == 0 + assert "adapter=codex" in out.getvalue() + + def test_opencode_adapter_skips_auth_check( + self, + fake_mailboxes: TriviaMailboxes, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Test that opencode adapter does not require ANTHROPIC_API_KEY.""" + out = io.StringIO() + err = io.StringIO() + mock_adapter: ProviderAdapter[TriviaResponse] = MagicMock() + + runtime = TriviaRuntime( + adapter=mock_adapter, + mailboxes=fake_mailboxes, + out=out, + err=err, + ) + + monkeypatch.setenv("REDIS_URL", "redis://localhost:6379") + monkeypatch.setenv("TRIVIA_ADAPTER", "opencode") + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("CLAUDE_CODE_USE_BEDROCK", raising=False) + + with patch("trivia_agent.agent_loop.LoopGroup") as mock_loop_group: + mock_instance = MagicMock() + mock_loop_group.return_value = mock_instance + result = main(runtime=runtime) + + assert result == 0 + assert "adapter=opencode" in out.getvalue() + + def test_startup_message_includes_adapter_name( + self, + fake_mailboxes: TriviaMailboxes, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Test that startup message includes the adapter name.""" + out = io.StringIO() + err = io.StringIO() + mock_adapter: ProviderAdapter[TriviaResponse] = MagicMock() + + runtime = TriviaRuntime( + adapter=mock_adapter, + mailboxes=fake_mailboxes, + out=out, + err=err, + ) + + monkeypatch.setenv("REDIS_URL", "redis://localhost:6379") + monkeypatch.setenv("TRIVIA_ADAPTER", "codex") + + with patch("trivia_agent.agent_loop.LoopGroup") as mock_loop_group: + mock_instance = MagicMock() + mock_loop_group.return_value = mock_instance + result = main(runtime=runtime) + + assert result == 0 + assert "Starting trivia agent worker (adapter=codex)..." in out.getvalue() From 12d41eec5b31de1dd9de31abdbe54e7a68b557fa Mon Sep 17 00:00:00 2001 From: Andrei Savu Date: Mon, 16 Feb 2026 23:05:02 -0800 Subject: [PATCH 2/3] Allow Bedrock auth for claude adapter in integration tests The skip logic only checked ANTHROPIC_API_KEY but the agent also supports CLAUDE_CODE_USE_BEDROCK for authentication. Accept either. Co-Authored-By: Claude Opus 4.6 --- integration-tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integration-tests/conftest.py b/integration-tests/conftest.py index 7174173..30778ba 100644 --- a/integration-tests/conftest.py +++ b/integration-tests/conftest.py @@ -26,7 +26,9 @@ def pytest_collection_modifyitems( skip_reason: str | None = None if adapter == "claude": - if not os.environ.get("ANTHROPIC_API_KEY"): + if not os.environ.get("ANTHROPIC_API_KEY") and not os.environ.get( + "CLAUDE_CODE_USE_BEDROCK" + ): skip_reason = "ANTHROPIC_API_KEY not set - skipping integration tests" elif adapter == "codex": if shutil.which("codex") is None: From 2b940d123541505af931ffc030537ae96734f9f5 Mon Sep 17 00:00:00 2001 From: Andrei Savu Date: Mon, 16 Feb 2026 23:06:27 -0800 Subject: [PATCH 3/3] Update README to document multi-adapter support Reflect that the same agent runs on Claude, Codex, and OpenCode adapters. Update Quick Start, Configuration Reference, architecture diagram, development commands, and troubleshooting sections. Co-Authored-By: Claude Opus 4.6 --- README.md | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index feec2a1..e55ef4c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This simple concept naturally demonstrates all of WINK's key capabilities: - Planning/act loop, sandboxing, tool-call orchestration - Scheduling, crash recovery, operational guardrails -**WINK's thesis**: Harnesses keep changing (and increasingly come from vendor runtimes), but your agent definition should not. WINK makes the definition a first-class artifact you can version, review, test, and port across runtimes via adapters. +**WINK's thesis**: Harnesses keep changing (and increasingly come from vendor runtimes), but your agent definition should not. WINK makes the definition a first-class artifact you can version, review, test, and port across runtimes via adapters. This starter demonstrates that portability — the same trivia agent runs on **Claude Agent SDK**, **Codex App Server**, and **OpenCode ACP** by switching a single environment variable. ## Project Structure @@ -84,17 +84,30 @@ make install make redis ``` -### 3. Set your API key +### 3. Configure authentication + +The starter supports three execution adapters. Set up credentials for the one you plan to use: ```bash +# Claude (default) — Anthropic API or AWS Bedrock export ANTHROPIC_API_KEY=your-api-key +# or +export CLAUDE_CODE_USE_BEDROCK=1 + +# Codex — requires the codex CLI on PATH +# (no extra env vars needed) + +# OpenCode — requires the opencode CLI on PATH +# (no extra env vars needed) ``` ### 4. Run the agent In one terminal, start the worker: ```bash -make agent +make agent # Claude adapter (default) +make agent ADAPTER=codex # Codex adapter +make agent ADAPTER=opencode # OpenCode adapter ``` In another terminal, ask about secrets: @@ -296,7 +309,8 @@ The evaluator checks: | Variable | Required | Default | Description | |----------|----------|---------|-------------| -| `ANTHROPIC_API_KEY` | Yes | - | Anthropic API key | +| `TRIVIA_ADAPTER` | No | `claude` | Adapter backend: `claude`, `codex`, or `opencode` | +| `ANTHROPIC_API_KEY` | Claude only | - | Anthropic API key (or set `CLAUDE_CODE_USE_BEDROCK`) | | `REDIS_URL` | No | `redis://localhost:6379` | Redis connection URL | | `TRIVIA_REQUESTS_QUEUE` | No | `trivia:requests` | Request queue name | | `TRIVIA_EVAL_REQUESTS_QUEUE` | No | `trivia:eval:requests` | Eval queue name | @@ -308,7 +322,9 @@ The evaluator checks: ```bash make check # Format, lint, typecheck, test make test # Run unit tests with coverage -make integration-test # Run integration tests (requires Redis + API key) +make integration-test # Run integration tests (default: claude adapter) +make integration-test ADAPTER=codex # Integration tests with Codex +make integration-test ADAPTER=opencode # Integration tests with OpenCode make format # Format code ``` @@ -343,8 +359,12 @@ make format # Format code │ ▼ ┌────────────────────────────────────────────────────────────────┐ -│ Claude Agent SDK (Harness) │ -│ - Planning/act loop, sandboxing, tool-call orchestration │ +│ Execution Harness (via Adapter) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ Claude Agent │ │ Codex App │ │ OpenCode ACP │ │ +│ │ SDK │ │ Server │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ +│ Set TRIVIA_ADAPTER=claude|codex|opencode (default: claude) │ └────────────────────────────────────────────────────────────────┘ │ ▼ @@ -378,7 +398,7 @@ Then read the TOOLS guide and explain how tools work in WINK. ### Manual Testing -With `ANTHROPIC_API_KEY` and `REDIS_URL` set in your environment, Claude can perform end-to-end manual testing: +With your adapter configured and `REDIS_URL` set, Claude can perform end-to-end manual testing: ``` Start Redis and the agent, then test all four secrets by dispatching @@ -465,6 +485,8 @@ Query the debug bundles to show me what data is captured during execution: **"Connection refused" errors**: Make sure Redis is running (`make redis`). -**"API key not found"**: Ensure `ANTHROPIC_API_KEY` is set. +**"API key not found"**: Only applies to the claude adapter. Set `ANTHROPIC_API_KEY` or `CLAUDE_CODE_USE_BEDROCK`. + +**"codex/opencode binary not found"**: Install the respective CLI and ensure it's on your `PATH`. **Agent doesn't know secrets**: Check that `skills/secret-trivia/SKILL.md` exists and contains the answers.