comparison mrjunejune/inference/copilot_sidecar.py @ 280:49e9e591c9bb

Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 19:14:53 -0700
parents 056790c4fb0d
children c57149ad216e
comparison
equal deleted inserted replaced
279:b3b547563ec7 280:49e9e591c9bb
1 from __future__ import annotations 1 from __future__ import annotations
2 2
3 import argparse 3 import argparse
4 import asyncio 4 import asyncio
5 import hashlib
5 import json 6 import json
6 import os 7 import os
7 import sys 8 import sys
8 import time 9 import time
9 import uuid 10 import uuid
18 19
19 20
20 JsonObject = dict[str, Any] 21 JsonObject = dict[str, Any]
21 Emit = Callable[[JsonObject], Awaitable[None]] 22 Emit = Callable[[JsonObject], Awaitable[None]]
22 23
23 _KNOWN_PROFILES: frozenset = frozenset({"public_visitor", "invited_friend", "june_admin"}) 24 _KNOWN_PROFILES: frozenset = frozenset({
25 "public_visitor",
26 "invited_friend",
27 "june_admin",
28 "canvas_orchestrator",
29 })
24 _PROMPT_VERSION: int = 1 30 _PROMPT_VERSION: int = 1
31 _CANVAS_ORCHESTRATOR_PROMPT = """\
32 You orchestrate spatial agent sessions on an infinite canvas.
33 There is one user input scratchpad and any number of conversation entities.
34 The user message includes only the visible canvas context plus one new thought.
35 Treat visible conversation entities as the only append candidates. Append when
36 the new thought clearly continues one of them; otherwise create a new
37 conversation. Never append to a conversation ID absent from visible context.
38 Return only one compact JSON object with these fields:
39 - action: "create" or "append"
40 - conversation_id: an existing numeric conversation ID for append, otherwise 0
41 - title: a short session title
42 - response: the direct response that should appear on the canvas
43 Do not use Markdown fences, tools, or text outside the JSON object.
44 """
25 45
26 # Fixed namespace for uuid5 SDK session ID derivation. Must never change. 46 # Fixed namespace for uuid5 SDK session ID derivation. Must never change.
27 _SDK_SESSION_NAMESPACE = uuid.UUID("3f7e8a1d-9b52-4c6f-a0d3-82e1f5c94b7a") 47 _SDK_SESSION_NAMESPACE = uuid.UUID("3f7e8a1d-9b52-4c6f-a0d3-82e1f5c94b7a")
28 48
29 _HISTORY_MAX_ENTRIES: int = 20 49 _HISTORY_MAX_ENTRIES: int = 20
252 self._cleanup_task = asyncio.create_task(self._cleanup_loop()) 272 self._cleanup_task = asyncio.create_task(self._cleanup_loop())
253 273
254 def _compile_all_profiles(self) -> None: 274 def _compile_all_profiles(self) -> None:
255 compiled: dict[str, CompiledProfile] = {} 275 compiled: dict[str, CompiledProfile] = {}
256 for profile in sorted(_KNOWN_PROFILES): 276 for profile in sorted(_KNOWN_PROFILES):
257 result = self._compile_fn(profile) 277 if profile == "canvas_orchestrator":
278 content = _CANVAS_ORCHESTRATOR_PROMPT.strip()
279 result = {
280 "content": content,
281 "version": 1,
282 "hash": hashlib.sha256(
283 content.encode("utf-8")
284 ).hexdigest(),
285 }
286 else:
287 result = self._compile_fn(profile)
258 compiled[profile] = CompiledProfile( 288 compiled[profile] = CompiledProfile(
259 profile=profile, 289 profile=profile,
260 content=result["content"], 290 content=result["content"],
261 prompt_version=_PROMPT_VERSION, 291 prompt_version=_PROMPT_VERSION,
262 knowledge_version=result["version"], 292 knowledge_version=result["version"],
325 ) 355 )
326 return 356 return
327 357
328 await self.evict_idle_sessions( 358 await self.evict_idle_sessions(
329 exclude=conversation_id, 359 exclude=conversation_id,
330 reserve=1 if command_name == "turn.start" else 0, 360 reserve=1
361 if command_name in {"turn.start", "conversation.warm"}
362 else 0,
331 ) 363 )
332 async with self._conversation_command_lock(conversation_id): 364 async with self._conversation_command_lock(conversation_id):
333 if command_name == "turn.start": 365 if command_name == "turn.start":
334 prompt = command.get("prompt") 366 prompt = command.get("prompt")
335 if not isinstance(prompt, str) or not prompt: 367 if not isinstance(prompt, str) or not prompt:
363 "invalid_prompt_profile", 395 "invalid_prompt_profile",
364 "a known prompt profile and current versions are required", 396 "a known prompt profile and current versions are required",
365 ) 397 )
366 return 398 return
367 await self._start_turn(request_id, conversation_id, prompt, compiled, history) 399 await self._start_turn(request_id, conversation_id, prompt, compiled, history)
400 elif command_name == "conversation.warm":
401 compiled = self._validate_profile_fields(
402 command.get("prompt_profile"),
403 command.get("prompt_version"),
404 command.get("knowledge_version"),
405 )
406 if compiled is None:
407 await self._fail(
408 request_id,
409 conversation_id,
410 "invalid_prompt_profile",
411 "a known prompt profile and current versions are required",
412 )
413 return
414 resume_existing = command.get("resume_existing", True)
415 if not isinstance(resume_existing, bool):
416 await self._fail(
417 request_id,
418 conversation_id,
419 "invalid_request",
420 "resume_existing must be a boolean",
421 )
422 return
423 await self._get_conversation(
424 conversation_id,
425 compiled,
426 [],
427 resume_existing=resume_existing,
428 )
429 await self._send(
430 "session.warmed",
431 request_id,
432 conversation_id,
433 )
434 await self._send(
435 "turn.done",
436 request_id,
437 conversation_id,
438 )
368 elif command_name == "turn.abort": 439 elif command_name == "turn.abort":
369 await self._abort_turn(request_id, conversation_id) 440 await self._abort_turn(request_id, conversation_id)
370 elif command_name == "conversation.delete": 441 elif command_name == "conversation.delete":
371 await self._delete_conversation(request_id, conversation_id) 442 await self._delete_conversation(request_id, conversation_id)
372 else: 443 else:
484 async def _get_conversation( 555 async def _get_conversation(
485 self, 556 self,
486 conversation_id: str, 557 conversation_id: str,
487 compiled: CompiledProfile, 558 compiled: CompiledProfile,
488 history: list[dict[str, str]], 559 history: list[dict[str, str]],
560 *,
561 resume_existing: bool = True,
489 ) -> Conversation: 562 ) -> Conversation:
490 derived_id = _derive_sdk_session_id(conversation_id, compiled) 563 derived_id = _derive_sdk_session_id(conversation_id, compiled)
491 async with self._conversations_lock: 564 async with self._conversations_lock:
492 existing = self._conversations.get(conversation_id) 565 existing = self._conversations.get(conversation_id)
493 if existing is not None: 566 if existing is not None:
506 579
507 if len(self._conversations) >= self._config.max_sessions: 580 if len(self._conversations) >= self._config.max_sessions:
508 raise RuntimeError("Copilot session capacity exhausted") 581 raise RuntimeError("Copilot session capacity exhausted")
509 582
510 base_options = self._session_options(compiled) 583 base_options = self._session_options(compiled)
511 # Try to resume a persisted derived session (no history injection).
512 session: Any = None 584 session: Any = None
513 try: 585 if resume_existing:
514 session = await self._client.resume_session(derived_id, **base_options) 586 # Resume persisted user-facing sessions, then create if absent.
515 except Exception: 587 try:
516 pass 588 session = await self._client.resume_session(derived_id, **base_options)
589 except Exception:
590 pass
517 591
518 if session is None: 592 if session is None:
519 # Fresh create — inject bounded transcript context into system message. 593 # Fresh create — inject bounded transcript context into system message.
520 create_options = ( 594 create_options = (
521 self._session_options_with_history(compiled, history) 595 self._session_options_with_history(compiled, history)