Skip to the page

Stirrup

Stirrup's built-in web_search calls the Brave Search API and needs BRAVE_API_KEY. Pass a SpecStream Tool in the Agent's tools argument and the same web_search name runs on the SpecStream Search API instead: one Python file, no fork, and a direct answer with its page on top of the results.

SpecStream for StirrupAgent frameworks & builders

What you need

  • Python and Stirrup 0.2.1 or later (pip install stirrup). The MCP option below also needs pip install 'stirrup[mcp]' 'mcp<2'.
  • A SpecStream account. New accounts get $10 of credit on sign-up, no card. Create one.
  1. 1. Create an API key

    In your SpecStream workspace, open Agents & API, go to the API keys tab, and click Create API key. Keep the permissions that are already on, including Authorize paid work, and click Set a total limit to cap what this key can spend. Copy the key from the next screen. SpecStream shows it once.

    Set it in the environment where the agent runs. Leave BRAVE_API_KEY unset: without it, Stirrup's WebToolProvider returns only its page fetch tool, so the SpecStream tool is the one named web_search.

    Terminal
    export SPECSTREAM_API_KEY="ss_live_..."
    unset BRAVE_API_KEY
  2. 2. Define the SpecStream search tool

    A Stirrup Tool is a name, a description, a Pydantic parameters model and an executor, and the executor can be async. This one sends the query to POST https://api.specstream.ai/v1/web-search and returns XML in the same form as Stirrup's Brave tool: one <result> with <title>, <url> and <description> for each page. When SpecStream finds one answer to the question, an <answer> with the value and the page that prints it comes first.

    Stirrup already depends on httpx and pydantic, so the file needs nothing new installed:

    specstream_tools.py
    import os
    from html import escape
    import httpx
    from pydantic import BaseModel, Field
    from stirrup import Tool, ToolResult, ToolUseCountMetadata
    SPECSTREAM_API = "https://api.specstream.ai"
    def _headers() -> dict[str, str]:
    return {"Authorization": f"Bearer {os.environ['SPECSTREAM_API_KEY']}"}
    class WebSearchParams(BaseModel):
    query: str = Field(description="The question or search terms, in plain words. site:, quotes and -word work.")
    async def web_search(params: WebSearchParams) -> ToolResult[ToolUseCountMetadata]:
    async with httpx.AsyncClient(timeout=60) as client:
    response = await client.post(
    f"{SPECSTREAM_API}/v1/web-search",
    headers=_headers(),
    json={"q": params.query},
    )
    if response.status_code != 200:
    return ToolResult(
    content=f"<error>{escape(response.text[:2000])}</error>",
    success=False,
    metadata=ToolUseCountMetadata(),
    )
    data = response.json()["data"]
    lines = ["<results>"]
    answer = data.get("answer")
    if answer:
    lines.append(
    f"<answer><value>{escape(str(answer.get('value') or ''))}</value>"
    f"<url>{escape(str(answer.get('page') or ''))}</url></answer>"
    )
    for source in data.get("sources", []):
    lines.append(
    f"<result><title>{escape(source.get('title') or '')}</title>"
    f"<url>{escape(source['url'])}</url>"
    f"<description>{escape(source.get('snippet') or '')}</description></result>"
    )
    lines.append("</results>")
    return ToolResult(content="\n".join(lines), metadata=ToolUseCountMetadata())
    WEB_SEARCH_TOOL = Tool(
    name="web_search",
    description="Search the live web with SpecStream. Returns a direct answer with its page when one is found, then the results with title, URL and description as XML.",
    parameters=WebSearchParams,
    executor=web_search,
    )
  3. 3. Pass it in the Agent's tools argument

    default_tools() is LocalCodeExecToolProvider() and WebToolProvider(). List those two yourself and add WEB_SEARCH_TOOL. The agent keeps code_exec and fetch_web_page, and its web_search calls now go to SpecStream:

    agent.py
    import asyncio
    from stirrup import Agent
    from stirrup.clients.chat_completions_client import ChatCompletionsClient
    from stirrup.tools import LocalCodeExecToolProvider, WebToolProvider
    from specstream_tools import WEB_SEARCH_TOOL
    async def main() -> None:
    client = ChatCompletionsClient(
    base_url="https://openrouter.ai/api/v1",
    model="anthropic/claude-opus-5",
    max_tokens=8_192,
    context_window_tokens=1_000_000,
    )
    agent = Agent(
    client=client,
    name="researcher",
    max_turns=15,
    tools=[LocalCodeExecToolProvider(), WebToolProvider(), WEB_SEARCH_TOOL],
    )
    async with agent.session(output_dir="./output/research") as session:
    finish_params, history, metadata = await session.run(
    "What is the population of Australia over the last 3 years? Search the web and cite each page you use."
    )
    print(finish_params)
    if __name__ == "__main__":
    asyncio.run(main())
  4. 4. Run it

    Run python agent.py. The metadata that session.run returns counts the web_search calls under that name, as it did with Brave. Every charged call is kept in your account's search history, with its query, answer and price.

What changes from Brave

Stirrup's own tool asks Brave for 5 results and returns each one's title, URL and description. The SpecStream tool returns its results in the same tags, and adds the answer and the page it was read from when the question has one. Search operators in the query hold on every result: site:, -site:, -word, "phrase", filetype:, and after: or before: with a date. A query in another language is searched in that language.

The Search API scores 97.9 F-score on all 1,000 SimpleQA Verified questions and 64.0 item F1 on all 200 WideSearch tasks. Scores and method are on Benchmarks.

Read pages with the Extract API

Stirrup's fetch_web_page reads a page from your own machine with one plain request. To read pages through SpecStream instead, add this tool to specstream_tools.py. It sends up to 20 URLs to POST https://api.specstream.ai/v1/extract and returns each page's main text as markdown. A page that refuses a plain read is tried as a browser would ask for it, then in a real browser, then from the site's own feed, then from its newest public archive capture. PDF, Word and Excel files come back as text too.

Use both tools without WebToolProvider

With the Extract API reading pages, leave WebToolProvider out:

Or connect the SpecStream MCP server

Install the extra with pip install 'stirrup[mcp]' 'mcp<2'. Stirrup 0.2.1 imports streamablehttp_client, which the 2.x releases of the mcp package no longer have, so stirrup.tools.mcp fails to import on them. MCPToolProvider connects to a remote server over Streamable HTTP when the config has a url. Build the config in code so the key comes from the environment. Each tool is named after the server, so the agent gets specstream__search_web, specstream__read_pages, and the Deep Search tools that return every match on the live web as structured rows.

Deep Search quotes the most a request can cost, and nothing runs until that maximum is approved. Add USER_INPUT_TOOL so the agent asks you before it approves one:

Cost

Each web_search call is one Search API call at $5 per 1,000 searches of up to 10 results. Each page read is one Extract API page at $0.80 per 1,000 pages, and a page with no text is not charged. Both come out of your balance with no quote and no approval, and a call that returns nothing costs nothing. New accounts start with $10 of credit, no card. To search at a fifth of the price, add "speed": "fast" to the JSON body: the results come back without the answer. Get your key on the Search API screen of your workspace, and read Balance, quotes, and billing for the details.

When a call fails

  • 401: no key was sent, or the key is unknown, expired or revoked. Check SPECSTREAM_API_KEY.
  • 402: the balance is empty, or the call would pass the account's monthly limit.
  • 403: the call would pass the key's spending limit, or the key cannot spend. Raise the limit under Agents & API.
  • 503 with Retry-After: wait that many seconds and send it again. Nothing is charged.
  • The tool returns the error body with success=False, so the agent sees the reason and can try again.

Remove

Take WEB_SEARCH_TOOL out of tools, or go back to tools=default_tools(), and set BRAVE_API_KEY again. To stop the key everywhere, delete it under Agents & API. Other tools to connect are in Connect your tools. Questions go to [email protected].

More

SpecStream for Stirrup covers what it does in Stirrup on one page. Every other tool has its guide in Connect your tools. You see the most a request can cost before it starts, and the matches are paid from your balance. Billing has the details. For help, email [email protected].