synth-acp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- synth_acp-0.1.0/.github/workflows/publish.yml +35 -0
- synth_acp-0.1.0/.gitignore +13 -0
- synth_acp-0.1.0/.synth.json +40 -0
- synth_acp-0.1.0/AGENTS.md +238 -0
- synth_acp-0.1.0/LICENSE +657 -0
- synth_acp-0.1.0/PKG-INFO +324 -0
- synth_acp-0.1.0/README.md +300 -0
- synth_acp-0.1.0/examples/synth.example.json +57 -0
- synth_acp-0.1.0/pyproject.toml +113 -0
- synth_acp-0.1.0/scripts/prob_config_options.py +512 -0
- synth_acp-0.1.0/scripts/probe_modes.py +89 -0
- synth_acp-0.1.0/scripts/test_terminal.py +164 -0
- synth_acp-0.1.0/scripts/test_updates.py +90 -0
- synth_acp-0.1.0/src/synth_acp/__init__.py +3 -0
- synth_acp-0.1.0/src/synth_acp/__main__.py +7 -0
- synth_acp-0.1.0/src/synth_acp/acp/__init__.py +0 -0
- synth_acp-0.1.0/src/synth_acp/acp/session.py +961 -0
- synth_acp-0.1.0/src/synth_acp/acp/state_machine.py +50 -0
- synth_acp-0.1.0/src/synth_acp/broker/__init__.py +0 -0
- synth_acp-0.1.0/src/synth_acp/broker/broker.py +679 -0
- synth_acp-0.1.0/src/synth_acp/broker/lifecycle.py +635 -0
- synth_acp-0.1.0/src/synth_acp/broker/message_bus.py +188 -0
- synth_acp-0.1.0/src/synth_acp/broker/permissions.py +78 -0
- synth_acp-0.1.0/src/synth_acp/broker/registry.py +106 -0
- synth_acp-0.1.0/src/synth_acp/cli.py +503 -0
- synth_acp-0.1.0/src/synth_acp/data/__init__.py +1 -0
- synth_acp-0.1.0/src/synth_acp/data/harnesses/__init__.py +1 -0
- synth_acp-0.1.0/src/synth_acp/data/harnesses/claude.toml +5 -0
- synth_acp-0.1.0/src/synth_acp/data/harnesses/gemini.toml +5 -0
- synth_acp-0.1.0/src/synth_acp/data/harnesses/kiro.toml +5 -0
- synth_acp-0.1.0/src/synth_acp/data/harnesses/opencode.toml +5 -0
- synth_acp-0.1.0/src/synth_acp/db.py +163 -0
- synth_acp-0.1.0/src/synth_acp/harnesses.py +23 -0
- synth_acp-0.1.0/src/synth_acp/mcp/__init__.py +0 -0
- synth_acp-0.1.0/src/synth_acp/mcp/notifier.py +37 -0
- synth_acp-0.1.0/src/synth_acp/mcp/server.py +339 -0
- synth_acp-0.1.0/src/synth_acp/models/__init__.py +3 -0
- synth_acp-0.1.0/src/synth_acp/models/agent.py +130 -0
- synth_acp-0.1.0/src/synth_acp/models/commands.py +76 -0
- synth_acp-0.1.0/src/synth_acp/models/config.py +194 -0
- synth_acp-0.1.0/src/synth_acp/models/events.py +256 -0
- synth_acp-0.1.0/src/synth_acp/models/permissions.py +28 -0
- synth_acp-0.1.0/src/synth_acp/models/visibility.py +97 -0
- synth_acp-0.1.0/src/synth_acp/terminal/__init__.py +1 -0
- synth_acp-0.1.0/src/synth_acp/terminal/manager.py +273 -0
- synth_acp-0.1.0/src/synth_acp/terminal/shell_read.py +42 -0
- synth_acp-0.1.0/src/synth_acp/ui/__init__.py +0 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/__init__.py +1 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_ansi.py +1620 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_ansi_colors.py +264 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_control_codes.py +37 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_dec.py +332 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_keys.py +251 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_sgr_styles.py +64 -0
- synth_acp-0.1.0/src/synth_acp/ui/ansi/_stream_parser.py +416 -0
- synth_acp-0.1.0/src/synth_acp/ui/app.py +671 -0
- synth_acp-0.1.0/src/synth_acp/ui/css/app.tcss +487 -0
- synth_acp-0.1.0/src/synth_acp/ui/messages.py +18 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/__init__.py +8 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/dashboard.py +0 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/help.py +51 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/launch.py +85 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/permission.py +221 -0
- synth_acp-0.1.0/src/synth_acp/ui/screens/session_picker.py +78 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/__init__.py +0 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/agent_list.py +229 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/agent_message.py +45 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/conversation.py +323 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/copy_button.py +37 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/diff_view.py +514 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/gradient_bar.py +144 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/input_bar.py +452 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/message_queue.py +169 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/plan_block.py +63 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/prompt_bubble.py +29 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/shell_result.py +46 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/terminal.py +360 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/thought_block.py +50 -0
- synth_acp-0.1.0/src/synth_acp/ui/widgets/tool_call.py +251 -0
- synth_acp-0.1.0/tests/__init__.py +0 -0
- synth_acp-0.1.0/tests/acp/__init__.py +0 -0
- synth_acp-0.1.0/tests/acp/test_session.py +637 -0
- synth_acp-0.1.0/tests/acp/test_session_permission.py +116 -0
- synth_acp-0.1.0/tests/acp/test_state_machine.py +52 -0
- synth_acp-0.1.0/tests/broker/__init__.py +0 -0
- synth_acp-0.1.0/tests/broker/test_broker.py +672 -0
- synth_acp-0.1.0/tests/broker/test_lifecycle.py +130 -0
- synth_acp-0.1.0/tests/broker/test_message_bus.py +142 -0
- synth_acp-0.1.0/tests/broker/test_permissions.py +80 -0
- synth_acp-0.1.0/tests/broker/test_registry.py +53 -0
- synth_acp-0.1.0/tests/conftest.py +0 -0
- synth_acp-0.1.0/tests/mcp/__init__.py +0 -0
- synth_acp-0.1.0/tests/mcp/test_notifier.py +79 -0
- synth_acp-0.1.0/tests/mcp/test_server.py +199 -0
- synth_acp-0.1.0/tests/models/__init__.py +0 -0
- synth_acp-0.1.0/tests/models/test_agent.py +63 -0
- synth_acp-0.1.0/tests/models/test_config.py +137 -0
- synth_acp-0.1.0/tests/terminal/__init__.py +0 -0
- synth_acp-0.1.0/tests/terminal/test_manager.py +84 -0
- synth_acp-0.1.0/tests/test_cli.py +39 -0
- synth_acp-0.1.0/tests/test_harnesses.py +16 -0
- synth_acp-0.1.0/tests/ui/__init__.py +0 -0
- synth_acp-0.1.0/tests/ui/screens/__init__.py +0 -0
- synth_acp-0.1.0/tests/ui/screens/test_help.py +34 -0
- synth_acp-0.1.0/tests/ui/screens/test_launch.py +115 -0
- synth_acp-0.1.0/tests/ui/screens/test_permission.py +142 -0
- synth_acp-0.1.0/tests/ui/test_app.py +305 -0
- synth_acp-0.1.0/tests/ui/widgets/__init__.py +0 -0
- synth_acp-0.1.0/tests/ui/widgets/test_agent_list.py +134 -0
- synth_acp-0.1.0/tests/ui/widgets/test_conversation.py +84 -0
- synth_acp-0.1.0/tests/ui/widgets/test_diff_view.py +34 -0
- synth_acp-0.1.0/tests/ui/widgets/test_message_queue.py +116 -0
- synth_acp-0.1.0/tests/ui/widgets/test_plan_block.py +71 -0
- synth_acp-0.1.0/tests/ui/widgets/test_tool_call.py +153 -0
- synth_acp-0.1.0/uv.lock +1895 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
13
|
+
- run: uv sync
|
|
14
|
+
- run: uv run ruff check --output-format concise
|
|
15
|
+
- run: uv run pytest -q --tb=short --no-header -rF
|
|
16
|
+
|
|
17
|
+
publish:
|
|
18
|
+
needs: test
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
environment: pypi
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v6
|
|
25
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
26
|
+
- name: Verify tag matches package version
|
|
27
|
+
run: |
|
|
28
|
+
TAG=${GITHUB_REF#refs/tags/v}
|
|
29
|
+
VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
30
|
+
if [ "$TAG" != "$VERSION" ]; then
|
|
31
|
+
echo "::error::Tag v$TAG does not match pyproject.toml version $VERSION"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
- run: uv build
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "test",
|
|
3
|
+
"settings": {
|
|
4
|
+
"auto_approve_tools": [
|
|
5
|
+
"synth-mcp/send_message",
|
|
6
|
+
"synth-mcp/check_delivery",
|
|
7
|
+
"synth-mcp/launch_agent",
|
|
8
|
+
"synth-mcp/list_agents",
|
|
9
|
+
"synth-mcp/get_my_context"
|
|
10
|
+
],
|
|
11
|
+
"hooks": {
|
|
12
|
+
"on_agent_join": {
|
|
13
|
+
"recipients": "none",
|
|
14
|
+
"template": "Agent \"{agent_id}\" is now active. Task: \"{task}\"."
|
|
15
|
+
},
|
|
16
|
+
"on_agent_exit": {
|
|
17
|
+
"recipients": "none",
|
|
18
|
+
"template": "Agent \"{agent_id}\" has left the session."
|
|
19
|
+
},
|
|
20
|
+
"on_agent_prompt": {
|
|
21
|
+
"prepend": "<orchestration_context>\nagent_id: {agent_id}\nparent_agent: {parent_id}\nreply_tool: send_message(to_agent='{parent_id}', kind='response')\nvisibility: Your text output goes to the UI only. Other agents cannot see it.\nrecovery: Call get_my_context() if you lose track of this information.\n</orchestration_context>\n\n"
|
|
22
|
+
},
|
|
23
|
+
"on_agent_startup": {
|
|
24
|
+
"prepend": "<orchestration_context>\nYour agent_id: {agent_id}\nsession: You are in a multi-agent session. Use list_agents() to see other agents.\ncommunication: Use send_message() to talk to other agents. Your text output goes to the user only.\nchild_agents: When launching child agents, instruct them to send_message(to_agent='{agent_id}', kind='response').\n</orchestration_context>\n\n"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"agents": [
|
|
29
|
+
{
|
|
30
|
+
"agent_id": "kiro",
|
|
31
|
+
"harness": "kiro",
|
|
32
|
+
"agent_mode": "coder"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"agent_id": "kiro-2",
|
|
36
|
+
"harness": "kiro",
|
|
37
|
+
"agent_mode": "coder"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
SYNTH (Synchronized Network of Teamed Harnesses over ACP) is a multi-agent orchestration dashboard that manages teams of AI coding agents through the Agent Client Protocol (ACP). A single process runs the broker (session lifecycle, message routing, permissions) and a Textual TUI.
|
|
6
|
+
|
|
7
|
+
Package: `synth-acp`
|
|
8
|
+
Source: `src/synth_acp/`
|
|
9
|
+
Python: 3.12+, async-first
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
Three layers with strict dependency rules — each layer may only import from layers below it:
|
|
14
|
+
|
|
15
|
+
| Layer | Package | Responsibility |
|
|
16
|
+
|-------|---------|----------------|
|
|
17
|
+
| 3 — Frontend | `synth_acp.ui` | Textual TUI rendering |
|
|
18
|
+
| 2 — Broker | `synth_acp.broker` | Session lifecycle, routing, permissions |
|
|
19
|
+
| 1 — ACP | `synth_acp.acp` | ACP SDK wrapper, subprocess management |
|
|
20
|
+
| Shared | `synth_acp.models` | Pydantic v2 models for events, commands, config |
|
|
21
|
+
|
|
22
|
+
Layers 1 and 2 have zero Textual imports. The frontend communicates with the broker through typed events and commands in `models/`.
|
|
23
|
+
|
|
24
|
+
### Package Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/synth_acp/
|
|
28
|
+
├── cli.py # typer CLI, entry point
|
|
29
|
+
├── db.py # Shared SQLite schema and helpers
|
|
30
|
+
├── harnesses.py # Harness registry loader (TOML → HarnessEntry)
|
|
31
|
+
├── data/
|
|
32
|
+
│ └── harnesses/ # Harness TOML definitions (kiro.toml, claude.toml, etc.)
|
|
33
|
+
├── models/
|
|
34
|
+
│ ├── agent.py # AgentState enum, AgentConfig
|
|
35
|
+
│ ├── config.py # SessionConfig, HooksConfig (parsed from .synth.json)
|
|
36
|
+
│ ├── events.py # BrokerEvent and subclasses (broker → frontend)
|
|
37
|
+
│ ├── commands.py # BrokerCommand and subclasses (frontend → broker)
|
|
38
|
+
│ ├── visibility.py # Agent visibility rules (MESH/LOCAL)
|
|
39
|
+
│ └── permissions.py # PermissionRule, PermissionDecision
|
|
40
|
+
├── acp/
|
|
41
|
+
│ ├── session.py # ACPSession — wraps acp SDK Client interface
|
|
42
|
+
│ └── state_machine.py # AgentStateMachine — typed state transitions
|
|
43
|
+
├── broker/
|
|
44
|
+
│ ├── broker.py # ACPBroker — thin coordinator, event sink, command dispatch
|
|
45
|
+
│ ├── lifecycle.py # AgentLifecycle — launch, terminate, prompt, hooks
|
|
46
|
+
│ ├── registry.py # AgentRegistry — sessions, parentage, metadata
|
|
47
|
+
│ ├── message_bus.py # MessageBus — notification-driven message delivery
|
|
48
|
+
│ └── permissions.py # PermissionEngine — rule persistence + auto-resolve
|
|
49
|
+
├── mcp/
|
|
50
|
+
│ ├── server.py # synth-mcp entrypoint (FastMCP, agent-to-agent messaging)
|
|
51
|
+
│ └── notifier.py # BrokerNotifier — Unix socket notification to message bus
|
|
52
|
+
├── terminal/
|
|
53
|
+
│ ├── manager.py # PTY terminal process management
|
|
54
|
+
│ └── shell_read.py # Buffered async stream reader for PTY output
|
|
55
|
+
└── ui/
|
|
56
|
+
├── app.py # SynthApp — bridges broker ↔ Textual messages
|
|
57
|
+
├── messages.py # Textual Message subclasses wrapping BrokerEvent
|
|
58
|
+
├── ansi/ # Vendored ANSI terminal state parser (from toad)
|
|
59
|
+
├── screens/
|
|
60
|
+
│ ├── launch.py
|
|
61
|
+
│ ├── permission.py
|
|
62
|
+
│ ├── session_picker.py
|
|
63
|
+
│ └── help.py
|
|
64
|
+
├── widgets/
|
|
65
|
+
│ ├── agent_list.py
|
|
66
|
+
│ ├── conversation.py
|
|
67
|
+
│ ├── prompt_bubble.py
|
|
68
|
+
│ ├── agent_message.py
|
|
69
|
+
│ ├── tool_call.py
|
|
70
|
+
│ ├── message_queue.py
|
|
71
|
+
│ ├── input_bar.py
|
|
72
|
+
│ ├── thought_block.py
|
|
73
|
+
│ ├── copy_button.py
|
|
74
|
+
│ ├── shell_result.py
|
|
75
|
+
│ ├── terminal.py
|
|
76
|
+
│ ├── plan_block.py
|
|
77
|
+
│ ├── diff_view.py
|
|
78
|
+
│ └── gradient_bar.py
|
|
79
|
+
└── css/
|
|
80
|
+
└── app.tcss
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Key Dependencies
|
|
84
|
+
|
|
85
|
+
- `agent-client-protocol` — ACP Python SDK (Pydantic models, `spawn_agent_process`, `SessionAccumulator`)
|
|
86
|
+
- `mcp>=1.0.0` — MCP server via `mcp.server.fastmcp.FastMCP` (agent-to-agent messaging)
|
|
87
|
+
- `textual` — TUI framework
|
|
88
|
+
- `typer` — CLI framework
|
|
89
|
+
- `aiosqlite` — async SQLite for message bus
|
|
90
|
+
|
|
91
|
+
### Reference Docs
|
|
92
|
+
|
|
93
|
+
- `README.md` — configuration reference, lifecycle hooks, MCP tools
|
|
94
|
+
- `examples/synth.example.json` — complete config with all available options
|
|
95
|
+
|
|
96
|
+
## Build System
|
|
97
|
+
|
|
98
|
+
This project uses uv as the package manager (standalone, not PeruHatch/Brazil).
|
|
99
|
+
|
|
100
|
+
### Setup
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv sync # Install all dependencies (creates .venv)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Running
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run synth # Run the TUI (requires .synth.json)
|
|
110
|
+
uv run pytest # Run tests
|
|
111
|
+
uv run pytest tests/acp/ # Run specific test directory
|
|
112
|
+
uv run pytest -k "test_foo" # Run matching tests
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Publishing
|
|
116
|
+
|
|
117
|
+
Releases are published to PyPI via GitHub Actions on version tags. The workflow
|
|
118
|
+
uses PyPI Trusted Publishing (OIDC) — no API tokens needed.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 1. Bump version in pyproject.toml
|
|
122
|
+
# 2. Commit the bump
|
|
123
|
+
git add pyproject.toml
|
|
124
|
+
git commit -m "release: v0.2.0"
|
|
125
|
+
|
|
126
|
+
# 3. Tag and push (triggers CI → test → publish)
|
|
127
|
+
git tag v0.2.0
|
|
128
|
+
git push origin main --tags
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The tag must match the version in `pyproject.toml` exactly (without the `v` prefix).
|
|
132
|
+
The CI workflow verifies this before publishing.
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
### Conventions
|
|
137
|
+
|
|
138
|
+
- **File structure**: Test files mirror the source tree. `src/synth_acp/acp/session.py` →
|
|
139
|
+
`tests/acp/test_session.py`. One test file per source module — don't split a module's
|
|
140
|
+
tests across multiple files. Use test classes within the file to organize by feature.
|
|
141
|
+
A test file may only import from one source module — crossing into another module's
|
|
142
|
+
territory is a structure violation.
|
|
143
|
+
- **Async**: `pytest-asyncio` with `asyncio_mode = "auto"`. All async tests are plain
|
|
144
|
+
`async def` — no decorator needed.
|
|
145
|
+
- **Fixtures**: Shared helpers used across 3+ test files belong in `tests/conftest.py`,
|
|
146
|
+
not duplicated per file.
|
|
147
|
+
|
|
148
|
+
### Textual UI tests
|
|
149
|
+
|
|
150
|
+
Two modes — choose the right one:
|
|
151
|
+
|
|
152
|
+
- **Live widget tree** (`app.run_test(headless=True, size=(120, 40))`): required when
|
|
153
|
+
the test needs to query the DOM, check CSS classes, simulate clicks or keypresses,
|
|
154
|
+
or mount widgets. Use `pilot.click(selector)`, `pilot.press(key)`, and
|
|
155
|
+
`pilot.pause()` to let pending messages settle before asserting.
|
|
156
|
+
- **Direct method calls with mocks**: sufficient for pure logic and routing tests that
|
|
157
|
+
don't need a rendered widget tree. Prefer this — it's faster and less brittle.
|
|
158
|
+
|
|
159
|
+
`run_test` is expensive. Don't reach for it to test something that can be verified
|
|
160
|
+
by calling a method directly. Do reach for it when the contract is "this event causes
|
|
161
|
+
this DOM change" — that's exactly what it's for.
|
|
162
|
+
|
|
163
|
+
What does not earn a Textual test: confirming that a Textual widget property works
|
|
164
|
+
(`Collapsible.collapsed` toggles, `Static.content` stores text). Test your logic,
|
|
165
|
+
not the framework.
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
### Running Tests
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run pytest -q --tb=short --no-header -rF # Quick summary
|
|
172
|
+
uv run pytest --co # List collected tests (dry run)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### aiosqlite / SQLite best practices
|
|
176
|
+
|
|
177
|
+
`aiosqlite` creates a dedicated **non-daemon thread** per connection. An unclosed
|
|
178
|
+
connection keeps the process alive after the event loop exits — the user sees a
|
|
179
|
+
hang requiring Ctrl-C. Sync `sqlite3` has no background threads, so a leaked
|
|
180
|
+
connection is just a file descriptor, not a hung process.
|
|
181
|
+
|
|
182
|
+
**Choose the right tool for the pattern:**
|
|
183
|
+
|
|
184
|
+
| Pattern | Use | Why |
|
|
185
|
+
|---------|-----|-----|
|
|
186
|
+
| Long-lived connection (lifecycle DB, message bus delivery loop) | `aiosqlite` | Dedicated thread amortises setup; `async with` or explicit `close_db()` ensures cleanup |
|
|
187
|
+
| Open-close per call (permission writes, one-off queries) | `asyncio.to_thread` + `sqlite3` | Borrows a daemon pool thread briefly; no shutdown hang risk |
|
|
188
|
+
| Sync init before event loop starts (`__init__`, CLI setup) | `sqlite3` directly | No event loop yet; sync is fine and has zero thread overhead |
|
|
189
|
+
| Agent subprocess (MCP server) | `aiosqlite` persistent conn | Subprocess gets killed anyway; `close_db()` hook exists for tests |
|
|
190
|
+
|
|
191
|
+
**Rules:**
|
|
192
|
+
|
|
193
|
+
- Every `aiosqlite.connect()` in the main process **must** be inside `async with`
|
|
194
|
+
or have a guaranteed `close()` in a `finally` block. A bare
|
|
195
|
+
`conn = await aiosqlite.connect(...)` without `try/finally` is a shutdown hang
|
|
196
|
+
waiting to happen.
|
|
197
|
+
- Never open `aiosqlite` connections for short-lived one-off operations. Use
|
|
198
|
+
`asyncio.to_thread` with sync `sqlite3` instead.
|
|
199
|
+
- Sync `sqlite3` with WAL mode can deadlock against `aiosqlite` connections to the
|
|
200
|
+
same database when both are in the same process. Keep sync `sqlite3` usage limited
|
|
201
|
+
to init-time schema creation (before `aiosqlite` connections are opened) or
|
|
202
|
+
offloaded to `asyncio.to_thread`.
|
|
203
|
+
|
|
204
|
+
**Test cleanup:** Any test that triggers `broker.handle(LaunchAgent(...))` or calls
|
|
205
|
+
`broker._start_message_bus()` **must** stop the bus and close the lifecycle DB
|
|
206
|
+
in a `finally` block — otherwise the aiosqlite background thread and the Unix
|
|
207
|
+
socket server keep the event loop alive and the test hangs indefinitely:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
try:
|
|
211
|
+
await broker.handle(LaunchAgent(agent_id="agent-1"))
|
|
212
|
+
# ... assertions ...
|
|
213
|
+
finally:
|
|
214
|
+
if broker._message_bus:
|
|
215
|
+
await broker._message_bus.stop()
|
|
216
|
+
if broker._lifecycle:
|
|
217
|
+
await broker._lifecycle.close_db()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Tests that create MCP servers with `create_mcp_server()` must call
|
|
221
|
+
`await server.close_db()` after the test (or use the `mcp_factory` fixture
|
|
222
|
+
in `tests/mcp/test_server.py` which handles this automatically).
|
|
223
|
+
|
|
224
|
+
## Tooling
|
|
225
|
+
|
|
226
|
+
| Tool | Purpose | Command |
|
|
227
|
+
|------|---------|---------|
|
|
228
|
+
| ruff | Linting and formatting | `ruff check --fix --output-format concise` |
|
|
229
|
+
| ty | Type checking | `ty check --output-format concise src/ tests/` |
|
|
230
|
+
| pytest | Testing | `uv run pytest -q --tb=short --no-header -rF` |
|
|
231
|
+
|
|
232
|
+
## Style
|
|
233
|
+
|
|
234
|
+
- `from __future__ import annotations` in all files.
|
|
235
|
+
- Google-style docstrings.
|
|
236
|
+
- Pydantic v2 `BaseModel` with `frozen=True` for all cross-layer types.
|
|
237
|
+
- Use the `agent-client-protocol` SDK's Pydantic models directly (e.g. `McpServerStdio`, `EnvVariable`) — don't hand-build dicts for ACP payloads.
|
|
238
|
+
- `SessionAccumulator` from `acp.contrib` is the canonical source of per-agent conversation history. Don't reimplement tool call tracking.
|