An exploration of making an agent sdk as lean as possible while being effective.
Project description
minimal-harness
Documentation: /docs
A lightweight Python agent harness for building LLM-powered agents with tool-calling support.
Latest version: 0.3.0.post4
What This Project Is For
Minimal-harness is a lean framework for building agents that can call tools. It provides:
- OpenAI-compatible API - Works with any OpenAI-compatible API provider
- Tool system - Create tools via decorators; includes built-in tools (bash, file ops)
- AsyncIterator events - Real-time async iteration for chunks, tool start/end, execution events
- Conversation memory - Tracks token usage across interactions
- ESC stop support - Gracefully stop LLM streaming and tool execution
Architecture
The framework uses an event-driven architecture with AsyncIterator-based event handling:
Agent (OpenAIAgent) → Internal Events → FrameworkClient → Client-Facing Events
Event flow:
async for event in agent.run(user_input=[{"type": "text", "text": "..."}]):
if isinstance(event, LLMChunk):
# handle chunk
elif isinstance(event, ToolEnd):
# handle tool result
How to Build an App
Project Structure
A typical app looks like this:
my-app/
├── cli.py # Entry point
└── tools.py # Your custom tools
1. Create Your Entry Point
import argparse
import os
from openai import AsyncOpenAI
from minimal_harness.agent.openai import OpenAIAgent
from minimal_harness.client.client import FrameworkClient
from minimal_harness.client.events import (
AgentStartEvent,
AgentEndEvent,
LLMChunkEvent,
ToolStartEvent,
ToolEndEvent,
)
from minimal_harness.llm.openai import OpenAILLMProvider
from minimal_harness.memory import ConversationMemory
from minimal_harness.tool.built_in.bash import get_tools as get_bash_tools
def main():
parser = argparse.ArgumentParser(description="My AI agent")
parser.add_argument("--base-url", required=True)
parser.add_argument("--api-key", required=True)
parser.add_argument("--model", default="qwen3.5-27b")
args = parser.parse_args()
client = AsyncOpenAI(base_url=args.base_url, api_key=args.api_key)
llm_provider = OpenAILLMProvider(client=client, model=args.model)
memory = ConversationMemory(system_prompt="You are a helpful assistant.")
agent = OpenAIAgent(
llm_provider=llm_provider,
tools=list(get_bash_tools().values()),
memory=memory,
)
framework_client = FrameworkClient(agent=agent)
async def run():
stop_event = asyncio.Event()
async for event in framework_client.run(
user_input=[{"type": "text", "text": "What files are in the current directory?"}],
stop_event=stop_event,
):
if isinstance(event, AgentStartEvent):
print(f"Agent starting...")
elif isinstance(event, LLMChunkEvent):
chunk = event.chunk
if chunk and chunk.choices:
content = chunk.choices[0].delta.content or ""
print(content, end="", flush=True)
elif isinstance(event, ToolStartEvent):
print(f"\n[Calling tool: {event.tool_call['function']['name']}]")
elif isinstance(event, ToolEndEvent):
print(f"\n[Tool result: {event.result[:100]}...]")
elif isinstance(event, AgentEndEvent):
break
import asyncio
asyncio.run(run())
if __name__ == "__main__":
main()
2. Add Custom Tools
Use the @register_tool decorator to add your own tools:
from typing import AsyncIterator
from minimal_harness.tool.registration import register_tool
@register_tool(
name="get_weather",
description="Get weather for a location",
parameters={
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
)
async def get_weather(location: str) -> AsyncIterator[dict]:
yield {"success": True, "result": f"The weather in {location} is sunny."}
The decorator auto-registers the tool. Just import it before running the agent.
3. Run
python cli.py --base-url https://api.openai.com/v1 --api-key sk-... --model gpt-4o
Or set environment variables:
export MH_BASE_URL=https://api.openai.com/v1
export MH_API_KEY=sk-...
export MH_MODEL=gpt-4o
python cli.py
Built-in Tools
| Tool | Description |
|---|---|
bash |
Execute shell commands with timeout |
read_file |
Read file contents with line range |
create_file |
Create new files |
patch_file |
Patch files (append, prepend, etc.) |
delete_file |
Delete files |
Event Types
| Event | Description |
|---|---|
AgentStartEvent |
Agent execution started |
AgentEndEvent |
Agent execution completed |
LLMStartEvent |
LLM generation started |
LLMChunkEvent |
LLM output chunk received |
LLMEndEvent |
LLM generation completed |
ExecutionStartEvent |
Tool execution started |
ExecutionEndEvent |
Tool execution completed |
ToolStartEvent |
Tool call started |
ToolProgressEvent |
Tool intermediate progress |
ToolEndEvent |
Tool call completed with result |
Environment Variables
| Variable | Description |
|---|---|
MH_BASE_URL |
API base URL |
MH_API_KEY |
API key |
MH_MODEL |
Model name (default: qwen3.5-27b) |
Stop Mechanism
Press ESC during execution to gracefully stop LLM streaming and tool execution.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file minimal_harness-0.3.1.post1.tar.gz.
File metadata
- Download URL: minimal_harness-0.3.1.post1.tar.gz
- Upload date:
- Size: 21.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af4618c420b9bb3fbda16d30b1c1b591be46d8b0936e47dd338bdc3278891bf1
|
|
| MD5 |
d2b543954925df1a42280cb377c115cd
|
|
| BLAKE2b-256 |
38a5cb93265412dcb54991cc8d27e94698547b9d2659f0021c1802f4ef9610cf
|
File details
Details for the file minimal_harness-0.3.1.post1-py3-none-any.whl.
File metadata
- Download URL: minimal_harness-0.3.1.post1-py3-none-any.whl
- Upload date:
- Size: 32.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d30e167d1eb619e82b4b34aee1babcd0cb649e81d56ad5a4ab3f48c30b8fec8
|
|
| MD5 |
b4890bf2b7a7459e34fd2070575e0930
|
|
| BLAKE2b-256 |
66764c860afa57b9716c5903d6b5716cdc06b6c4ba64cbda1578d629bdd9263f
|