# API reference

Public interfaces, defaults, and ownership.

These are the primary public interfaces of the 0.4 SDK. Each topic page includes usage and guarantee boundaries; the linked source provides full signatures. Upgrading from 0.3? [Migrating to 0.4](../migrating/) lists every renamed, regrouped, and removed name with its replacement.

## Agent

~~~python
from harnessx import Agent, AgentConfig, Limits

agent = Agent(
    config=AgentConfig(limits=Limits(max_iterations=20)),
    tools=[],
)
~~~

| Method / property | Contract |
| --- | --- |
| `await agent.run(message)` | Final `RunResult` |
| `agent.run_stream(message)` | `RunStream`: async context manager and `RunEvent` iterator |
| `agent.stream_text(message, on_reset=...)` | Async iterator of answer text; raises `RunFailed` if the run does not complete |
| `await stream.result()` | Final result for that stream |
| `stream.text(on_reset=...)` | Text-only view over any `RunStream` |
| `agent.closed` / `agent.busy` | `True` once `aclose()` has started / while a run is in progress |
| `agent.tools` | `ToolRegistry`; `agent.tools.policy` and `agent.tools.sandbox` expose the effective tool policy and the bound sandbox |
| `agent.mcp_tools` | Names of the MCP tools bridged from `Agent(mcp=...)` |
| `agent.memory` | `ConversationMemory` |
| `agent.permissions` | `PermissionManager` |
| `agent.guardrails.usage_summary` | Accumulated usage |
| `await agent.save_session(storage_dir=...)` | Save a snapshot; returns session ID |
| `await Agent.load_session(session_id, ...)` | Construct an agent from a validated snapshot |
| `await agent.aclose()` | Release owned resources |

One Agent permits one active run; starting a run on a busy or closed agent raises `RuntimeStateError`. Use a distinct agent per concurrent conversation. Constructor injection includes providers, tool registries, memory, permissions, hooks, middleware, sandbox, MCP, skills, knowledge bundles, extensions, and subagent definitions. `Agent(client=...)` was removed in 0.4; wrap the SDK client in a provider: `Agent(provider=AnthropicProvider(client=client))`.

## SubAgent

~~~python
from harnessx import Agent, AgentConfig, Limits, SubAgent

agent = Agent(subagents=[SubAgent(
    name="reviewer", description="Review supplied code",
    config=AgentConfig(system_prompt="Review code carefully", limits=Limits(max_iterations=5)),
)])
~~~

Required: name, description, config. Optional: tools, skills (paths), permission,
and timeout_seconds (`DEFAULT_TIMEOUT_SECONDS`, 300 seconds, by default). Agent.subagents is a tuple of bound
specialist definitions. Each task gets a fresh conversation; delegation returns
to the orchestrator. The generated delegate_<name> tool is serial and uses manual
replay policy. Child progress is not independently durable. See
[multi-agent orchestration](../multi-agent/).

## AgentConfig

Five top-level fields plus four sub-policies. Only `model`, `provider`, and `max_tokens` may be passed positionally; everything else is keyword-only.

| Field | Default | Meaning |
| --- | --- | --- |
| model | claude-sonnet-4-6 | Provider model ID |
| provider | anthropic | A built-in name or one added with `register_provider()` |
| max_tokens | None | Reply token budget; unset, the provider chooses for the model (20,000 on Claude 4 models, 8,192 elsewhere) |
| system_prompt | You are a helpful assistant. | Base instructions |
| temperature | None | Omitted unless configured |
| limits | `Limits()` | Iteration, context, result-size, and cost budgets |
| retry | `RetryPolicy()` | Transient-failure handling for model calls |
| prompt_cache | `PromptCachePolicy()` | Prompt-prefix caching policy; None disables it |
| tools | `ToolPolicy()` | Registry-wide tool options applied to the registry the agent adopts |

| Sub-policy | Fields and defaults |
| --- | --- |
| `Limits` | `max_iterations=50` (0 means unlimited), `max_context_tokens=150_000`, `max_result_chars=12_000`, `max_cost_dollars=None`, `input_cost_per_m=None`, `output_cost_per_m=None` |
| `RetryPolicy` | `attempts=2` (1 disables retry), `backoff_seconds=0.5`, `call_timeout_seconds=None` (sized to the reply budget, never below 300 s) |
| `ToolPolicy` | `default_timeout_seconds=None`, `dedupe_calls=False` |
| `PromptCachePolicy` | `ttl_seconds=None`, `cache_history=True`, `key_salt=""` |

The sub-policies are frozen dataclasses; derive one with `dataclasses.replace`. Values are validated at construction and raise `ConfigurationError`. The flat 0.3 names (`max_iterations`, `max_context_tokens`, `max_result_chars`, `max_cost_dollars`, `input_cost_per_m`, `output_cost_per_m`, `llm_max_attempts`, `llm_retry_backoff_seconds`, `model_timeout_seconds`) still work as keyword arguments and attributes but warn on every use; they are removed in 0.5. `AgentConfig.from_dict()` reads the 0.3 flat and the 0.4 nested shape without warnings. Model availability is checked by the provider service.

## ToolRegistry

| Method | Use |
| --- | --- |
| `ToolRegistry(default_timeout_seconds=..., dedupe_calls=..., sandbox=...)` | Constructor values take precedence over `AgentConfig.tools` |
| register(...) | Decorator registration |
| register_tool(handler, ...) | Register a Python callable |
| register_with_schema(name, description, input_schema, handler, *, ...) | Supply an explicit input schema; metadata is keyword-only |
| get_tool(name) / has_tool(name) | Inspect registration |
| unregister(name) | Remove a tool |
| policy | The effective `ToolPolicy` after adopting the agent's config |
| await execute(call, permissions=...) | Validate, authorize, and execute |

Policies include permission, replay_policy (a `ReplayPolicy` member or `"manual"`, `"safe"`, `"idempotent"`), concurrent, and timeout_seconds (`DEFAULT_TIMEOUT_SECONDS` unless a `ToolPolicy` changes the default). Duplicate names require replace=True. An omitted permission inherits the manager default, normally ALLOW. A denied call returns the error result `"Permission denied"`. See [Tools](../tools/#registry-wide-tool-policy) for precedence.

## AgentRuntime

For SQLite or PostgreSQL, construct AgentRuntime(agent, backend=backend) with a configured Agent. Recreate a fresh compatible agent before resuming in another process. AgentRef and AgentRegistry remain supported for named definitions; the current Temporal path requires AgentRef and worker-side bindings. See **Advanced topics → [Durable agent runs](../durable-agent-runs/)** for the complete API walkthrough and [PostgreSQL setup](../postgres-durability/#use-an-agent-directly) for the runnable example.

| Method | Use |
| --- | --- |
| await start() | Create a new durable session |
| await submit(message, request_id=...) | Return RunHandle |
| await run(message, request_id=...) | Run one durable turn and wait for its RunResult |
| run_stream(message, request_id=...) | Run one durable turn as a `RunStream` of typed events |
| stream_text(message, request_id=..., on_reset=...) | Text-only view; raises `RunError` if the run does not complete |
| await resume(session_id) | Resume unfinished work or return None |
| await approve(target, allow=True, resume=False) | Record a decision for a `PendingTool` or execution key; `resume=True` returns the finished RunResult |
| await decline(target, resume=False) | Refuse a paused tool; the model sees `"Permission denied"` |
| await resolve_tool(target, result=..., retry=..., abort=...) | Reconcile an uncertain tool by `PendingTool` or execution key |
| await pause() / cancel() | Control execution |
| await stop() / aclose() | Pause local work, close the agent, and close a backend the runtime created |
| await status() | `session_id`, `state`, and `run`, the latest RunResult or None |
| await export_incident(run_id, ...) | Export a recorded incident |

`execute()`, `execute_stream()`, and `get_status()` remain as deprecated aliases until 0.5. `RunHandle.backend` is deprecated; runtimes own their backend.

### RunHandle and RunResult

submit() and a successful resume() return a RunHandle. resume() returns None for a session with no run or a latest run that is already completed/cancelled. A missing session raises an error.

| Handle API | Use |
| --- | --- |
| session_id / run_id | Identify the conversation and execution |
| await result() | Wait for a RunResult, including approval waits, pauses, and failures |
| events(after=cursor) | Read stored events and follow new events |
| stream(after=cursor) | Open an async-context-managed event stream |

RunResult contains output, status, error, pending operations, IDs, usage, stop_reason, and usage_incomplete. Inspect status before treating output as a successful final answer. Controls such as approve(), decline(), pause(), cancel(), and resume() belong to AgentRuntime.

| Predicate / method | Meaning |
| --- | --- |
| `result.ok` | status is COMPLETED |
| `result.failed` | status is FAILED; `result.error` has `type` and `message` |
| `result.needs_input` | status is AWAITING_INPUT; `result.pending` lists `PendingTool` objects |
| `result.raise_for_status()` | Returns the result, or raises `RunFailed`, `RunAwaitingInput`, `RunTruncated`, or `RunCancelled`, each carrying `.result` |

The [advanced guide](../durable-agent-runs/#use-a-runhandle) explains result handling, event cursors, reconnection, and the difference between a handle and an answer. A disconnected subscriber does not itself cancel durable execution; the runtime and worker must remain alive.

### PendingTool

| Attribute | Meaning |
| --- | --- |
| `execution_key` | Identifies the paused tool for `approve`, `decline`, and `resolve_tool` |
| `call` | The `ToolCall`: `call.id`, `call.name`, `call.input` |
| `status` | `"approval"` or `"uncertain"` |
| `attempt` | Attempt counter |
| `policy` | The tool's replay policy string |
| `concurrent` | Whether the tool was declared concurrent |
| `timeout_seconds` | The tool's timeout |

Indexing (`pending["execution_key"]`) still works and is deprecated. The persisted shape (`to_dict()` / `from_dict()`) is unchanged from 0.3.

## Errors

Every exception the SDK raises on purpose derives from `harnessx.HarnessError`. Classes that predate 0.4 keep their previous bases, so `except ValueError` and `except RuntimeError` clauses keep working.

| Error | Also a | Raised for |
| --- | --- | --- |
| `ConfigurationError` | `ValueError` | invalid `AgentConfig`, `Limits`, `RuntimeConfig`, `SandboxConfig`, mismatched sessions |
| `RuntimeStateError` | `RuntimeError` | closed or busy agent, runtime not started, run not awaiting a resolution |
| `ResolutionError` | `ValueError` | an approval or recovery decision that does not fit the tool's state |
| `UnknownExecutionKey` | `KeyError` | `approve` / `resolve_tool` with a key the run does not have |
| `RunError`, `RunFailed`, `RunAwaitingInput`, `RunTruncated`, `RunCancelled` | | `raise_for_status()` and `stream_text()`; carry `.result` |
| `StorageError` and subclasses, `IncidentError`, `RecordingError`, `MaxIterationsError`, `CostLimitError`, `ToolNotFoundError`, `ToolApprovalRequired` | their 0.3 bases | unchanged sites |

## Typed results and events

RunResult exposes output, status, run_id, error, and `pending`. RunEvent carries a type and a validated payload; `RunEvent(data=...)` is required. ToolCall uses id, name, and input; ToolResult uses tool_call_id, content, and is_error.

`PermissionLevel`, `StopReason`, `RunStatus`, `RunEventType`, and `ReplayPolicy` are `str` enums (`PermissionLevel.ASK == "ask"`) and serialise with `json.dumps` directly. `ReplayPolicy` lives in `harnessx.types` and is accepted everywhere `replay_policy` is, alongside the plain strings. `StopReason.TOOL_USE` replaces the removed `TOOL_CALLS`; the removed `Role` enum gives way to the `"user"` / `"assistant"` strings. `DEFAULT_TIMEOUT_SECONDS = 300.0` is the shared default for tools, sub-agents, and model calls.

ProviderResponse's text, thinking, and tool_calls are canonical. Its content property is a compatibility view. Persistent values must be JSON-compatible and finite.

## Extension and integration interfaces

| Interface | Guide |
| --- | --- |
| LLMProvider / `register_provider` | [Model providers](../providers/) |
| DecisionProvider / JevDecisionProvider | [Decisions with Jev](../decisions/) |
| MCPManager | [MCP servers](../mcp/) |
| MCPServerConfig.stdio(...) / .http(...) | [MCP servers](../mcp/) |
| SkillManager | [Skills](../skills/) |
| KnowledgeManager / OKFBundle / OKFConcept / GitSource | [Knowledge (OKF)](../knowledge/) |
| Extension / ExtensionContext | [Extensions](../extensions/) |
| Middleware / HookManager / `HOOK_PAYLOADS` | [Hooks & middleware](../hooks/) |
| Sandbox | [Code execution](../sandbox/) |
| IncidentRecorder / ExportPolicy | [Flight recorder](../flight-recorder/) |
| SQLiteBackend / PostgresBackend / TemporalBackend | [Backends](../backends/) |
| `evaluate_agent` / `evaluate_agent_async` | [Tracing & evaluations](../evaluations/) |

## Exports added in 0.4

`Limits`, `RetryPolicy`, `ToolPolicy`, `DEFAULT_TIMEOUT_SECONDS`, `HarnessError`, `ConfigurationError`, `RuntimeStateError`, `ResolutionError`, `UnknownExecutionKey`, `RunError`, `RunFailed`, `RunAwaitingInput`, `RunTruncated`, `RunCancelled`, `ToolApprovalRequired`, `ToolExecutionContext`, `Registration`, `register_provider`, `evaluate_agent_async`, `KnowledgeManager`, `OKFBundle`, `OKFConcept`, and `GitSource` are top-level `harnessx` exports. `harnessx.durable` groups the runtime, backends, recorder, and their errors under one import.

Removed in 0.4: `Agent(client=...)`, `harnessx.streaming` and `StreamingAgent`, `Role`, `StopReason.TOOL_CALLS`, `RuntimeConfig.checkpoint_interval` and `max_checkpoints`, `LLMProvider.format_tools`, and `make_provider("azure-openai")`. See [Migrating to 0.4](../migrating/#removed-names) for each replacement.

## Decision SDK

Import these types from `harnessx.decisions`; they are not top-level `harnessx` exports.

| Contract | Public shape |
| --- | --- |
| `ChoiceQuestion` | `instructions`, `options`: at least two labels with descriptions |
| `ScoreQuestion` | `instructions`, `levels`: two to ten ordered descriptions |
| `BooleanCriteria` | Optional `true` and `false` descriptions |
| `BooleanQuestion` | `instructions`, optional `criteria` |
| `DecisionRequest` | `check_id`, `version`, strict JSON `state`, typed `questions` |
| `ChoiceAnswer` | `choice`, label `probabilities`, nullable `confidence` |
| `ScoreAnswer` | Fractional `score`, ordered `probabilities`, `levels`, nullable `confidence` |
| `BooleanAnswer` | `probability`; no implicit boolean conversion |
| `DecisionBatch` | `status`, typed `answers`, read-only `choices` / `scores` / `booleans`, identity, model metadata, latency, attempts, usage, error |
| `DecisionUsage` | Nullable `input_tokens`, `output_tokens`, `cost_dollars`; `incomplete` flag |
| `DecisionError` | Stable `code`, sanitized `message`, optional `status_code`, `retryable` flag |

`DecisionProvider.evaluate(request, *, timeout_seconds=None, max_attempts=None)` is async and returns `DecisionBatch`. The provider supports `aclose()` and async context management. `DecisionRequest` and `DecisionBatch` have `to_dict()` / `from_dict()` codecs using `schema_version=1`.

`JevDecisionProvider` defaults to model `jev-1.13.0`, a two-second total deadline, one transient retry, and 16,384 bytes of state. Optional constructor settings include `api_key`, `base_url`, and a borrowed `AsyncTypeSafeClient`. Per-call limits can shorten the deadline and cap attempts.

Every requested answer must validate for a completed batch. Unavailable batches expose no partial answers. The Jev adapter requires confidence for Choice and Score; the vendor-neutral answer types allow another provider to return `None`. See the [decision guide](../decisions/) for validation, retries, usage, and runnable examples.

## Resource ownership

Use async context managers whenever available. Agents close resources they construct. Injected providers, memory, MCP managers, and sandboxes remain application-owned: `Agent.aclose()` does not disconnect an `MCPManager`, so use `async with MCPManager()`. Runtimes close their executing agent and a backend they created; injected backends remain caller-owned, and every backend is an async context manager.

`JevDecisionProvider` closes the TypeSafe clients it creates and leaves injected clients open. Its `aclose()` cancels outstanding evaluations and is idempotent. Concurrent evaluations are supported; calls after close raise.
