feat: couple broadcast to voice + voice-controlled broadcast toggle

Completes the STREAM_BROWSER=true behaviour:
- handleJoin auto-starts the broadcast on voice join and wires the session to
  the guild streamer; each turn reports the live state to the brain so search
  routes Chrome (live) vs Gemini (off).
- New setBroadcast tool lets the user toggle the broadcast by voice ("방송
  켜줘/꺼줘") via the LLM (no hardcoded phrases); it refuses when
  STREAM_BROWSER=false. The directive flows brain -> bridge (broadcast_action)
  -> bot streamer.start/stop, guarded by isActive() so it's idempotent.
- Per-turn IPC uses a thread-local (reply/turn_state.py) instead of threading
  params through the whole engine chain: bridge sets broadcasting in, tool
  records the directive out; Tool.execute exposes broadcasting on ToolContext.

Bot typecheck clean; brain covered by tests/test_set_broadcast.py (+ existing
routing tests). Specs + docs updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-11 10:08:30 +09:00
parent 35e754d6ee
commit ca86390407
11 changed files with 316 additions and 14 deletions

View File

@@ -0,0 +1,30 @@
# setBroadcast tool
Lets the user turn the live screen-share broadcast (Go-Live) on or off by voice
or text, e.g. "방송 켜줘 / 화면 보여줘" or "방송 꺼줘". The LLM picks the intent
and calls the tool (no hardcoded phrase matching), so it works in any language.
## Contract
- Input: `{ "action": "start" | "stop" }`.
- The brain cannot drive the Discord broadcast itself (the bot owns it). The
tool records the directive in request-scoped state
(`jarvis/reply/turn_state.request_broadcast`); `bridge/server.py` returns it as
`broadcast_action` in the `/converse` (and `/text`) response, and the bot
(`voice.ts` -> `index.ts` `onBroadcastAction`) calls `streamer.start/stop`.
- Mode gate: when `cfg.stream_browser` is false (`STREAM_BROWSER=false`)
broadcasting is disabled — the tool refuses with a spoken explanation and
records no directive. This is the same capability flag that makes `/stream`
refuse and forces real-time search to Gemini.
## Principles
- Tool returns raw intent/result; it performs no broadcast itself and makes no
LLM call.
- Idempotent at the bot edge: `start` is a no-op if already live, `stop` a no-op
if already off (`streamer.isActive()` guard).
- Fail-safe: a missing/invalid action records nothing; the bot only acts on an
explicit `"start"`/`"stop"` directive.
See `docs/stream_browser_modes.md` for how this fits the broadcast-coupled
search routing.