Interfaces (ports)
The seams the SDK is built to swap. A port exists only where there's more than one real implementation — one local, in-process default, and one Kendra-hosted or otherwise networked alternative. See the architecture guide for how these fit into the layering.
ARI ports
Portability between the local default and a Kendra-hosted deployment.
IModelGateway
Bases: Protocol
Route a reasoning request to a model and return a typed completion.
ARI port. Default implementations: providers/mock_lm.py (offline, deterministic — the
default gateway) and providers/gateway_openai.py; an enterprise deployment supplies the
Kendra AI Gateway.
Concurrency: implementations MUST be safe to call concurrently from within a superstep, and MUST NOT introduce nondeterminism into workflow scope — any retry, timing, or sampling decision belongs inside the implementation, which the runtime treats as an activity boundary.
available_models
async
available_models() -> list[ModelCard]
List the models this gateway can route to.
complete
async
complete(
messages: list[Message],
*,
model: str,
max_tokens: int | None = None,
) -> Message
Return the model's completion to messages as an assistant :class:Message.
IDurableRuntime
Bases: Protocol
Execute a Pregel run and own its durability, timing, and control signals.
Implementations: runtime/local_runtime.py (in-process, the default) and
runtime/temporal_runtime.py (durable, behind [temporal]).
The graph, the model gateway, the channel schema, and the clock are supplied to the concrete
runtime at construction — the runtime constructs no collaborators, and this protocol never
references the core graph type, keeping the contract dependent on models alone (ADR 0010
resolves spec 06 §6's start(graph, ...) against the inward-only layering rule).
Determinism: workflow-scope code inside an implementation MUST use :meth:now for time and no
randomness; all nondeterminism lives in activities (spec 06 §5).
now
now() -> datetime
Return the replay-safe current time. NEVER datetime.now().
signal
async
signal(
run_id: str, name: str, payload: Mapping[str, str]
) -> None
Deliver a durable control signal (resume, cancel, edit_resume).
start
async
start(
state: AgentState, *, max_supersteps: int = 10
) -> str
Begin a run from state and return its stable run_id without waiting.
wait
async
wait(
run_id: str, *, timeout_seconds: float | None = None
) -> RunResult
Block until run_id reaches a terminal or paused state and return its result.
IExecutionSandbox
Bases: Protocol
Execute a tool invocation in isolation, bounded by a timeout and resource limits.
ARI port. Default implementation: providers/sandbox_local.py (subprocess isolation); an
enterprise deployment supplies OpenSandbox.
Concurrency: implementations MUST be safe to call concurrently and MUST enforce the timeout.
Tool output is untrusted input — it is returned as a :class:ToolResult for the caller to
validate, redact, and never execute. Tenant scope is mandatory.
Note: the P1 contract is intentionally minimal; the invocation shape may be enriched, via an ADR, when the AUB bridge (P6) and the enterprise sandbox land.
execute
async
execute(
tool: str,
args: Mapping[str, JSONValue],
*,
tenant_id: str = "default",
timeout_seconds: float = 30.0,
) -> ToolResult
Run tool with args under isolation; return a :class:ToolResult.
IIdentityProvider
Bases: Protocol
Authenticate an agent and resolve it to a DID within a tenant scope.
ARI port. Default implementation: providers/identity_local.py — an unsecured,
single-tenant provider that MUST log a WARNING on construction and MUST be rejected under a
durable multi-tenant deployment (spec 08 §5); an enterprise deployment supplies KIAM / KACP.
Concurrency: implementations MUST be safe to call concurrently. Tenant scope is mandatory data — it is passed in and enforced, never inferred from a client-supplied field.
Note: the P1 contract is intentionally minimal; a richer identity model may be introduced, via an ADR, when the enterprise provider lands.
authenticate
async
authenticate(
agent_id: str, *, tenant_id: str = "default"
) -> str
Authenticate agent_id in tenant_id and return its resolved DID.
tenant_of
tenant_of(agent_id: str) -> str
Return the tenant this agent is scoped to.
Supporting protocols
GraphRepository
Bases: Protocol
Persist and retrieve run state and Context Graph nodes, always scoped by tenant.
Implementations: an in-memory backend (the default; PERSISTENCE_BACKEND=memory), a mock,
and external backends post-1.0. PERSISTENCE_BACKEND=none runs fully standalone.
Concurrency: implementations MUST be safe for concurrent reads and writes. tenant_id is
mandatory on every call and MUST scope the data — cross-tenant access is a defect.
save_state/load_state (P1) checkpoint run state; record_node/query_nodes
(P7.6) are the bitemporal decision/event node API the Context Graph client
(persistence.ContextGraphClient) is layered on — the extension the P1 docstring
anticipated ("layered on this protocol when it lands").
load_state
async
load_state(
run_id: str, *, tenant_id: str
) -> AgentState | None
Return the latest saved state for run_id in tenant_id, or None if absent.
query_nodes
async
query_nodes(
*,
tenant_id: str,
run_id: str | None = None,
as_of: datetime | None = None,
valid_at: datetime | None = None,
) -> tuple[GraphNode, ...]
Return tenant-scoped nodes, optionally filtered by run_id and time-travelled.
as_of keeps only nodes recorded at or before that transaction_time ("what did we
know as of this recording time"); valid_at keeps only nodes true at or before that
valid_time ("what was true as of this moment"). Both may be combined.
record_node
async
record_node(node: GraphNode, *, tenant_id: str) -> None
Append an immutable :class:GraphNode (event sourcing — nodes are never overwritten).
save_state
async
save_state(state: AgentState, *, tenant_id: str) -> None
Persist state under its run_id within tenant_id.
TenantStore
Bases: Protocol
Resolve and scope tenant data.
Default implementation: an in-memory store. Used to validate that a tenant_id is known
before data is scoped to it.
is_known
async
is_known(tenant_id: str) -> bool
Return whether tenant_id is a recognised tenant.
BaseRouter
Bases: Protocol
Select a model for a task and return an explainable decision.
Implementations (strategies behind one factory, get_router()): explicit (the default,
works with no extra), algorithmic, semantic ([routing]), composite, and a user-supplied
function. A custom router plugs in via config with no package edit.
Concurrency: select_model MUST be safe to call concurrently and MUST be pure with respect
to its :class:RoutingContext input — the same context yields the same
:class:RoutingResult, so routing cannot introduce nondeterminism into a superstep.
select_model
async
select_model(context: RoutingContext) -> RoutingResult
Choose a model for context and return the decision with its rationale.
AUBConnector
Bases: Protocol
Execute a single tool invocation and return a normalised ToolResult.
Implementations: filesystem, search (with a mock fallback), MCP-backed, and user-defined connectors, registered in the connector registry (P6). A new connector plugs in without a core edit.
Concurrency: execute MUST be safe to call concurrently. Arguments are validated against
the connector's schema before this call; output is untrusted and returned as a
:class:ToolResult for the bridge to redact. tenant_id is mandatory scope.
Note: the P1 contract passes tenant scope explicitly; a richer invocation context may be introduced, via an ADR, when the AUB bridge lands (P6.2).
execute
async
execute(
tool: str,
args: Mapping[str, JSONValue],
*,
tenant_id: str = "default",
) -> ToolResult
Invoke tool with args in tenant_id; return a :class:ToolResult.
Connector
Bases: AUBConnector, Protocol
A single named tool: an :class:AUBConnector that also advertises its name and schema.
This is the contract the bridge and registry key on. Native connectors and MCP-backed connectors
both implement it. execute MUST NOT raise for expected failures — it returns
ToolResult(ok=False, error_code=...); only unexpected failures propagate (the bridge wraps
them as ToolError). Arguments are validated by the bridge before execute is called.
description
property
description: str
A short human/model-readable description of what the tool does.
name
property
name: str
The tool name agents mount by (AgentConfig.tools) and the registry keys on.
schema
property
schema: Mapping[str, JSONValue]
The JSON-Schema (object) the bridge validates arguments against.
IToolInvoker
Bases: Protocol
Invoke one mounted tool by name and return its normalised :class:ToolResult.
ARI-style port. The one implementation is tools.bridge.RegistryToolInvoker, which binds a
ConnectorRegistry and delegates to invoke_tool — the mount gate, schema validation,
timeout, rate limiting, and redaction all still apply; this port only narrows the surface a
reasoning agent needs down to what agents/ is allowed to import.
describe_tool
describe_tool(tool: str) -> str
Return tool's human-readable description, or "" if it is not registered.
A reasoning agent uses this to tell a model what a mounted tool does; synchronous because it is metadata lookup, never I/O.
invoke_tool
async
invoke_tool(
tool: str,
args: Mapping[str, JSONValue],
*,
tenant_id: str,
mounted: Collection[str],
) -> ToolResult
Run tool with args in tenant_id, restricted to the mounted tool names.