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
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.
wall_msfloat, optionalWall-clock limit in milliseconds, measured from
context(budget=...)entry.max_stepsint, optionalMaximum number of cooperative-checkpoint calls the guarded block may make (see
crate::budget::checkon the Rust side).seedint, optionalDeterminism seed available to RNG-consuming samplers via
budget_seed()— two runs entering the sameBudget(seed=7)observe the same seed at every call site that consults it.max_bytesint, optionalCeiling, 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 raisesBudgetExceededErrorwith codeE-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 memoryfollowed byabort(), 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 finiteRLIMIT_AS(ulimit -v, or a container memory limit).
>>> 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-")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.
Clear a previously requested cancellation.
Call this before starting the next candidate in a fan-out loop.
Return True if request_cancel() was called and not yet cleared.
Return True if a Budget is active on this thread.
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.
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.
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.
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.
context(budget=...)for engines that check the cooperative budget —integrate()andlimit()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.
fncallableThe function to call.
*args, **kwargsForwarded to
fn.budgetBudgetEntered on the worker thread for the duration of the call, so
max_stepsandseedreach cooperative call sites too. Ifbudget.wall_msisNone, this is equivalent tofn(*args, **kwargs)— no thread is spawned, and the caller's own ambient budget (if any) applies unchanged.
- BudgetExceededError
(
E-BUDGET-001) iffndoes not return withinbudget.wall_msmilliseconds. Raised oncefnhas actually finished — see above.
>>> import alkahest as ak
>>> b = ak.Budget(wall_ms=5)
>>> ak.run_with_wall_fallback(lambda: ak.simplify(x**2), budget=b) # doctest: +SKIPBatch evaluation
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.
indexintPosition of this item in the original input sequence. Stable under
parallel=Trueand underbatch_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.okboolTrueiff fn returned normally for this item.valueAny or Nonefn's return value on success;
Noneon failure.errordict or None{"code", "message", "remediation", "type"}on failure,Noneon success.codeThe raised exception's.code(e.g."E-INT-001") when it is anAlkahestError-like exception — including the native PyO3 error types, which expose the same attribute — otherwiseUNEXPECTED_ERROR_CODE("E-BATCH-001").messagestr(exc).remediationThe exception's.remediation, orNonewhen it has none.typetype(exc).__name__.elapsed_msfloat or NoneWall-clock time spent inside fn for this item, in milliseconds.
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.
fncallableCalled as
fn(item, **kwargs)for each item. AnyExceptionit raises is captured into that item'sBatchItemrather than propagating.itemsiterableThe candidates to evaluate. Consumed once, eagerly (so a generator works, but is fully materialised before any call happens).
parallelboolFan out over a
ThreadPoolExecutorwhen true. Useful when fn releases the GIL for some or all of its work (I/O, or a Rust call that callspy.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 ambientcontext(budget=...)is re-entered inside each worker so a trip is reported asE-BUDGET-00xon the item that tripped, exactly as it would be sequentially (see the module docstring for the per-field semantics).max_workersint, optionalForwarded to
ThreadPoolExecutor. Ignored whenparallel=False.**kwargsForwarded to every call to fn.
- list of BatchItem
Exactly
len(items)entries, in input order regardless of parallel — this is the ordering guaranteebatch_map_iter()does not make underparallel=True.
>>> 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]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().
parallel=False: yields in input order — item i is fully computed and yielded before item i + 1 starts, sozip(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 yieldedBatchItemstill carries its originalindex, so a caller that needs input order can sort by it — or just usebatch_map(), which always returns in input order.
fn, items, max_workers, **kwargsAs
batch_map().parallelboolAs
batch_map(); also selects the completion-order streaming behaviour described above.
- BatchItem
Sequential streaming preserves input order:
>>> 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]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.
exprsiterable of Expr or DerivedResultIntegrands, evaluated independently.
varExprIntegration variable, shared by every call.
*boundsPass
a, bfor a definite integral over every integrand (seeintegrate()); omit for the indefinite integral.parallel, max_workersAs
batch_map().**kwargsForwarded to
integrate().
- list of BatchItem
valueis theDerivedResultantiderivative (or definite value) on success; on failure,error["code"]is the integrator's own code (typicallyE-INT-001) rather thanUNEXPECTED_ERROR_CODE.
>>> 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]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.
exprsiterable of Expr or DerivedResultExpressions to simplify independently.
parallel, max_workersAs
batch_map().**kwargsForwarded to
simplify()(e.g.assumptions=).
- list of BatchItem
>>> 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]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.
exprsiterable of Expr or DerivedResultExpressions to differentiate independently.
varExprDifferentiation variable, shared by every call.
parallel, max_workersAs
batch_map().**kwargsForwarded to
diff().
- list of BatchItem
>>> 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
Documented together with STEP_FIELDS_COMPACT.
Documented together with STEP_FIELDS_COMPACT.
STEP_FIELDS = ('rule', 'before', 'after', 'side_conditions')Documented together with 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>_.