SYNTH — Synchronized Network of Teamed Harnesses over ACP
Project description
SYNTH
SYnchronized Network of Teamed Harnesses over ACP
A multi-agent orchestration dashboard that manages teams of AI coding agents through the Agent Client Protocol (ACP). Agent-agnostic — works with any ACP-compatible harness (Kiro CLI, Claude Code, Gemini CLI, OpenCode, etc.).
What It Does
- Launches and manages multiple ACP agent subprocesses from a single terminal
- Streams agent responses with rendered markdown in a Textual TUI
- Surfaces permission requests inline with one-click approve/reject
- Enables agent-to-agent messaging via a bundled MCP server
- Supports flexible topologies: human-dispatch, orchestrator, peer-to-peer
- Configurable lifecycle hooks for agent join, exit, and prompt injection
Install
# Install as a global CLI tool (run `synth` from anywhere)
uv tool install synth-acp
Quick Start
# Navigate to your project
cd my-project
# Initialize a config file (interactive)
synth init
# Or create one manually
cat > .synth.json << 'EOF'
{
"project": "my-project",
"agents": [
{ "agent_id": "kiro", "harness": "kiro" }
]
}
EOF
# Launch the TUI
synth
# Or run headless (CLI mode)
synth --headless
# Or launch directly with a harness (no config file needed)
synth --harness kiro
synth --harness kiro --agent-mode plan
Configuration
Create a .synth.json in your project root. A minimal config:
{
"project": "my-project",
"agents": [
{ "agent_id": "lead", "harness": "kiro" },
{ "agent_id": "worker", "harness": "claude" }
]
}
Full Config Reference
See examples/synth.example.json for a complete config with all available options.
{
"project": "my-project",
"agents": [
{
"agent_id": "lead",
"harness": "kiro",
"agent_mode": "coder",
"cwd": ".",
"env": { "CUSTOM_VAR": "value" }
}
],
"settings": {
"communication_mode": "MESH",
"auto_approve_tools": ["synth-mcp/send_message", "synth-mcp/list_agents"],
"hooks": {
"on_agent_startup": {
"prepend": "<orchestration_context>\nagent_id: {agent_id}\nsession: You are in a multi-agent session.\n</orchestration_context>\n\n"
},
"on_agent_prompt": {
"prepend": "<orchestration_context>\nagent_id: {agent_id}\nparent_agent: {parent_id}\nreply_tool: send_message(to_agent='{parent_id}', kind='response')\n</orchestration_context>\n\n"
},
"on_agent_join": {
"recipients": "none",
"template": "Agent \"{agent_id}\" is now active. Task: \"{task}\".",
"kind": "system"
},
"on_agent_exit": {
"recipients": "none",
"template": "Agent \"{agent_id}\" has left the session.",
"kind": "system"
}
}
},
"ui": {
"web_port": 8000,
"theme": "dark"
}
}
Agent Fields
| Field | Required | Default | Description |
|---|---|---|---|
agent_id |
yes | — | Unique identifier, shown to other agents |
harness |
yes | — | Short name: kiro, claude, opencode, gemini |
agent_mode |
no | — | ACP mode ID applied after session creation |
cwd |
no | "." |
Working directory (relative to config file) |
env |
no | {} |
Extra environment variables passed to the harness |
Settings
| Field | Default | Description |
|---|---|---|
communication_mode |
"MESH" |
MESH (all agents visible) or LOCAL (family only) |
auto_approve_tools |
[] |
Tool name patterns to auto-approve without prompting |
Lifecycle Hooks
Hooks fire at specific moments in an agent's lifecycle. They are configurable in settings.hooks.
on_agent_startup
Fires on the first prompt to any config-defined or TUI-launched agent. Prepends context so the agent knows it's in a multi-agent session.
| Field | Default | Description |
|---|---|---|
prepend |
(identity + session awareness block) | Text prepended to the agent's first prompt |
Template slots: {agent_id}
on_agent_prompt
Fires when a dynamically launched child agent receives its initial message from the parent. Prepends routing context so the child knows who it is and how to reply.
| Field | Default | Description |
|---|---|---|
prepend |
(routing context with agent_id, parent, reply_tool) | Text prepended to the initial prompt |
Template slots: {agent_id}, {parent_id}, {task}, {message}
on_agent_join
Fires when a dynamically launched agent is registered. Sends a templated message to the configured recipients.
| Field | Default | Description |
|---|---|---|
recipients |
"none" |
none, parent, family, or mesh |
template |
"" |
Message body template |
kind |
"system" |
Message kind: system or chat |
Template slots: {agent_id}, {task}, {parent_id}, {sibling_ids}
Recipient modes:
| Mode | Recipients |
|---|---|
none |
No message sent (default) |
parent |
Direct parent only |
family |
Parent + siblings (agents sharing the same parent) |
mesh |
All visible agents |
on_agent_exit
Fires when an agent is terminated. Same fields and recipient modes as on_agent_join.
Environment Variable Overrides
For quick experimentation without editing the config file:
| Env var | Overrides |
|---|---|
SYNTH_JOIN_RECIPIENTS |
settings.hooks.on_agent_join.recipients |
SYNTH_JOIN_TEMPLATE |
settings.hooks.on_agent_join.template |
SYNTH_ROUTING_TEMPLATE |
settings.hooks.on_agent_prompt.prepend |
Architecture
synth (single process: broker + TUI)
┌──────────────────────────────────────────┐
│ ACPBroker │
│ ├── AgentLifecycle (launch/terminate) │
│ ├── AgentRegistry (sessions/metadata) │
│ ├── MessageBus (notification-driven) │
│ └── PermissionEngine (rule persistence)│
│ │ │
│ ▼ │
│ SynthApp (Textual TUI) │
└──────────────────────────────────────────┘
│ spawns N agent subprocesses via ACP
▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ kiro-cli │ │ kiro-cli │ │ claude │
│ acp │ │ acp │ │ acp │
│ MCP: │ │ MCP: │ │ MCP: │
│ synth-mcp│ │ synth-mcp│ │ synth-mcp│
└──────────┘ └──────────┘ └──────────┘
Three layers with strict dependency rules:
| Layer | Package | Responsibility |
|---|---|---|
| Frontend | synth_acp.ui |
Textual TUI |
| Broker | synth_acp.broker |
Session lifecycle, permissions, message routing |
| ACP | synth_acp.acp |
ACP SDK wrapper, subprocess management |
| Shared | synth_acp.models |
Pydantic v2 models |
The broker has zero UI imports. A future web frontend can consume the same broker.events() / broker.handle() interface.
Inter-Agent Messaging
Agents communicate via a bundled MCP server (synth-mcp) injected into every agent session. Available tools:
| Tool | Description |
|---|---|
send_message |
Send a message to another agent (the only inter-agent communication channel) |
check_delivery |
Poll whether a sent message has been delivered |
list_agents |
List all visible agents with their status, parent, and task |
launch_agent |
Launch a new child agent |
terminate_agent |
Terminate a child agent you previously launched |
get_my_context |
Get your identity, parent, task, and communication rules |
Messages are delivered to idle agents between turns. Message kinds (chat, request, response) are formatted distinctly on delivery so agents can distinguish requests from responses.
Key Bindings
| Key | Action |
|---|---|
Tab |
Cycle agent focus |
m |
MCP messages panel |
l |
Launch agent |
Ctrl+r |
Restore session |
F1 |
Help |
q |
Quit |
Development
uv sync # Install dependencies
uv run pytest -q --tb=short --no-header -rF # Run tests
uv run ruff check --fix --output-format concise # Lint
uv run ruff format # Format
uv run ty check --output-format concise src/ # Type check
Publishing
Releases are published to PyPI via GitHub Actions when a version tag is pushed.
# 1. Bump version in pyproject.toml
# 2. Commit and tag
git add pyproject.toml
git commit -m "release: v0.2.0"
git tag v0.2.0
git push origin main --tags
The tag must match the version in pyproject.toml (without the v prefix). CI runs
tests and lint before publishing — if either fails, nothing is uploaded.
Install from PyPI:
uv pip install synth-acp
Security & Trust Model
Synth is a single-user desktop tool. All agents run as child processes of the synth process and inherit the user's OS-level privileges.
Trust boundary: Agents have the same access as the user. Each agent subprocess receives environment variables (SYNTH_DB_PATH, SYNTH_SESSION_ID) that grant direct access to the shared SQLite database. The MCP server provides structured access, but does not enforce an authentication boundary — any child process can read or write the database directly.
Implications:
- Do not expose synth over a network or use it in a multi-user/multi-tenant context without additional sandboxing.
- The permission system (approve/reject tool calls) is a UX convenience for human oversight, not a security boundary against a malicious agent.
- The
!shell escape in the input bar executes commands with the full privileges of the synth process.
File permissions: The synth database directory (~/.synth/) is created with mode 0o700 and the database file with mode 0o600 to prevent access by other local users.
License
See LICENSE file.
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 synth_acp-0.2.0.tar.gz.
File metadata
- Download URL: synth_acp-0.2.0.tar.gz
- Upload date:
- Size: 270.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d9dff7f12c78948979b4bf092934d72952198a6cfd3a5a8f9e679a717ed67c0
|
|
| MD5 |
2163224195c2126eabc31f4ac30475e5
|
|
| BLAKE2b-256 |
fc9df9fa67403ca919f9845cc5c65863ad71c7b4eae58c769acd9ce85d58be5a
|
Provenance
The following attestation bundles were made for synth_acp-0.2.0.tar.gz:
Publisher:
publish.yml on DerekDardzinski/synth-acp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
synth_acp-0.2.0.tar.gz -
Subject digest:
4d9dff7f12c78948979b4bf092934d72952198a6cfd3a5a8f9e679a717ed67c0 - Sigstore transparency entry: 1351932809
- Sigstore integration time:
-
Permalink:
DerekDardzinski/synth-acp@7e683a6ca2f690a485c7814e8945bd58145e8786 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DerekDardzinski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e683a6ca2f690a485c7814e8945bd58145e8786 -
Trigger Event:
push
-
Statement type:
File details
Details for the file synth_acp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: synth_acp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 139.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c7d26dd994840d4c1fb596f4ffdd7dbc0a088e3a0e231a9bc4587b5d1278503
|
|
| MD5 |
8d736f904c25053dbfec5b332628bd35
|
|
| BLAKE2b-256 |
72bee468328429b55f43e8b805fa7909415e6b4454b983e18e7e0b2a22ed0f02
|
Provenance
The following attestation bundles were made for synth_acp-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on DerekDardzinski/synth-acp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
synth_acp-0.2.0-py3-none-any.whl -
Subject digest:
7c7d26dd994840d4c1fb596f4ffdd7dbc0a088e3a0e231a9bc4587b5d1278503 - Sigstore transparency entry: 1351932906
- Sigstore integration time:
-
Permalink:
DerekDardzinski/synth-acp@7e683a6ca2f690a485c7814e8945bd58145e8786 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DerekDardzinski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e683a6ca2f690a485c7814e8945bd58145e8786 -
Trigger Event:
push
-
Statement type: