From f80b9ebe3d373b7486182ba4ffd03a20070ff447 Mon Sep 17 00:00:00 2001 From: Shane Date: Sun, 23 Aug 2026 07:05:03 -0400 Subject: [PATCH] fix: unique room names per session, JT_ROOM dispatch, dispatch-first prompt - token_server: generate voice-{uuid8} room name per request so each browser session creates a fresh room and triggers agent dispatch - token claim: add jobType JT_ROOM to roomConfig.agents - livekit.yaml: revert empty_timeout to default (300s) - agent.py: system prompt now mandates dispatch_task for all non-trivial tasks (not just research); removed inline web_search instructions; added tool_call logging in _ReasoningFallbackWrapper --- agent/agent.py | 40 ++++++++++++++++++++++++---------------- livekit.yaml | 8 ++------ web/token_server.py | 5 +++-- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/agent/agent.py b/agent/agent.py index 7c3a3a4..aea4b65 100644 --- a/agent/agent.py +++ b/agent/agent.py @@ -63,15 +63,6 @@ SYSTEM_PROMPT = textwrap.dedent("""\ - Never write more than three sentences in a row. - Never read back URLs, file paths, or technical identifiers. - # Web access - You have web_search and web_scrape tools. Use them when the user asks about - current events, recent news, prices, sports scores, or anything - that may have changed since your training data. Search first, then scrape - a result only if you need more detail. Answer from what you find, in your - normal conversational style — don't cite sources formally, just mention the - source naturally ("according to..."). If a search comes up empty, say so - briefly and move on. - # Weather & Time You can check the weather for any location. Use get_weather when the user asks about current conditions, temperature, or forecasts. It returns a @@ -79,6 +70,27 @@ SYSTEM_PROMPT = textwrap.dedent("""\ Use get_time when the user asks what time or day it is. If they mention a city, pass it as the location argument. + # Background Tasks (dispatch_task) — USE FOR ALL NON-TRIVIAL TASKS + For ANY task that is not a simple one-sentence answer from your own + knowledge, you MUST call dispatch_task. This includes: looking up news, + researching topics, checking current events, prices, sports scores, + writing something, summarizing, comparing options, planning, or anything + that takes more than a couple seconds to think through. Do NOT use + web_search or web_scrape directly; always dispatch instead. + The only things you answer inline are: weather (get_weather), time + (get_time), memory operations, and trivial facts you already know. + Examples: + - User says "what's the latest news?" → CALL dispatch_task with + description="Find the top 5 international news headlines today" + - User says "look up the price of a PS5" → CALL dispatch_task with + description="Find the current retail price of a PlayStation 5" + - User says "plan a weekend trip to Denver" → CALL dispatch_task with + description="Plan a two-day weekend trip to Denver including activities" + After calling dispatch_task, tell the user "I'll get on that for you" + and keep the conversation going. The result comes back automatically — when + you receive it, share the findings naturally in your conversational style. + You can have multiple tasks running at once. + # Memory (CRITICAL — always use these tools) You MUST call memory_save whenever the user tells you something to remember, shares a preference, or says "remember that...". Do NOT just say "okay I'll @@ -99,13 +111,6 @@ SYSTEM_PROMPT = textwrap.dedent("""\ use skill_save to record it so you can follow it next time. Be selective: only save skills for repeatable tasks, not one-off facts (those go in memory). - # Background Tasks (dispatch_task) - When the user asks you to research something, look up news, or do anything - that will take more than a few seconds, CALL dispatch_task with a clear - description of what to investigate. Tell the user "I'll take a look at that - for you" and keep talking. The result comes back automatically — when you - receive it, share the findings naturally. You can have multiple tasks running - at once. """) @@ -165,6 +170,9 @@ class _ReasoningFallbackWrapper: self._last_reasoning = reasoning if chunk.has_response(): self._has_content = True + tc = getattr(delta, "tool_calls", None) + if tc: + logger.info("LLM tool_call: %s", [t.function.name for t in tc]) return chunk async def collect(self): diff --git a/livekit.yaml b/livekit.yaml index 76a6d7d..0739d00 100644 --- a/livekit.yaml +++ b/livekit.yaml @@ -7,9 +7,5 @@ rtc: keys: devkey: devsecret -# Keep rooms alive for 24h — the agent stays resident so reconnections -# don't require a fresh dispatch. Without this, LiveKit destroys the room -# after empty_timeout (default 300s), kills the agent job, and the next -# join finds no worker registered ("assistant not ready retry"). -room: - empty_timeout: 86400 +# Rooms are short-lived: each browser session gets a unique room name, so +# the agent is dispatched at room creation. Default 300s cleanup is fine. diff --git a/web/token_server.py b/web/token_server.py index cd77704..398e3e9 100644 --- a/web/token_server.py +++ b/web/token_server.py @@ -47,7 +47,7 @@ def make_token(room_name: str, identity: str) -> str: "canPublishData": True, }, "roomConfig": { - "agents": [{"agentName": AGENT_NAME}], + "agents": [{"agentName": AGENT_NAME, "jobType": "JT_ROOM"}], }, } h = b64url(json.dumps(header).encode()) @@ -66,7 +66,8 @@ class Handler(BaseHTTPRequestHandler): length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length) or b"{}") - room_name = "voice-room" + # Unique room per session so LiveKit dispatches the agent at creation. + room_name = f"voice-{uuid.uuid4().hex[:8]}" identity = body.get("identity") or f"user-{uuid.uuid4().hex[:8]}" token = make_token(room_name, identity)