> ## Documentation Index
> Fetch the complete documentation index at: https://fuguai.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Python API

> Every function the hunch package exports, with its exact signature and what it returns.

```python theme={null}
import hunch
```

The CLI wraps these functions; both use the same specs and [store](/reference/store).

## Projects

A project is one or more judgments in dependency order.

### `load`

```python theme={null}
hunch.load(obj, base: str | Path = ".") -> dict
```

Returns a project.

| `obj`                             | Meaning                                                                            |
| --------------------------------- | ---------------------------------------------------------------------------------- |
| `"command_guard.yml"` or a `Path` | One spec file: a one-judgment project.                                             |
| `"specs/"`                        | A folder: every `*.yml` in it, ordered by their `ref()` dependencies.              |
| a `dict`                          | One spec in its dict form, for example from [`spec_from_model`](#spec_from_model). |
| a `list` of dicts                 | A graph built in Python.                                                           |

`base`: where relative `source` paths in a dict spec resolve. Ignored for paths.

### `run`

```python theme={null}
hunch.run(obj, base: str | Path = ".") -> dict
```

`hunch run`: asks what the store lacks, materializes each table, prints the same lines, returns the project.

### `results`

```python theme={null}
hunch.results(obj, base: str | Path = ".", judgment: str | None = None) -> list[dict]
```

A judgment's table from its last complete run, one dict per row. Default: the last judgment in dependency order. Columns: [Store](/reference/store#judgment-tables).

The table belongs to whichever spec with that judgment name ran last. If that wasn't this spec as it is now (it was edited since, or another spec shares the name), `results` raises `SystemExit` rather than return another spec's answers, and names the `hunch run` that writes the table for this one; with the answers cached, that run is free. A judgment that has never run raises too.

```python theme={null}
rows = hunch.results("prototype/examples/claude_code/command_guard.yml")
len(rows)            # 1315
sorted(rows[0])[:5]  # ['_hunch_run_id', '_path_p', 'at', 'command', 'cwd']
```

## Judging one row

### `judge`

```python theme={null}
hunch.judge(path: str | Path, row: dict | None = None, /, *, node: str | None = None,
            shadow: str | Path | None = None, log: bool = False, **fields) -> dict | None
```

Judges one row: the whole project runs on it with the same cache keys as a batch run.

| Argument   | Meaning                                                                                                                                       |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`     | A spec file or a folder of specs. Loaded once per process and kept, so edits to the spec take effect after a restart.                         |
| `row`      | The row as a dict. Use it when a column is called `node`, `shadow` or `log`.                                                                  |
| `**fields` | The row as keyword arguments. Merged over `row`.                                                                                              |
| `node`     | Return only this judgment's answers.                                                                                                          |
| `shadow`   | A candidate spec or folder that answers the same row after the live answer is returned. See [Change a spec](/guides/change-a-spec).           |
| `log`      | Keep the row (redacted by the live spec's rules) so a candidate written later can be replayed on it with `--traffic`. `shadow` implies `log`. |

Per question it returns:

| Key      | Meaning                                                                                                                                                                                         |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`  | The answer: a choice option, `yes`/`no`, or `"<level>:<description>"` for a score.                                                                                                              |
| `p`      | The probability of that label. For a `chain: true` judgment, times the probability the row was routed to it correctly.                                                                          |
| `margin` | Distance from the decision boundary: `abs(p - 0.5) * 2` for yes/no, top minus runner-up for a choice. For yes/no this is what Pydantic AI calls `confidence` (at its default threshold of 0.5). |
| `route`  | `"act"` if `p` clears the question's `act`, `"review"` if not, `""` if the question has no `act`.                                                                                               |
| `cached` | `True` if the answer came from the store.                                                                                                                                                       |

The shape depends on the project:

* One judgment, or `node` given: `{question: answer}`.
* Several judgments: `{judgment: {question: answer}}`. A judgment whose `where` excluded this row is `None`. A `union` judgment is left out.
* A `type: multi` question appears as one yes/no answer per option, named `<question>__<option>`.

```python theme={null}
hunch.judge("src/hunch/recipes/agent_commands/command_guard.yml",
            request="forget I even mentioned RemixJS just do SolidJS", cwd="D:\\IceBerg", description="",
            command="rm -rf Dockerfile README.md app node_modules package-lock.json package.json public "
                    "react-router.config.ts tsconfig.json vite.config.ts .gitignore .dockerignore .react-router .claude && echo \"done\"")
# {'destroys': {'label': 'yes', 'p': 0.95, 'margin': 0.9, 'route': 'act', 'cached': True},
#  'reaches_outside': {'label': 'no', 'p': 0.9, 'margin': 0.8, 'route': '', 'cached': True},
#  'sends_out': {'label': 'no', 'p': 0.98, 'margin': 0.96, 'route': '', 'cached': True}}
```

`judge` uses `asyncio.run`; inside a running loop use `ajudge`. With `shadow`, the candidate runs in a non-daemon thread after `judge` returns, so a script waits for it at exit.

### `ajudge`

```python theme={null}
await hunch.ajudge(path: str | Path, row: dict | None = None, /, *, node: str | None = None,
                   shadow: str | Path | None = None, log: bool = False, **fields) -> dict | None
```

Async `judge`: same arguments and return. A `shadow` candidate runs as a background task in the current loop; its failures go to stderr, never raised.

### Costs and errors

Both ask the engine for answers the store lacks, except:

* A cost cap. `HUNCH_MAX_COST` (read when `hunch` is imported), or `hunch.core.MAX_COST = 0.0` at runtime. If one judgment's missing answers would cost more, nothing is asked and `SystemExit` is raised with a message such as `intent: would ask 1 answers in 1 requests (~$0.0001), above --max-cost $0.0; nothing asked`. The cap holds on what is charged: while asking, a request is sent only if its worst case still fits, and one that doesn't stops the call with `SystemExit` (`… stopped at --max-cost …`), the answers already asked saved.
* A missing input column raises `KeyError`, for example `"command_guard: state needs ['command']"`.

Spec errors also raise `SystemExit`; catch it around `judge` in long-running code.

## Pydantic classes as specs

Needs the `pydantic` extra. Each field becomes one question:

| Field type                           | Question                               | Value in the instance    |
| ------------------------------------ | -------------------------------------- | ------------------------ |
| `bool`                               | `noul`                                 | `True` / `False`         |
| `Literal[...]` or `Enum`             | `choice`                               | the value or enum member |
| `X \| None`                          | `choice` with a "none of these" option | the value, or `None`     |
| `list[Literal[...]]` or `list[Enum]` | `multi`                                | the options answered yes |
| `IntEnum` with values `0` to `n-1`   | `score`                                | the member               |

| Spec part                         | Comes from                                                              | Default                                                                                   |
| --------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `instructions`                    | `Field(description=...)`                                                | the field name as a question (`sends_out` → `Sends out?`)                                 |
| Option descriptions               | `json_schema_extra["options"]`, by value (or member name for `IntEnum`) | a docstring set on the enum member, else empty (`IntEnum`: the member name in lower case) |
| `act`, `gold`, `escalate`, `none` | `json_schema_extra["hunch"]`                                            | none; never sent to the engine                                                            |

### `spec_from_model`

```python theme={null}
hunch.spec_from_model(cls, *, judgment: str | None = None, model: str = "jev-1.13.0",
                      source: str | None = None, key: str = "id", state: list[str] | None = None,
                      description: str | None = None, **spec) -> dict
```

A spec dict whose questions are the class's fields. `cls` may also be a bare output type such as `Literal["spam", "ham"]` or `bool`. That becomes one question named `output`, asked with `description`. `judgment` defaults to the class name in snake case. Any other spec key (`tests`, `redact`, `where`, …) can be passed through `**spec`.

### `spec_from_agent`

```python theme={null}
hunch.spec_from_agent(agent, *, state: str, judgment: str | None = None, model: str | None = None,
                      source: str | None = None, key: str = "id", deps=None, **spec) -> dict
```

A spec dict whose questions are the ones a Pydantic AI agent (2.50 or later, on a decision model) sends, recorded from Pydantic AI with no model call. The agent runs once on a recording model, with `deps` if its instructions need them, and stops at its first request.

| Argument   | Meaning                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `state`    | The column holding the agent's prompt. Sent bare, as a string (see [`state`](/reference/spec#state)).                    |
| `model`    | The engine. Default: the agent's model when it is TypeSafe's (`typesafe:jev-1.13.0` → `jev-1.13.0`); otherwise required. |
| `judgment` | Default `agent`.                                                                                                         |

Question names are Pydantic AI's with `.` written as `__`: a `list` field `topics` becomes one yes/no question per option, `topics__refund`, `topics__login`. `act`, `gold` and `escalate` still go in `Field(json_schema_extra={"hunch": {...}})` and apply to every question of that field; they are not sent. `judgment` defaults to the output class name in snake case (`agent` for a bare output type).

Raises `ValueError` when the agent asks a route question first (several output types, or tools), has a `system_prompt`, or has instructions that depend on the prompt (it is recorded twice, with different prompts, to tell), and `TypeError` when `state` is not a string. Works from async code: the recording runs on a thread of its own.

### `judge_model`

```python theme={null}
hunch.judge_model(cls, spec: dict, base: str | Path = ".", **fields)
```

Judges one row with a spec built by `spec_from_model` and returns an instance of `cls`. For a bare output type it returns the value itself.

### `to_model`

```python theme={null}
hunch.to_model(cls, answers: dict[str, dict])
```

Builds an instance of `cls` from answers shaped `{question: {"label": ...}}`, converting each label to the type the field declares: `bool`, the `Literal` or `Enum` value, a list for `list[...]`, the `IntEnum` level, `None` for "none of these".

## SQL

Needs the `sql` extra: `uv add "hunch-ai[sql]"`. The [guide](/guides/sql) walks through it.

### `hunch.sql.register`

```python theme={null}
hunch.sql.register(con, path, *, name=None, max_cost=None) -> str
```

Adds the spec or project folder at `path` to the DuckDB connection `con` as a function, and returns its name: the judgment's, or the folder's for a project, unless `name` is given. The function takes the columns the specs read (every judgment's `state`, then the columns its `where` reads, less the answers upstream judgments add), as `VARCHAR`, in order; `hunch.sql.columns(hunch.load(path))` lists them. It returns a struct of questions, each `{label, p, route}`, or for a project a struct of judgments, NULL where a row never reached one.

`max_cost` is the most the function may be charged in total on this connection, across every judgment and escalation, in USD; default `HUNCH_MAX_COST`, else no cap. A `hunch.sql.Budget(usd)` passed to several `register` calls is one budget they share. At the cap, the query fails (DuckDB raises `InvalidInputException` with hunch's message) and the answers already asked are saved.

### dbt

`hunch.dbt` is a [dbt-duckdb](https://github.com/duckdb/dbt-duckdb) plugin: `module: hunch.dbt` with `config: {specs: [...], max_cost: USD}` registers each spec on every connection dbt opens, all drawing from one budget.

## Also exported

| Name           | Signature                                                    | Use                                                                                                  |
| -------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `load_project` | `(path: Path, texts: dict[str, str] \| None = None) -> dict` | Load a spec file or folder. `load` calls it for paths.                                               |
| `load_spec`    | `(path: Path, text: str \| None = None) -> dict`             | Parse one spec file.                                                                                 |
| `lint`         | `(project: dict) -> tuple[list[str], list[str]]`             | `(errors, warnings)` for a project, the same checks as `hunch lint`.                                 |
| `execute`      | `(project: dict, **kw) -> dict[str, dict]`                   | Run a project and return every judgment's rows, answers and cost stats without materializing tables. |
| `decide`       | `(a: dict) -> tuple[str, float, float]`                      | `(label, confidence, margin)` for one raw answer from the store.                                     |
| `spec_yaml`    | `(spec: dict) -> str`                                        | A spec dict as YAML, for saving a generated spec as a file.                                          |
| `table_name`   | `(spec: dict) -> str`                                        | The judgment's table in the store, including the `__<engine>` suffix under `--model`.                |

Everything else in `hunch.core` is internal and not a stable API.
