fetch_url), checks claims against more than one source, and ends with a report. It also works with the user’s internal knowledge base: it remembers the important snippets it finds as it goes and querys the knowledge base besides the web (see below). gligabao runs it as a job: you start it, it returns at once with a run, and you follow what it does until it ends.
Every run is stored in the relational database, and so is every state the agent goes through while it works (searching a query, reading a page, waiting for the model, …), so its progress can be followed in detail, and kept afterwards.
It needs an Anthropic API key (else 503). Each run costs money: up to max_steps model calls plus the web searches (a fee for each, on top of the tokens, see web-search.md). The cost is recorded and updated as the run goes.
Access: REST (below), MCP (research_start, research_list, research_get) and the CLI (gligabao research start|ls|get). They all use the same handlers, so they behave the same.
Runs and states
A run (gb_agent_research_runs) is one execution of the agent on a topic. Its status is its lifecycle:
A run left
running by a process that died is marked failed (“interrupted”) 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 from one state to the next; the table is append-only (a state is only ended, never rewritten), and state of the run is always the last row’s. The states of a run:
The agent goes
starting, then thinking, searching/reading/querying/remembering and thinking again, as many times as it needs, and ends: thinking, completed when the model answers before the last step; or writing_report, completed when it uses all its steps (the last step cannot call tools, so a run always ends with a report). A model that does not use tools in the first step goes starting, thinking, completed.
Columns of a state: seq (1, 2, 3, … in the order entered), step (the model call it was on; 0 for starting, completed and failed), detail, error, started_at and finished_at (null for the state the run is in; millisecond precision). A tool that fails (a page that cannot be read) does not fail the run: the model is told and goes on, so the state has an error and the run can still complete. When a run fails, the state it failed in and the failed state both carry the error.
The run keeps steps (model calls made), tool_calls, input_tokens, output_tokens and cost_usd, updated after every model call and tool run. The cost includes the model calls of the web searches.
REST
All three routes are user routes:user_id is required (in the body, or in the query string for GET), and with the Auth hook the API key sets it. A user only sees their own runs: another user’s run is a 404.
POST /v1/gb/research
Starts an agent. Body:
topic(required, at most 2000 characters): what to research. A question, or a subject to write a report on.max_steps(optional, default 20, at most 40): the most model calls the agent makes, the last of which writes the report. More steps let it search and read more, and cost more.model(optional): the model it runs on, which must support Anthropic’s web search tool. Default: the server’s default model.dataset(optional, defaultmain): the dataset of the user’s knowledge base the agent queries and remembers into (below). It is created by the firstrememberif it does not exist; a bad name is a400. It is not stored with the run.
202 with the run (status running, state starting); the agent goes on in the background. 400 for a bad body, 409 if the user already has 3 agents working (wait for one to end), 503 without an Anthropic API key.
GET /v1/gb/research?user_id=...
Lists the agents of the user, newest first, without their states: {"runs": [...]}.
active=true: only the agents that are working now (statusrunning).status=running|completed|failed: only the runs with that status (not together withactive).parent_id=<run id>: only the sub-agents that run spawned.limit: at most this many (default 50, at most 500).
parent_id, name and reason set (null/"" for a research a user started).
GET /v1/gb/research/{id}?user_id=...
Reads one run with its states, in the order they were entered. The report is in it once status is completed.
research_get, or gligabao research get).
CLI
start prints the run right away; with --wait it waits for the agent to end and prints the finished run (exit code 1 if it failed), and --raw prints only the report. get --raw prints only the report. See cli.md.
The knowledge base
Besides the web, the agent works with the user’s internal knowledge base (the graph memory of remember and query), through two more tools (details in deep-research-agent.md):remember(text, optionalsource, optionalsync): while it searches the web and reads pages, the agent submits the important parts (short snippets with their context, the URL as the source) to the remember manager, as the user, into the run’s dataset.synctoggles whether the call blocks. By default (sync=false) the call only queues the text and returns at once, so the agent goes on without waiting for it to be absorbed; withsync=trueit waits until the text is in the knowledge base, for when the agent needs it right away (toqueryfor it).query(question): besides web searches and page reads, the agent asks the knowledge base what it knows already, and checks what it found against it. It gets the answer and the facts it is based on. An empty (not yet existing) dataset answers that it is empty.
remember and query endpoints and the session research-<run id>, so they show in the remember run log (gb_remember_runs) and query run log with it. Text that is queued is not tied to the run: it is absorbed even after the run ended, and a failure of it does not fail the run. The cost of these calls is recorded as inferences (gb_inferences) and is not part of the run’s cost_usd.
Sub-agents
A research agent can spawn sub-agents, other research agents that take sub-tasks (spawn_subagent, wait_subagents), and sibling sub-agents can see each other and their reasons (list_siblings) and message each other (send_message, read_messages). Every sub-agent is spawned with a reason. A sub-agent is a run like any other: it has its own states, usage and report, and a parent_id, a name and a reason (null/"" for a research a user started); GET /v1/gb/research?parent_id= lists the sub-agents of a run. They are bounded (5 per agent, 10 per research, 2 levels deep) and are cancelled when their parent ends. All of it, the tools, the limits, who can message whom and the replaceable message system, is in subagents.md.
How it works
internal/agent: the loop and the research agent (deep-research-agent.md). It has anObserver(before and after each model call and each tool run) that the manager uses to record the states, andReportUsage, with which a tool that calls a model (the web search) adds its tokens and cost to the run.internal/agent_research: theManagermakes aresearcherfor each agent, the struct that holds all of its state (see isolation), and starts it in a goroutine of its own (at most 3 working at once per user); the manager creates the run and its first state in one transaction, and the researcher enters a state in a transaction at each event (end the previous state, insert the new one, set the run’sstate), writes the usage after every model call and tool run, and ends the run (its status, report or error, and the terminal state) in a transaction. A failed write of the progress is logged and does not stop the agent. Storage is inagent_research/db/sqliteandagent_research/db/postgres(each with its schema); the types are inagent_research/core.internal/agent_research/team.goandmessages.go,internal/agent/team.go: the sub-agents and their messages; the prompts are ininternal/agent/prompts/(see subagents.md).internal/agent_research/memory.go:Memory(the remember manager, the database, the LLM and the options of the remember and query endpoints), whichapi.Registergives the manager (Manager.UseMemory); each agent gets its own view of it (knowledge: its user, dataset and session) that implementsagent.Knowledge, the interface behind the toolsrememberandquery(internal/agent/knowledge.go). Without aMemorythe agents only have the web tools.internal/api/research: the REST handlers;internal/api/mcpandclicall them (the MCP tools in process, the CLI over HTTP).- When the server stops, the agents are cancelled and their runs are ended as
failed(“interrupted”) before the databases close.
Not included yet
Cancelling a run by the user, messages between agents that are not siblings (a parent and its sub-agents, cousins), storing the messages, a stream of its states (pollGET /v1/gb/research/{id}), storing the report itself in a dataset, waiting for the queued snippets before a run ends, counting the cost of remember and query in the run’s cost, recording the agent’s model calls in gb_inferences, a view in the admin UI, and a cap on the cost of a run. See the roadmap.