agent daemon
Project description
agentd
LLM agent utilities featuring:
- Programmatic Tool Calling (PTC) - Bash-enabled agents with MCP tools exposed as AgentSkills
- Patched Responses API + Agent Daemon - Traditional tool_calls with MCP, plus YAML-configured reactive agents
Installation
pip install agentd
# or
uv add agentd
Programmatic Tool Calling (PTC)
PTC gives you a bash-enabled agent that unifies MCP tools with the AgentSkills spec.
Instead of JSON tool_calls, the LLM writes code in fenced blocks. MCP tools and @tool functions are auto-converted to Python bindings in a discoverable skills directory.
from agentd import patch_openai_with_ptc, display_events, tool
from openai import OpenAI
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression."""
import math
return str(eval(expression, {"__builtins__": {}}, {"sqrt": math.sqrt}))
client = patch_openai_with_ptc(OpenAI(), cwd="./workspace")
stream = client.responses.create(
model="claude-sonnet-4-20250514",
input=[{"role": "user", "content": "List files, then calculate sqrt(144)"}],
stream=True
)
for event in display_events(stream):
if event.type == "text_delta":
print(event.text, end="", flush=True)
elif event.type == "code_execution":
print(f"\n$ {event.code}\n{event.output}\n")
Key Features
Bash-enabled agent: The LLM can run shell commands directly:
```bash:execute
ls -la
git status
curl https://api.example.com/data
```
MCP + AgentSkills unified: Tools from MCP servers and @tool decorators are exposed as Python functions following the AgentSkills spec:
```python:execute
from lib.tools import read_file, fetch_url
result = read_file(path="/tmp/data.txt")
print(result)
```
File creation: The LLM can create new scripts:
```my_script.py:create
print("Hello from generated script!")
```
XML support: Also parses Claude's XML function call format:
<invoke name="bash:execute">
<parameter name="command">ls -la</parameter>
</invoke>
Auto-Generated Skills Directory
PTC generates a skills directory combining MCP tools and local functions:
skills/
lib/
tools.py # Python bindings for ALL tools (MCP + @tool)
filesystem/ # From @modelcontextprotocol/server-filesystem
SKILL.md # AgentSkills spec: YAML frontmatter + docs
scripts/
read_file_example.py
local/ # From @tool decorated functions
SKILL.md
scripts/
calculate_example.py
The LLM discovers tools by exploring:
ls skills/ # List available skills
cat skills/filesystem/SKILL.md # Read skill documentation
Then imports and uses them:
from lib.tools import read_file, calculate
MCP Bridge
An HTTP bridge runs locally to route tool calls:
# Auto-generated in skills/lib/tools.py
def read_file(path: str) -> dict:
return _call("read_file", path=path) # POST to http://localhost:PORT/call/read_file
The bridge dispatches to MCP servers or local Python functions as appropriate.
PTC with MCP Servers
from agents.mcp.server import MCPServerStdio
from agentd import patch_openai_with_ptc
mcp_server = MCPServerStdio(
params={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"]},
cache_tools_list=True
)
client = patch_openai_with_ptc(OpenAI(), cwd="./workspace")
response = client.responses.create(
model="claude-sonnet-4-20250514",
input="Explore the available skills and use one",
mcp_servers=[mcp_server],
stream=True
)
Display Events
from agentd import display_events
for event in display_events(stream):
match event.type:
case "text_delta":
print(event.text, end="")
case "code_execution":
print(f"Code: {event.code}")
print(f"Output: {event.output}")
print(f"Status: {event.status}") # "completed" or "failed"
case "turn_end":
print("\n---")
Microsandbox Executor
Run code in hardware-isolated microVMs instead of subprocesses for secure execution.
Install microsandbox:
# Linux (requires KVM) or macOS (Apple Silicon only)
curl -sSL https://get.microsandbox.dev | sh
# Start the server
msb server start --dev
Usage:
from agentd import patch_openai_with_ptc, create_microsandbox_cli_executor
from openai import OpenAI
# Create sandboxed executor
executor = create_microsandbox_cli_executor(
conversation_id="my_session",
image="python",
memory=1024,
timeout=60,
)
client = patch_openai_with_ptc(
OpenAI(),
cwd=str(executor.snapshot_manager.workspace_dir),
executor=executor,
)
stream = client.responses.create(
model="claude-sonnet-4-20250514",
input=[{"role": "user", "content": "Run some Python code"}],
stream=True
)
# ... handle events ...
# Create snapshot for time travel
executor.snapshot("checkpoint_1")
# Restore to previous state
executor.restore("checkpoint_1")
executor.close()
Features:
- Hardware isolation: Code runs in microVMs, not just containers
- Persistent workspace: Volume mounting preserves files across executions
- Snapshots: Save and restore workspace state at any point
- Drop-in replacement: Same interface as the default subprocess executor
Sandbox Runtime Executor
Lightweight OS-level sandboxing using Anthropic's sandbox-runtime. Uses sandbox-exec on macOS and bubblewrap on Linux - no containers or VMs required.
Install sandbox-runtime:
npm install -g @anthropic-ai/sandbox-runtime
Linux only: If using AppArmor, you may need to allow unprivileged user namespaces:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
Usage:
from agentd import patch_openai_with_ptc, create_sandbox_runtime_executor
from openai import OpenAI
executor = create_sandbox_runtime_executor(
conversation_id="my_session",
# Network restrictions (allow-list)
allowed_domains=["github.com", "pypi.org"],
# Filesystem restrictions
deny_read=["~/.ssh", "~/.aws", "~/.gnupg"],
# allow_write defaults to workspace only
)
# Verify sandbox works on this system
ok, msg = executor.verify()
if not ok:
print(f"Sandbox unavailable: {msg}")
client = patch_openai_with_ptc(
OpenAI(),
cwd=str(executor.workspace_dir),
executor=executor,
)
stream = client.responses.create(
model="claude-sonnet-4-20250514",
input=[{"role": "user", "content": "Run some Python code"}],
stream=True
)
# ... handle events ...
# Snapshots work the same as microsandbox
executor.snapshot("checkpoint_1")
executor.restore("checkpoint_1")
executor.close()
Features:
- OS-level isolation: Network and filesystem restrictions via OS primitives
- No containers: Lighter weight than microVMs, faster startup
- Network allow-list: Only specified domains are accessible
- Filesystem protection: Block reads to sensitive paths, restrict writes
- Snapshots: Same time-travel API as microsandbox executor
- MCP tools support: Uses Unix sockets to bridge tool calls from sandbox to host
MCP Tools: The sandbox runtime uses network namespace isolation, but MCP tools work via Unix sockets. The MCP bridge listens on a socket in the workspace directory, which is accessible from inside the sandbox.
Comparison:
| Executor | Isolation | Requirements | Best For |
|---|---|---|---|
SubprocessExecutor |
None | - | Development, trusted code |
SandboxRuntimeExecutor |
OS-level | srt CLI | Lightweight isolation |
MicrosandboxCLIExecutor |
MicroVM | msb CLI + KVM | Maximum isolation |
Traditional Tool Calling
For cases where you want standard JSON tool_calls instead of code fences.
Patched Responses API
A lightweight agentic loop that patches the OpenAI client to transparently handle MCP tool calls. Works with any provider via LiteLLM.
from agents.mcp.server import MCPServerStdio
from agentd import patch_openai_with_mcp
from openai import OpenAI
fs_server = MCPServerStdio(
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/"],
},
cache_tools_list=True
)
client = patch_openai_with_mcp(OpenAI())
response = client.chat.completions.create(
model="gemini/gemini-2.0-flash", # Any provider via LiteLLM
messages=[{"role": "user", "content": "List files in /tmp/"}],
mcp_servers=[fs_server],
)
print(response.choices[0].message.content)
What it does:
- Patches
chat.completions.createandresponses.create - Auto-connects to MCP servers and extracts tool schemas
- Intercepts tool calls, executes via MCP, feeds results back
- Loops until no more tool calls (max 20 iterations)
- Supports streaming
Multi-provider support:
model="gpt-4o" # OpenAI
model="claude-sonnet-4-20250514" # Anthropic
model="gemini/gemini-2.0-flash" # Google
Agent Daemon
YAML-configured agents with MCP resource subscriptions. Agents react to resource changes automatically.
uvx agentd config.yaml
Configuration:
agents:
- name: news_agent
model: gpt-4o-mini
system_prompt: |
You monitor a URL for changes. When new content arrives,
save it to ./output/data.txt using the edit_file tool.
mcp_servers:
- type: stdio
command: uv
arguments: ["run", "mcp_subscribe", "--poll-interval", "5", "--", "uvx", "mcp-server-fetch"]
- type: stdio
command: npx
arguments: ["-y", "@modelcontextprotocol/server-filesystem", "./output/"]
subscriptions:
- "tool://fetch/?url=https://example.com/api/data"
How subscriptions work:
- Agent connects to MCP servers
- Subscribes to resource URIs (e.g.,
tool://fetch/?url=...) - When resource changes, MCP server sends notification
- Agent calls the tool, gets result, sends to LLM
- LLM responds (can call more tools)
Built on mcp-subscribe.
Each agent also has an interactive REPL:
news_agent> What files have you saved?
Assistant: I've saved 3 files to ./output/...
API Reference
Patching Functions
from agentd import patch_openai_with_mcp, patch_openai_with_ptc
# PTC: bash + skills (no isolation)
client = patch_openai_with_ptc(OpenAI(), cwd="./workspace")
# PTC with OS-level sandbox (lightweight)
from agentd import create_sandbox_runtime_executor
executor = create_sandbox_runtime_executor(conversation_id="my_session")
client = patch_openai_with_ptc(OpenAI(), executor=executor)
# PTC with microsandbox isolation (microVM)
from agentd import create_microsandbox_cli_executor
executor = create_microsandbox_cli_executor(conversation_id="my_session")
client = patch_openai_with_ptc(OpenAI(), executor=executor)
# Traditional tool_calls
client = patch_openai_with_mcp(OpenAI())
Microsandbox Executor
from agentd import create_microsandbox_cli_executor
executor = create_microsandbox_cli_executor(
conversation_id="session_1", # Sandbox name prefix
image="python", # microsandbox image
memory=1024, # MB
timeout=60, # seconds
)
# Snapshot API
snapshot = executor.snapshot("label") # Save state
executor.restore(snapshot.id) # Restore state
snapshots = executor.list_snapshots() # List all snapshots
executor.close() # Cleanup
Sandbox Runtime Executor
from agentd import create_sandbox_runtime_executor
executor = create_sandbox_runtime_executor(
conversation_id="session_1",
timeout=60,
# Network (allow-list pattern)
allowed_domains=["github.com", "*.python.org"],
denied_domains=[],
allow_local_binding=False,
# Filesystem
deny_read=["~/.ssh", "~/.aws"], # Block reading these paths
allow_write=None, # None = workspace only
deny_write=[".env"], # Block within allowed zones
)
# Check if sandbox works on this system
ok, msg = executor.verify()
# Same snapshot API as microsandbox
snapshot = executor.snapshot("label")
executor.restore(snapshot.id)
executor.close()
Tool Decorator
from agentd import tool
@tool
def my_function(arg1: str, arg2: int = 10) -> str:
"""Description goes here.
arg1: Description of arg1
arg2: Description of arg2
"""
return f"Result: {arg1}, {arg2}"
Examples
See examples/:
ptc_with_mcp.py- PTC with MCP serversptc_with_tools.py- PTC with @tool decoratorptc_microsandbox.py- PTC with microsandbox isolation (microVM)ptc_sandbox_runtime.py- PTC with sandbox-runtime isolation (OS-level)
See config/ for agent daemon configs.
Architecture
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ PTC │ │ Agent Daemon │
│ (bash, skills, MCP) │ │ (YAML, subscriptions) │
└──────────────┬──────────────┘ └──────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Patched OpenAI Client │
│ ┌────────────────────────┐ ┌────────────────────────────┐ │
│ │ patch_openai_ptc │ │ patch_openai_mcp │ │
│ │ (fence parse/exec) │ │ (tool_calls loop) │ │
│ └───────────┬────────────┘ └─────────────┬──────────────┘ │
└──────────────┼───────────────────────────────┼──────────────────┘
│ │
▼ │
┌──────────────────────────────────────────┐ │
│ Executors │ │
│ ┌──────────┐ ┌─────────┐ ┌───────────┐ │ │
│ │Subprocess│ │ Sandbox │ │Microsandbox│ │ │
│ │(default) │ │ Runtime │ │ (microVM) │ │ │
│ │ │ │(OS-level)│ │ │ │ │
│ └──────────┘ └─────────┘ └───────────┘ │ │
└──────────────────┬───────────────────────┘ │
│ │
▼ │
┌──────────────────────────────┐ │
│ MCP Bridge │ │
│ (HTTP server) │ │
└──────────────┬───────────────┘ │
│ │
└───────────────┬───────────────┘
▼
┌───────────────────────────────┐
│ MCP Servers │
└───────────────────────────────┘
License
MIT
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 agentd-0.8.7.tar.gz.
File metadata
- Download URL: agentd-0.8.7.tar.gz
- Upload date:
- Size: 44.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bd1241727b05cba814b2b302090cb8a03183ab5a6a4c2652c43ae70575bd168
|
|
| MD5 |
25d6ea65e7e66c535232ea5e3c20bcc6
|
|
| BLAKE2b-256 |
2d6ef08e013f0d848f1eee85464d6f29d888233e47557a4bce1103beaaf97c21
|
File details
Details for the file agentd-0.8.7-py3-none-any.whl.
File metadata
- Download URL: agentd-0.8.7-py3-none-any.whl
- Upload date:
- Size: 64.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fcdc7a6d5cd35bd069d22ddd425305280ce88b470dcb999df77490c4bc3c79f4
|
|
| MD5 |
2ef90c465cc3db040c64bf577153d150
|
|
| BLAKE2b-256 |
3e71e16440a4d80d3769a8e05cfdea3ab43cd9adb5234312046d16b3b68d25a0
|