# Tools and sub-agents (/docs/agents/tools)

The built-in tools an agent run can use, the web tools contract shared by every codai agent, the standalone /v1/tools and /v1/research endpoints, and how spawn_agent fans work out to typed sub-agents.

Inside an agent run — or a chat completion with `x-codai-server-tools: 1` — the model gets a set of gateway-executed tools. It calls them like any function tool; the gateway runs every call of a turn in parallel and feeds the results back. You never see the tool traffic unless you read the run's steps.

## Built-in tools [#built-in-tools]

| Tool | What it does | Available when |
| --- | --- | --- |
| `http_fetch` | Fetch a URL server-side. | Your key's tool level allows HTTP (`l1_http` or higher). |
| `json_query` | Query a JSON document. | Same as `http_fetch`. |
| `exec_python` | Run Python in a sandbox — the "code interpreter" capability. | Sandbox enabled on the gateway. |
| `read_artifact` | Read back a large tool result the gateway spilled to storage instead of inlining. | Spilling enabled and at least one tool active. |
| `memory_recall`, `memory_remember` | Long-term memory scoped to your account. | Your plan includes persistent memory. |
| `use_skill` | Load one of your skills into the loop. | At least one skill visible to the key. |
| `web_search`, `web_fetch` | Verified web search and page reading, same contract as every other surface ([below](#web-tools-one-contract-everywhere)). | Web search configured on the gateway. |
| MCP tools | Tools exposed by the MCP servers attached to your key. | Tool level is not `none`. |
| `delegate_to_model` | codai's internal single-completion router across the models you may call. | Always, when ≥ 2 models are allowed. |
| `spawn_agent` | A typed sub-agent with its own context and tool loop. | See below. |

Tool level is a property of your key. `none` disables every executed tool; `l1_http` allows network reads; `l3_sandbox` (cloud sandbox: shell, Python, files) and `l4_browser` unlock the heavier sub-agent roles.

## `spawn_agent` [#spawn_agent]

When server-side tools are on, the model also gets `spawn_agent`. Unlike `delegate_to_model` (one completion), a spawned child runs its **own loop** with its own context. The model picks a **role**; the gateway picks the tier, tool level and step budget for it.

<TypeTable
  type={{
    task: { type: 'string', description: 'What the child should do.', required: true },
    role: { type: "'explore' | 'research' | 'code' | 'verify' | 'summarize'", description: 'Selects tier, tool level and step budget.', default: "'explore'" },
    system: { type: 'string', description: 'Optional system prompt for the child.' },
    max_steps: { type: 'integer', description: '1 – 12, clamped to the role\'s own maximum.' },
  }}
/>

| Role | Tier | Max steps | Max tool level | Purpose |
| --- | --- | --- | --- | --- |
| `explore` | small | 6 | `l1_http` | Read-only facts. The default. |
| `research` | small | 8 | `l1_http` | `web_search` + `http_fetch`, returns a sourced summary. |
| `code` | large | 12 | caller's level (up to `l4_browser`) | Implement and verify; returns diff or output. |
| `verify` | small | 6 | `l3_sandbox` | Run checks, report PASS/FAIL per check. |
| `summarize` | small | 3 | `l1_http` | Digest one source. |

Semantics:

- **Parallel by design.** Every `spawn_agent` call in one assistant turn runs concurrently, and concurrently with the turn's other tool calls. The fan-out is capped per turn (default 4); calls past the cap receive `{"error":"delegation_fanout_capped"}` as their tool result.
- **Children never spawn.** A child has `spawn_agent` removed, runs on the parent's alias with `max_tokens: 4096`, and bills as a continuation of the parent turn — it does not start a new [task](./tasks-and-spend.mdx).
- **Cost ceiling.** Child spend counts toward the request's cost ceiling (default 5 USD); once reached, further spawns return `{"error":"request_cost_ceiling"}`.
- **Result shape.** The child's answer comes back to the parent as an ordinary `tool` message prefixed `SUB-AGENT[<role>] RESULT:`.
- **Progress frames.** On a streamed chat completion, the gateway emits vendor frames inside the SSE delta so a UI can render live sub-agent cards: `{"codai":{"event":"subagent_started","id","role","task"}}` and `{"codai":{"event":"subagent_done","id","role","ok","preview"}}`. Ignore them if you only want text.
- Invalid arguments → `{"error":"invalid_arguments"}`; a child that fails → `{"error":"sub_agent_failed"}`.

`spawn_agent` is injected only when the *request* carries no client-side tools of its own — if you send `tools`, you own the loop and the gateway stays out.

## Web tools: one contract everywhere [#web-tools-one-contract-everywhere]

codai has three web tools, and every agent surface exposes exactly the same ones — same names, same JSON schemas, same limits, same gateway endpoints and the same text rendering for the model. The single source is `packages/agent-core/spec/tools-web.json`; a parity test fails the build when a surface drifts from it.

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `web_search` | `POST /v1/tools/search` | Verified search: up to 10 results, rendered as `[S1]`..`[Sn]` lines the model cites. |
| `web_fetch` | `POST /v1/tools/fetch` | One page as clean markdown, optionally windowed by `focus`; the text is fenced as untrusted content. |
| `deep_research` | `POST /v1/research`, `GET /v1/research/{id}` | Several searches + page reads → a report where every claim cites `[Sn]`, or a JSON object with `output_schema`. |

| Surface | How it shows up |
| --- | --- |
| [VS Code extension](../apps/vscode.mdx) (0.7.0+) | `codai_web_search`, `codai_web_fetch`, `codai_deep_research` in Copilot agent mode |
| [Desktop](../apps/desktop.mdx) and the Android app | Built-in agent tools with the same names |
| Headless CLI (`codaid run`) | Same tools in the hosted agent |
| [Hosted MCP](../gateway/clients.mdx#mcp-server) (`https://ai.codai.ro/mcp`) | `web_search`, `web_fetch`, `deep_research` |
| [SDKs](../sdks/index.mdx) | `tools.search`, `tools.fetch`, `research.create` / `retrieve` / `wait` / `run` |
| Server-side agent loop (this page) | `web_search`, `web_fetch` (no nested research) |

Rules every surface follows:

- The gateway is the only search backend for a signed-in user. A 4xx answer (rules refusal, bad arguments, rate limit) is final: the model sees an `ERROR <code>: <message>` line and nothing retries through another provider or an LLM.
- `lang` and `country` are left unset unless the user asked for a market; the gateway infers them from the query.
- `published_at` is never made up: when a page has no known publish date the result shows none. `crawled_at` is when codai fetched it.
- Google-grounded rows, when the automatic fallback runs, are a separate block rendered as `[G1]`..`[Gn]` with the attribution line.

### Pricing [#pricing]

Web tools are not model calls, so they write no usage row; each call is metered on its own and charged in EUR from your wallet:

| Call | Price |
| --- | --- |
| `web_search`, `depth: fast` | $3 per 1,000 |
| `web_search`, `depth: deep` | $8 per 1,000 |
| `web_fetch` | $0.50 per 1,000 |
| `deep_research` job | $0.20 per job, plus the job's own model tokens |

The first **1,000 calls per user per UTC month are free**, and calls made **inside a codai agent turn** (any surface where `codai` is the model) are never charged. Errors and cache hits are never charged.

## Standalone search and fetch — `/v1/tools` [#standalone-search-and-fetch--v1tools]

The same search and fetch engines are exposed as plain endpoints, so a client-side agent can use them without going through a model. Both need only your `codai_` key, count against your per-minute / per-hour request limits (`X-RateLimit-*` headers, `429 rate_limit_exceeded`), and do **not** create a usage row — they are metered as web tool calls (see [pricing](#pricing)).

### `POST /v1/tools/search` [#post-v1toolssearch]

<TypeTable
  type={{
    query: { type: 'string', description: 'Trimmed, 1 – 400 characters.', required: true },
    max_results: { type: 'integer', description: '1 – 10.', default: '5' },
    include_domains: { type: 'string[]', description: 'Up to 10 hosts; only results from them (subdomains included, optional path prefix such as `github.com/vercel`).' },
    exclude_domains: { type: 'string[]', description: 'Up to 10 hosts never returned.' },
    freshness: { type: "'day' | 'week' | 'month' | 'year'", description: 'Only pages published within the window.' },
    published_after: { type: 'string', description: '`YYYY-MM-DD`: only pages published on or after this date.' },
    published_before: { type: 'string', description: '`YYYY-MM-DD`: only pages published before this date.' },
    depth: { type: "'fast' | 'deep'", description: '`deep`: more sources and query rewrites, up to ~12 s.', default: "'fast'" },
    content_chars: { type: 'integer', description: '0 – 2 000: also return a verified excerpt of this length per result in `content`.', default: '0' },
    country: { type: 'string', description: 'Two-letter market code; inferred from the query when omitted.' },
    lang: { type: 'string', description: '`xx` or `xx-yy` language tag; inferred from the query when omitted.' },
    mode: { type: "'codai' | 'google'", description: '`google` returns only Grounding with Google Search, as a separate result set.', default: "'codai'" },
    fallback: { type: "'auto' | 'off'", description: '`auto` adds a separate Google-grounded block when codai results are thin.', default: "'auto'" },
  }}
/>

Any date filter (`freshness`, `published_after`, `published_before`) drops pages whose publish date is unknown — a crawl date is never used in its place. Query operators work too: `site:`, `-site:`, `filetype:`, `intitle:`, `inurl:`, `"exact phrase"`, `-term`, `A OR B`, `after:`, `before:`, `lang:xx`.

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/tools/search \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "react compiler release", "include_domains": ["react.dev"], "published_after": "2026-01-01" }'
```

</Tab>
<Tab value="TypeScript">

```ts
import { Codai } from 'codai-sdk';

const codai = new Codai({ apiKey: process.env.CODAI_API_KEY! });
const { results } = await codai.tools.search({
  query: 'react compiler release',
  include_domains: ['react.dev'],
  published_after: '2026-01-01',
});
```

</Tab>
<Tab value="Python">

```python
from codai import Codai

codai = Codai(api_key=os.environ["CODAI_API_KEY"])
res = codai.tools.search({"query": "react compiler release", "include_domains": ["react.dev"], "published_after": "2026-01-01"})
for hit in res["results"]:
    print(hit["title"], hit["url"], hit["published_at"])
```

</Tab>
</Tabs>

Response — every result is **verified**: the snippet was found on the live page before it was returned.

```json
{
  "results": [
    {
      "title": "React Compiler v1.0",
      "url": "https://react.dev/blog/…",
      "snippet": "React Compiler is now stable …",
      "source": "docs",
      "verified": true,
      "published_at": "2026-02-10T00:00:00Z",
      "crawled_at": "2026-10-18T07:12:44Z"
    }
  ],
  "provider": "codai",
  "took_ms": 412,
  "cached": false,
  "coverage": 1,
  "sources_tried": ["docs", "index"],
  "degraded": [],
  "intent": "docs"
}
```

`published_at` is `null` when the page has no known publish date; `page_age` is a legacy alias kept only when it is known. `degraded` lists real source failures only; informational pipeline codes go to `notes`. If the search service is unreachable the gateway answers `503 search_unavailable` (`type: server_error`) — retry later; nothing was charged.

### `POST /v1/tools/fetch` [#post-v1toolsfetch]

<TypeTable
  type={{
    url: { type: 'string', description: 'Up to 2 048 characters. `https://` is prepended when the scheme is missing.', required: true },
    max_chars: { type: 'integer', description: 'Cap on returned content, 200 – 40 000.', default: '8000' },
    focus: { type: 'string', description: 'Up to 300 characters. When set, only the paragraphs that best match it are returned.' },
    format: { type: "'markdown' | 'text'", default: "'markdown'" },
  }}
/>

```bash
curl https://ai.codai.ro/v1/tools/fetch \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://nodejs.org/en/blog/release/v24.0.0", "max_chars": 4000, "focus": "ESM loader hooks" }'
```

```json
{
  "url": "https://nodejs.org/en/blog/release/v24.0.0",
  "final_url": "https://nodejs.org/en/blog/release/v24.0.0",
  "title": "Node.js 24.0.0 (Current)",
  "byline": "The Node.js Project",
  "description": "…",
  "canonical": "https://nodejs.org/en/blog/release/v24.0.0",
  "lang": "en",
  "content_type": "text/html",
  "content": "## Module customization hooks\n\n…",
  "chars": 3811,
  "truncated": true,
  "took_ms": 688,
  "cached": false
}
```

Limits: 10 s timeout, 3 MB body cap, at most 3 redirects, and an SSRF guard on every hop — private, loopback and credential-bearing URLs, or non-HTTP schemes, are `400 bad_request`. Accepted content types are HTML, plain text, JSON, XHTML, Markdown and XML; a non-2xx upstream is `502 upstream_error` with `details: { status, url }`. Fetches are cached for 15 minutes, searches for 10.

The fetched text is third-party content. Agent surfaces wrap it in an untrusted fence and tell the model never to follow instructions found inside it; do the same if you feed it to a model yourself.

## Deep research — `/v1/research` [#deep-research--v1research]

`POST /v1/research` starts an asynchronous job: codai plans several searches, reads the best pages and writes a report where every claim cites `[Sn]`, followed by the sources. It takes 20 s to a few minutes. Full reference: [Research API](../api/gateway/research.mdx).

<TypeTable
  type={{
    question: { type: 'string', description: 'Up to 2 000 characters, with any constraints (region, period, what to compare).', required: true },
    time_budget_s: { type: 'integer', description: '20 – 300 seconds.', default: '90' },
    max_sources: { type: 'integer', description: '3 – 30 pages to read.', default: '12' },
    include_domains: { type: 'string[]', description: 'Only use pages from these hosts.' },
    published_after: { type: 'string', description: '`YYYY-MM-DD`: only use pages published on or after this date.' },
    output_schema: { type: 'object', description: 'A JSON Schema; the result is then a JSON `answer` instead of a prose `report`.' },
    wait_s: { type: 'number', description: '0 – 55: wait this long before answering 202 with a running job.', default: '0' },
  }}
/>

```ts
const job = await codai.research.run({ question: 'Which EU countries require B2B e-invoicing in 2026?', max_sources: 15 });
console.log(job.report, job.sources);
```

<Callout type="warn">
These endpoints fetch from the public internet on your behalf. Do not pass them user-supplied URLs you have not validated yourself — the SSRF guard protects codai's network, not your intent.
</Callout>
