Deep-research agents

Give gligabao a topic and an agent researches it in the background: it searches the web, reads pages, checks claims against more than one source, works with your knowledge base, and ends with a report with its source URLs. Start it over REST, MCP or the CLI, and follow it state by state.

A plain tool-calling loop

No agent framework. An agent is a model, a system prompt, a list of tools and three limits (model, answer size, steps). Each step is one model call.

1. Ask

Send the conversation so far to the model (Anthropic's Messages API) with the tool list.

2. Run the tools

Run every tool the model asked for, one after the other, and send the results back in the next turn. A tool that fails (a page that cannot be read, a search that errors) is not a failed run: the error is sent to the model, which decides what to do.

3. Repeat or report

If the model asked for no tool, its text is the report. Otherwise go back to 1.

The last step cannot use tools: at step max_steps the request carries tool_choice: none, so a run always ends with a report (or an error). A narrow question usually ends well before; a broad topic uses every step. The conversation only grows: nothing is summarised or dropped, so later steps cost more input tokens than early ones. That is why the number of steps is capped (default 20, at most 40) and why the text of a page is cut. It needs an Anthropic API key (ANTHROPIC_API_KEY or anthropic_api_key in gligabao.toml); without it a research cannot start (503).

What it is told to do

The system prompt is all that steers it, besides max_steps. How many searches and pages it uses, and when it stops, is the model's decision.

Research

Search the web several times with different queries, read the most relevant pages with fetch_url, check claims against more than one source, and stop when it can answer well.

Write a report

A short summary first, then the details, then the list of source URLs used. It says what it could not find out and never guesses. The user turn is the topic as given.

Use the knowledge base

query it before searching the web and to check what it found against it; remember the important parts (short self-contained snippets, never whole pages) with the URL as the source.

Its tools

Two web tools, and, when the server has an LLM for the memory, two for the internal knowledge base. As a job it also has the sub-agent tools.

ToolInputWhat it does
web_search{"query"}Not a bare search engine: a separate call to Anthropic's cloud web search tool (web_search_20250305), with at most 2 searches, 1024 output tokens and a summary of at most 300 words with the facts and dates. The agent sees only that briefing and the Sources: (one line per page: URL and title), never raw results. $0.01 per search on top of the tokens.
fetch_url{"url"}A plain GET (redirects followed, no cookies, no JavaScript) of an http:// or https:// URL. At most 1 MiB is downloaded and the HTML is turned into text crudely on purpose (scripts, styles and tags removed). The model gets the first 15,000 characters. A status other than 200 is an error the model is told about.
query{"question"}Asks the internal knowledge base of the user who started the run, in the run's dataset: the same pipeline as POST /v1/gb/query, run in process. Returns the answer and the facts it is based on (at most 15). A dataset that does not exist yet answers that the knowledge base is empty.
remember{"text", "source", "sync"}Submits a snippet (at most 20,000 characters, Source: <url> put in front) to the remember manager as the user. sync=false (default) returns as soon as the text is queued and the agent goes on; sync=true blocks until it is stored, for when the agent needs it right away to query for it.

fetch_url fetches whatever URL the model chooses, from the machine gligabao runs on, with no allow-list: a page on a private network is reachable if the server can reach it. Keep that in mind where the topic comes from untrusted users.

Runs and states

You start an agent, it returns at once with a run, and you follow what it does until it ends. A run (gb_agent_research_runs) is one execution on a topic, with a status of running, completed (it ended with a report) or failed (a model call failed, or the server stopped; error says why). A run left running by a process that died is marked failed when the server starts again.

A state (gb_agent_research_states) is one stage of its work. A row is inserted every time the agent moves to the next one; the table is append-only and the state of the run is always the last row.

StateWhat the agent is doing
startingthe run is recorded, the first model call is not made yet
thinkinga model call is running: it decides what to do next (step 3 of 20)
searchingweb_search is running (the query)
readingfetch_url is running (the URL)
queryingquery is running (the question)
rememberingremember is running (async: or sync: , then the start of the text)
spawningspawn_subagent is running
waitingwaiting for sub-agents (wait_subagents) or for a message
messagingsend_message, list_siblings or read_messages is running
using_toolany other tool is running (its name)
writing_reportthe last model call, which may not use tools, is running: its text is the report
completed / failedthe run ended (recorded too, so the states are the whole history)

Each state has seq, step, detail, error, started_at and finished_at (millisecond precision). A tool that fails sets the error of its state but the run can still complete. The run keeps steps, tool_calls, input_tokens, output_tokens and cost_usd, updated after every model call and tool run, so the cost can be followed while it works.

Sub-agents

An agent can split its work: it spawns sub-agents, other research agents that each take a sub-task, each with a reason, and sibling sub-agents can list each other and message each other. It is on for every agent that runs as a job; the limits bound what it can cost.

spawn_subagent

{"name", "reason", "task", "max_steps"}: starts a sub-agent in the background and returns at once, so the parent can spawn several and go on. The reason (required, at most 300 characters) says why the sub-agent has to exist; a missing one sends the agent back to decide whether it needs the sub-agent at all. The task must be self-contained; the sub-agent runs on the same model and dataset, so siblings share what they remember.

wait_subagents

{"names"}: blocks until those sub-agents have ended and returns each one's report (or its error). A failed sub-agent does not fail its parent: the parent is told and decides.

list_siblings

No input. For a sub-agent: its siblings, one line each (- name [working|ended]: reason), read fresh at each call. It is how it finds out whether someone else covers a related part of the topic, or can give it a fact, before it messages anyone.

send_message / read_messages

Only siblings (the children of one parent) can message each other, by name. send_message never waits; read_messages takes out what arrived, optionally waiting up to 60 s. Messages are in memory, behind a replaceable Messenger interface.

Tracked like any run

A sub-agent is a run with its own states, usage and report, plus a parent_id, a name and a reason. GET /v1/gb/research?parent_id= lists the sub-agents of a run. When an agent ends, the sub-agents it spawned that still work are cancelled.

Prompts in their own files

The prompts are Markdown files in internal/agent/prompts/, embedded in the binary and joined by agent: the base, the knowledge base, when and how to spawn, who a sub-agent is, and messaging. The spawn prompt makes not spawning the default: only for a topic that splits into independent parts, each a research of its own, never for a narrow or factual question.

Limits

LimitValue
Sub-agents of one agent5
Sub-agents in the whole tree of a research10
Generations of sub-agents2 (a root spawns sub-agents, which may spawn their own; those get no spawn_subagent)
max_steps of a sub-agentdefault 10, at most 40

A sub-agent's cost is its own (cost_usd of its run) and is not added to its parent's.

Using it

REST, MCP and the CLI use the same handlers, so they behave the same. All routes are user routes: user_id is required, and a user only sees their own runs (another user's run is a 404).

REST

POST /v1/gb/research starts an agent (202; 409 if the user already has 3 working, 503 without an Anthropic key). GET /v1/gb/research lists runs (active, status, parent_id, limit). GET /v1/gb/research/{id} reads one with its states and, once completed, its report.

MCP

research_start, research_list and research_get, with the same arguments. Start it, then read it with research_get until its status is completed or failed.

CLI

gligabao research start|ls|get. start --wait waits for the end and prints the finished run (exit code 1 if it failed); --raw prints only the report.

The body of POST /v1/gb/research: topic (required, at most 2000 characters), max_steps (default 20, at most 40), model (must support Anthropic's web search tool; default: the server's) and dataset (the dataset of the knowledge base the agent queries and remembers into, default main).

terminal
$ gligabao research start "How do solid-state batteries compare with lithium-ion?" --wait --raw
$ gligabao research ls --active
$ gligabao research get 7

# or over REST
$ curl -X POST localhost:8080/v1/gb/research -d '{"user_id": "u1", "topic": "...", "max_steps": 20}'

What a run costs

Each run costs money, and the cost is recorded and updated as it goes. It is the sum of:

The loop

One model call per step (up to max_steps), each with the whole conversation as input, priced per token by the model's price table.

The web searches

The model calls inside web_search, in tokens, plus $0.01 per search (up to 2 per call).

The worst case is bounded by max_steps and the number of tool calls per step, but there is no cap in dollars yet. The LLM calls of remember and query are recorded as inferences of their own and are not in the run's cost_usd, so remembering many snippets costs more than the run shows.

Isolated agents

Every agent is isolated from the others, so that one cannot slow down, confuse or break another.

One goroutine each

The manager starts every agent on a goroutine of its own and does not touch it afterwards. A user can have 3 researches working at once.

One master struct each

All the state of an agent is in a single researcher struct that only its own goroutine uses, so it needs no lock.

Its own page reader

fetch_url uses an HTTP client of the agent (own transport and connections, no cookies), closed when the agent ends.

Shares only what is meant

The database handle and the LLM client, which are safe for concurrent use. Agents write to their own rows only.

Not included yet

Planned work is on the roadmap.