Skip to content

keelgate.tools

Stability of each symbol (stable or provisional) is listed in the API contract. Narrative and semantics are in the integration contract.

Tool registry and declaration, with each tool tagged by side effect.

CallContext dataclass

Per-call facts, split by trust.

tenant_id and policy_context come from the harness and are trusted: they feed the policy engine. rationale, sources, confidence and verifier_flags come from the model or its tooling and are only ever shown to a human approver; they never influence a decision.

Claim dataclass

The outcome of claiming an idempotency key.

state says whether the caller may run the call, must replay the stored output, or is looking at a call that is still in flight or of unknown outcome.

ClaimState

Bases: StrEnum

Where an idempotency key stands when a caller tries to claim it.

NEW may run; DONE replays the stored result; IN_FLIGHT is running now; UNKNOWN died mid-flight and must never be retried automatically; CONFLICT means the key was reused for different arguments.

DirectInvocationError

Bases: RuntimeError

A tool was called without going through the gateway.

ErrorCode

Bases: StrEnum

Why the gateway refused or failed a call.

Stable machine-readable codes, safe to show a model: they never carry model-supplied text.

IdempotencyStore

Bases: Protocol

Where idempotency keys and stored results live.

Implement this to share them across processes. claim must be atomic, and a key left in flight by a process that died must read as UNKNOWN.

InMemoryIdempotencyStore

Process-local idempotency store.

Fine for tests and single-process use. It is lost on restart, so use SqliteIdempotencyStore (or your own IdempotencyStore) when a WRITE must never run twice across restarts.

claim

claim(
    tenant_id: str, tool: str, key: str, args_hash: str
) -> Claim

Atomically claim a key for execution; NEW means the caller may run.

peek

peek(
    tenant_id: str, tool: str, key: str, args_hash: str
) -> Claim

Read the state of a key without claiming it.

release

release(tenant_id: str, tool: str, key: str) -> None

Forget a claim that provably did not execute, so a retry is allowed.

OutcomeStatus

Bases: StrEnum

The four things a gateway call can end as.

OK, DENIED (policy or authority said no), APPROVAL_REQUIRED (parked for a human) and ERROR.

RegistryFrozenError

Bases: RuntimeError

A tool was registered after the registry was frozen.

SideEffect

Bases: StrEnum

What a tool can do to the world. This is what the policy gate keys off.

SqliteIdempotencyStore

Durable idempotency, safe to share between processes on one host.

This is what makes resume safe. If a process dies after a WRITE completed but before the loop checkpointed it, the key is DONE and the retry replays the stored result. If it dies during the WRITE, the key is still IN_FLIGHT, which a retry reads as outcome-unknown and refuses to repeat: a possibly-executed money movement is never run twice automatically.

release

release(tenant_id: str, tool: str, key: str) -> None

Forget a claim that provably did not execute, so a retry is allowed.

Tool

A declared tool. Not callable: execute it through the gateway.

ToolDefinitionError

Bases: ValueError

The tool is declared unsafely or inconsistently. Raised at import time.

ToolError

Bases: BaseModel

A refusal or failure a model can act on: a code, fixed-wording message and hint, and whether a retry could help.

ToolGateway

The single choke point every tool call passes through.

Grant, tenant, tool, capability, side-effect ceiling, paper-only mode, argument validation, budget, idempotency, policy, approval, then execution. A refusal is returned as a result, never raised, and the audit record is written before any side effect.

call async

call(
    *,
    tool_name: str,
    arguments: Mapping[str, Any],
    grant_token: str,
    context: CallContext,
) -> ToolOutcome

Run one proposed tool call through every gate. Never raises for a refusal.

ToolOutcome dataclass

The structured result of one gateway call.

A status, the (untrusted) output or a model-actionable error, and for a parked call the approval to wait for.

for_model

for_model() -> dict[str, Any]

The control-plane part of the result, safe to show a model.

Deliberately excludes output: that is untrusted data, and the caller must decide explicitly how to present it.

ToolRefusedError

Bases: Exception

Raised by a tool body to say: I did not do anything, so a retry is safe.

Any other exception from a WRITE tool is treated as possibly-partial, and the call is never retried automatically.

ToolRegistry

The set of tools an agent may be offered.

A gateway freezes its registry on construction, so a tool cannot appear mid-run.

describe

describe() -> list[dict[str, Any]]

Tool schemas to offer a model. Omits capabilities and policy details.

ToolSpec dataclass

Everything the gateway knows about a tool.

Its schemas, capability, side effect, timeout, cost estimate, and the functions that derive its idempotency key and the facts policy needs.

Untrusted

Bases: Generic[T]

Marks data that came from outside the harness.

Tool output, retrieved documents and web content may contain text written by an attacker. Wrapping it makes the trust boundary visible in the types: code must call :meth:unwrap_untrusted by name to read it, and a reviewer can grep for every place that does.

tool

tool(
    *,
    capability: str,
    side_effect: SideEffect,
    name: str | None = None,
    description: str | None = None,
    timeout_s: float = 10.0,
    cost_estimate: float = 0.0,
    idempotency_key: IdempotencyKeyFn | None = None,
    resource: ResourceFn | None = None,
) -> Callable[[Callable[..., Any]], Tool]

Declare a tool.

The function takes exactly one argument, a Pydantic model, and returns a Pydantic model; both annotations are required and become the input and output schemas. resource maps the validated input to the facts the policy needs ({"symbol": ..., "notional": ...}) so policy packs stay generic. A WRITE tool must supply idempotency_key.