Back to Introducing the Vidbyte SDK
July 27th, 2026

Research Report Agent

Research

Structured output

Tools

Sources

8 min read

The problem

Answering one research question well costs an hour of open tabs, and at the end you still cannot say which claim came from where.

How it goes today

  • You paste the question into a chat model and get confident prose with no sources you can check.
  • So you search manually, skim eight pages, and keep the findings in your head or a scratch doc.
  • Next week the same question comes up and none of that work is reusable.

What you'll build

  • An agent that takes a question, searches and fetches until it has real evidence, then writes the brief.
  • A typed brief — summary, findings, open questions — so the output is data, not a wall of text.
  • Rules that stop it answering from memory when it has not looked anything up.

Done looks like

  • Every finding names the URL a tool actually returned it from.
  • The agent cannot finish a run without at least two tool calls across two different tools.
  • You read reply.structured.findings as Python objects, with no parsing code of your own.
  • Thin evidence shows up under open questions instead of being smoothed over.

Paste into Claude, Codex, Cursor, or another coding agent to scaffold this harness.

# Context: Vidbyte SDK Vidbyte SDK is a Python package (import as `vidbyte`) for building agent workflows yourself. Use it when you want to own the agent loop: system prompts, model/provider choice, tools, managed context, middleware, pipelines, tracing, evals, and multi-agent composition. Core mental model: 1. Create an Agent or BaseAgent. 2. Attach a system prompt, provider/model, optional tools, context, middleware, and runtime choices. 3. Call run() or arun(). 4. Let the SDK handle message assembly, tool schemas/calls, iteration limits, and optional pipelines. Public package boundary: reusable local agent infrastructure. It does not ship private Vidbyte learning models or proprietary platform internals.

Build it step by step

Every step adds to the same file. By the end you have one runnable research_agent.py.

  1. 01

    Decide what a finished brief looks like

    output_schema

    Write the answer's shape before writing any agent code. Declaring a Pydantic model here is what turns the output from prose into data you can render, store, or diff.

    + the shape of a finished brief

    from pydantic import BaseModel, Field class Finding(BaseModel): claim: str = Field(description="One specific finding, stated plainly.") source_url: str = Field(description="The URL a tool returned this claim from.") class ResearchBrief(BaseModel): summary: str = Field(description="Two or three sentences a busy reader can skim.") findings: list[Finding] = Field(min_length=3, description="Evidence, one claim per item.") open_questions: list[str] = Field(description="What the evidence did not settle.")
  2. 02

    Give it a way to gather evidence

    @tool

    A tool is an ordinary Python function with a decorator. The SDK reads the signature and docstring to build the schema it shows the model, so you never hand-write a JSON tool definition.

    + search_web and fetch_page

    from vidbyte import tool @tool def search_web(query: str) -> list[dict]: """Search the web and return result titles, snippets, and URLs.""" # Call your search provider here and return one dict per result. ... @tool def fetch_page(url: str) -> str: """Fetch one URL and return its readable text.""" ...
  3. 03

    Stop it answering before it researches

    output contracts

    Output contracts are deterministic floors the runtime checks when the agent tries to finish. An unmet floor sends the agent back to work, which holds better than asking politely in the system prompt.

    + a floor on effort before finishing

    from vidbyte.agents import AgentLoopSettings from vidbyte.agents.contracts import MinDistinctTools, MinToolCalls loop_settings = AgentLoopSettings( max_iterations=10, output_contracts=(MinToolCalls(2), MinDistinctTools(2)), max_contract_rejections=2, )
  4. 04

    Keep a record of what it did

    Trace.debug

    Trace.debug collects every model call and tool call into a plain list you can print. Use it while you are tuning the prompt, then swap in a hosted tracer later without touching the agent.

    + a readable record of the run

    from vidbyte import Trace events: list[dict] = [] trace = Trace.debug(events)
  5. 05

    Assemble the agent

    Agent + ContextWindow

    Everything built so far is passed in one place. The compact_tool_outputs preset shortens tool results as they age, so a few fetched pages do not crowd the question out of the context window.

    + the agent, wired from the four pieces above

    from vidbyte import Agent, ContextWindow agent = Agent( name="research-reporter", system_prompt=( "Research the question with the tools before answering. " "Every finding must name the source it came from. " "Say plainly when the evidence is thin." ), provider="openai", model_name="gpt-4.1", tools=[search_web, fetch_page], output_schema=ResearchBrief, algorithm=ContextWindow.preset.compact_tool_outputs, agent_loop_settings=loop_settings, trace=trace, )
  6. 06

    Run it and read the object

    reply.structured

    Because a schema was declared, reply.structured is a validated ResearchBrief — never None, never a string you have to parse. get_cost_usd totals what the run actually spent.

    + the run, and what it cost

    reply = await agent.arun("What are the main approaches teams use for AI code review in 2026?") brief = reply.structured print(brief.summary) for finding in brief.findings: print(f"- {finding.claim} ({finding.source_url})") print("open questions:", brief.open_questions) print("run cost (usd):", agent.get_cost_usd())
  7. 07

    The finished harness

    The six steps above, in one file.

    research_agent.py — complete

    """Research Report Agent - gathers evidence with tools, returns a typed brief.""" import asyncio from pydantic import BaseModel, Field from vidbyte import Agent, ContextWindow, Trace, tool from vidbyte.agents import AgentLoopSettings from vidbyte.agents.contracts import MinDistinctTools, MinToolCalls class Finding(BaseModel): claim: str = Field(description="One specific finding, stated plainly.") source_url: str = Field(description="The URL a tool returned this claim from.") class ResearchBrief(BaseModel): summary: str = Field(description="Two or three sentences a busy reader can skim.") findings: list[Finding] = Field(min_length=3, description="Evidence, one claim per item.") open_questions: list[str] = Field(description="What the evidence did not settle.") @tool def search_web(query: str) -> list[dict]: """Search the web and return result titles, snippets, and URLs.""" ... @tool def fetch_page(url: str) -> str: """Fetch one URL and return its readable text.""" ... events: list[dict] = [] agent = Agent( name="research-reporter", system_prompt=( "Research the question with the tools before answering. " "Every finding must name the source it came from. " "Say plainly when the evidence is thin." ), provider="openai", model_name="gpt-4.1", tools=[search_web, fetch_page], output_schema=ResearchBrief, algorithm=ContextWindow.preset.compact_tool_outputs, agent_loop_settings=AgentLoopSettings( max_iterations=10, output_contracts=(MinToolCalls(2), MinDistinctTools(2)), max_contract_rejections=2, ), trace=Trace.debug(events), ) async def main() -> None: reply = await agent.arun("What are the main approaches teams use for AI code review in 2026?") brief = reply.structured print(brief.summary) for finding in brief.findings: print(f"- {finding.claim} ({finding.source_url})") print("open questions:", brief.open_questions) print("run cost (usd):", agent.get_cost_usd()) if __name__ == "__main__": asyncio.run(main())