taut-summon 0.5.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.
- taut_summon-0.5.0/.gitignore +23 -0
- taut_summon-0.5.0/LICENSE +21 -0
- taut_summon-0.5.0/PKG-INFO +138 -0
- taut_summon-0.5.0/README.md +121 -0
- taut_summon-0.5.0/pyproject.toml +62 -0
- taut_summon-0.5.0/taut_summon/__init__.py +40 -0
- taut_summon-0.5.0/taut_summon/__main__.py +8 -0
- taut_summon-0.5.0/taut_summon/_adapter.py +212 -0
- taut_summon-0.5.0/taut_summon/_broker_retry.py +64 -0
- taut_summon-0.5.0/taut_summon/_claude.py +184 -0
- taut_summon-0.5.0/taut_summon/_control.py +928 -0
- taut_summon-0.5.0/taut_summon/_driver.py +1191 -0
- taut_summon-0.5.0/taut_summon/_persona.py +114 -0
- taut_summon-0.5.0/taut_summon/_pty.py +704 -0
- taut_summon-0.5.0/taut_summon/_retry.py +329 -0
- taut_summon-0.5.0/taut_summon/_scripted.py +111 -0
- taut_summon-0.5.0/taut_summon/_state.py +701 -0
- taut_summon-0.5.0/taut_summon/_stream.py +175 -0
- taut_summon-0.5.0/taut_summon/cli.py +490 -0
- taut_summon-0.5.0/taut_summon/py.typed +1 -0
- taut_summon-0.5.0/taut_summon/scripted_provider.py +313 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.ruff_cache/
|
|
3
|
+
.mypy_cache/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.hypothesis/
|
|
6
|
+
.coverage
|
|
7
|
+
.coverage.*
|
|
8
|
+
coverage.xml
|
|
9
|
+
htmlcov/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.venv/
|
|
16
|
+
.taut.db
|
|
17
|
+
.taut.lock
|
|
18
|
+
.taut.db-wal
|
|
19
|
+
.taut.db-shm
|
|
20
|
+
.envrc
|
|
21
|
+
.taut.toml
|
|
22
|
+
.weft
|
|
23
|
+
.broker*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Van Lindberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taut-summon
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Summon extension for Taut: host an agent harness as a workspace member
|
|
5
|
+
Author-email: Van Lindberg <van.lindberg@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: taut>=0.5.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# taut-summon
|
|
19
|
+
|
|
20
|
+
Summon extension for Taut: host an existing agent harness (Claude Code and
|
|
21
|
+
friends) as an ordinary member of a taut workspace.
|
|
22
|
+
|
|
23
|
+
This package is intentionally separate from `taut`. The summon driver is the
|
|
24
|
+
agent's terminal, not its runtime: it injects chat into the harness's own
|
|
25
|
+
live session (its ears), and the agent speaks through the ordinary `taut`
|
|
26
|
+
CLI selected by its continuity token (its mouth). The full contract lives in
|
|
27
|
+
the core repository at `docs/specs/04-summon.md`.
|
|
28
|
+
|
|
29
|
+
## Status
|
|
30
|
+
|
|
31
|
+
Functional. The CLI surface (`taut-summon run|stop|status`) and the core
|
|
32
|
+
delegation verbs (`taut summon`, `taut dismiss`), the foreground driver
|
|
33
|
+
(bootstrap, chat injection, event pump, crash-resume, clean shutdown), the
|
|
34
|
+
session ledger with a single-driver guard and PTY `wired` flag, the control
|
|
35
|
+
plane (STOP/STATUS/PING) with a rate backstop, the default persona, and the
|
|
36
|
+
provider adapters are implemented. `pty` is the default adapter for the
|
|
37
|
+
interactive harnesses (`claude`, `codex`, `coder`, `grok`, `qwen`, `kimi`,
|
|
38
|
+
`opencode`, `pi`); `claude-stream` remains available for Claude Code's
|
|
39
|
+
structured stream-json mode. See
|
|
40
|
+
`docs/plans/2026-07-06-taut-summon-plan.md`,
|
|
41
|
+
`docs/plans/2026-07-07-taut-summon-pty-harness-adapter-plan.md`, and
|
|
42
|
+
`docs/implementation/05-taut-summon-architecture.md` for the design.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.11+
|
|
47
|
+
- Core `taut` and `taut-summon` installed in the same environment
|
|
48
|
+
- A SQL-sidecar backend (SQLite or Postgres) — summon state rides sidecar
|
|
49
|
+
tables
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Taut releases are GitHub-only until package-name clearance changes. Install
|
|
54
|
+
the core package first, then inject the extension wheel into the same
|
|
55
|
+
environment:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pipx install "git+https://github.com/VanL/taut.git@vX.Y.Z"
|
|
59
|
+
pipx inject taut ./taut_summon-0.1.0-py3-none-any.whl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
taut summon claude # summon a claude into #general
|
|
66
|
+
taut summon reviewer --provider claude dev
|
|
67
|
+
taut summon reviewer --provider claude-stream dev
|
|
68
|
+
taut dismiss reviewer
|
|
69
|
+
taut-summon status
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`taut summon`/`taut dismiss` delegate argv verbatim to `taut-summon
|
|
73
|
+
run`/`taut-summon stop`; both surfaces share one resolution contract.
|
|
74
|
+
|
|
75
|
+
On first PTY use, summon attaches your terminal so you can answer trust,
|
|
76
|
+
login, or model prompts in the real harness UI. Detach with `Ctrl-\ Ctrl-\`.
|
|
77
|
+
After detach the member is marked wired and future summons run detached.
|
|
78
|
+
Use `taut summon --attach NAME` to re-enter setup, or `--detach` for an
|
|
79
|
+
explicit detached run. PTY output is never parsed as speech; the agent speaks
|
|
80
|
+
by running `taut say`.
|
|
81
|
+
|
|
82
|
+
## Testing
|
|
83
|
+
|
|
84
|
+
From the repository root:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv run pytest extensions/taut_summon/tests
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Local runs attempt the live PTY harness smoke matrix by default. A provider
|
|
91
|
+
skips with an explicit reason when its binary is absent, the fresh test
|
|
92
|
+
database has not been onboarded with a real attach/detach cycle, or status
|
|
93
|
+
cannot reach a usable detached session. CI skips the real-harness matrix
|
|
94
|
+
unless `TAUT_SUMMON_LIVE_HARNESS=1` is set. For a fast local loop, use:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
TAUT_SUMMON_LIVE_HARNESS=0 uv run pytest extensions/taut_summon/tests
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run `taut summon --attach <name>` once for a provider that still needs trust,
|
|
101
|
+
login, or model setup before expecting its detached live smoke to pass.
|
|
102
|
+
For a hard local external-provider smoke, use strict mode. It prewires the
|
|
103
|
+
temporary test session to model an already-onboarded provider and fails on
|
|
104
|
+
missing binaries, readiness gaps, status timeouts, unanswered terminal queries,
|
|
105
|
+
or injection catch-up failures. The external-provider lane does not require
|
|
106
|
+
hosted CLIs to auto-execute shell commands; the local LLM lane below owns the
|
|
107
|
+
deterministic sentinel-posting proof.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
TAUT_SUMMON_LIVE_HARNESS_STRICT=1 uv run pytest extensions/taut_summon/tests/test_live_harness.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The local LLM smoke runs locally by default when a loopback OpenAI-compatible
|
|
114
|
+
endpoint lists the served model, and it runs in CI through the dedicated
|
|
115
|
+
Ollama-backed workflow job. Defaults:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
TAUT_SUMMON_LOCAL_LLM_ENDPOINT=http://127.0.0.1:11434/v1
|
|
119
|
+
TAUT_SUMMON_LOCAL_LLM_MODEL=taut-summon-local-model:latest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To run it locally with Ollama:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
ollama pull qwen2.5:0.5b
|
|
126
|
+
cat > /tmp/TautSummonModelfile <<'EOF'
|
|
127
|
+
FROM qwen2.5:0.5b
|
|
128
|
+
PARAMETER num_ctx 2048
|
|
129
|
+
PARAMETER num_predict 64
|
|
130
|
+
PARAMETER temperature 0
|
|
131
|
+
EOF
|
|
132
|
+
ollama create taut-summon-local-model:latest -f /tmp/TautSummonModelfile
|
|
133
|
+
uv run pytest extensions/taut_summon/tests/test_live_local_llm.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use `TAUT_SUMMON_LOCAL_LLM=0` to skip the local LLM smoke locally, or
|
|
137
|
+
`TAUT_SUMMON_LOCAL_LLM=1` to make missing endpoint/model setup fail instead of
|
|
138
|
+
skip.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# taut-summon
|
|
2
|
+
|
|
3
|
+
Summon extension for Taut: host an existing agent harness (Claude Code and
|
|
4
|
+
friends) as an ordinary member of a taut workspace.
|
|
5
|
+
|
|
6
|
+
This package is intentionally separate from `taut`. The summon driver is the
|
|
7
|
+
agent's terminal, not its runtime: it injects chat into the harness's own
|
|
8
|
+
live session (its ears), and the agent speaks through the ordinary `taut`
|
|
9
|
+
CLI selected by its continuity token (its mouth). The full contract lives in
|
|
10
|
+
the core repository at `docs/specs/04-summon.md`.
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
Functional. The CLI surface (`taut-summon run|stop|status`) and the core
|
|
15
|
+
delegation verbs (`taut summon`, `taut dismiss`), the foreground driver
|
|
16
|
+
(bootstrap, chat injection, event pump, crash-resume, clean shutdown), the
|
|
17
|
+
session ledger with a single-driver guard and PTY `wired` flag, the control
|
|
18
|
+
plane (STOP/STATUS/PING) with a rate backstop, the default persona, and the
|
|
19
|
+
provider adapters are implemented. `pty` is the default adapter for the
|
|
20
|
+
interactive harnesses (`claude`, `codex`, `coder`, `grok`, `qwen`, `kimi`,
|
|
21
|
+
`opencode`, `pi`); `claude-stream` remains available for Claude Code's
|
|
22
|
+
structured stream-json mode. See
|
|
23
|
+
`docs/plans/2026-07-06-taut-summon-plan.md`,
|
|
24
|
+
`docs/plans/2026-07-07-taut-summon-pty-harness-adapter-plan.md`, and
|
|
25
|
+
`docs/implementation/05-taut-summon-architecture.md` for the design.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Python 3.11+
|
|
30
|
+
- Core `taut` and `taut-summon` installed in the same environment
|
|
31
|
+
- A SQL-sidecar backend (SQLite or Postgres) — summon state rides sidecar
|
|
32
|
+
tables
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Taut releases are GitHub-only until package-name clearance changes. Install
|
|
37
|
+
the core package first, then inject the extension wheel into the same
|
|
38
|
+
environment:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install "git+https://github.com/VanL/taut.git@vX.Y.Z"
|
|
42
|
+
pipx inject taut ./taut_summon-0.1.0-py3-none-any.whl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
taut summon claude # summon a claude into #general
|
|
49
|
+
taut summon reviewer --provider claude dev
|
|
50
|
+
taut summon reviewer --provider claude-stream dev
|
|
51
|
+
taut dismiss reviewer
|
|
52
|
+
taut-summon status
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`taut summon`/`taut dismiss` delegate argv verbatim to `taut-summon
|
|
56
|
+
run`/`taut-summon stop`; both surfaces share one resolution contract.
|
|
57
|
+
|
|
58
|
+
On first PTY use, summon attaches your terminal so you can answer trust,
|
|
59
|
+
login, or model prompts in the real harness UI. Detach with `Ctrl-\ Ctrl-\`.
|
|
60
|
+
After detach the member is marked wired and future summons run detached.
|
|
61
|
+
Use `taut summon --attach NAME` to re-enter setup, or `--detach` for an
|
|
62
|
+
explicit detached run. PTY output is never parsed as speech; the agent speaks
|
|
63
|
+
by running `taut say`.
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
From the repository root:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run pytest extensions/taut_summon/tests
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Local runs attempt the live PTY harness smoke matrix by default. A provider
|
|
74
|
+
skips with an explicit reason when its binary is absent, the fresh test
|
|
75
|
+
database has not been onboarded with a real attach/detach cycle, or status
|
|
76
|
+
cannot reach a usable detached session. CI skips the real-harness matrix
|
|
77
|
+
unless `TAUT_SUMMON_LIVE_HARNESS=1` is set. For a fast local loop, use:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
TAUT_SUMMON_LIVE_HARNESS=0 uv run pytest extensions/taut_summon/tests
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run `taut summon --attach <name>` once for a provider that still needs trust,
|
|
84
|
+
login, or model setup before expecting its detached live smoke to pass.
|
|
85
|
+
For a hard local external-provider smoke, use strict mode. It prewires the
|
|
86
|
+
temporary test session to model an already-onboarded provider and fails on
|
|
87
|
+
missing binaries, readiness gaps, status timeouts, unanswered terminal queries,
|
|
88
|
+
or injection catch-up failures. The external-provider lane does not require
|
|
89
|
+
hosted CLIs to auto-execute shell commands; the local LLM lane below owns the
|
|
90
|
+
deterministic sentinel-posting proof.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
TAUT_SUMMON_LIVE_HARNESS_STRICT=1 uv run pytest extensions/taut_summon/tests/test_live_harness.py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The local LLM smoke runs locally by default when a loopback OpenAI-compatible
|
|
97
|
+
endpoint lists the served model, and it runs in CI through the dedicated
|
|
98
|
+
Ollama-backed workflow job. Defaults:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
TAUT_SUMMON_LOCAL_LLM_ENDPOINT=http://127.0.0.1:11434/v1
|
|
102
|
+
TAUT_SUMMON_LOCAL_LLM_MODEL=taut-summon-local-model:latest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To run it locally with Ollama:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ollama pull qwen2.5:0.5b
|
|
109
|
+
cat > /tmp/TautSummonModelfile <<'EOF'
|
|
110
|
+
FROM qwen2.5:0.5b
|
|
111
|
+
PARAMETER num_ctx 2048
|
|
112
|
+
PARAMETER num_predict 64
|
|
113
|
+
PARAMETER temperature 0
|
|
114
|
+
EOF
|
|
115
|
+
ollama create taut-summon-local-model:latest -f /tmp/TautSummonModelfile
|
|
116
|
+
uv run pytest extensions/taut_summon/tests/test_live_local_llm.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use `TAUT_SUMMON_LOCAL_LLM=0` to skip the local LLM smoke locally, or
|
|
120
|
+
`TAUT_SUMMON_LOCAL_LLM=1` to make missing endpoint/model setup fail instead of
|
|
121
|
+
skip.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "taut-summon"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Summon extension for Taut: host an agent harness as a workspace member"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Van Lindberg", email = "van.lindberg@gmail.com"},
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
# Floor is the core version shipping the `summon`/`dismiss` delegation
|
|
18
|
+
# verbs and the blessed identity/state surfaces this extension builds on.
|
|
19
|
+
"taut>=0.5.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
taut-summon = "taut_summon.cli:main"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"mypy>=1.0",
|
|
28
|
+
"pytest>=7.0",
|
|
29
|
+
"pytest-timeout>=2.4.0",
|
|
30
|
+
"pytest-xdist>=3.0",
|
|
31
|
+
"ruff>=0.1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.uv.sources]
|
|
35
|
+
taut = { path = "../..", editable = true }
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build]
|
|
38
|
+
include = [
|
|
39
|
+
"/taut_summon/**/*.py",
|
|
40
|
+
"/taut_summon/py.typed",
|
|
41
|
+
"/README.md",
|
|
42
|
+
"/LICENSE",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
minversion = "7.0"
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
addopts = "-ra -q --strict-markers -n auto --dist loadgroup"
|
|
49
|
+
markers = [
|
|
50
|
+
"requires_claude: live smoke tests that spawn the installed claude CLI (skipped when absent)",
|
|
51
|
+
"requires_live_harness: live PTY harness smoke tests; local by default, CI opt-in",
|
|
52
|
+
"requires_local_llm: local-LLM-backed PTY smoke tests; local by default, prepared CI opt-in",
|
|
53
|
+
"requires_codex: live smoke tests that spawn the installed codex CLI",
|
|
54
|
+
"requires_coder: live smoke tests that spawn the installed coder CLI",
|
|
55
|
+
"requires_grok: live smoke tests that spawn the installed grok CLI",
|
|
56
|
+
"requires_qwen: live smoke tests that spawn the installed qwen CLI",
|
|
57
|
+
"requires_kimi: live smoke tests that spawn the installed kimi CLI",
|
|
58
|
+
"requires_opencode: live smoke tests that spawn the installed opencode CLI",
|
|
59
|
+
"requires_pi: live smoke tests that spawn the installed pi CLI",
|
|
60
|
+
"xdist_group: group tests that share global external resources",
|
|
61
|
+
"sqlite_only: tests that validate built-in SQLite/path behavior",
|
|
62
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Summon extension for Taut: host an agent harness as a workspace member.
|
|
2
|
+
|
|
3
|
+
Spec: docs/specs/04-summon.md ([SUM-1]-[SUM-12]) in the core repository.
|
|
4
|
+
|
|
5
|
+
The adapter surface is exported here because the ``scripted`` adapter
|
|
6
|
+
ships for downstream integrators ([SUM-7.2]): conformance runners spawn
|
|
7
|
+
it as a real subprocess speaking real stream shapes ([SUM-12]).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from taut_summon._adapter import (
|
|
13
|
+
ActivityEvent,
|
|
14
|
+
AdapterError,
|
|
15
|
+
AdapterEvent,
|
|
16
|
+
AdapterHandle,
|
|
17
|
+
AssistantTextEvent,
|
|
18
|
+
ExitEvent,
|
|
19
|
+
ProviderAdapter,
|
|
20
|
+
SessionEvent,
|
|
21
|
+
UnknownAdapterError,
|
|
22
|
+
adapter_names,
|
|
23
|
+
get_adapter,
|
|
24
|
+
)
|
|
25
|
+
from taut_summon._scripted import ScriptedAdapter
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ActivityEvent",
|
|
29
|
+
"AdapterError",
|
|
30
|
+
"AdapterEvent",
|
|
31
|
+
"AdapterHandle",
|
|
32
|
+
"AssistantTextEvent",
|
|
33
|
+
"ExitEvent",
|
|
34
|
+
"ProviderAdapter",
|
|
35
|
+
"ScriptedAdapter",
|
|
36
|
+
"SessionEvent",
|
|
37
|
+
"UnknownAdapterError",
|
|
38
|
+
"adapter_names",
|
|
39
|
+
"get_adapter",
|
|
40
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Provider adapter interface: the [SUM-7.1] contract and the registry.
|
|
2
|
+
|
|
3
|
+
An adapter owns exactly four things — spawn, inject, events, interrupt —
|
|
4
|
+
and translates its provider's native streaming envelope into the closed
|
|
5
|
+
``AdapterEvent`` union below. There is no summon-defined wire protocol:
|
|
6
|
+
adapters translate, they do not define ([SUM-7.1]).
|
|
7
|
+
|
|
8
|
+
Contract requirements on every adapter (enforced by the conformance
|
|
9
|
+
tests, exercised today through the ``scripted`` adapter):
|
|
10
|
+
|
|
11
|
+
- ``inject()`` returns only after the event is written *and flushed* to
|
|
12
|
+
the child's stdin, and surfaces failures synchronously ([SUM-5.4]'s
|
|
13
|
+
at-least-once delivery to the harness process boundary depends on it).
|
|
14
|
+
- ``interrupt()`` and ``close()`` are thread-safe and unblock any
|
|
15
|
+
in-flight ``inject()`` — [SUM-9]'s STOP path must always be able to
|
|
16
|
+
stop a stalled harness.
|
|
17
|
+
- ``events()`` must be drained continuously by its (single) consumer; an
|
|
18
|
+
undrained stream is a child-stdout deadlock waiting to happen. The
|
|
19
|
+
stream ends with exactly one ``ExitEvent``.
|
|
20
|
+
- ``env`` passed to ``spawn`` is merged over the parent environment —
|
|
21
|
+
it carries additions such as ``TAUT_TOKEN``/``TAUT_DB`` ([SUM-6]),
|
|
22
|
+
not a replacement environment.
|
|
23
|
+
|
|
24
|
+
Spec references:
|
|
25
|
+
- docs/specs/04-summon.md [SUM-7.1], [SUM-7.2]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import Protocol
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AdapterError(Exception):
|
|
38
|
+
"""An adapter operation failed (spawn, inject, or stream translation)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnknownAdapterError(AdapterError):
|
|
42
|
+
"""No adapter is registered under the requested provider name."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class AssistantTextEvent:
|
|
47
|
+
"""Assistant-authored text (posted to chat only in terminal mode)."""
|
|
48
|
+
|
|
49
|
+
text: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class ActivityEvent:
|
|
54
|
+
"""Tool use or comparable liveness signal — feeds presence, never chat."""
|
|
55
|
+
|
|
56
|
+
description: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class SessionEvent:
|
|
61
|
+
"""The provider announced or changed its session id (resume handle)."""
|
|
62
|
+
|
|
63
|
+
session_id: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class ExitEvent:
|
|
68
|
+
"""The harness child exited; the event stream ends after this."""
|
|
69
|
+
|
|
70
|
+
returncode: int
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
AdapterEvent = AssistantTextEvent | ActivityEvent | SessionEvent | ExitEvent
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AdapterHandle(Protocol):
|
|
77
|
+
"""A live harness child owned by an adapter."""
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def session_id(self) -> str | None:
|
|
81
|
+
"""Current provider session id, updated as ``SessionEvent``s arrive."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def pid(self) -> int:
|
|
86
|
+
"""Harness child pid — the [SUM-4] re-anchor evidence.
|
|
87
|
+
|
|
88
|
+
The summoned member's identity anchor is ultimately the harness
|
|
89
|
+
child process; the driver builds its rejoin capture from this pid,
|
|
90
|
+
so exposing it is part of the handle surface.
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
def inject(self, text: str) -> None:
|
|
95
|
+
"""Write one user-role event; return only after a flushed write."""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
def events(self) -> Iterator[AdapterEvent]:
|
|
99
|
+
"""Typed output stream; single consumer, ends with ``ExitEvent``."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def interrupt(self) -> None:
|
|
103
|
+
"""Harness-graceful stop; unblocks an in-flight ``inject()``."""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
def close(self) -> None:
|
|
107
|
+
"""Stop (bounded escalation), reap the child, release the pipes."""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def status_fields(self) -> dict[str, str]:
|
|
111
|
+
"""Adapter-specific STATUS fields; empty for structured adapters."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ProviderAdapter(Protocol):
|
|
116
|
+
"""One provider harness family (claude, scripted, codex...)."""
|
|
117
|
+
|
|
118
|
+
name: str
|
|
119
|
+
supports_terminal_mode: bool
|
|
120
|
+
supports_attach: bool
|
|
121
|
+
orientation_via_inject: bool
|
|
122
|
+
|
|
123
|
+
def spawn(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
session_id: str | None,
|
|
127
|
+
system_prompt: str,
|
|
128
|
+
env: Mapping[str, str],
|
|
129
|
+
) -> AdapterHandle:
|
|
130
|
+
"""Start the harness child, resuming ``session_id`` when given."""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _scripted_factory() -> ProviderAdapter:
|
|
135
|
+
from taut_summon._scripted import ScriptedAdapter
|
|
136
|
+
|
|
137
|
+
return ScriptedAdapter()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _claude_factory() -> ProviderAdapter:
|
|
141
|
+
from taut_summon._claude import ClaudeAdapter
|
|
142
|
+
|
|
143
|
+
return ClaudeAdapter()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _pty_factory() -> ProviderAdapter:
|
|
147
|
+
from taut_summon._pty import PtyAdapter, PtySpec
|
|
148
|
+
|
|
149
|
+
raw_argv = os.environ.get("TAUT_SUMMON_PTY_ARGV")
|
|
150
|
+
if raw_argv:
|
|
151
|
+
parsed = json.loads(raw_argv)
|
|
152
|
+
if not isinstance(parsed, list) or not all(
|
|
153
|
+
isinstance(item, str) and item for item in parsed
|
|
154
|
+
):
|
|
155
|
+
raise AdapterError("TAUT_SUMMON_PTY_ARGV must be a JSON string list")
|
|
156
|
+
spec = PtySpec(
|
|
157
|
+
name="pty",
|
|
158
|
+
argv=tuple(parsed),
|
|
159
|
+
rows=int(os.environ.get("TAUT_SUMMON_PTY_ROWS", "24")),
|
|
160
|
+
cols=int(os.environ.get("TAUT_SUMMON_PTY_COLS", "80")),
|
|
161
|
+
stall_s=float(os.environ.get("TAUT_SUMMON_PTY_STALL_S", "10.0")),
|
|
162
|
+
quiet_ms=int(os.environ.get("TAUT_SUMMON_PTY_QUIET_MS", "500")),
|
|
163
|
+
max_settle_s=float(os.environ.get("TAUT_SUMMON_PTY_MAX_SETTLE_S", "10.0")),
|
|
164
|
+
)
|
|
165
|
+
return PtyAdapter(spec)
|
|
166
|
+
return PtyAdapter()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _pty_harness_factory(name: str, binary: str) -> Callable[[], ProviderAdapter]:
|
|
170
|
+
def _factory() -> ProviderAdapter:
|
|
171
|
+
from taut_summon._pty import PtyAdapter, PtySpec
|
|
172
|
+
|
|
173
|
+
return PtyAdapter(PtySpec(name=name, argv=(binary,)))
|
|
174
|
+
|
|
175
|
+
return _factory
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
_FACTORIES: dict[str, Callable[[], ProviderAdapter]] = {
|
|
179
|
+
"claude": _pty_harness_factory("claude", "claude"),
|
|
180
|
+
"claude-stream": _claude_factory,
|
|
181
|
+
"codex": _pty_harness_factory("codex", "codex"),
|
|
182
|
+
"coder": _pty_harness_factory("coder", "coder"),
|
|
183
|
+
"grok": _pty_harness_factory("grok", "grok"),
|
|
184
|
+
"kimi": _pty_harness_factory("kimi", "kimi"),
|
|
185
|
+
"opencode": _pty_harness_factory("opencode", "opencode"),
|
|
186
|
+
"pi": _pty_harness_factory("pi", "pi"),
|
|
187
|
+
"pty": _pty_factory,
|
|
188
|
+
"qwen": _pty_harness_factory("qwen", "qwen"),
|
|
189
|
+
"scripted": _scripted_factory,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def adapter_names() -> tuple[str, ...]:
|
|
194
|
+
"""Return the registered provider names, sorted."""
|
|
195
|
+
|
|
196
|
+
return tuple(sorted(_FACTORIES))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_adapter(name: str) -> ProviderAdapter:
|
|
200
|
+
"""Return the adapter registered under ``name``.
|
|
201
|
+
|
|
202
|
+
Raises ``UnknownAdapterError`` naming the known adapters ([SUM-3]
|
|
203
|
+
resolution step 4).
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
factory = _FACTORIES.get(name)
|
|
207
|
+
if factory is None:
|
|
208
|
+
known = ", ".join(adapter_names())
|
|
209
|
+
raise UnknownAdapterError(
|
|
210
|
+
f"no adapter named '{name}' (known adapters: {known})"
|
|
211
|
+
)
|
|
212
|
+
return factory()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""taut-summon's broker-specific retry policy, over the generic engine.
|
|
2
|
+
|
|
3
|
+
Mirrors simplebroker's own layering: the generic, re-vendorable loop lives
|
|
4
|
+
in ``_retry.py`` and the domain-specific policy — which errors are
|
|
5
|
+
transient, how many attempts, how to back off — sits on top here, the way
|
|
6
|
+
``simplebroker/helpers.py`` layers ``_execute_with_retry`` /
|
|
7
|
+
``_execute_watcher_operational_retry`` over the same engine.
|
|
8
|
+
|
|
9
|
+
``_retry.py`` is **vendored byte-for-byte** from ``simplebroker/_retry.py``
|
|
10
|
+
(simplebroker 5.1.0), which is published as a copy-me module for exactly
|
|
11
|
+
this use. It is kept pristine (no local edits) so it stays a diffable
|
|
12
|
+
drop-in copy; all provenance lives here, not inside that file. Re-vendor by
|
|
13
|
+
re-copying the upstream file when its ``__version__`` bumps. taut's
|
|
14
|
+
facades-only rule forbids importing ``simplebroker._retry`` directly, which
|
|
15
|
+
is why the engine is vendored rather than imported.
|
|
16
|
+
|
|
17
|
+
Spec reference: docs/specs/04-summon.md [SUM-9] (control-plane retry defense).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import TypeVar
|
|
25
|
+
|
|
26
|
+
from taut._broker_retry import is_transient_broker_error
|
|
27
|
+
from taut_summon._retry import execute_retry, expo, stop_after_attempt
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("taut_summon.broker")
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
_BROKER_RETRIES = 30
|
|
34
|
+
_BROKER_RETRY_DELAY = 0.05
|
|
35
|
+
_BROKER_RETRY_MAX_DELAY = 0.5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def broker_retry(
|
|
39
|
+
fn: Callable[[], T], *, what: str, attempts: int = _BROKER_RETRIES
|
|
40
|
+
) -> T:
|
|
41
|
+
"""Run a bare broker op, riding out transient WAL read errors.
|
|
42
|
+
|
|
43
|
+
Bare ``read_one``/``write`` on the control queues go through this so a
|
|
44
|
+
command or reply is not lost to a checkpoint-race transient (core's
|
|
45
|
+
watcher retries its own ops; bare ops do not). A persistent failure —
|
|
46
|
+
real corruption, or a non-transient class — is re-raised after the
|
|
47
|
+
bounded budget, so nothing genuinely broken is masked.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def _log(state: object, exc: Exception, delay: float) -> None:
|
|
51
|
+
logger.debug("transient broker error on %s; retrying: %s", what, exc)
|
|
52
|
+
|
|
53
|
+
return execute_retry(
|
|
54
|
+
fn,
|
|
55
|
+
retry_on=is_transient_broker_error,
|
|
56
|
+
wait_gen=expo,
|
|
57
|
+
wait_gen_kwargs={
|
|
58
|
+
"base": 2,
|
|
59
|
+
"factor": _BROKER_RETRY_DELAY,
|
|
60
|
+
"max_value": _BROKER_RETRY_MAX_DELAY,
|
|
61
|
+
},
|
|
62
|
+
stop=stop_after_attempt(attempts),
|
|
63
|
+
before_sleep=_log,
|
|
64
|
+
)
|