chopratejas--headroom
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
228 行
9.0 KiB
Python
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
|