interaction-harness 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 (57) hide show
  1. interaction_harness-0.1.0/.env.example +71 -0
  2. interaction_harness-0.1.0/.github/ISSUE_TEMPLATE/bug-report.yml +34 -0
  3. interaction_harness-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. interaction_harness-0.1.0/.github/ISSUE_TEMPLATE/server-report.yml +61 -0
  5. interaction_harness-0.1.0/.github/workflows/ci.yml +46 -0
  6. interaction_harness-0.1.0/.github/workflows/publish.yml +29 -0
  7. interaction_harness-0.1.0/.gitignore +25 -0
  8. interaction_harness-0.1.0/CONTRIBUTING.md +49 -0
  9. interaction_harness-0.1.0/LICENSE +21 -0
  10. interaction_harness-0.1.0/PKG-INFO +269 -0
  11. interaction_harness-0.1.0/README.md +224 -0
  12. interaction_harness-0.1.0/demos/barge-in.png +0 -0
  13. interaction_harness-0.1.0/demos/dashboard.png +0 -0
  14. interaction_harness-0.1.0/demos/session.gif +0 -0
  15. interaction_harness-0.1.0/docs/backends.md +73 -0
  16. interaction_harness-0.1.0/docs/endpoints.md +77 -0
  17. interaction_harness-0.1.0/docs/interaction-models.md +146 -0
  18. interaction_harness-0.1.0/docs/protocol.md +83 -0
  19. interaction_harness-0.1.0/docs/tools.md +75 -0
  20. interaction_harness-0.1.0/examples/my_tools.py +25 -0
  21. interaction_harness-0.1.0/pyproject.toml +103 -0
  22. interaction_harness-0.1.0/src/interaction_harness/__init__.py +1 -0
  23. interaction_harness-0.1.0/src/interaction_harness/__main__.py +3 -0
  24. interaction_harness-0.1.0/src/interaction_harness/cli.py +140 -0
  25. interaction_harness-0.1.0/src/interaction_harness/config.py +210 -0
  26. interaction_harness-0.1.0/src/interaction_harness/doctor.py +171 -0
  27. interaction_harness-0.1.0/src/interaction_harness/llm.py +174 -0
  28. interaction_harness-0.1.0/src/interaction_harness/prompts.py +29 -0
  29. interaction_harness-0.1.0/src/interaction_harness/selftest.py +289 -0
  30. interaction_harness-0.1.0/src/interaction_harness/server.py +106 -0
  31. interaction_harness-0.1.0/src/interaction_harness/session.py +848 -0
  32. interaction_harness-0.1.0/src/interaction_harness/speech/__init__.py +93 -0
  33. interaction_harness-0.1.0/src/interaction_harness/speech/assets/SILERO_VAD_LICENSE +21 -0
  34. interaction_harness-0.1.0/src/interaction_harness/speech/assets/silero_vad.onnx +0 -0
  35. interaction_harness-0.1.0/src/interaction_harness/speech/base.py +142 -0
  36. interaction_harness-0.1.0/src/interaction_harness/speech/endpoint.py +112 -0
  37. interaction_harness-0.1.0/src/interaction_harness/speech/mlx.py +143 -0
  38. interaction_harness-0.1.0/src/interaction_harness/speech/onnx.py +88 -0
  39. interaction_harness-0.1.0/src/interaction_harness/speech/silero.py +53 -0
  40. interaction_harness-0.1.0/src/interaction_harness/speech/text.py +75 -0
  41. interaction_harness-0.1.0/src/interaction_harness/static/app.js +277 -0
  42. interaction_harness-0.1.0/src/interaction_harness/static/index.html +97 -0
  43. interaction_harness-0.1.0/src/interaction_harness/static/mic-worklet.js +37 -0
  44. interaction_harness-0.1.0/src/interaction_harness/static/style.css +151 -0
  45. interaction_harness-0.1.0/src/interaction_harness/tools/__init__.py +5 -0
  46. interaction_harness-0.1.0/src/interaction_harness/tools/builtin.py +161 -0
  47. interaction_harness-0.1.0/src/interaction_harness/tools/registry.py +155 -0
  48. interaction_harness-0.1.0/src/interaction_harness/tools/web.py +359 -0
  49. interaction_harness-0.1.0/tests/conftest.py +136 -0
  50. interaction_harness-0.1.0/tests/data/vad_clip.npy +0 -0
  51. interaction_harness-0.1.0/tests/data/vad_ref.npy +0 -0
  52. interaction_harness-0.1.0/tests/test_config.py +88 -0
  53. interaction_harness-0.1.0/tests/test_llm_and_web.py +49 -0
  54. interaction_harness-0.1.0/tests/test_session.py +138 -0
  55. interaction_harness-0.1.0/tests/test_speech.py +167 -0
  56. interaction_harness-0.1.0/tests/test_tools.py +89 -0
  57. interaction_harness-0.1.0/uv.lock +2472 -0
@@ -0,0 +1,71 @@
1
+ # interaction-harness settings. Copy to .env (or export the variables) and fill in what you use.
2
+ # Every model role is any OpenAI-compatible endpoint: <ROLE>_BASE_URL, <ROLE>_API_KEY, <ROLE>_MODEL.
3
+ # A key is only ever sent to the host it was set for.
4
+
5
+ # --- FOREGROUND: holds the conversation, calls quick tools, hands off hard work, judges end-of-turn --------
6
+ # Required. Falls back to OPENAI_BASE_URL / OPENAI_API_KEY when these are unset.
7
+ FOREGROUND_BASE_URL=http://127.0.0.1:8080/v1
8
+ FOREGROUND_API_KEY=
9
+ # Blank: the first model the server lists.
10
+ FOREGROUND_MODEL=
11
+ # How this server switches thinking: chat_template_kwargs (vLLM, llama.cpp, SGLang) | reasoning_effort | off
12
+ FOREGROUND_THINKING=chat_template_kwargs
13
+
14
+ # --- BACKGROUND: works on handed-off tasks with thinking on, browses, reports back on its own ---------------
15
+ # Blank: the foreground's server and model (field by field).
16
+ BACKGROUND_BASE_URL=
17
+ BACKGROUND_API_KEY=
18
+ BACKGROUND_MODEL=
19
+ BACKGROUND_THINKING=
20
+ BACKGROUND_TEMPERATURE=0.7
21
+ # Model calls per background task
22
+ BACKGROUND_ROUNDS=10
23
+
24
+ # --- ASR: speech -> text (POST /audio/transcriptions) ------------------------------------------------------
25
+ # Blank or "local": Parakeet TDT 0.6B v3 in-process (MLX on Apple Silicon, ONNX elsewhere).
26
+ ASR_BASE_URL=
27
+ ASR_API_KEY=
28
+ # Endpoint: the server's transcription model. In-process: override the Parakeet model id.
29
+ ASR_MODEL=
30
+ ASR_LANGUAGE=
31
+ # Partial transcripts every N ms while you talk; 0 = only when the end-of-turn judge needs one.
32
+ # On an endpoint each partial is one request.
33
+ ASR_PARTIAL_MS=640
34
+
35
+ # --- TTS: text -> speech (POST /audio/speech) --------------------------------------------------------------
36
+ # Blank or "local": Kokoro-82M in-process (MLX on Apple Silicon, ONNX elsewhere).
37
+ TTS_BASE_URL=
38
+ TTS_API_KEY=
39
+ TTS_MODEL=
40
+ # Blank: af_heart in-process, else the first voice the server lists (required if the server lists none).
41
+ TTS_VOICE=
42
+ # Comma-separated voices for the dashboard's picker, if the server does not list them.
43
+ TTS_VOICES=
44
+ # Endpoint audio: wav (default) or pcm; with pcm, the server's sample rate.
45
+ TTS_FORMAT=wav
46
+ TTS_SAMPLE_RATE=24000
47
+ TTS_SPEED=1.05
48
+
49
+ # --- in-process speech --------------------------------------------------------------------------------------
50
+ # auto (mlx on Apple Silicon, onnx elsewhere) | mlx | onnx
51
+ BACKEND=auto
52
+ # onnx backend: cpu | cuda (needs the onnx-gpu extra)
53
+ DEVICE=cpu
54
+ # onnx model variants: PARAKEET fp32|int8, KOKORO fp32|fp16|int8
55
+ PARAKEET_ONNX_VARIANT=int8
56
+ KOKORO_ONNX_VARIANT=int8
57
+
58
+ # --- everything else ----------------------------------------------------------------------------------------
59
+ # Comma-separated Python files of @tool functions
60
+ TOOLS=
61
+ # A directory to keep a WAV of every user turn the ASR heard (debugging)
62
+ SAVE_UTTERANCES=
63
+ HOST=127.0.0.1
64
+ PORT=8765
65
+ # HTTPS, so a browser on another machine may use the microphone
66
+ SSL_CERTFILE=
67
+ SSL_KEYFILE=
68
+ # Prompt overrides (easier in interaction-harness.toml under [prompts])
69
+ # FOREGROUND_PROMPT=
70
+ # BACKGROUND_PROMPT=
71
+ # END_OF_TURN_PROMPT=
@@ -0,0 +1,34 @@
1
+ name: Bug report
2
+ description: Something doesn't work the way the README says it should.
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened
9
+ description: What you did, what you expected, and what happened instead.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: events
14
+ attributes:
15
+ label: Events or logs
16
+ description: |
17
+ The relevant lines from the dashboard's Events panel or the server's terminal. If it's about speech
18
+ or turn-taking, setting `SAVE_UTTERANCES=DIR` keeps a WAV of each turn the ASR heard.
19
+ render: text
20
+ - type: input
21
+ id: version
22
+ attributes:
23
+ label: Version
24
+ description: The output of `interaction-harness --version`.
25
+ validations:
26
+ required: true
27
+ - type: input
28
+ id: setup
29
+ attributes:
30
+ label: Setup
31
+ description: OS and hardware, which roles are in-process or on an endpoint, and which servers and models.
32
+ placeholder: e.g. macOS 26, M4 Pro; speech in-process (MLX); foreground llama.cpp with Qwen3-8B
33
+ validations:
34
+ required: true
@@ -0,0 +1 @@
1
+ blank_issues_enabled: true
@@ -0,0 +1,61 @@
1
+ name: Server report
2
+ description: You ran the harness against a model server. Tell us whether it worked.
3
+ title: "Server report: "
4
+ labels: [server report]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Reports like this fill in the README's "Tested with" table. Run `interaction-harness doctor` with your
10
+ settings first; it checks each role on its own and prints what to paste below.
11
+ - type: dropdown
12
+ id: roles
13
+ attributes:
14
+ label: Roles
15
+ description: Which roles did this server play?
16
+ multiple: true
17
+ options: [ASR, TTS, foreground, background]
18
+ validations:
19
+ required: true
20
+ - type: input
21
+ id: server
22
+ attributes:
23
+ label: Server and version
24
+ placeholder: e.g. vLLM 0.13.1, Speaches 0.9, OpenAI API
25
+ validations:
26
+ required: true
27
+ - type: input
28
+ id: model
29
+ attributes:
30
+ label: Model
31
+ placeholder: e.g. Qwen/Qwen3-8B
32
+ validations:
33
+ required: true
34
+ - type: input
35
+ id: hardware
36
+ attributes:
37
+ label: Hardware and OS
38
+ placeholder: e.g. RTX 4090, Ubuntu 24.04
39
+ - type: dropdown
40
+ id: result
41
+ attributes:
42
+ label: Did it work?
43
+ options:
44
+ - "Yes: doctor passes and conversation works"
45
+ - "Partly: some checks warn or fail"
46
+ - "No"
47
+ validations:
48
+ required: true
49
+ - type: textarea
50
+ id: doctor
51
+ attributes:
52
+ label: doctor output
53
+ description: The output of `interaction-harness doctor`. Remove any keys or private hostnames.
54
+ render: text
55
+ validations:
56
+ required: true
57
+ - type: textarea
58
+ id: notes
59
+ attributes:
60
+ label: Notes
61
+ description: Settings you needed (thinking switch, server flags), and anything else worth knowing.
@@ -0,0 +1,46 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: astral-sh/setup-uv@v10.3.3
18
+ with:
19
+ enable-cache: true
20
+ - name: Install
21
+ run: uv sync --locked
22
+ - name: Lint
23
+ run: uv run ruff check src tests examples
24
+ - name: Unit tests (fakes and stubs; no models, no network)
25
+ run: uv run pytest -q
26
+ - name: CLI loads
27
+ run: |
28
+ uv run interaction-harness --version
29
+ uv run interaction-harness serve --help > /dev/null
30
+
31
+ speech-onnx-linux:
32
+ # the in-process ONNX speech stack on Linux, for real: download, TTS -> ASR round trip, partials, VAD
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: astral-sh/setup-uv@v10.3.3
37
+ with:
38
+ enable-cache: true
39
+ - name: Install
40
+ run: uv sync --locked
41
+ - uses: actions/cache@v6
42
+ with:
43
+ path: ~/.cache/interaction-harness
44
+ key: onnx-models-v1-int8
45
+ - name: Speech round trip
46
+ run: uv run interaction-harness warmup --backend onnx
@@ -0,0 +1,29 @@
1
+ name: publish
2
+
3
+ # Publishing a GitHub release uploads that version to PyPI. PyPI trusts this workflow directly (a
4
+ # "trusted publisher"), so no token is stored anywhere.
5
+ on:
6
+ release:
7
+ types: [published]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ pypi:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: astral-sh/setup-uv@v10.3.3
19
+ - name: The tag matches the package version
20
+ if: github.event_name == 'release'
21
+ run: |
22
+ version=$(uv version --short)
23
+ test "${GITHUB_REF_NAME}" = "v${version}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
24
+ - name: Build
25
+ run: uv build
26
+ - name: Check the PyPI page renders
27
+ run: uvx twine check dist/*
28
+ - name: Publish
29
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,25 @@
1
+ # secrets and local settings
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ interaction-harness.toml
6
+
7
+ # python
8
+ __pycache__/
9
+ *.py[cod]
10
+ .venv/
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+
17
+ # runtime output
18
+ utterances/
19
+ *.log
20
+ *.pid
21
+
22
+ # editors and OS
23
+ .DS_Store
24
+ .idea/
25
+ .vscode/
@@ -0,0 +1,49 @@
1
+ # Contributing
2
+
3
+ Issues and pull requests are welcome. The most useful contributions are:
4
+
5
+ - **Server reports.** Run `interaction-harness doctor` against a server not yet in the README's "Tested
6
+ with" table (OpenAI, Speaches, Kokoro-FastAPI, …) and file a
7
+ [server report](https://github.com/IMJONEZZ/interaction-harness/issues/new?template=server-report.yml)
8
+ with the output.
9
+ - **Backends.** In-process speech on other hardware or with other models. See
10
+ [docs/backends.md](docs/backends.md).
11
+ - **Interaction-model experiments.** New end-of-turn policies, backchannel handling, or better barge-in
12
+ rules. `session.py` is the one place the policy lives.
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ git clone https://github.com/IMJONEZZ/interaction-harness && cd interaction-harness
18
+ uv sync # Python 3.11-3.13
19
+ uv run pytest # unit tests: fakes and stubs, no models, no network
20
+ uv run ruff check src tests examples
21
+ ```
22
+
23
+ With a model server, check your change end to end:
24
+
25
+ ```bash
26
+ uv run interaction-harness doctor --foreground-base-url http://127.0.0.1:8080/v1
27
+ uv run interaction-harness selftest --foreground-base-url http://127.0.0.1:8080/v1
28
+ ```
29
+
30
+ `selftest` talks to the agent with synthesized speech, checking endpointing, tools, barge-in, background
31
+ delivery, timers, push-to-talk and typed turns. Some checks depend on the model's choices, such as which
32
+ tool it picks; those are warnings, not failures.
33
+
34
+ ## Where things are
35
+
36
+ | Path | What it holds |
37
+ |---|---|
38
+ | `src/interaction_harness/session.py` | the interaction model: state machine, endpointing, barge-in, both models, proactive delivery |
39
+ | `src/interaction_harness/config.py` | settings and the four roles |
40
+ | `src/interaction_harness/llm.py` | the chat roles, the thinking switch, the end-of-turn judge |
41
+ | `src/interaction_harness/speech/` | VAD, and ASR and TTS on MLX, ONNX or an endpoint |
42
+ | `src/interaction_harness/tools/` | the tool registry, built-in tools, web search and fetch |
43
+ | `src/interaction_harness/static/` | the dashboard: vanilla JS, no build step |
44
+ | `tests/` | unit tests; `conftest.py` has fakes for every model role |
45
+
46
+ ## Style
47
+
48
+ Keep the dashboard dependency-free, and keep the state machine readable. If a change affects latency,
49
+ say how you measured it; the waterfall and `doctor` are there for that.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Brousseau
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,269 @@
1
+ Metadata-Version: 2.5
2
+ Name: interaction-harness
3
+ Version: 0.1.0
4
+ Summary: A voice agent whose interaction model (turn-taking, barge-in, background work) is visible and swappable
5
+ Project-URL: Homepage, https://github.com/IMJONEZZ/interaction-harness
6
+ Project-URL: Source, https://github.com/IMJONEZZ/interaction-harness
7
+ Project-URL: Issues, https://github.com/IMJONEZZ/interaction-harness/issues
8
+ Author: Chris Brousseau
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: src/interaction_harness/speech/assets/SILERO_VAD_LICENSE
12
+ Keywords: barge-in,end-of-turn,llm,openai-compatible,speech,turn-taking,voice agent
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: <3.14,>=3.11
26
+ Requires-Dist: ddgs>=9
27
+ Requires-Dist: fastapi>=0.110
28
+ Requires-Dist: httpx>=0.27
29
+ Requires-Dist: kokoro-onnx>=0.4
30
+ Requires-Dist: misaki[en]>=0.9; sys_platform == 'darwin' and platform_machine == 'arm64'
31
+ Requires-Dist: mlx-audio>=0.5; sys_platform == 'darwin' and platform_machine == 'arm64'
32
+ Requires-Dist: numpy>=1.26
33
+ Requires-Dist: onnx-asr[hub]>=0.10
34
+ Requires-Dist: onnxruntime>=1.18
35
+ Requires-Dist: openai>=1.66
36
+ Requires-Dist: parakeet-mlx>=0.5; sys_platform == 'darwin' and platform_machine == 'arm64'
37
+ Requires-Dist: pypdf>=5
38
+ Requires-Dist: python-dotenv>=1.0
39
+ Requires-Dist: soundfile>=0.12
40
+ Requires-Dist: uvicorn[standard]>=0.29
41
+ Requires-Dist: websockets>=13
42
+ Provides-Extra: onnx-gpu
43
+ Requires-Dist: onnxruntime-gpu>=1.18; extra == 'onnx-gpu'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # interaction-harness
47
+
48
+ A voice agent whose **interaction model** is visible and swappable: when it decides you've finished
49
+ talking, how it lets you cut in, and how it keeps the conversation going while a second model works on
50
+ something hard and then brings the answer up on its own.
51
+
52
+ Every model it uses is an **OpenAI-compatible endpoint you choose**: speech-to-text, text-to-speech,
53
+ the foreground model that holds the conversation, and the background model it hands work to. Speech can
54
+ also run in-process, on Apple Silicon (MLX) or anywhere else (ONNX).
55
+
56
+ ![A real session: a tool call, a barge-in, a timer, and a background task delivered proactively](https://raw.githubusercontent.com/IMJONEZZ/interaction-harness/main/demos/session.gif)
57
+
58
+ <sub>A real session on one Mac, every model live: Parakeet and Kokoro in-process, Qwen3.6-35B-A3B on
59
+ llama.cpp as both foreground and background. The user's lines are spoken by Kokoro through Chrome's fake
60
+ microphone. Real time, no cuts.</sub>
61
+
62
+ ## What it shows
63
+
64
+ - **Turn-taking you can switch mid-conversation.** Push-to-talk, a VAD silence timeout, or *semantic
65
+ end-of-turn*: after 300 ms of silence the foreground model reads the transcript and answers COMPLETE or
66
+ INCOMPLETE, so "I was wondering whether…" keeps the floor with you.
67
+ - **Barge-in that keeps the history honest.** About 190 ms of confident speech over the agent stops the
68
+ model, the TTS queue and playback. The history keeps only what you actually heard, so the model never
69
+ quotes a reply it didn't finish.
70
+ - **A foreground and a background model.** The foreground answers in a sentence or two and hands real
71
+ work (analysis, planning, research with web search) to the background model, then keeps chatting. When
72
+ the background result is ready and you go quiet, the agent takes its own turn to tell you. Timers work
73
+ the same way.
74
+ - **Every step on screen.** VAD probability, partial and final transcripts, the judge's verdicts, streamed
75
+ tokens and reasoning, tool calls, the TTS queue (struck through when you interrupt), background tasks,
76
+ and a per-turn latency waterfall from the end of your speech to the first audio.
77
+
78
+ ## Four model roles, four endpoints
79
+
80
+ | Role | What it does | OpenAI-compatible API | If left blank |
81
+ |---|---|---|---|
82
+ | `ASR` | speech → text | `POST /v1/audio/transcriptions` | Parakeet TDT 0.6B v3, in-process |
83
+ | `TTS` | text → speech | `POST /v1/audio/speech` | Kokoro-82M, in-process |
84
+ | `FOREGROUND` | holds the conversation, calls quick tools, hands off; also judges end-of-turn | `POST /v1/chat/completions` | required |
85
+ | `BACKGROUND` | works on handed-off tasks with thinking on; can browse | `POST /v1/chat/completions` | the foreground's server and model |
86
+
87
+ Each role takes `<ROLE>_BASE_URL`, `<ROLE>_API_KEY` and `<ROLE>_MODEL`, so you can mix them freely. For
88
+ example, run ASR on a GPU box, TTS on your laptop, the foreground on a small fast local model, and the
89
+ background on a large model in the cloud. A key is only ever sent to the host it was set for.
90
+
91
+ Voice activity detection is the one model that is not a role. It runs on every 32 ms of microphone audio,
92
+ so a network round trip would break endpointing, and there is no OpenAI API for it. Silero VAD runs
93
+ in-process on onnxruntime, on every platform, with no torch.
94
+
95
+ ## Quickstart
96
+
97
+ You need:
98
+
99
+ - Python 3.11–3.13 and [uv](https://docs.astral.sh/uv/)
100
+ - Chrome or Safari
101
+ - an OpenAI-compatible chat server that streams and supports tool calls, such as llama.cpp's
102
+ `llama-server --jinja`, vLLM, LM Studio, Ollama, or a hosted API
103
+
104
+ Headphones make barge-in reliable.
105
+
106
+ **All speech local (Mac or Linux).** In-process speech is MLX on Apple Silicon and ONNX everywhere else,
107
+ picked automatically. Models download on first use.
108
+
109
+ ```bash
110
+ uv tool install git+https://github.com/IMJONEZZ/interaction-harness
111
+ interaction-harness doctor --foreground-base-url http://127.0.0.1:8080/v1 # checks every role
112
+ interaction-harness serve --foreground-base-url http://127.0.0.1:8080/v1 --open
113
+ ```
114
+
115
+ **On a remote GPU box.** Browsers only allow the microphone on `localhost` or HTTPS. Either tunnel the
116
+ port (`ssh -L 8765:localhost:8765 gpu-box`) and open http://localhost:8765, or pass
117
+ `--ssl-certfile/--ssl-keyfile`.
118
+
119
+ **Every role on an endpoint.**
120
+
121
+ ```bash
122
+ export FOREGROUND_BASE_URL=http://127.0.0.1:8080/v1 # fast model, thinking off
123
+ export BACKGROUND_BASE_URL=http://gpu-box:8000/v1 BACKGROUND_MODEL=your/reasoning-model
124
+ export ASR_BASE_URL=http://speech-box:8000/v1 ASR_MODEL=your/transcription-model
125
+ export TTS_BASE_URL=http://speech-box:8000/v1 TTS_MODEL=your/speech-model TTS_VOICE=af_heart
126
+ interaction-harness serve --open
127
+ ```
128
+
129
+ Settings come from flags, then the environment, then `.env` in the working directory, then
130
+ `interaction-harness.toml`. [`.env.example`](https://github.com/IMJONEZZ/interaction-harness/blob/main/.env.example) lists every setting, and
131
+ [`examples/interaction-harness.toml`](https://github.com/IMJONEZZ/interaction-harness/blob/main/examples/interaction-harness.toml) shows a config file.
132
+
133
+ ### Try this
134
+
135
+ 1. Start the mic and say *"What time is it?"* Watch the VAD bar, the partial transcript, the judge's
136
+ `COMPLETE`, the `get_time` tool card, the first TTS chunk, and the waterfall row.
137
+ 2. Say *"I was wondering whether…"* and pause. The judge answers `INCOMPLETE` and the floor stays with you.
138
+ 3. Ask for a long story, then talk over it. The reply stops, the sentences you didn't hear are struck
139
+ through, and the Brain panel marks the message ✂.
140
+ 4. Say *"In the background, compare a 15-year and a 30-year mortgage on 400k and tell me which costs less
141
+ in interest. Also remind me in 20 seconds to stretch."* Keep chatting. The Background panel streams the
142
+ background model's reasoning, and when it's done and you go quiet, the agent brings it up on its own.
143
+ 5. Switch to **VAD timeout** and repeat step 1, then compare the endpointing segment in the waterfall.
144
+
145
+ ## Commands
146
+
147
+ | Command | What it does |
148
+ |---|---|
149
+ | `interaction-harness serve` | the dashboard and voice server (default http://localhost:8765) |
150
+ | `interaction-harness doctor` | checks each role on its own: streaming, tool calls, the end-of-turn judge, a TTS → ASR round trip, VAD; reports latency |
151
+ | `interaction-harness selftest` | talks to the agent with synthesized speech and checks endpointing, tools, barge-in, background delivery, timers, push-to-talk and typed turns |
152
+ | `interaction-harness warmup` | downloads and loads the in-process speech models, then exits |
153
+
154
+ They all take the same settings flags (`--foreground-base-url`, `--asr-model`, `--backend onnx`, …).
155
+ `interaction-harness serve --help` lists them.
156
+
157
+ ## Your own tools
158
+
159
+ ```python
160
+ # my_tools.py, loaded with: interaction-harness serve --tools my_tools.py
161
+ from typing import Annotated
162
+ from interaction_harness.tools import tool
163
+
164
+ @tool
165
+ def stock_level(sku: Annotated[str, "the product code"]) -> dict:
166
+ """How many of a product are in stock."""
167
+ return {"sku": sku, "in_stock": 12}
168
+
169
+ @tool(lanes=["background"]) # only the background model may call this one
170
+ async def search_tickets(query: str, limit: int = 5) -> list[dict]:
171
+ """Search the support ticket system."""
172
+ ...
173
+ ```
174
+
175
+ The JSON schema comes from the signature and the description from the docstring. See
176
+ [docs/tools.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/tools.md).
177
+
178
+ ## Measured latency
179
+
180
+ Latency here is the time from the end of your speech to the first audio, as the waterfall measures it.
181
+ That includes 300 ms of silence before the judge is asked, then the judge, ASR, the foreground model's
182
+ first sentence, and the first TTS chunk.
183
+
184
+ The numbers below are the median of 10 short spoken turns with no tools, in semantic mode. They were
185
+ measured on an M5 Max MacBook Pro, plugged in and otherwise idle, with Qwen3.6-35B-A3B (UD-Q8_K_XL,
186
+ llama.cpp via Unsloth Studio) as the foreground model.
187
+
188
+ | Speech | Median | Range |
189
+ |---|---|---|
190
+ | In-process MLX | 908 ms | 741–1,780 ms |
191
+ | In-process ONNX on CPU (int8) | 909 ms | 795–1,571 ms |
192
+ | ASR + TTS on an endpoint (mlx-audio server, same machine) | 787 ms | 734–1,060 ms |
193
+
194
+ On a machine this fast the three are within noise of each other. About 500 ms of a typical turn is deciding
195
+ that you've finished: the 300 ms of silence, then the judge. That is the place to tune first. Turns that
196
+ call a tool add a model round trip; the waterfall shows each segment.
197
+
198
+ ## Tested with
199
+
200
+ | Role | Server or backend | Status |
201
+ |---|---|---|
202
+ | foreground, background | llama.cpp (Unsloth Studio), Qwen3.6-35B-A3B | tested: `doctor`, `selftest` |
203
+ | background | vLLM, GLM-5.3-Flash (FP4) on a DGX Spark | tested: `doctor`, `selftest` |
204
+ | ASR, TTS | mlx-audio 0.6.4 server (Parakeet, Kokoro) | tested: `doctor`, `selftest` |
205
+ | in-process speech | MLX on Apple Silicon (macOS) | tested: `doctor`, `selftest` |
206
+ | in-process speech | ONNX on CPU, macOS arm64 | tested: `doctor`, `selftest` |
207
+ | in-process speech | ONNX on CPU, Linux x86_64 | tested in CI: `warmup` round trip |
208
+ | in-process speech | ONNX on CPU, Linux arm64 (DGX Spark) | not yet tested |
209
+ | foreground, background | Ollama, LM Studio | built on llama.cpp, the engine tested above |
210
+ | any role | OpenAI, Speaches, Kokoro-FastAPI, other servers | untested; should work if they implement the endpoint. Run `doctor`, and please [report what you find](https://github.com/IMJONEZZ/interaction-harness/issues/new?template=server-report.yml) |
211
+
212
+ How a server switches thinking on and off varies. Set `FOREGROUND_THINKING` / `BACKGROUND_THINKING` to
213
+ `chat_template_kwargs` (vLLM, llama.cpp, SGLang with Qwen- or GLM-style templates), `reasoning_effort`, or
214
+ `off`. See [docs/endpoints.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/endpoints.md).
215
+
216
+ ## Where this fits
217
+
218
+ [Pipecat](https://github.com/pipecat-ai/pipecat) and [LiveKit Agents](https://github.com/livekit/agents)
219
+ are production frameworks for shipping voice agents. This is a harness for *studying and tuning* the
220
+ interaction model:
221
+
222
+ - the whole turn-taking policy is one readable state machine
223
+ ([`session.py`](https://github.com/IMJONEZZ/interaction-harness/blob/main/src/interaction_harness/session.py))
224
+ - every decision streams to the dashboard
225
+ - every model is swappable through the same OpenAI API you already run
226
+
227
+ Use it to choose an end-of-turn policy, measure what barge-in or a background model costs, or try a new
228
+ model in a real conversation before you build it into something bigger.
229
+
230
+ [docs/interaction-models.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/interaction-models.md) covers the background: what "interaction model"
231
+ means, the turn-taking regimes in use today, and the latency budgets people cite.
232
+
233
+ ### Where it stops short
234
+
235
+ This is a cascade, and a cascade has limits that a native full-duplex model doesn't:
236
+
237
+ - no backchannel while you talk
238
+ - no overlapping speech
239
+ - no reaction to *how* you said something, because ASR drops prosody
240
+ - the end-of-turn judge only sees text
241
+
242
+ In exchange you get a swappable brain, tools, auditability, and a latency budget you can see.
243
+
244
+ ## Docs
245
+
246
+ - [docs/endpoints.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/endpoints.md): the four roles, keys, thinking switches, and server notes
247
+ - [docs/backends.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/backends.md): in-process speech on MLX and ONNX, model variants, GPUs
248
+ - [docs/protocol.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/protocol.md): the WebSocket protocol, if you want your own front end
249
+ - [docs/tools.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/tools.md): writing tools, and how the background hand-off and timers work
250
+ - [docs/interaction-models.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/docs/interaction-models.md): background reading
251
+ - [CONTRIBUTING.md](https://github.com/IMJONEZZ/interaction-harness/blob/main/CONTRIBUTING.md)
252
+
253
+ ## Credits
254
+
255
+ | Component | By | License |
256
+ |---|---|---|
257
+ | [Parakeet TDT 0.6B v3](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) | NVIDIA | CC-BY-4.0 |
258
+ | [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) | hexgrad | Apache-2.0 |
259
+ | [Silero VAD](https://github.com/snakers4/silero-vad) (its ONNX model ships in this package) | Silero Team | MIT |
260
+ | [parakeet-mlx](https://github.com/senstella/parakeet-mlx) | senstella | Apache-2.0 |
261
+ | [mlx-audio](https://github.com/Blaizzy/mlx-audio) | Blaizzy | MIT |
262
+ | [onnx-asr](https://github.com/istupakov/onnx-asr) | istupakov | MIT |
263
+ | [kokoro-onnx](https://github.com/thewh1teagle/kokoro-onnx) | thewh1teagle | MIT |
264
+
265
+ The web tools follow the design of Unsloth Studio's, re-implemented from scratch.
266
+
267
+ ## License
268
+
269
+ MIT. See [LICENSE](https://github.com/IMJONEZZ/interaction-harness/blob/main/LICENSE).