From 6ca2556b4e1c5e27a5eee4545bce0b672b7679e5 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 9 Jun 2026 18:20:05 +0800 Subject: [PATCH] chore(runtime): remove the unused cc (Claude Code) runtime The `cc` runtime was never a documented/usable path (no PyPI extra, depends on claude_agent_sdk) and the project standardizes on `adk` (default) and `codex`. Remove it and all references: - delete veadk/runtime/cc/ (runtime, proxy, translate) - drop the `cc` branch from get_runtime and the runtime package docstring - Agent.runtime: Literal["adk", "codex"] (was ["adk", "cc", "codex"]) + docstring - base_runtime docstring example -> "codex" - codex/translate docstring no longer references the cc runtime --- veadk/agent.py | 6 +- veadk/runtime/__init__.py | 14 +- veadk/runtime/base_runtime.py | 2 +- veadk/runtime/cc/__init__.py | 19 --- veadk/runtime/cc/proxy.py | 270 ------------------------------- veadk/runtime/cc/runtime.py | 175 -------------------- veadk/runtime/cc/translate.py | 128 --------------- veadk/runtime/codex/translate.py | 4 +- 8 files changed, 6 insertions(+), 612 deletions(-) delete mode 100644 veadk/runtime/cc/__init__.py delete mode 100644 veadk/runtime/cc/proxy.py delete mode 100644 veadk/runtime/cc/runtime.py delete mode 100644 veadk/runtime/cc/translate.py diff --git a/veadk/agent.py b/veadk/agent.py index 86ed9b26..357be5a8 100644 --- a/veadk/agent.py +++ b/veadk/agent.py @@ -169,10 +169,10 @@ class Agent(LlmAgent): enable_skills_checklist: bool = False _skills_with_checklist: Dict[str, Any] = {} - runtime: Literal["adk", "cc", "codex"] = "adk" + runtime: Literal["adk", "codex"] = "adk" """Agent runtime backend. ``"adk"`` (default) uses Google ADK's built-in LLM - flow. ``"cc"`` delegates the inner agent loop to the Claude Code SDK; ``"codex"`` - is reserved. Non-``adk`` runtimes are implemented under :mod:`veadk.runtime`.""" + flow. ``"codex"`` delegates the inner agent loop to the OpenAI Codex SDK. + Non-``adk`` runtimes are implemented under :mod:`veadk.runtime`.""" enable_a2ui: bool = False """Enable A2UI (agent-driven UI). When True, a `SendA2uiToClientToolset` is diff --git a/veadk/runtime/__init__.py b/veadk/runtime/__init__.py index 350e10cd..53df3422 100644 --- a/veadk/runtime/__init__.py +++ b/veadk/runtime/__init__.py @@ -18,8 +18,7 @@ - ``"adk"`` (default): Google ADK's built-in ``BaseLlmFlow`` (handled directly in :class:`veadk.agent.Agent`, no runtime object). -- ``"cc"``: the Claude Code SDK as the agent harness. -- ``"codex"``: reserved for a future Codex SDK runtime. +- ``"codex"``: the OpenAI Codex SDK as the agent harness. """ from __future__ import annotations @@ -44,17 +43,6 @@ def get_runtime(name: str) -> BaseRuntime: NotImplementedError: If ``name`` is a known-but-unimplemented runtime. ValueError: If ``name`` is unknown. """ - if name == "cc": - try: - from veadk.runtime.cc import ClaudeCodeRuntime - except ModuleNotFoundError as e: - raise ImportError( - f"The 'cc' runtime requires extra dependencies (missing: {e.name}). " - "Install them with: pip install claude-agent-sdk fastapi uvicorn" - ) from e - - return ClaudeCodeRuntime() - if name == "codex": try: from veadk.runtime.codex import CodexRuntime diff --git a/veadk/runtime/base_runtime.py b/veadk/runtime/base_runtime.py index bf7869a7..5f148fde 100644 --- a/veadk/runtime/base_runtime.py +++ b/veadk/runtime/base_runtime.py @@ -70,7 +70,7 @@ class BaseRuntime(ABC): Attributes: name (str): Stable identifier of the runtime, matching the value passed - to ``Agent(runtime=...)`` (for example ``"cc"``). + to ``Agent(runtime=...)`` (for example ``"codex"``). """ name: str = "base" diff --git a/veadk/runtime/cc/__init__.py b/veadk/runtime/cc/__init__.py deleted file mode 100644 index 1036c23e..00000000 --- a/veadk/runtime/cc/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Claude Code SDK runtime.""" - -from veadk.runtime.cc.runtime import ClaudeCodeRuntime - -__all__ = ["ClaudeCodeRuntime"] diff --git a/veadk/runtime/cc/proxy.py b/veadk/runtime/cc/proxy.py deleted file mode 100644 index 0ebd6dc2..00000000 --- a/veadk/runtime/cc/proxy.py +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Anthropic ``/v1/messages`` translation shim for OpenAI-compatible backends. - -The Claude Code SDK only speaks the Anthropic protocol over HTTP -(``ANTHROPIC_BASE_URL``). When the user's model endpoint is OpenAI-compatible -(VeADK's default, e.g. Volcengine Ark), this module stands up a tiny in-process -FastAPI server that accepts Anthropic ``/v1/messages`` requests and forwards them -through :func:`litellm.anthropic_messages` to the OpenAI-compatible backend. The -Claude Code SDK is then pointed at the local server's URL. - -:func:`detect_endpoint_kind` decides whether a translation shim is needed at all. -""" - -from __future__ import annotations - -import asyncio -import json -from typing import Any, AsyncIterator, Literal, cast - -import litellm -import uvicorn -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse, StreamingResponse -from litellm.exceptions import APIError - -from veadk.utils.logger import get_logger - -logger = get_logger(__name__) - -EndpointKind = Literal["anthropic", "openai"] - -# Parameters accepted by litellm.anthropic_messages; anything else in the inbound -# request body is dropped to avoid leaking unsupported fields into the backend. -_PASSTHROUGH_KEYS = ( - "max_tokens", - "messages", - "metadata", - "stop_sequences", - "stream", - "system", - "temperature", - "thinking", - "tool_choice", - "tools", - "top_k", - "top_p", -) - - -def detect_endpoint_kind(base_url: str | None, provider: str | None) -> EndpointKind: - """Detect whether a model endpoint speaks the Anthropic or OpenAI protocol. - - Provider is authoritative when present: any provider naming Anthropic/Claude - maps to ``"anthropic"``; any other explicit provider maps to ``"openai"``. - When the provider is empty, the endpoint host is probed as a fallback. - - Args: - base_url (str | None): The model API base URL. - provider (str | None): The configured model provider (e.g. ``"openai"``). - - Returns: - EndpointKind: ``"anthropic"`` or ``"openai"``. - """ - p = (provider or "").lower() - if "anthropic" in p or "claude" in p: - return "anthropic" - if p: - return "openai" - - host = (base_url or "").lower() - if "anthropic" in host: - return "anthropic" - return "openai" - - -class AnthropicShim: - """In-process Anthropic ``/v1/messages`` server backed by an OpenAI endpoint. - - Translates inbound Anthropic requests via :func:`litellm.anthropic_messages` - and forwards them to ``api_base`` using ``api_key`` with - ``custom_llm_provider="openai"``. Supports both streaming (SSE) and - non-streaming responses. - - Attributes: - api_base (str): OpenAI-compatible backend base URL. - api_key (str): API key for the backend. - url (str | None): Local server URL once started. - """ - - def __init__(self, api_base: str, api_key: str) -> None: - self.api_base = api_base - self.api_key = api_key - self.url: str | None = None - self._server: uvicorn.Server | None = None - self._task: asyncio.Task[Any] | None = None - self._app = self._build_app() - - def _build_app(self) -> FastAPI: - app = FastAPI() - - @app.post("/v1/messages") - async def messages(request: Request) -> Any: - body = await request.json() - model = body["model"] - stream = bool(body.get("stream", False)) - - call_kwargs: dict[str, Any] = { - key: body[key] for key in _PASSTHROUGH_KEYS if key in body - } - call_kwargs.update( - model=f"openai/{model}", - api_base=self.api_base, - api_key=self.api_key, - custom_llm_provider="openai", - # Anthropic-only params (e.g. `thinking`) that the OpenAI-compatible - # backend doesn't support are dropped rather than erroring. - drop_params=True, - # Surface backend errors immediately instead of retrying inside - # litellm (the SDK has its own retry layer on top). - num_retries=0, - ) - - result = await litellm.anthropic_messages(**call_kwargs) - - if stream: - # Pull the first chunk here (still inside the handler) so an - # immediate backend error like 401 propagates to the exception - # handler as a proper HTTP status instead of breaking a stream - # that already returned 200 (which makes the SDK retry). - stream_iter = cast(AsyncIterator[Any], result).__aiter__() - first = await anext(stream_iter, None) - return StreamingResponse( - _encode_sse(stream_iter, first), - media_type="text/event-stream", - ) - return JSONResponse(_to_dict(result)) - - @app.exception_handler(APIError) - async def _on_api_error(_request: Request, exc: APIError) -> JSONResponse: - # Surface backend errors (auth, not-found, ...) as Anthropic-format - # errors with the right status code, so the SDK fails immediately - # instead of retrying an opaque 500. - status = getattr(exc, "status_code", 500) or 500 - return JSONResponse( - status_code=status, - content={ - "type": "error", - "error": { - "type": _anthropic_error_type(status), - "message": getattr(exc, "message", str(exc)), - }, - }, - ) - - return app - - async def start(self) -> str: - """Start the server on an ephemeral local port and return its URL.""" - if self.url: - return self.url - - config = uvicorn.Config( - self._app, host="127.0.0.1", port=0, log_level="warning" - ) - server = uvicorn.Server(config) - # Do not hijack process signal handlers from a library context. - server.install_signal_handlers = lambda: None # type: ignore[method-assign] - self._server = server - self._task = asyncio.create_task(server.serve()) - - while not server.started: - await asyncio.sleep(0.02) - - port = server.servers[0].sockets[0].getsockname()[1] - self.url = f"http://127.0.0.1:{port}" - logger.info(f"Anthropic shim started at {self.url} -> {self.api_base}") - return self.url - - async def stop(self) -> None: - """Stop the server and await its task.""" - if self._server is not None: - self._server.should_exit = True - if self._task is not None: - await self._task - self.url = None - - -def _anthropic_error_type(status: int) -> str: - """Map an HTTP status code to an Anthropic API error ``type`` string.""" - return { - 400: "invalid_request_error", - 401: "authentication_error", - 403: "permission_error", - 404: "not_found_error", - 429: "rate_limit_error", - 529: "overloaded_error", - }.get(status, "api_error") - - -def _to_dict(obj: Any) -> dict[str, Any]: - """Normalize a litellm Anthropic response object into a plain dict.""" - if isinstance(obj, dict): - return obj - if hasattr(obj, "model_dump"): - return obj.model_dump() - return dict(obj) - - -def _encode_chunk(chunk: Any) -> bytes: - """Encode one litellm stream chunk as Anthropic SSE bytes.""" - if isinstance(chunk, (bytes, bytearray)): - return bytes(chunk) - if isinstance(chunk, str): - return chunk.encode() - data = _to_dict(chunk) - event_type = data.get("type", "message") - return f"event: {event_type}\ndata: {json.dumps(data)}\n\n".encode() - - -async def _encode_sse( - chunks: AsyncIterator[Any], first: Any = None -) -> AsyncIterator[bytes]: - """Re-encode litellm stream chunks as Anthropic-style SSE bytes. - - ``first`` is the already-pulled leading chunk (or ``None``). A mid-stream - backend error is emitted as an Anthropic ``error`` SSE event so the client - sees a terminal error rather than a silently truncated stream. - """ - if first is not None: - yield _encode_chunk(first) - try: - async for chunk in chunks: - yield _encode_chunk(chunk) - except APIError as exc: - status = getattr(exc, "status_code", 500) or 500 - err = { - "type": "error", - "error": { - "type": _anthropic_error_type(status), - "message": getattr(exc, "message", str(exc)), - }, - } - yield f"event: error\ndata: {json.dumps(err)}\n\n".encode() - - -# Reuse one shim per (api_base, api_key) for the lifetime of the process. -_SHIMS: dict[tuple[str, str], AnthropicShim] = {} - - -async def get_shim_url(api_base: str, api_key: str) -> str: - """Return a started shim URL for the given backend, creating it if needed.""" - key = (api_base, api_key) - shim = _SHIMS.get(key) - if shim is None: - shim = AnthropicShim(api_base=api_base, api_key=api_key) - _SHIMS[key] = shim - return await shim.start() diff --git a/veadk/runtime/cc/runtime.py b/veadk/runtime/cc/runtime.py deleted file mode 100644 index d46d3407..00000000 --- a/veadk/runtime/cc/runtime.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Claude Code SDK runtime for VeADK. - -Drives an agent invocation through the Claude Code SDK (``claude-agent-sdk``) -instead of ADK's built-in LLM flow, while the surrounding ``Runner`` keeps owning -session, memory and tracing. - -Key guarantees: - -- The model is always the one configured on the agent (or via ``ANTHROPIC_MODEL``); - if none resolves, the runtime fails fast. -- The SDK is fully isolated from the host's ``~/.claude`` settings via - ``setting_sources=[]``; all credentials/endpoint are injected through - ``ClaudeAgentOptions.env``. A wrong key therefore surfaces as an error rather - than silently falling back to the host's working credentials. -- OpenAI-compatible endpoints are reached through an in-process Anthropic shim - (see :mod:`veadk.runtime.cc.proxy`). -""" - -from __future__ import annotations - -import os -from typing import TYPE_CHECKING, AsyncGenerator - -from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query -from claude_agent_sdk.types import SystemPromptPreset - -from veadk.runtime.base_runtime import BaseRuntime, build_system_append -from veadk.runtime.cc.proxy import detect_endpoint_kind, get_shim_url -from veadk.runtime.cc.translate import build_prompt, sdk_message_to_events -from veadk.utils.logger import get_logger - -if TYPE_CHECKING: - from google.adk.agents.invocation_context import InvocationContext - from google.adk.events.event import Event - - from veadk.agent import Agent - -logger = get_logger(__name__) - -_LOCAL_SHIM_TOKEN = "veadk-local" - - -def _model_env(model: str) -> dict[str, str]: - """Pin every Claude Code model tier to ``model``. - - Claude Code resolves several model "tiers" (opus/sonnet/haiku, the small fast - model, etc.) from separate environment variables. Setting only - ``ANTHROPIC_MODEL`` lets the host's inherited ``ANTHROPIC_DEFAULT_*_MODEL`` - leak into sub-tasks, so we override the whole family to guarantee the agent - only ever calls the configured model. - """ - return { - "ANTHROPIC_MODEL": model, - "ANTHROPIC_DEFAULT_OPUS_MODEL": model, - "ANTHROPIC_DEFAULT_SONNET_MODEL": model, - "ANTHROPIC_DEFAULT_HAIKU_MODEL": model, - "ANTHROPIC_SMALL_FAST_MODEL": model, - } - - -class ClaudeCodeRuntime(BaseRuntime): - """Run an agent invocation via the Claude Code SDK.""" - - name = "cc" - - async def run_async( - self, agent: "Agent", ctx: "InvocationContext" - ) -> AsyncGenerator["Event", None]: - model = self._resolve_model(agent) - api_base = agent.model_api_base or os.getenv("ANTHROPIC_BASE_URL") - api_key = ( - agent.model_api_key - or os.getenv("ANTHROPIC_AUTH_TOKEN") - or os.getenv("ANTHROPIC_API_KEY") - ) - - kind = detect_endpoint_kind(api_base, agent.model_provider) - env = await self._build_env(kind, model, api_base, api_key) - - # Append the agent identity/instruction to Claude Code's own system - # prompt (preset), rather than replacing it. - append_text = build_system_append(agent) - system_prompt: SystemPromptPreset = {"type": "preset", "preset": "claude_code"} - if append_text: - system_prompt["append"] = append_text - options = ClaudeAgentOptions( - model=model, - env=env, - setting_sources=[], # never inherit the host's ~/.claude settings - system_prompt=system_prompt, - allowed_tools=[], - permission_mode="default", - ) - - prompt = build_prompt(ctx) - logger.info(f"cc runtime: model={model}, endpoint_kind={kind}") - - # ResultMessage is the terminal message; capture any error and raise only - # after the SDK stream completes (raising mid-iteration leaves the SDK's - # async generator in a running state and breaks its cleanup). - error: ResultMessage | None = None - async for message in query(prompt=prompt, options=options): - for event in sdk_message_to_events(message, agent.name, ctx.invocation_id): - yield event - if isinstance(message, ResultMessage) and message.is_error: - error = message - - if error is not None: - raise RuntimeError( - f"Claude Code runtime error (subtype={error.subtype}): {error.result}" - ) - - def _resolve_model(self, agent: "Agent") -> str: - name = agent.model_name - if isinstance(name, list): - name = name[0] if name else "" - name = name or os.getenv("ANTHROPIC_MODEL", "") - if not name: - raise ValueError( - "cc runtime requires a model: set Agent(model_name=...) " - "or the ANTHROPIC_MODEL environment variable." - ) - return name - - async def _build_env( - self, - kind: str, - model: str, - api_base: str | None, - api_key: str | None, - ) -> dict[str, str]: - if kind == "openai": - if not api_base or not api_key: - raise ValueError( - "cc runtime with an OpenAI-compatible endpoint requires both " - "model_api_base and model_api_key." - ) - base_url = await get_shim_url(api_base, api_key) - # Credentials are validated by the shim against the backend; the token - # the SDK sends to the local shim is irrelevant but must override any - # inherited one. - return { - "ANTHROPIC_BASE_URL": base_url, - "ANTHROPIC_AUTH_TOKEN": _LOCAL_SHIM_TOKEN, - "ANTHROPIC_API_KEY": _LOCAL_SHIM_TOKEN, - **_model_env(model), - } - - # Native Anthropic endpoint. - if not api_key: - raise ValueError( - "cc runtime with an Anthropic endpoint requires an API key." - ) - # Set both header variants to the configured key so whichever the - # endpoint reads, it is the configured one and never an inherited token. - return { - "ANTHROPIC_BASE_URL": api_base or "https://api.anthropic.com", - "ANTHROPIC_API_KEY": api_key, - "ANTHROPIC_AUTH_TOKEN": api_key, - **_model_env(model), - } diff --git a/veadk/runtime/cc/translate.py b/veadk/runtime/cc/translate.py deleted file mode 100644 index d86a8225..00000000 --- a/veadk/runtime/cc/translate.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Translation between ADK and the Claude Code SDK. - -Two directions: - -- :func:`build_prompt` flattens an ADK session (history + current message) into a - single prompt string for the SDK. ADK remains the single source of truth for - conversation state (stateless replay), which keeps multi-tenancy clean. -- :func:`sdk_message_to_events` maps SDK stream messages back into ADK - :class:`~google.adk.events.event.Event` objects so the surrounding ``Runner`` - can persist them unchanged. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from claude_agent_sdk import ( - AssistantMessage, - TextBlock, - ThinkingBlock, - ToolUseBlock, -) -from google.adk.events.event import Event -from google.genai import types - -if TYPE_CHECKING: - from claude_agent_sdk import Message - from google.adk.agents.invocation_context import InvocationContext - -_USER_PREFIX = "User" -_ASSISTANT_PREFIX = "Assistant" - - -def build_prompt(ctx: "InvocationContext") -> str: - """Render the session into a single prompt string for the SDK. - - Walks ``ctx.session.events`` in order and renders each turn as a - ``User:``/``Assistant:`` line. The new user message is already the last event - appended by the ``Runner``, so it naturally terminates the transcript. - ``thought`` parts are skipped; only ``text`` parts contribute. - - Args: - ctx (google.adk.agents.invocation_context.InvocationContext): Invocation - context holding the session. - - Returns: - str: The flattened transcript. When the session has a single user turn - this is just that message. - """ - lines: list[str] = [] - for event in ctx.session.events: - if event.content is None or not event.content.parts: - continue - text = "".join( - part.text for part in event.content.parts if part.text and not part.thought - ).strip() - if not text: - continue - prefix = _USER_PREFIX if event.author == "user" else _ASSISTANT_PREFIX - lines.append(f"{prefix}: {text}") - - # Single user turn: pass the raw message instead of a labelled transcript. - if len(lines) == 1 and lines[0].startswith(f"{_USER_PREFIX}: "): - return lines[0][len(_USER_PREFIX) + 2 :] - - return "\n".join(lines) - - -def sdk_message_to_events( - message: "Message", author: str, invocation_id: str -) -> list[Event]: - """Convert one SDK stream message into ADK events. - - Only :class:`~claude_agent_sdk.AssistantMessage` carries renderable content - and is translated; other message types (user/system/result) produce no - events here. Final usage/session bookkeeping is handled by the caller. - - Args: - message (claude_agent_sdk.Message): A message yielded by the SDK stream. - author (str): Event author (the agent name). - invocation_id (str): The ADK invocation id to stamp on each event. - - Returns: - list[google.adk.events.event.Event]: Zero or more events for this message. - """ - if not isinstance(message, AssistantMessage): - return [] - - events: list[Event] = [] - for block in message.content: - part: types.Part | None = None - if isinstance(block, TextBlock): - part = types.Part(text=block.text) - elif isinstance(block, ThinkingBlock): - part = types.Part(text=block.thinking, thought=True) - elif isinstance(block, ToolUseBlock): - part = types.Part( - function_call=types.FunctionCall( - id=block.id, name=block.name, args=block.input - ) - ) - - if part is None: - continue - - events.append( - Event( - invocation_id=invocation_id, - author=author, - content=types.Content(role="model", parts=[part]), - ) - ) - - return events diff --git a/veadk/runtime/codex/translate.py b/veadk/runtime/codex/translate.py index 5e699941..643056eb 100644 --- a/veadk/runtime/codex/translate.py +++ b/veadk/runtime/codex/translate.py @@ -15,9 +15,7 @@ """Translation between ADK and the Codex SDK. - :func:`build_prompt` flattens an ADK session into a single prompt string - (stateless replay; ADK stays the single source of truth). This mirrors the - ``cc`` runtime's helper but is duplicated here so the ``codex`` package does - not import ``claude_agent_sdk``. + (stateless replay; ADK stays the single source of truth). - :func:`result_to_events` maps a Codex run result into ADK events. """