voice-vibecoder 2.26.0__tar.gz → 2.26.1__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.
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/CHANGELOG.md +22 -0
- voice_vibecoder-2.26.1/CLAUDE.md +62 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/PKG-INFO +25 -6
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/README.md +24 -5
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/pyproject.toml +1 -1
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/config.py +3 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/session.py +8 -2
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/agent_task.py +16 -24
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/dispatch.py +85 -28
- voice_vibecoder-2.26.1/tests/test_openai_events.py +109 -0
- voice_vibecoder-2.26.0/index.html +0 -30
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/.gitignore +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/LICENSE +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/app.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/audio_macos.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/worktrees.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/tests/test_audio_gating.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/tests/test_file_tree.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/tests/test_merge_branch.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/tests/test_session_interrupts.py +0 -0
- {voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/tests/test_startup_context.py +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.26.1 (2026-07-22)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Per-connection dispatch state via ContextVars
|
|
10
|
+
([#7](https://github.com/snokam/voice-vibecoder/pull/7),
|
|
11
|
+
[`1d28e3b`](https://github.com/snokam/voice-vibecoder/commit/1d28e3b045a8d0f7623fb0b0d6faa46aedd09357))
|
|
12
|
+
|
|
13
|
+
### Chores
|
|
14
|
+
|
|
15
|
+
- Remove leftover hello-world test artifact
|
|
16
|
+
([`1f6f47c`](https://github.com/snokam/voice-vibecoder/commit/1f6f47c98ac203203e8acdce3047a9510072f3fb))
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- Add CLAUDE.md so agents on this repo have context
|
|
21
|
+
([`c7267cc`](https://github.com/snokam/voice-vibecoder/commit/c7267ccf76fabc2579f53264047ef9de504e8897))
|
|
22
|
+
|
|
23
|
+
- Sell the real product and delegate non-merge git to agents
|
|
24
|
+
([`a83ceb5`](https://github.com/snokam/voice-vibecoder/commit/a83ceb536bdd55e7c2199cd9af4f9571f70ffd07))
|
|
25
|
+
|
|
26
|
+
|
|
5
27
|
## v2.26.0 (2026-07-13)
|
|
6
28
|
|
|
7
29
|
### Features
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# voice-vibecoder (Olaf The Vibecoder)
|
|
2
|
+
|
|
3
|
+
Voice-controlled coding assistant: OpenAI/Azure Realtime API for conversation,
|
|
4
|
+
Claude Code agents in git worktrees for the actual coding. The user talks;
|
|
5
|
+
Olaf converses and delegates invisibly.
|
|
6
|
+
|
|
7
|
+
## Product principles
|
|
8
|
+
|
|
9
|
+
- The conversation must feel human: short answers, no self-interruption,
|
|
10
|
+
barge-in works, follow the user's language, resume where you left off.
|
|
11
|
+
- The voice bot stays thin: conversation + delegation. Only `merge_branch`
|
|
12
|
+
touches git natively (speed + protecting main); all other git operations
|
|
13
|
+
are delegated to the coding agent in plain words.
|
|
14
|
+
- Agents work invisibly. Olaf never says "I'll send this to an agent."
|
|
15
|
+
|
|
16
|
+
## Architecture map
|
|
17
|
+
|
|
18
|
+
- `session.py` — conversation-floor state machine. A response is active from
|
|
19
|
+
ResponseCreated to ResponseDone; background injections (agent results,
|
|
20
|
+
narration) queue until the floor is free. Cancel on SpeechStarted only when
|
|
21
|
+
a response is in flight.
|
|
22
|
+
- `audio.py` — sounddevice playback + fallback mic gating (echo gate).
|
|
23
|
+
- `audio_macos.py` — echo-cancelled capture via AVAudioEngine Voice
|
|
24
|
+
Processing I/O (system AEC). Mic stays open; server VAD handles barge-in.
|
|
25
|
+
PyObjC gotcha: `varlist.as_buffer(n)` takes an element count, not bytes.
|
|
26
|
+
- `voice_providers/` — provider protocol; `openai.py` speaks both GA
|
|
27
|
+
(`response.output_audio.*`, nested session schema) and preview wire formats.
|
|
28
|
+
Azure GA: transcription must be `whisper-1` (`gpt-4o-mini-transcribe`
|
|
29
|
+
fails server-side); pass `language` for accuracy.
|
|
30
|
+
- `tools/` — the model's tool surface (definitions.py + dispatch.py +
|
|
31
|
+
handlers.py). New tools need all three plus `_SLOW_TOOLS` in session.py if
|
|
32
|
+
they do subprocess I/O.
|
|
33
|
+
- `worktrees.py` — worktree lifecycle; always prune stale registrations
|
|
34
|
+
(deleted directories stay registered in git as "prunable").
|
|
35
|
+
- `ui/` — Textual TUI. `screen.py` orchestrates; panels/callbacks are mixins.
|
|
36
|
+
- `server.py` — websocket bridge for remote clients (sync_state protocol).
|
|
37
|
+
|
|
38
|
+
## Testing
|
|
39
|
+
|
|
40
|
+
- `uv run --with pytest pytest tests/` — must pass before any push.
|
|
41
|
+
- Live-endpoint testing: synthesize speech with macOS `say` + `afconvert`
|
|
42
|
+
to 24kHz PCM16, stream over the websocket using `RealtimeSettings.load()`
|
|
43
|
+
credentials. Verify conversation behavior against the real API, not just
|
|
44
|
+
unit tests.
|
|
45
|
+
|
|
46
|
+
## Releases
|
|
47
|
+
|
|
48
|
+
- Conventional commits pushed to main ARE the release mechanism:
|
|
49
|
+
python-semantic-release cuts the version, updates CHANGELOG, publishes to
|
|
50
|
+
PyPI on every push (feat → minor, fix/perf → patch).
|
|
51
|
+
- Never hand-edit VERSION or CHANGELOG.
|
|
52
|
+
- Downstream consumer: `snokam/monorepo` `olaf/bridge` pins this package —
|
|
53
|
+
bump it after notable releases.
|
|
54
|
+
|
|
55
|
+
## Gotchas
|
|
56
|
+
|
|
57
|
+
- `app.py` is a PEP 723 script with `editable = true` — required, or uv
|
|
58
|
+
serves stale cached builds of the package.
|
|
59
|
+
- `uv tool` installs live in a separate environment from the checkout; the
|
|
60
|
+
in-app updater detects install kind via `direct_url.json`.
|
|
61
|
+
- CI runs import/syntax/build only (no pytest) across Python 3.10–3.13; keep
|
|
62
|
+
code 3.10-compatible, and mark macOS-only deps with `sys_platform == 'darwin'`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.26.
|
|
3
|
+
Version: 2.26.1
|
|
4
4
|
Summary: Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code
|
|
5
5
|
Project-URL: Homepage, https://github.com/snokam/voice-vibecoder
|
|
6
6
|
Project-URL: Repository, https://github.com/snokam/voice-vibecoder
|
|
@@ -33,7 +33,18 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
# Olaf The Vibecoder
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Talk to your codebase like a colleague. You speak an idea; Olaf holds a natural
|
|
37
|
+
voice conversation while coding agents (Claude Code and friends) quietly build
|
|
38
|
+
it on separate git branches. Ask to see the diff, say "merge it", and it lands
|
|
39
|
+
on main — the whole loop, hands-free.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
you: "lag en fil med et dikt i repoet"
|
|
43
|
+
olaf: "Klart, jeg setter i gang!" → agent builds on branch 'dikt'
|
|
44
|
+
you: "vis diff" → colorized diff in the TUI
|
|
45
|
+
you: "merge det inn i main"
|
|
46
|
+
olaf: "Ferdig! Dikt er merget inn i main."
|
|
47
|
+
```
|
|
37
48
|
|
|
38
49
|
## Quick Start
|
|
39
50
|
|
|
@@ -47,12 +58,20 @@ On first launch, a setup wizard will guide you through connecting your OpenAI or
|
|
|
47
58
|
|
|
48
59
|
## Features
|
|
49
60
|
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
- **Natural conversation** — full-duplex voice: interrupt Olaf mid-sentence and
|
|
62
|
+
it stops and listens (system echo cancellation on macOS, adaptive echo gate
|
|
63
|
+
elsewhere). Short spoken answers, follows your language, resumes previous
|
|
64
|
+
conversations where they left off
|
|
65
|
+
- **Invisible agent orchestration** — multi-instance Claude Code agents in
|
|
66
|
+
separate git worktrees; Olaf narrates progress and delivers results
|
|
67
|
+
- **Voice-driven git** — show diffs, merge branches into main, clean up
|
|
68
|
+
worktrees, all by voice; conflicts are reported, never half-merged
|
|
69
|
+
- Live file tree per agent (including files the agent just created) and
|
|
70
|
+
GitHub-style diff view
|
|
53
71
|
- Session persistence across restarts
|
|
54
72
|
- Push-to-talk and voice activity detection modes
|
|
55
|
-
-
|
|
73
|
+
- OpenAI and Azure OpenAI realtime (GA voices marin/cedar), Gemini Live
|
|
74
|
+
- Multi-language (English, Norwegian, Swedish) — spoken and transcribed
|
|
56
75
|
|
|
57
76
|
## Requirements
|
|
58
77
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Olaf The Vibecoder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Talk to your codebase like a colleague. You speak an idea; Olaf holds a natural
|
|
4
|
+
voice conversation while coding agents (Claude Code and friends) quietly build
|
|
5
|
+
it on separate git branches. Ask to see the diff, say "merge it", and it lands
|
|
6
|
+
on main — the whole loop, hands-free.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
you: "lag en fil med et dikt i repoet"
|
|
10
|
+
olaf: "Klart, jeg setter i gang!" → agent builds on branch 'dikt'
|
|
11
|
+
you: "vis diff" → colorized diff in the TUI
|
|
12
|
+
you: "merge det inn i main"
|
|
13
|
+
olaf: "Ferdig! Dikt er merget inn i main."
|
|
14
|
+
```
|
|
4
15
|
|
|
5
16
|
## Quick Start
|
|
6
17
|
|
|
@@ -14,12 +25,20 @@ On first launch, a setup wizard will guide you through connecting your OpenAI or
|
|
|
14
25
|
|
|
15
26
|
## Features
|
|
16
27
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
- **Natural conversation** — full-duplex voice: interrupt Olaf mid-sentence and
|
|
29
|
+
it stops and listens (system echo cancellation on macOS, adaptive echo gate
|
|
30
|
+
elsewhere). Short spoken answers, follows your language, resumes previous
|
|
31
|
+
conversations where they left off
|
|
32
|
+
- **Invisible agent orchestration** — multi-instance Claude Code agents in
|
|
33
|
+
separate git worktrees; Olaf narrates progress and delivers results
|
|
34
|
+
- **Voice-driven git** — show diffs, merge branches into main, clean up
|
|
35
|
+
worktrees, all by voice; conflicts are reported, never half-merged
|
|
36
|
+
- Live file tree per agent (including files the agent just created) and
|
|
37
|
+
GitHub-style diff view
|
|
20
38
|
- Session persistence across restarts
|
|
21
39
|
- Push-to-talk and voice activity detection modes
|
|
22
|
-
-
|
|
40
|
+
- OpenAI and Azure OpenAI realtime (GA voices marin/cedar), Gemini Live
|
|
41
|
+
- Multi-language (English, Norwegian, Swedish) — spoken and transcribed
|
|
23
42
|
|
|
24
43
|
## Requirements
|
|
25
44
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voice-vibecoder"
|
|
7
|
-
version = "2.26.
|
|
7
|
+
version = "2.26.1"
|
|
8
8
|
description = "Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -80,6 +80,7 @@ Avbryt/reset:
|
|
|
80
80
|
Merge:
|
|
81
81
|
- "merge det", "ta det inn i main" → merge_branch. Ved konflikter: foreslå å be \
|
|
82
82
|
agenten merge main inn i sin branch først.
|
|
83
|
+
- Andre git-operasjoner (push, rebase, revert) → send_to_agent med brukerens ord.
|
|
83
84
|
|
|
84
85
|
Visning:
|
|
85
86
|
- "vis diff" → show_diff
|
|
@@ -139,6 +140,7 @@ Cancel/reset:
|
|
|
139
140
|
Merge:
|
|
140
141
|
- "merge it", "bring it into main" → merge_branch. On conflicts: suggest asking \
|
|
141
142
|
the agent to merge main into its branch first.
|
|
143
|
+
- Other git operations (push, rebase, revert) → send_to_agent with the user's words.
|
|
142
144
|
|
|
143
145
|
Views:
|
|
144
146
|
- "show diff" → show_diff
|
|
@@ -198,6 +200,7 @@ Avbryt/reset:
|
|
|
198
200
|
Merge:
|
|
199
201
|
- "merga det", "ta in det i main" → merge_branch. Vid konflikter: föreslå att \
|
|
200
202
|
agenten mergar main in i sin branch först.
|
|
203
|
+
- Övriga git-operationer (push, rebase, revert) → send_to_agent med användarens ord.
|
|
201
204
|
|
|
202
205
|
Visning:
|
|
203
206
|
- "visa diff" → show_diff
|
|
@@ -12,6 +12,7 @@ via the voice provider so ChatGPT can summarize it to the user.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import contextvars
|
|
15
16
|
import logging
|
|
16
17
|
import time
|
|
17
18
|
from pathlib import Path
|
|
@@ -622,9 +623,14 @@ class RealtimeSession:
|
|
|
622
623
|
# External tools are always async — pass user JWT for OBO flows
|
|
623
624
|
result = await handle_tool_call_async(name, arguments, self._user_jwt)
|
|
624
625
|
elif name in self._SLOW_TOOLS:
|
|
625
|
-
# Run slow I/O tools in executor to avoid blocking
|
|
626
|
+
# Run slow I/O tools in executor to avoid blocking. Copy the
|
|
627
|
+
# context so the worker thread sees this connection's dispatch
|
|
628
|
+
# state (ContextVars don't cross into executor threads).
|
|
626
629
|
loop = asyncio.get_event_loop()
|
|
627
|
-
|
|
630
|
+
ctx = contextvars.copy_context()
|
|
631
|
+
result = await loop.run_in_executor(
|
|
632
|
+
None, lambda: ctx.run(handle_tool_call, name, arguments)
|
|
633
|
+
)
|
|
628
634
|
else:
|
|
629
635
|
# Sync tools can run directly
|
|
630
636
|
result = handle_tool_call(name, arguments)
|
|
@@ -5,6 +5,7 @@ background thread, and relaying interactive questions back through voice.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
import contextvars
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
10
11
|
import threading
|
|
@@ -19,6 +20,17 @@ logger = logging.getLogger(__name__)
|
|
|
19
20
|
|
|
20
21
|
ANSWER_TIMEOUT = 300 # 5 min to answer a question
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
def _start_background(target, *args) -> None:
|
|
25
|
+
"""Run ``target`` in a daemon thread that inherits the current context.
|
|
26
|
+
|
|
27
|
+
Dispatch state (registry, repo root, enabled agents, ...) lives in
|
|
28
|
+
ContextVars, which do not propagate to bare threads — so copy the context
|
|
29
|
+
and run inside it, else the thread reads process defaults.
|
|
30
|
+
"""
|
|
31
|
+
ctx = contextvars.copy_context()
|
|
32
|
+
threading.Thread(target=lambda: ctx.run(target, *args), daemon=True).start()
|
|
33
|
+
|
|
22
34
|
_CLAUDE_JSON = Path.home() / ".claude.json"
|
|
23
35
|
|
|
24
36
|
# Cached MCP servers — loaded once per session, files rarely change
|
|
@@ -168,20 +180,10 @@ def _handle_send_to_agent(message: str, branch: str | None = None, agent: str |
|
|
|
168
180
|
is_helper = _repo_root and str(instance.worktree_path) == str(_repo_root)
|
|
169
181
|
|
|
170
182
|
if is_helper and _helper_override:
|
|
171
|
-
|
|
172
|
-
target=_run_helper_override_background,
|
|
173
|
-
args=(instance, message, registry),
|
|
174
|
-
daemon=True,
|
|
175
|
-
)
|
|
176
|
-
thread.start()
|
|
183
|
+
_start_background(_run_helper_override_background, instance, message, registry)
|
|
177
184
|
return "Sent to helper."
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
target=_run_agent_background,
|
|
181
|
-
args=(instance, message, registry),
|
|
182
|
-
daemon=True,
|
|
183
|
-
)
|
|
184
|
-
thread.start()
|
|
186
|
+
_start_background(_run_agent_background, instance, message, registry)
|
|
185
187
|
|
|
186
188
|
agent_label = get_agent(instance.agent_type).label
|
|
187
189
|
return f"Sent to {agent_label}."
|
|
@@ -249,12 +251,7 @@ def _handle_send_to_session(message: str, session_name: str | None = None, agent
|
|
|
249
251
|
|
|
250
252
|
# Route through helper override if registered (backward compat)
|
|
251
253
|
if _helper_override:
|
|
252
|
-
|
|
253
|
-
target=_run_helper_override_background,
|
|
254
|
-
args=(instance, message, registry),
|
|
255
|
-
daemon=True,
|
|
256
|
-
)
|
|
257
|
-
thread.start()
|
|
254
|
+
_start_background(_run_helper_override_background, instance, message, registry)
|
|
258
255
|
result = f"Sent to session '{instance.display_name}'."
|
|
259
256
|
if is_new_session:
|
|
260
257
|
all_sessions = registry.get_all_sessions()
|
|
@@ -263,12 +260,7 @@ def _handle_send_to_session(message: str, session_name: str | None = None, agent
|
|
|
263
260
|
result += f" (Active sessions: {', '.join(names)})"
|
|
264
261
|
return result
|
|
265
262
|
|
|
266
|
-
|
|
267
|
-
target=_run_agent_background,
|
|
268
|
-
args=(instance, message, registry),
|
|
269
|
-
daemon=True,
|
|
270
|
-
)
|
|
271
|
-
thread.start()
|
|
263
|
+
_start_background(_run_agent_background, instance, message, registry)
|
|
272
264
|
|
|
273
265
|
result = f"Sent to session '{instance.display_name}'."
|
|
274
266
|
if is_new_session:
|
|
@@ -11,6 +11,7 @@ tools are automatically included in the tool definitions sent to OpenAI and
|
|
|
11
11
|
dispatched through ``handle_tool_call_async``.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import contextvars
|
|
14
15
|
import logging
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any, Callable, Awaitable
|
|
@@ -21,12 +22,52 @@ from voice_vibecoder.instances import AgentInstance, InstanceRegistry, InstanceT
|
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# Per-connection dispatch state. Held in ContextVars so concurrent realtime
|
|
26
|
+
# sessions in one process each configure and read their own registry/config
|
|
27
|
+
# instead of clobbering shared module globals. The names below are still
|
|
28
|
+
# exposed as module attributes via ``__getattr__`` so callers may keep doing
|
|
29
|
+
# ``from voice_vibecoder.tools.dispatch import _registry`` inside a function.
|
|
30
|
+
_registry_var: contextvars.ContextVar[InstanceRegistry | None] = contextvars.ContextVar(
|
|
31
|
+
"vv_registry", default=None
|
|
32
|
+
)
|
|
33
|
+
_repo_root_var: contextvars.ContextVar[Path | None] = contextvars.ContextVar(
|
|
34
|
+
"vv_repo_root", default=None
|
|
35
|
+
)
|
|
36
|
+
_agent_timeout_var: contextvars.ContextVar[int] = contextvars.ContextVar(
|
|
37
|
+
"vv_agent_timeout", default=300
|
|
38
|
+
)
|
|
39
|
+
_permission_mode_var: contextvars.ContextVar[PermissionMode] = contextvars.ContextVar(
|
|
40
|
+
"vv_permission_mode", default="bypass"
|
|
41
|
+
)
|
|
42
|
+
_enabled_agents_var: contextvars.ContextVar[list[str] | None] = contextvars.ContextVar(
|
|
43
|
+
"vv_enabled_agents", default=None
|
|
44
|
+
)
|
|
45
|
+
_voice_config_var: contextvars.ContextVar[VoiceConfig | None] = contextvars.ContextVar(
|
|
46
|
+
"vv_voice_config", default=None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_SCOPED_VARS: dict[str, contextvars.ContextVar] = {
|
|
50
|
+
"_registry": _registry_var,
|
|
51
|
+
"_repo_root": _repo_root_var,
|
|
52
|
+
"_agent_timeout": _agent_timeout_var,
|
|
53
|
+
"_permission_mode": _permission_mode_var,
|
|
54
|
+
"_enabled_agents": _enabled_agents_var,
|
|
55
|
+
"_voice_config": _voice_config_var,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str) -> Any:
|
|
60
|
+
var = _SCOPED_VARS.get(name)
|
|
61
|
+
if var is None:
|
|
62
|
+
raise AttributeError(name)
|
|
63
|
+
value = var.get()
|
|
64
|
+
if name == "_enabled_agents" and not value:
|
|
65
|
+
return ["claude"]
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _enabled_agents_or_default() -> list[str]:
|
|
70
|
+
return _enabled_agents_var.get() or ["claude"]
|
|
30
71
|
|
|
31
72
|
# ── External tool registry ───────────────────────────────────────────────────
|
|
32
73
|
# Maps tool name → async handler(name, arguments, user_jwt) -> str
|
|
@@ -87,21 +128,33 @@ def configure(
|
|
|
87
128
|
user_jwt: str | None = None,
|
|
88
129
|
voice_config: VoiceConfig | None = None,
|
|
89
130
|
) -> None:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
_registry_var.set(registry)
|
|
132
|
+
_repo_root_var.set(repo_root)
|
|
133
|
+
_agent_timeout_var.set(agent_timeout)
|
|
134
|
+
_permission_mode_var.set(permission_mode)
|
|
135
|
+
_enabled_agents_var.set(enabled_agents or ["claude"])
|
|
136
|
+
_voice_config_var.set(voice_config)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def set_repo_root(repo_root: Path) -> None:
|
|
140
|
+
"""Point the current context's dispatch at another repo (per-connection)."""
|
|
141
|
+
_repo_root_var.set(repo_root)
|
|
142
|
+
registry = _registry_var.get()
|
|
143
|
+
if registry is not None:
|
|
144
|
+
registry.repo_root = str(repo_root)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def current_repo_root() -> Path | None:
|
|
148
|
+
return _repo_root_var.get()
|
|
97
149
|
|
|
98
150
|
|
|
99
151
|
def _default_agent_type() -> str:
|
|
100
152
|
"""Return the default agent type from config, or first enabled agent."""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
voice_config = _voice_config_var.get()
|
|
154
|
+
if voice_config and voice_config.default_agent:
|
|
155
|
+
logger.info(f"Using default_agent from config: {voice_config.default_agent}")
|
|
156
|
+
return voice_config.default_agent
|
|
157
|
+
result = _enabled_agents_or_default()[0]
|
|
105
158
|
logger.info(f"Using fallback agent: {result}")
|
|
106
159
|
return result
|
|
107
160
|
|
|
@@ -120,28 +173,30 @@ def _fuzzy_match_branch(query: str, branches: list[str]) -> str | None:
|
|
|
120
173
|
|
|
121
174
|
|
|
122
175
|
def _resolve_instance(branch: str | None = None, session_name: str | None = None) -> AgentInstance | None:
|
|
123
|
-
|
|
176
|
+
registry = _registry_var.get()
|
|
177
|
+
if not registry:
|
|
124
178
|
return None
|
|
125
179
|
if session_name:
|
|
126
180
|
return _resolve_session(session_name)
|
|
127
181
|
if branch:
|
|
128
|
-
inst =
|
|
182
|
+
inst = registry.get_by_branch(branch)
|
|
129
183
|
if inst:
|
|
130
184
|
return inst
|
|
131
185
|
# Fuzzy match against registered branches
|
|
132
|
-
all_branches = [i.branch for i in
|
|
186
|
+
all_branches = [i.branch for i in registry.get_all()]
|
|
133
187
|
matched = _fuzzy_match_branch(branch, all_branches)
|
|
134
188
|
if matched:
|
|
135
|
-
return
|
|
189
|
+
return registry.get_by_branch(matched)
|
|
136
190
|
return None
|
|
137
|
-
return
|
|
191
|
+
return registry.get_default()
|
|
138
192
|
|
|
139
193
|
|
|
140
194
|
def _resolve_session(session_name: str) -> AgentInstance | None:
|
|
141
195
|
"""Resolve a session instance by display_name (fuzzy match)."""
|
|
142
|
-
|
|
196
|
+
registry = _registry_var.get()
|
|
197
|
+
if not registry:
|
|
143
198
|
return None
|
|
144
|
-
return
|
|
199
|
+
return registry.get_session_by_name(session_name)
|
|
145
200
|
|
|
146
201
|
|
|
147
202
|
def cancel_instance_task(instance: AgentInstance) -> bool:
|
|
@@ -154,19 +209,21 @@ def cancel_instance_task(instance: AgentInstance) -> bool:
|
|
|
154
209
|
|
|
155
210
|
|
|
156
211
|
def cancel_all_tasks() -> int:
|
|
157
|
-
|
|
212
|
+
registry = _registry_var.get()
|
|
213
|
+
if not registry:
|
|
158
214
|
return 0
|
|
159
215
|
count = 0
|
|
160
|
-
for inst in
|
|
216
|
+
for inst in registry.get_all():
|
|
161
217
|
if cancel_instance_task(inst):
|
|
162
218
|
count += 1
|
|
163
219
|
return count
|
|
164
220
|
|
|
165
221
|
|
|
166
222
|
def cancel_active_task() -> bool:
|
|
167
|
-
|
|
223
|
+
registry = _registry_var.get()
|
|
224
|
+
if not registry:
|
|
168
225
|
return False
|
|
169
|
-
inst =
|
|
226
|
+
inst = registry.get_default()
|
|
170
227
|
return cancel_instance_task(inst) if inst else False
|
|
171
228
|
|
|
172
229
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Event mapping for the OpenAI/Azure provider — GA and preview wire formats."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from voice_vibecoder.voice_providers import (
|
|
6
|
+
AudioCommitted,
|
|
7
|
+
AudioDelta,
|
|
8
|
+
FunctionCall,
|
|
9
|
+
ResponseCreated,
|
|
10
|
+
ResponseDone,
|
|
11
|
+
SpeechStarted,
|
|
12
|
+
SpeechStopped,
|
|
13
|
+
TranscriptDone,
|
|
14
|
+
UserTranscript,
|
|
15
|
+
VoiceError,
|
|
16
|
+
)
|
|
17
|
+
from voice_vibecoder.voice_providers.openai import OpenAIVoiceProvider
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def provider():
|
|
22
|
+
return OpenAIVoiceProvider(ws_url="wss://x", ws_headers={})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize("msg_type", ["response.output_audio.delta", "response.audio.delta"])
|
|
26
|
+
def test_audio_delta_ga_and_preview(provider, msg_type):
|
|
27
|
+
ev = provider._map_event({"type": msg_type, "delta": "QUJD"})
|
|
28
|
+
assert isinstance(ev, AudioDelta)
|
|
29
|
+
assert ev.delta == "QUJD"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("msg_type", [
|
|
33
|
+
"response.output_audio_transcript.done", "response.audio_transcript.done",
|
|
34
|
+
])
|
|
35
|
+
def test_transcript_done_ga_and_preview(provider, msg_type):
|
|
36
|
+
ev = provider._map_event({"type": msg_type, "transcript": "hei"})
|
|
37
|
+
assert isinstance(ev, TranscriptDone)
|
|
38
|
+
assert ev.transcript == "hei"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_lifecycle_events(provider):
|
|
42
|
+
assert isinstance(provider._map_event({"type": "input_audio_buffer.speech_started"}), SpeechStarted)
|
|
43
|
+
assert isinstance(provider._map_event({"type": "input_audio_buffer.speech_stopped"}), SpeechStopped)
|
|
44
|
+
assert isinstance(provider._map_event({"type": "input_audio_buffer.committed"}), AudioCommitted)
|
|
45
|
+
assert isinstance(provider._map_event({"type": "response.created"}), ResponseCreated)
|
|
46
|
+
assert isinstance(provider._map_event({"type": "response.done"}), ResponseDone)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_user_transcript_stripped(provider):
|
|
50
|
+
ev = provider._map_event({
|
|
51
|
+
"type": "conversation.item.input_audio_transcription.completed",
|
|
52
|
+
"transcript": " hei du \n",
|
|
53
|
+
})
|
|
54
|
+
assert isinstance(ev, UserTranscript)
|
|
55
|
+
assert ev.transcript == "hei du"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_function_call_parses_arguments(provider):
|
|
59
|
+
ev = provider._map_event({
|
|
60
|
+
"type": "response.function_call_arguments.done",
|
|
61
|
+
"call_id": "c1",
|
|
62
|
+
"name": "merge_branch",
|
|
63
|
+
"arguments": '{"branch": "dikt"}',
|
|
64
|
+
})
|
|
65
|
+
assert isinstance(ev, FunctionCall)
|
|
66
|
+
assert ev.name == "merge_branch"
|
|
67
|
+
assert ev.arguments == {"branch": "dikt"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_function_call_invalid_json_gives_empty_args(provider):
|
|
71
|
+
ev = provider._map_event({
|
|
72
|
+
"type": "response.function_call_arguments.done",
|
|
73
|
+
"call_id": "c1",
|
|
74
|
+
"name": "x",
|
|
75
|
+
"arguments": "{broken",
|
|
76
|
+
})
|
|
77
|
+
assert ev.arguments == {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cancel_errors_are_ignorable(provider):
|
|
81
|
+
ev = provider._map_event({
|
|
82
|
+
"type": "error",
|
|
83
|
+
"error": {"message": "Cannot cancel: no active response"},
|
|
84
|
+
})
|
|
85
|
+
assert isinstance(ev, VoiceError)
|
|
86
|
+
assert ev.is_ignorable
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_real_errors_are_not_ignorable(provider):
|
|
90
|
+
ev = provider._map_event({
|
|
91
|
+
"type": "error",
|
|
92
|
+
"error": {"message": "Invalid session configuration"},
|
|
93
|
+
})
|
|
94
|
+
assert isinstance(ev, VoiceError)
|
|
95
|
+
assert not ev.is_ignorable
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_unknown_events_are_dropped(provider):
|
|
99
|
+
assert provider._map_event({"type": "rate_limits.updated"}) is None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_ga_session_body_includes_transcription_language():
|
|
103
|
+
p = OpenAIVoiceProvider(
|
|
104
|
+
ws_url="wss://x", ws_headers={},
|
|
105
|
+
transcription_model="whisper-1", transcription_language="no", ga=True,
|
|
106
|
+
)
|
|
107
|
+
body = p._session_body("inst", "marin", [], None)
|
|
108
|
+
assert body["audio"]["input"]["transcription"] == {"model": "whisper-1", "language": "no"}
|
|
109
|
+
assert body["audio"]["output"]["voice"] == "marin"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Hello World</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
margin: 0;
|
|
10
|
-
display: flex;
|
|
11
|
-
justify-content: center;
|
|
12
|
-
align-items: center;
|
|
13
|
-
min-height: 100vh;
|
|
14
|
-
background-color: #f0f4f8;
|
|
15
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
h1 {
|
|
19
|
-
font-size: 3rem;
|
|
20
|
-
color: #2d3748;
|
|
21
|
-
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
22
|
-
-webkit-background-clip: text;
|
|
23
|
-
-webkit-text-fill-color: transparent;
|
|
24
|
-
}
|
|
25
|
-
</style>
|
|
26
|
-
</head>
|
|
27
|
-
<body>
|
|
28
|
-
<h1>Hello, World!</h1>
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/code_providers/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/gemini.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.26.0 → voice_vibecoder-2.26.1}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|