Python client for the Tandem autonomous agent engine HTTP + SSE API
Project description
tandem-client
Python client for the Tandem autonomous agent engine HTTP + SSE API.
Install
pip install tandem-client
Python 3.10+ required.
Quick start
import asyncio
from tandem_client import TandemClient
async def main():
async with TandemClient(
base_url="http://localhost:39731",
token="your-engine-token", # from `tandem-engine token generate`
) as client:
# 1. Create a session
session_id = await client.sessions.create(
title="My agent",
directory="/path/to/my/project",
)
# 2. Start an async run
run = await client.sessions.prompt_async(
session_id, "Summarize the README and list the top 3 TODOs"
)
# 3. Stream the response
async for event in client.stream(session_id, run.run_id):
if event.type == "session.response":
print(event.properties.get("delta", ""), end="", flush=True)
if event.type in ("run.complete", "run.completed", "run.failed", "session.run.finished"):
break
asyncio.run(main())
Sync usage (scripts)
from tandem_client import SyncTandemClient
client = SyncTandemClient(base_url="http://localhost:39731", token="...")
session_id = client.sessions.create(title="My agent")
run = client.sessions.prompt_async(session_id, "Analyze this folder")
print(f"Run started: {run.run_id}")
# Note: stream() is async-only; use the async client to receive events
client.close()
API
TandemClient(base_url, token, *, timeout=20.0)
Use as an async context manager or call await client.aclose() manually.
| Method | Description |
|---|---|
await client.health() |
Check engine readiness |
client.stream(session_id, run_id?) |
Async generator of EngineEvent |
client.global_stream() |
Stream all engine events |
await client.list_tool_ids() |
List all registered tool IDs |
await client.list_tools() |
List tool schemas |
await client.execute_tool(...) |
Execute a specific engine tool |
client.sessions
| Method | Description |
|---|---|
create(title?, directory?, provider?, model?) |
Create session, returns session_id |
list(q?, page?, page_size?, archived?, scope?, workspace?) |
List sessions |
get(session_id) |
Get session details |
delete(session_id) |
Delete a session |
messages(session_id) |
Get message history |
active_run(session_id) |
Get active run state |
prompt_async(session_id, prompt) |
Start async run, returns PromptAsyncResult(run_id) |
prompt_async_parts(session_id, parts) |
Start async run with text/file parts |
Prompt with file attachments:
run = await client.sessions.prompt_async_parts(
session_id,
[
{
"type": "file",
"mime": "image/png",
"filename": "diagram.png",
"url": "/srv/tandem/channel_uploads/telegram/667596788/diagram.png",
},
{"type": "text", "text": "Explain this diagram."},
],
)
Prompt with browser tools enabled for QA:
run = await client.sessions.prompt_async(
session_id,
"Run QA on the target web app. Capture screenshots for failures.",
tool_mode="required",
tool_allowlist=[
"browser_status",
"browser_open",
"browser_navigate",
"browser_snapshot",
"browser_click",
"browser_type",
"browser_press",
"browser_wait",
"browser_extract",
"browser_screenshot",
"browser_close",
],
)
Browser automation via tools
The SDK exposes two different browser paths:
client.browserfor readiness, install, and smoke-test flowsclient.execute_tool(...)or session runs for actual browser automation
The browser namespace does not currently wrap open, click, type, extract, or screenshot. Those actions are exposed as standard engine tools.
status = await client.execute_tool("browser_status", {})
opened = await client.execute_tool("browser_open", {
"url": "https://example.com",
})
session_id = opened.output["session_id"]
snapshot = await client.execute_tool("browser_snapshot", {
"session_id": session_id,
"include_screenshot": True,
})
html = await client.execute_tool("browser_extract", {
"session_id": session_id,
"format": "html",
})
await client.execute_tool("browser_close", {
"session_id": session_id,
})
Use this pattern for agents that need to open pages, click elements, enter text, wait for content, extract page HTML or text, and capture screenshots through the engine.
client.routines
| Method | Description |
|---|---|
list(family?) |
List routines or automations |
create(options, family?) |
Create a scheduled routine |
delete(routine_id, family?) |
Delete a routine |
run_now(routine_id, family?) |
Trigger a routine immediately |
list_runs(family?, limit?) |
List recent run records |
list_artifacts(run_id, family?) |
List run artifacts |
Create a cron routine:
await client.routines.create({
"name": "Daily digest",
"schedule": "0 8 * * *",
"prompt": "Summarize today's activity and write a report to daily-digest.md",
"allowed_tools": ["read", "write", "websearch"],
})
client.automations_v2
automation = await client.automations_v2.create({
"name": "Daily Marketing Engine",
"status": "active",
"schedule": {
"type": "interval",
"interval_seconds": 86400,
"timezone": "UTC",
"misfire_policy": "run_once",
},
"agents": [
{
"agent_id": "research",
"display_name": "Research",
"model_policy": {
"default_model": {
"provider_id": "openrouter",
"model_id": "openai/gpt-4o-mini",
}
},
"tool_policy": {"allowlist": ["read", "websearch"], "denylist": []},
"mcp_policy": {"allowed_servers": []},
}
],
"flow": {
"nodes": [
{"node_id": "market-scan", "agent_id": "research", "objective": "Find 3 trend signals."}
]
},
})
run = await client.automations_v2.run_now(automation.automation_id or "")
client.workflow_plans
preview = await client.workflow_plans.preview(
prompt="Create a release checklist automation",
plan_source="chat",
)
started = await client.workflow_plans.chat_start(
prompt="Create a release checklist automation",
)
updated = await client.workflow_plans.chat_message(
plan_id=started.plan.plan_id or "",
message="Add a smoke-test step before rollout.",
)
applied = await client.workflow_plans.apply(
plan_id=updated.plan.plan_id,
creator_id="operator-1",
)
Additional namespaces
The Python SDK already includes the newer engine surfaces that have landed across the repo:
client.browserforstatus(),install(), andsmoke_test()client.workflowsfor workflow registry, runs, hooks, simulation, and live eventsclient.resourcesfor key-value resourcesclient.skillsfor list/get/import plus preview, templates, validation, routing, evals, compile, and generate flowsclient.packsandclient.capabilitiesfor pack lifecycle and capability resolutionclient.automations_v2,client.bug_monitor,client.coder,client.agent_teams, andclient.missionsfor newer orchestration APIs
browser = await client.browser.status()
workflows = await client.workflows.list()
resources = await client.resources.list(prefix="agent-config/")
templates = await client.skills.templates()
client.coder
client.coder also includes project-scoped GitHub Project intake helpers:
await client.coder.put_project_binding("repo-123", {
"github_project_binding": {
"owner": "acme-inc",
"project_number": 7,
"repo_slug": "acme-inc/tandem",
}
})
inbox = await client.coder.get_project_github_inbox("repo-123")
intake = await client.coder.intake_project_item("repo-123", {
"project_item_id": inbox.items[0].project_item_id,
"source_client": "sdk_test",
})
client.agent_teams template management
await client.agent_teams.create_template({"templateID": "marketing-writer", "role": "worker"})
await client.agent_teams.update_template("marketing-writer", {"system_prompt": "Write concise copy."})
await client.agent_teams.delete_template("marketing-writer")
client.mcp
await client.mcp.add("arcade", "https://mcp.arcade.ai/mcp")
await client.mcp.connect("arcade")
tools = await client.mcp.list_tools()
| Method | Description |
|---|---|
list() |
List registered MCP servers |
list_tools() |
List discovered tools |
add(name, transport, *, headers?, enabled?) |
Register an MCP server |
connect(name) |
Connect and discover tools |
disconnect(name) |
Disconnect |
refresh(name) |
Re-discover tools |
set_enabled(name, enabled) |
Enable/disable |
client.channels
await client.channels.put("discord", {
"bot_token": "bot:xxx",
"guild_id": "1234567890",
"security_profile": "public_demo",
})
status = await client.channels.status()
config = await client.channels.config()
prefs = await client.channels.tool_preferences("discord")
await client.channels.set_tool_preferences("discord", {"disabled_tools": ["webfetch_html"]})
verification = await client.channels.verify("discord")
print(status.discord.connected)
print(config.discord.security_profile)
print(prefs.enabled_tools)
print(verification.ok)
client.packs
packs = await client.packs.list()
detected = await client.packs.detect(path="/tmp/my-pack.zip")
if detected.get("is_pack"):
await client.packs.install(path="/tmp/my-pack.zip", source={"kind": "local"})
client.capabilities
bindings = await client.capabilities.get_bindings()
discovery = await client.capabilities.discovery()
resolution = await client.capabilities.resolve(
{
"workflow_id": "wf-pr",
"required_capabilities": ["github.create_pull_request"],
}
)
client.permissions
snapshot = await client.permissions.list()
for req in snapshot.requests:
await client.permissions.reply(req.id, "allow")
client.memory
# Put (SDK accepts `text`; server persists global `content`)
await client.memory.put(
"Use WAL mode for sqlite in long-lived services.",
run_id="run-123",
)
# Search
result = await client.memory.search("sqlite wal", limit=5)
# List by user scope
listing = await client.memory.list(user_id="user-123", q="sqlite")
# Audit
audit = await client.memory.audit(run_id="run-123")
# Promote / demote / delete
await client.memory.promote(listing.items[0].id)
await client.memory.demote(listing.items[0].id, run_id="run-123")
await client.memory.delete(listing.items[0].id)
client.providers
catalog = await client.providers.catalog()
await client.providers.set_defaults("openrouter", "anthropic/claude-3.7-sonnet")
await client.providers.set_api_key("openrouter", "sk-or-...")
Common event types
event.type |
Description |
|---|---|
session.response |
Text delta in event.properties["delta"] |
session.tool_call |
Tool invocation |
session.tool_result |
Tool result |
run.complete |
Run finished successfully (legacy event name) |
run.completed |
Run finished successfully |
run.failed |
Run failed |
session.run.finished |
Session-scoped terminal run event |
permission.request |
Approval needed |
memory.write.succeeded |
Memory write persisted |
memory.search.performed |
Memory retrieval telemetry |
memory.context.injected |
Prompt context injection telemetry |
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 tandem_client-0.4.15.tar.gz.
File metadata
- Download URL: tandem_client-0.4.15.tar.gz
- Upload date:
- Size: 28.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c56f232a4ea26326c41493ee1aca565f88e224aceade3c389307d68e7b306143
|
|
| MD5 |
e89d83eaaf5f614c3c1bd8bf6064089a
|
|
| BLAKE2b-256 |
236e05ebe659d0843551c8c57471d5af2773e423320c1d62277352f0ec2e304b
|
Provenance
The following attestation bundles were made for tandem_client-0.4.15.tar.gz:
Publisher:
publish-registries.yml on frumu-ai/tandem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tandem_client-0.4.15.tar.gz -
Subject digest:
c56f232a4ea26326c41493ee1aca565f88e224aceade3c389307d68e7b306143 - Sigstore transparency entry: 1172226044
- Sigstore integration time:
-
Permalink:
frumu-ai/tandem@54405a60225a9d9076ea582442e4d852d1bc1e07 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/frumu-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-registries.yml@54405a60225a9d9076ea582442e4d852d1bc1e07 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tandem_client-0.4.15-py3-none-any.whl.
File metadata
- Download URL: tandem_client-0.4.15-py3-none-any.whl
- Upload date:
- Size: 28.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
535a8b5521900118dd66dba2c963a62cc8c2246f22ccf04f664674c62578171c
|
|
| MD5 |
493106099c90bae2dc75ac872144aaf5
|
|
| BLAKE2b-256 |
87f521c5eacb20d7a67b9218c5cf85a9fbcca5ebc2b9fe095f53f7789413ea34
|
Provenance
The following attestation bundles were made for tandem_client-0.4.15-py3-none-any.whl:
Publisher:
publish-registries.yml on frumu-ai/tandem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tandem_client-0.4.15-py3-none-any.whl -
Subject digest:
535a8b5521900118dd66dba2c963a62cc8c2246f22ccf04f664674c62578171c - Sigstore transparency entry: 1172226049
- Sigstore integration time:
-
Permalink:
frumu-ai/tandem@54405a60225a9d9076ea582442e4d852d1bc1e07 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/frumu-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-registries.yml@54405a60225a9d9076ea582442e4d852d1bc1e07 -
Trigger Event:
workflow_dispatch
-
Statement type: