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.
Files changed (115) hide show
  1. synth_acp-0.1.0/.github/workflows/publish.yml +35 -0
  2. synth_acp-0.1.0/.gitignore +13 -0
  3. synth_acp-0.1.0/.synth.json +40 -0
  4. synth_acp-0.1.0/AGENTS.md +238 -0
  5. synth_acp-0.1.0/LICENSE +657 -0
  6. synth_acp-0.1.0/PKG-INFO +324 -0
  7. synth_acp-0.1.0/README.md +300 -0
  8. synth_acp-0.1.0/examples/synth.example.json +57 -0
  9. synth_acp-0.1.0/pyproject.toml +113 -0
  10. synth_acp-0.1.0/scripts/prob_config_options.py +512 -0
  11. synth_acp-0.1.0/scripts/probe_modes.py +89 -0
  12. synth_acp-0.1.0/scripts/test_terminal.py +164 -0
  13. synth_acp-0.1.0/scripts/test_updates.py +90 -0
  14. synth_acp-0.1.0/src/synth_acp/__init__.py +3 -0
  15. synth_acp-0.1.0/src/synth_acp/__main__.py +7 -0
  16. synth_acp-0.1.0/src/synth_acp/acp/__init__.py +0 -0
  17. synth_acp-0.1.0/src/synth_acp/acp/session.py +961 -0
  18. synth_acp-0.1.0/src/synth_acp/acp/state_machine.py +50 -0
  19. synth_acp-0.1.0/src/synth_acp/broker/__init__.py +0 -0
  20. synth_acp-0.1.0/src/synth_acp/broker/broker.py +679 -0
  21. synth_acp-0.1.0/src/synth_acp/broker/lifecycle.py +635 -0
  22. synth_acp-0.1.0/src/synth_acp/broker/message_bus.py +188 -0
  23. synth_acp-0.1.0/src/synth_acp/broker/permissions.py +78 -0
  24. synth_acp-0.1.0/src/synth_acp/broker/registry.py +106 -0
  25. synth_acp-0.1.0/src/synth_acp/cli.py +503 -0
  26. synth_acp-0.1.0/src/synth_acp/data/__init__.py +1 -0
  27. synth_acp-0.1.0/src/synth_acp/data/harnesses/__init__.py +1 -0
  28. synth_acp-0.1.0/src/synth_acp/data/harnesses/claude.toml +5 -0
  29. synth_acp-0.1.0/src/synth_acp/data/harnesses/gemini.toml +5 -0
  30. synth_acp-0.1.0/src/synth_acp/data/harnesses/kiro.toml +5 -0
  31. synth_acp-0.1.0/src/synth_acp/data/harnesses/opencode.toml +5 -0
  32. synth_acp-0.1.0/src/synth_acp/db.py +163 -0
  33. synth_acp-0.1.0/src/synth_acp/harnesses.py +23 -0
  34. synth_acp-0.1.0/src/synth_acp/mcp/__init__.py +0 -0
  35. synth_acp-0.1.0/src/synth_acp/mcp/notifier.py +37 -0
  36. synth_acp-0.1.0/src/synth_acp/mcp/server.py +339 -0
  37. synth_acp-0.1.0/src/synth_acp/models/__init__.py +3 -0
  38. synth_acp-0.1.0/src/synth_acp/models/agent.py +130 -0
  39. synth_acp-0.1.0/src/synth_acp/models/commands.py +76 -0
  40. synth_acp-0.1.0/src/synth_acp/models/config.py +194 -0
  41. synth_acp-0.1.0/src/synth_acp/models/events.py +256 -0
  42. synth_acp-0.1.0/src/synth_acp/models/permissions.py +28 -0
  43. synth_acp-0.1.0/src/synth_acp/models/visibility.py +97 -0
  44. synth_acp-0.1.0/src/synth_acp/terminal/__init__.py +1 -0
  45. synth_acp-0.1.0/src/synth_acp/terminal/manager.py +273 -0
  46. synth_acp-0.1.0/src/synth_acp/terminal/shell_read.py +42 -0
  47. synth_acp-0.1.0/src/synth_acp/ui/__init__.py +0 -0
  48. synth_acp-0.1.0/src/synth_acp/ui/ansi/__init__.py +1 -0
  49. synth_acp-0.1.0/src/synth_acp/ui/ansi/_ansi.py +1620 -0
  50. synth_acp-0.1.0/src/synth_acp/ui/ansi/_ansi_colors.py +264 -0
  51. synth_acp-0.1.0/src/synth_acp/ui/ansi/_control_codes.py +37 -0
  52. synth_acp-0.1.0/src/synth_acp/ui/ansi/_dec.py +332 -0
  53. synth_acp-0.1.0/src/synth_acp/ui/ansi/_keys.py +251 -0
  54. synth_acp-0.1.0/src/synth_acp/ui/ansi/_sgr_styles.py +64 -0
  55. synth_acp-0.1.0/src/synth_acp/ui/ansi/_stream_parser.py +416 -0
  56. synth_acp-0.1.0/src/synth_acp/ui/app.py +671 -0
  57. synth_acp-0.1.0/src/synth_acp/ui/css/app.tcss +487 -0
  58. synth_acp-0.1.0/src/synth_acp/ui/messages.py +18 -0
  59. synth_acp-0.1.0/src/synth_acp/ui/screens/__init__.py +8 -0
  60. synth_acp-0.1.0/src/synth_acp/ui/screens/dashboard.py +0 -0
  61. synth_acp-0.1.0/src/synth_acp/ui/screens/help.py +51 -0
  62. synth_acp-0.1.0/src/synth_acp/ui/screens/launch.py +85 -0
  63. synth_acp-0.1.0/src/synth_acp/ui/screens/permission.py +221 -0
  64. synth_acp-0.1.0/src/synth_acp/ui/screens/session_picker.py +78 -0
  65. synth_acp-0.1.0/src/synth_acp/ui/widgets/__init__.py +0 -0
  66. synth_acp-0.1.0/src/synth_acp/ui/widgets/agent_list.py +229 -0
  67. synth_acp-0.1.0/src/synth_acp/ui/widgets/agent_message.py +45 -0
  68. synth_acp-0.1.0/src/synth_acp/ui/widgets/conversation.py +323 -0
  69. synth_acp-0.1.0/src/synth_acp/ui/widgets/copy_button.py +37 -0
  70. synth_acp-0.1.0/src/synth_acp/ui/widgets/diff_view.py +514 -0
  71. synth_acp-0.1.0/src/synth_acp/ui/widgets/gradient_bar.py +144 -0
  72. synth_acp-0.1.0/src/synth_acp/ui/widgets/input_bar.py +452 -0
  73. synth_acp-0.1.0/src/synth_acp/ui/widgets/message_queue.py +169 -0
  74. synth_acp-0.1.0/src/synth_acp/ui/widgets/plan_block.py +63 -0
  75. synth_acp-0.1.0/src/synth_acp/ui/widgets/prompt_bubble.py +29 -0
  76. synth_acp-0.1.0/src/synth_acp/ui/widgets/shell_result.py +46 -0
  77. synth_acp-0.1.0/src/synth_acp/ui/widgets/terminal.py +360 -0
  78. synth_acp-0.1.0/src/synth_acp/ui/widgets/thought_block.py +50 -0
  79. synth_acp-0.1.0/src/synth_acp/ui/widgets/tool_call.py +251 -0
  80. synth_acp-0.1.0/tests/__init__.py +0 -0
  81. synth_acp-0.1.0/tests/acp/__init__.py +0 -0
  82. synth_acp-0.1.0/tests/acp/test_session.py +637 -0
  83. synth_acp-0.1.0/tests/acp/test_session_permission.py +116 -0
  84. synth_acp-0.1.0/tests/acp/test_state_machine.py +52 -0
  85. synth_acp-0.1.0/tests/broker/__init__.py +0 -0
  86. synth_acp-0.1.0/tests/broker/test_broker.py +672 -0
  87. synth_acp-0.1.0/tests/broker/test_lifecycle.py +130 -0
  88. synth_acp-0.1.0/tests/broker/test_message_bus.py +142 -0
  89. synth_acp-0.1.0/tests/broker/test_permissions.py +80 -0
  90. synth_acp-0.1.0/tests/broker/test_registry.py +53 -0
  91. synth_acp-0.1.0/tests/conftest.py +0 -0
  92. synth_acp-0.1.0/tests/mcp/__init__.py +0 -0
  93. synth_acp-0.1.0/tests/mcp/test_notifier.py +79 -0
  94. synth_acp-0.1.0/tests/mcp/test_server.py +199 -0
  95. synth_acp-0.1.0/tests/models/__init__.py +0 -0
  96. synth_acp-0.1.0/tests/models/test_agent.py +63 -0
  97. synth_acp-0.1.0/tests/models/test_config.py +137 -0
  98. synth_acp-0.1.0/tests/terminal/__init__.py +0 -0
  99. synth_acp-0.1.0/tests/terminal/test_manager.py +84 -0
  100. synth_acp-0.1.0/tests/test_cli.py +39 -0
  101. synth_acp-0.1.0/tests/test_harnesses.py +16 -0
  102. synth_acp-0.1.0/tests/ui/__init__.py +0 -0
  103. synth_acp-0.1.0/tests/ui/screens/__init__.py +0 -0
  104. synth_acp-0.1.0/tests/ui/screens/test_help.py +34 -0
  105. synth_acp-0.1.0/tests/ui/screens/test_launch.py +115 -0
  106. synth_acp-0.1.0/tests/ui/screens/test_permission.py +142 -0
  107. synth_acp-0.1.0/tests/ui/test_app.py +305 -0
  108. synth_acp-0.1.0/tests/ui/widgets/__init__.py +0 -0
  109. synth_acp-0.1.0/tests/ui/widgets/test_agent_list.py +134 -0
  110. synth_acp-0.1.0/tests/ui/widgets/test_conversation.py +84 -0
  111. synth_acp-0.1.0/tests/ui/widgets/test_diff_view.py +34 -0
  112. synth_acp-0.1.0/tests/ui/widgets/test_message_queue.py +116 -0
  113. synth_acp-0.1.0/tests/ui/widgets/test_plan_block.py +71 -0
  114. synth_acp-0.1.0/tests/ui/widgets/test_tool_call.py +153 -0
  115. 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,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+
10
+ .kiro/
11
+ .serena/
12
+ synth-debug.log
13
+ *.bak
@@ -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.