A production-grade Python SDK for the Autohand CLI, enabling programmatic control of code agents through a high-level async API over the CLI's JSON-RPC mode.
Beta: this SDK is actively evolving while the Agent SDK APIs stabilize. Pin versions in production and review release notes before upgrading.
The Agent SDK is available in multiple beta language packages. Use the same CLI-backed SDK model from another programming language:
- TypeScript -
Agent,Run, streaming, and JSON helpers for Node and Bun hosts. - Go - idiomatic Go package with
context.Context, typed events, and channel-based streaming. - Python - this package, with
asyncio,async forevent streams, and typed Pydantic models. - Java - Java 21 records, sealed events, and virtual-thread-ready APIs.
- Swift - SwiftPM package with
Agent,Runner, async streams, tools, hooks, and permissions. - Rust - async Rust crate with Tokio, typed events, and stream-based runs.
- C++ - modern C++20 package with CMake targets and typed event callbacks.
- C# - .NET package with
IAsyncEnumerable,CancellationToken, andSystem.Text.Json.
- Async/await native - First-class support for async programming
- Type-safe - Full type hints with Pydantic models
- Skill support - Configure skills by name or file path
- Skill discovery - Search the community registry and install skills with typed results
- MCP discovery - List servers, tools, and server configurations through typed APIs
- Event streaming - Real-time JSON-RPC notifications from the agent
- Typed event parsing - Optional Pydantic parsing for known event types
- Replayable autoresearch - Typed lifecycle, history, replay, rescore, compare, Pareto, pin, and prune APIs
- Automatic cleanup - Context manager support for resource management
- Production transport - Response matching, notification routing, timeouts, and typed RPC errors
- 90%+ test coverage - Subprocess-backed transport and streaming tests
Using uv:
uv add autohand-sdkUsing pip:
pip install autohand-sdkimport asyncio
import os
from autohand_sdk import AutohandSDK
async def main():
async with AutohandSDK(
cwd=".",
provider="autohandai",
model="fantail",
api_key=os.environ["AUTOHAND_AI_API_KEY"],
) as sdk:
async for event in sdk.stream_prompt("Hello, world!"):
if event["type"] == "message_update":
print(event.get("delta", ""), end="")
elif event["type"] == "tool_start":
print(f"\nRunning {event.get('tool_name') or event.get('toolName')}")
asyncio.run(main())Configure skills so the agent can reference them via /skill <name>:
sdk = AutohandSDK(
cwd=".",
provider="autohandai",
model="fantail",
api_key=os.environ["AUTOHAND_AI_API_KEY"],
skill_refs=["typescript", "testing", "react"],
)Provide skills directly, including custom skill files:
sdk = AutohandSDK(
cwd=".",
provider="autohandai",
model="fantail",
api_key=os.environ["AUTOHAND_AI_API_KEY"],
skill_refs=[
"typescript", # Built-in
"./skills/my-custom/SKILL.md", # Local file
{"name": "api", "path": "/path/SKILL.md"}, # Named skill
],
)The SDK automatically:
- Copies skill files to
~/.autohand/skills/ - Extracts skill names from file paths
- Passes skill names to CLI via
--skillsflag
Set copy_skill_files=False if you only want to activate skills that are
already installed and do not want startup to write into ~/.autohand/skills.
Assign sdk.skills only before start(). Changing it while the SDK is running
raises RuntimeError; stop the SDK first so the live CLI subprocess remains
owned and is not replaced.
Discover and install skills at runtime:
registry = await sdk.get_skills_registry(force_refresh=False)
for skill in registry.skills:
print(skill.name, skill.download_count)
installed = await sdk.install_skill("typescript", scope="project")
if not installed.success:
raise RuntimeError(installed.error)MCP discovery uses the same typed style:
servers = await sdk.list_mcp_servers()
tools = await sdk.list_mcp_tools(server_name="github")
configs = await sdk.get_mcp_server_configs()import os
from autohand_sdk import AutohandSDK
sdk = AutohandSDK(
cwd=".", # Working directory
provider="autohandai", # Provider to use
model="fantail", # Model to use
api_key=os.environ["AUTOHAND_AI_API_KEY"],
temperature=0.7, # Sampling temperature
max_iterations=10, # Max iterations in auto-mode
startup_check=True, # Probe CLI readiness on start
debug=True, # Enable debug logging
unrestricted=True, # Unrestricted mode
auto_mode=True, # Enable auto-mode
permission_mode="default", # CLI permission mode
env_vars={"AUTOHAND_NO_BANNER": "1"},
)Full guides live in docs/:
docs/GETTING_STARTED.md- install, authenticate, configure providers, and run the first prompt.docs/API_REFERENCE.md- constructor, lifecycle, prompts, events, permissions, and config models.docs/configuration.md- provider, execution, skills, context, session, and environment options.docs/event-streaming.md- event shapes and streaming patterns.docs/error-handling.md- transport, RPC, timeout, abort, and recovery patterns.docs/advanced-patterns.md- system prompts, sessions, model switching, JSON output, and AGENTS.md.docs/permissions.md- permission modes and programmatic approval.docs/plan-mode.md- read-only planning and gated implementation.docs/memory.md- CLI memory behavior through Python event streams.docs/sdlc-workflows.md- discovery, gated implementation, and release-readiness flows.docs/autoresearch.md- replayable/autoresearchlifecycle and experiment ledger.
The Python SDK exposes the same typed JSON-RPC autoresearch contract as the TypeScript v1.0.3 SDK. Start or resume a session, execute the returned loop instruction through the normal event stream, then inspect or replay its ledger:
async with AutohandSDK(cwd=".", unrestricted=True) as sdk:
started = await sdk.start_autoresearch(
objective="Reduce test latency without changing behavior",
max_iterations=10,
metric_name="test_ms",
metric_unit="ms",
direction="lower",
measure_command="uv run pytest",
files_in_scope=["src", "tests"],
)
if not started.success:
raise RuntimeError(started.error or "autoresearch could not start")
if started.instruction:
async for event in sdk.stream_prompt(started.instruction):
if event["type"] == "autoresearch":
print(event)
history = await sdk.get_autoresearch_history()
pareto = await sdk.get_autoresearch_pareto()
await sdk.prune_autoresearch(dry_run=True)
await sdk.stop_autoresearch()See docs/autoresearch.md and
docs/examples/27-autoresearch-ledger.py.
Main SDK class for interacting with the Autohand CLI.
start()- Start the SDK and connect to CLIstop()/close()- Stop the SDK and cleanupstream_prompt(message, **kwargs)- Send a prompt and stream eventsabort(reason=None)- Abort current operationget_state()- Get current agent stateget_messages(limit=None, before=None)- Get messagesget_skills_registry(force_refresh=None)/install_skill(...)- Discover and install skillslist_mcp_servers()/list_mcp_tools(...)/get_mcp_server_configs()- Inspect MCP integrationsset_plan_mode(enabled)- Change plan mode for the live sessionset_model(model)- Change the modelset_temperature(temperature)- Set temperatureget_account_info()- Get account informationstart_autoresearch(...)/get_autoresearch_status()/stop_autoresearch()- Manage the persisted experiment loopget_autoresearch_history()/replay_autoresearch(...)/rescore_autoresearch(...)- Inspect and re-evaluate ledger entriescompare_autoresearch(...)/get_autoresearch_pareto()- Analyze candidate qualitypin_autoresearch(...)/prune_autoresearch(...)- Manage retained candidate artifacts
Prompt RPC responses are acknowledgements, so stream_prompt() stays open
through the terminal agent_end notification. Closing an iterator early sends
an abort and drains that turn; a CLI that cannot settle within two seconds is
retired before another prompt can use the transport. Event queues are bounded
at 1,024 entries per consumer.
The repository gates cold public import, public start(), and fixture
spawn-to-first-getState p95 latency below 50 ms. Interpreter boot is excluded
from the import metric; provider/network readiness is environment-dependent.
The command emits stable JSON with top-level language, budgetMs, metrics,
and passed. Each metric contains samples, medianMs, p95Ms, maxMs, and
its own passed result.
uv run python benchmarks/startup.pyconfig- Current SDK configurationskills- Get/set skill references beforestart()
The SDK emits events during agent execution:
agent_start- Agent startedturn_start/turn_end- Prompt turn lifecyclemessage_start- Assistant message startedmessage_update- New token/chunkmessage_end- Message completetool_start- Tool execution startedtool_update- Streaming tool outputtool_end- Tool execution completedpermission_request- Permission neededfile_modified- File mutation hook notificationautoresearch- Lifecycle or replay-ledger operation notificationagent_end- Agent finishederror- Error occurred
Events are dictionaries. Known camelCase RPC keys are also exposed with Python-friendly snake_case aliases, so toolName is available as tool_name, requestId as request_id, and so on.
For typed handling, parse known event dictionaries into Pydantic models:
from autohand_sdk import parse_sdk_event
async for raw_event in sdk.stream_prompt("Hello"):
event = parse_sdk_event(raw_event)
if not isinstance(event, dict) and event.type == "message_update":
print(event.delta or "", end="")Transport and RPC failures raise SDK-specific exceptions:
from autohand_sdk import RPCError, RequestTimeoutError, TransportNotStartedErrorTransportNotStartedError- request attempted beforestart()RequestTimeoutError- request exceededtimeoutRPCError- CLI returned a JSON-RPC error response; exposescodeanddata
Using uv for dependency management:
# Install dependencies
uv sync
# Run tests with coverage
uv run pytest
# Run type checking
uv run mypy src
# Run linting
uv run ruff check .
# Format code
uv run ruff format .MIT License - see LICENSE file for details.