alkahest
Python API

Search / workload API

APIs for running Alkahest inside agent math-search loops: per-call budgets, batch fan-out that never aborts on one bad candidate, and related helpers.

Conceptual guide: Autoresearch / agent loops <../search-plumbing.html>, Budgets <../budgets.html>, Batch <../batch.html>_.

Budgets

class
class Budget(wall_ms=None, max_steps=None, seed=None)

A per-call resource budget for search-style workloads.

Every field is optional; Budget() never trips a cooperative check on its own — only request_cancel() can stop a call entered with a bare Budget(). This mirrors the Rust side: entering an otherwise-empty budget is how a caller opts a code path into consulting seed without also imposing a wall/step limit.

Parameters
wall_msfloat, optional

Wall-clock limit in milliseconds, measured from context(budget=...) entry.

max_stepsint, optional

Maximum number of cooperative-checkpoint calls the guarded block may make (see crate::budget::check on the Rust side).

seedint, optional

Determinism seed available to RNG-consuming samplers via budget_seed() — two runs entering the same Budget(seed=7) observe the same seed at every call site that consults it.

max_bytesint, optional

Ceiling, in bytes, on the exact-arithmetic (GMP) memory the guarded block may hold live. The memory analogue of wall_ms: the existing engine ceilings bound the shape of a linear system (how many unknowns) but not the size of its numbers, and it is coefficient growth that exhausts memory. Exceeding it raises BudgetExceededError with code E-BUDGET-004.

Without it, an exact solve that outgrows the machine is not catchable at all: GMP's reaction to a failed allocation is GNU MP: Cannot allocate memory followed by abort(), which takes the interpreter down with it. Independently of this setting, Alkahest also refuses (E-BUDGET-005) when the process is about to exhaust a finite RLIMIT_AS (ulimit -v, or a container memory limit).

Examples
Python
>>> import alkahest as ak
>>> with ak.context(budget=ak.Budget(wall_ms=50, max_steps=10_000, seed=7)):
...     try:
...         ak.integrate(hard_expr, x)  # doctest: +SKIP
...     except ak.BudgetExceededError as e:
...         assert e.code.startswith("E-BUDGET-")
Properties
wall_ms: float | None
max_steps: int | None
seed: int | None
max_bytes: int | None
function
request_cancel() → None

Request cancellation of the current cooperative operation(s), process-wide.

See alkahest_core::budget::request_cancel — checked by every cooperative checkpoint on every thread until clear_cancel() is called.

function
clear_cancel() → None

Clear a previously requested cancellation.

Call this before starting the next candidate in a fan-out loop.

function
is_cancelled() → bool

Return True if request_cancel() was called and not yet cleared.

function
is_budget_active() → bool

Return True if a Budget is active on this thread.

function
budget_seed() → int | None

Return the seed of the innermost active Budget on this thread.

None if no budget is active or the active budget did not set one.

function
active_budget() → Any | None

Return the Budget from the innermost active context, or None if no context set one.

The budget is already active in the Rust-side cooperative checkpoint while its context(budget=...) block is open (see context()); this accessor is for introspection, e.g. a fan-out loop wanting to log/adjust its own remaining budget.

function
run_with_wall_fallback(
    fn: Callable[..., _T],
    /,
    *args: Any,
    budget: Budget,
    **kwargs: Any
) → _T

Run fn(*args, **kwargs) under budget, raising E-BUDGET-001 when it overruns budget.wall_ms — but without a hard deadline.

Read this before relying on it

This turns "the callee quietly gave up early" into a raised, coded error, and it re-enters budget on the worker thread so cooperative checkpoints actually see it. What it does not do is return control at wall_ms: the worker is joined before the exception propagates, so for a callee that never reaches a cooperative checkpoint — time.sleep(3), a single long FLINT call, third-party code — Budget(wall_ms=50) raises the right error after the callee finishes. Three seconds, in that example. The error message reports how long control was actually withheld, so this is visible in a log rather than inferred.

Why not just abandon the worker and return at the deadline? Because Python cannot kill a thread, so "return early" means leaking a live thread that still holds the GIL in bursts, still allocates into the pool, and cannot be stopped except through the process-wide cancellation flag — which would also abort every unrelated in-flight call in the process, and which nobody could then safely clear (clearing it before the orphan observes it is a no-op; leaving it set poisons every subsequent cooperative call). In a multi-day loop that trades a bounded stall for unbounded orphan-thread accumulation plus collateral cancellation. Joining is the honest lesser evil, so it is what this does.

What *does* bound wall time
  • context(budget=...) for engines that check the cooperative budget — integrate() and limit() today. That is the real mechanism; this function is a reporting shim over it.
  • An OS-level bound — a subprocess with a timeout, or a process-level watchdog — for anything else. Nothing inside one Python process can preempt a thread.

So: reach for this to get a raise out of a cooperatively-budgeted call that would otherwise return a silently-truncated answer (the documented case is simplify(), whose DerivedExpr return type has no error channel). Do not reach for it to contain an unknown callee.

Parameters
fncallable

The function to call.

*args, **kwargs

Forwarded to fn.

budgetBudget

Entered on the worker thread for the duration of the call, so max_steps and seed reach cooperative call sites too. If budget.wall_ms is None, this is equivalent to fn(*args, **kwargs) — no thread is spawned, and the caller's own ambient budget (if any) applies unchanged.

Raises
BudgetExceededError

(E-BUDGET-001) if fn does not return within budget.wall_ms milliseconds. Raised once fn has actually finished — see above.

Examples
Python
>>> import alkahest as ak
>>> b = ak.Budget(wall_ms=5)
>>> ak.run_with_wall_fallback(lambda: ak.simplify(x**2), budget=b)  # doctest: +SKIP

Batch evaluation

class
class BatchItem

One slot's outcome from batch_map() or a *_many helper.

Exactly one of value / error is populated: ok=True implies error is None and value is whatever fn returned (often a DerivedResult); ok=False implies value is None and error describes what went wrong.

Attributes
indexint

Position of this item in the original input sequence. Stable under parallel=True and under batch_map_iter() streaming in completion order — an outcome can always be matched back to its input via this field, even when results arrive out of order.

okbool

True iff fn returned normally for this item.

valueAny or None

fn's return value on success; None on failure.

errordict or None

{"code", "message", "remediation", "type"} on failure, None on success.

code The raised exception's .code (e.g. "E-INT-001") when it is an AlkahestError-like exception — including the native PyO3 error types, which expose the same attribute — otherwise UNEXPECTED_ERROR_CODE ("E-BATCH-001"). message str(exc). remediation The exception's .remediation, or None when it has none. type type(exc).__name__.

elapsed_msfloat or None

Wall-clock time spent inside fn for this item, in milliseconds.

Properties
index: int
ok: bool
value: Any | None
error: dict[str, Any] | None
elapsed_ms: float | None
function
batch_map(
    fn: Callable[..., Any],
    items: Iterable[Any],
    *,
    parallel: bool=False,
    max_workers: int | None=None,
    **kwargs: Any
) → list[BatchItem]

Call fn(item, **kwargs) for every item in items, never raising.

Parameters
fncallable

Called as fn(item, **kwargs) for each item. Any Exception it raises is captured into that item's BatchItem rather than propagating.

itemsiterable

The candidates to evaluate. Consumed once, eagerly (so a generator works, but is fully materialised before any call happens).

parallelbool

Fan out over a ThreadPoolExecutor when true. Useful when fn releases the GIL for some or all of its work (I/O, or a Rust call that calls py.allow_threads); on pure Python, GIL-bound work it will not speed anything up, but it also will not make anything incorrect — order is preserved either way, and the ambient context(budget=...) is re-entered inside each worker so a trip is reported as E-BUDGET-00x on the item that tripped, exactly as it would be sequentially (see the module docstring for the per-field semantics).

max_workersint, optional

Forwarded to ThreadPoolExecutor. Ignored when parallel=False.

**kwargs

Forwarded to every call to fn.

Returns
list of BatchItem

Exactly len(items) entries, in input order regardless of parallel — this is the ordering guarantee batch_map_iter() does not make under parallel=True.

Examples
Python
>>> import alkahest as ak
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> outs = ak.batch_map(ak.simplify, [x + 0 * x, ak.log(ak.log(x))])
>>> [o.ok for o in outs]
[True, True]
function
batch_map_iter(
    fn: Callable[..., Any],
    items: Iterable[Any],
    *,
    parallel: bool=False,
    max_workers: int | None=None,
    **kwargs: Any
) → Iterator[BatchItem]

Streaming counterpart of batch_map().

Order guarantee
  • parallel=False: yields in input order — item i is fully computed and yielded before item i + 1 starts, so zip(items, batch_map_iter(fn, items)) lines up.
  • parallel=True: yields in completion order, not input order. This is deliberate: the point of streaming under fan-out is that a fast failure surfaces immediately instead of waiting behind a slow item that happened to be submitted first. Every yielded BatchItem still carries its original index, so a caller that needs input order can sort by it — or just use batch_map(), which always returns in input order.
Parameters
fn, items, max_workers, **kwargs

As batch_map().

parallelbool

As batch_map(); also selects the completion-order streaming behaviour described above.

Yields
BatchItem
Examples

Sequential streaming preserves input order:

Python
>>> import alkahest as ak
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> [item.index for item in ak.batch_map_iter(ak.simplify, [x, x + 0 * x])]
[0, 1]
function
integrate_many(
    exprs: Iterable[Any],
    var: Any,
    *bounds: Any,
    parallel: bool=False,
    max_workers: int | None=None,
    **kwargs: Any
) → list[BatchItem]

batch_map() over integrate(), one call per integrand.

Parameters
exprsiterable of Expr or DerivedResult

Integrands, evaluated independently.

varExpr

Integration variable, shared by every call.

*bounds

Pass a, b for a definite integral over every integrand (see integrate()); omit for the indefinite integral.

parallel, max_workers

As batch_map().

**kwargs

Forwarded to integrate().

Returns
list of BatchItem

value is the DerivedResult antiderivative (or definite value) on success; on failure, error["code"] is the integrator's own code (typically E-INT-001) rather than UNEXPECTED_ERROR_CODE.

Examples
Python
>>> import alkahest as ak
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> outs = ak.integrate_many([x**2, ak.log(ak.log(x)), ak.sin(x)], x)
>>> [o.ok for o in outs]
[True, False, True]
function
simplify_many(
    exprs: Iterable[Any],
    *,
    parallel: bool=False,
    max_workers: int | None=None,
    **kwargs: Any
) → list[BatchItem]

batch_map() over simplify(), one call per expression.

Parameters
exprsiterable of Expr or DerivedResult

Expressions to simplify independently.

parallel, max_workers

As batch_map().

**kwargs

Forwarded to simplify() (e.g. assumptions=).

Returns
list of BatchItem
Examples
Python
>>> import alkahest as ak
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> outs = ak.simplify_many([x + 0 * x, x / x])
>>> [o.value.value for o in outs]
[x, 1]
function
diff_many(
    exprs: Iterable[Any],
    var: Any,
    *,
    parallel: bool=False,
    max_workers: int | None=None,
    **kwargs: Any
) → list[BatchItem]

batch_map() over diff(), one call per expression.

Parameters
exprsiterable of Expr or DerivedResult

Expressions to differentiate independently.

varExpr

Differentiation variable, shared by every call.

parallel, max_workers

As batch_map().

**kwargs

Forwarded to diff().

Returns
list of BatchItem
Examples
Python
>>> import alkahest as ak
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> outs = ak.diff_many([x**2, ak.sin(x)], x)
>>> [o.ok for o in outs]
[True, True]

Schema constants

constant
RESULT_SCHEMA_VERSION

Documented together with STEP_FIELDS_COMPACT.

constant
STEPS_SCHEMA_VERSION

Documented together with STEP_FIELDS_COMPACT.

constant
STEP_FIELDS
STEP_FIELDS = ('rule', 'before', 'after', 'side_conditions')

Documented together with STEP_FIELDS_COMPACT.

constant
STEP_FIELDS_COMPACT
STEP_FIELDS_COMPACT = ('r', 's')

Version and field-name tables for DerivedResult.to_dict(). See derivation logs <../derivations.html#machine-parseable-output-to_dict--to_json>_.