项目文件夹

文件
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

228 行
9.0 KiB
Python

"""HeadroomBundle — single-helper MCP wiring for a Strands Agent.
The cleanest production setup for Strands is the same kit
``headroom wrap claude`` installs for Claude Code, restated as
Strands-native primitives:
* **Headroom MCP** (``headroom mcp serve``) — exposes
``headroom_retrieve`` / ``headroom_compress`` / ``headroom_stats``
via stdio. The proxy emits ``Retrieve original: hash=...`` markers
in compressed content; the LLM calls ``headroom_retrieve`` when it
needs the original; Strands' MCP dispatcher resolves it via this
server. Works identically in streaming and non-streaming.
* **tokensave MCP** — the primary coding-task compressor: a local
semantic code-graph server (``tokensave serve``) the agent queries
for symbols, call chains, and impact analysis instead of reading
whole files. Requires the ``tokensave`` binary on PATH.
* **Serena MCP** — the backup coding-task compressor (symbol search,
references, etc.), auto-installed via ``uvx`` on first launch.
Off by default; enable with ``enable_serena_mcp=True``.
* **HeadroomHookProvider** — the RTK-equivalent for Strands.
Compresses tool outputs in-place via ``AfterToolCallEvent`` so
verbose JSON / log / search outputs are shrunk before they
pollute the agent's context.
Pattern
-------
.. code-block:: python
from strands import Agent
from strands.models.openai import OpenAIModel
from headroom.integrations.strands import HeadroomBundle
model = OpenAIModel(
model_id="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
client_args={"base_url": "http://127.0.0.1:8787/v1", "api_key": "x"},
)
bundle = HeadroomBundle(proxy_url="http://127.0.0.1:8787")
agent = Agent(
model=model,
tools=bundle.tools, # Strands starts the MCP subprocesses on first use
hooks=bundle.hooks,
)
response = agent("Search the codebase for the auth middleware.")
Lifecycle
---------
The bundle does **not** start the MCP subprocesses itself —
:class:`strands.tools.mcp.MCPClient` is lazily started by Strands'
``Agent`` when it loads tools, and stopped when the agent is torn
down. Construct the bundle, hand its ``tools`` / ``hooks`` to the
agent, and let Strands own the lifecycle. This matches Strands'
contract: MCP clients passed via ``tools=[...]`` MUST be unstarted.
The bundle does **not** start the proxy either — it connects to one.
Production deploys run the proxy as a long-lived service
(ECS / k8s / EC2); local-dev users start it manually with
``headroom proxy``. This keeps the bundle stateless and lets the
proxy scale independently of the agent fleet.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from functools import partial
from typing import Any
# Strands + MCP SDK imports are required dependencies of this bundle —
# fail loud on import so a missing dep surfaces at construction time,
# not three frames deep inside an Agent call.
from mcp import StdioServerParameters # noqa: E402
from mcp.client.stdio import stdio_client # noqa: E402
from strands.tools.mcp import MCPClient # noqa: E402
from headroom import HeadroomConfig
from headroom.mcp_registry.install import (
DEFAULT_PROXY_URL,
build_headroom_spec,
build_serena_spec,
build_tokensave_spec,
)
from .hooks import HeadroomHookProvider
logger = logging.getLogger(__name__)
#: Default Serena context — see https://github.com/oraios/serena for the
#: full context catalog. ``ide-assistant`` is the closest match for a
#: code-aware agent loop (the same context ``headroom wrap claude`` uses).
DEFAULT_SERENA_CONTEXT = "ide-assistant"
def _client_for(spec: Any) -> MCPClient:
params = StdioServerParameters(
command=spec.command,
args=list(spec.args),
env=dict(spec.env) if spec.env else None,
)
# `partial` (not lambda) so mypy can infer the callable's signature.
return MCPClient(partial(stdio_client, params))
def _make_headroom_client(proxy_url: str) -> MCPClient:
return _client_for(build_headroom_spec(proxy_url))
def _make_tokensave_client() -> MCPClient:
return _client_for(build_tokensave_spec())
def _make_serena_client(context: str) -> MCPClient:
return _client_for(build_serena_spec(context))
@dataclass
class HeadroomBundle:
"""Single helper that hands a Strands Agent every Headroom integration.
Attributes:
proxy_url: HTTP URL the Headroom MCP server should contact for
retrieval. Default :data:`DEFAULT_PROXY_URL`
(``http://127.0.0.1:8787``).
serena_context: Serena context label. Default ``"ide-assistant"``.
enable_headroom_mcp: Include the Headroom MCP server. Default True.
enable_tokensave_mcp: Include the tokensave MCP server — the primary
coding-task compressor. Default True. Requires the ``tokensave``
binary on PATH (``tokensave serve``).
enable_serena_mcp: Include the Serena MCP server — the backup
coding-task compressor. Default False (tokensave is primary).
Enabling adds the ``uvx`` first-launch download.
enable_hooks: Include :class:`HeadroomHookProvider` for in-place
tool-output compression (the RTK-equivalent for Strands).
Default True.
config: Optional :class:`HeadroomConfig` passed to
:class:`HeadroomHookProvider`. Default uses framework
defaults.
The bundle is **stateless** w.r.t. subprocess management — Strands'
``Agent`` owns the MCP subprocess lifecycle once you pass
``bundle.tools`` to it. Constructing a bundle is cheap; the
subprocesses don't start until ``Agent`` calls ``load_tools``.
"""
proxy_url: str = DEFAULT_PROXY_URL
serena_context: str = DEFAULT_SERENA_CONTEXT
enable_headroom_mcp: bool = True
# tokensave is the primary coding-task compressor; Serena is the backup
# and stays off unless explicitly enabled.
enable_tokensave_mcp: bool = True
enable_serena_mcp: bool = False
# The proxy is the single source of truth for compression — it sees
# the full message list, owns CompressionPolicy, owns PrefixCacheTracker,
# and places `cache_control` breakpoints. The in-process hook
# (HeadroomHookProvider) is an optimisation for memory/network when
# Strands runs on a different host or holds very long conversations.
# Default is OFF so the bundle stays "one helper, just the proxy
# does the work" for the typical case. Flip on for long-running or
# cross-host deploys.
enable_hooks: bool = False
config: HeadroomConfig | None = None
_headroom_mcp: MCPClient | None = field(default=None, init=False, repr=False, compare=False)
_tokensave_mcp: MCPClient | None = field(default=None, init=False, repr=False, compare=False)
_serena_mcp: MCPClient | None = field(default=None, init=False, repr=False, compare=False)
_hook: HeadroomHookProvider | None = field(default=None, init=False, repr=False, compare=False)
def __post_init__(self) -> None:
if self.enable_headroom_mcp:
self._headroom_mcp = _make_headroom_client(self.proxy_url)
logger.info(
"HeadroomBundle: Headroom MCP client constructed (proxy_url=%s)",
self.proxy_url,
)
if self.enable_tokensave_mcp:
self._tokensave_mcp = _make_tokensave_client()
logger.info("HeadroomBundle: tokensave MCP client constructed (primary)")
if self.enable_serena_mcp:
self._serena_mcp = _make_serena_client(self.serena_context)
logger.info(
"HeadroomBundle: Serena MCP client constructed (backup, context=%s)",
self.serena_context,
)
if self.enable_hooks:
self._hook = HeadroomHookProvider(config=self.config)
logger.info("HeadroomBundle: HeadroomHookProvider attached")
@property
def tools(self) -> list[Any]:
"""MCP clients to hand to ``Agent(tools=...)``.
Returned MCPClient instances are **unstarted** — Strands' Agent
starts them on first use and stops them on teardown.
"""
out: list[Any] = []
if self._headroom_mcp is not None:
out.append(self._headroom_mcp)
if self._tokensave_mcp is not None:
out.append(self._tokensave_mcp)
if self._serena_mcp is not None:
out.append(self._serena_mcp)
return out
@property
def hooks(self) -> list[Any]:
"""Hook providers to hand to ``Agent(hooks=...)``."""
return [self._hook] if self._hook is not None else []
@property
def headroom_mcp(self) -> MCPClient | None:
"""Direct handle to the Headroom MCPClient (for advanced callers)."""
return self._headroom_mcp
@property
def tokensave_mcp(self) -> MCPClient | None:
"""Direct handle to the tokensave MCPClient (for advanced callers)."""
return self._tokensave_mcp
@property
def serena_mcp(self) -> MCPClient | None:
"""Direct handle to the Serena MCPClient (for advanced callers)."""
return self._serena_mcp