comparison mrjunejune/inference/copilot_sidecar.py @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -0700
parents 49e9e591c9bb
children
comparison
equal deleted inserted replaced
280:49e9e591c9bb 281:c57149ad216e
32 You orchestrate spatial agent sessions on an infinite canvas. 32 You orchestrate spatial agent sessions on an infinite canvas.
33 There is one user input scratchpad and any number of conversation entities. 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. 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 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 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. 37 conversation only when the request benefits from retained discussion. Never
38 append to a conversation ID absent from visible context.
38 Return only one compact JSON object with these fields: 39 Return only one compact JSON object with these fields:
39 - action: "create" or "append" 40 - action: "create" or "append"
41 - presentation: "action" for a one-off canvas action, or "conversation" for
42 retained discussion
40 - conversation_id: an existing numeric conversation ID for append, otherwise 0 43 - conversation_id: an existing numeric conversation ID for append, otherwise 0
41 - title: a short session title 44 - title: a short session title
42 - response: the direct response that should appear on the canvas 45 - response: the direct response that should appear on the canvas
46 - entities: an array of zero to four useful visual entities to create
47 Each entities item may contain:
48 - kind: one of "text", "button", "text_area", "accordion", "card",
49 "calendar", "table", "notification", "scroll_area", "image", or
50 "web_content"
51 - label and text: short display strings; image/web_content text must be an
52 absolute http:// or https:// URL
53 - x and y: optional world coordinates; omit both to let the canvas place it
54 - width and height: optional dimensions; omit both for the native default
55 - value: optional integer state; for a calendar it is the 1-based selected
56 day of the month
57 Create entities only when they materially demonstrate or organize the answer.
58 Never create more than four. Do not create conversation entities through this
59 array; action and conversation_id own the conversation lifecycle.
60 For a simple one-off task, use presentation "action", action "create",
61 conversation_id 0, and at least one useful entity. Handle it directly in this
62 orchestrator: do not create or delegate to another agent, worker, or session.
63 For a complex answer that should remain discussable, use presentation
64 "conversation". Append always requires presentation "conversation".
43 Do not use Markdown fences, tools, or text outside the JSON object. 65 Do not use Markdown fences, tools, or text outside the JSON object.
44 """ 66 """
45 67
46 # Fixed namespace for uuid5 SDK session ID derivation. Must never change. 68 # Fixed namespace for uuid5 SDK session ID derivation. Must never change.
47 _SDK_SESSION_NAMESPACE = uuid.UUID("3f7e8a1d-9b52-4c6f-a0d3-82e1f5c94b7a") 69 _SDK_SESSION_NAMESPACE = uuid.UUID("3f7e8a1d-9b52-4c6f-a0d3-82e1f5c94b7a")
302 "hash": cp.hash, 324 "hash": cp.hash,
303 } 325 }
304 for profile, cp in self._compiled.items() 326 for profile, cp in self._compiled.items()
305 } 327 }
306 await self._send("ready", None, None, status="ok", profiles=profile_meta) 328 await self._send("ready", None, None, status="ok", profiles=profile_meta)
329
330 def _resume_marker(self, session_id: str) -> str:
331 return os.path.join(
332 self._config.base_directory,
333 ".session-markers",
334 session_id,
335 )
336
337 def _should_attempt_resume(self, session_id: str) -> bool:
338 if not os.path.isdir(self._config.base_directory):
339 return True
340 return os.path.isfile(self._resume_marker(session_id))
341
342 def _mark_session_created(self, session_id: str) -> None:
343 marker = self._resume_marker(session_id)
344 try:
345 os.makedirs(os.path.dirname(marker), mode=0o700, exist_ok=True)
346 with open(marker, "wb"):
347 pass
348 except OSError as error:
349 print(
350 f"unable to write Copilot session marker: {error}",
351 file=sys.stderr,
352 )
353
354 def _remove_resume_marker(self, session_id: str) -> None:
355 try:
356 os.remove(self._resume_marker(session_id))
357 except FileNotFoundError:
358 pass
359 except OSError:
360 pass
307 361
308 async def dispatch(self, command: JsonObject) -> None: 362 async def dispatch(self, command: JsonObject) -> None:
309 command_name = command.get("command") 363 command_name = command.get("command")
310 request_id = command.get("request_id") 364 request_id = command.get("request_id")
311 conversation_id = command.get("conversation_id") 365 conversation_id = command.get("conversation_id")
580 if len(self._conversations) >= self._config.max_sessions: 634 if len(self._conversations) >= self._config.max_sessions:
581 raise RuntimeError("Copilot session capacity exhausted") 635 raise RuntimeError("Copilot session capacity exhausted")
582 636
583 base_options = self._session_options(compiled) 637 base_options = self._session_options(compiled)
584 session: Any = None 638 session: Any = None
585 if resume_existing: 639 if resume_existing and self._should_attempt_resume(derived_id):
586 # Resume persisted user-facing sessions, then create if absent. 640 # Resume persisted user-facing sessions, then create if absent.
587 try: 641 try:
588 session = await self._client.resume_session(derived_id, **base_options) 642 session = await self._client.resume_session(derived_id, **base_options)
589 except Exception: 643 except Exception:
590 pass 644 self._remove_resume_marker(derived_id)
591 645
592 if session is None: 646 if session is None:
593 # Fresh create — inject bounded transcript context into system message. 647 # Fresh create — inject bounded transcript context into system message.
594 create_options = ( 648 create_options = (
595 self._session_options_with_history(compiled, history) 649 self._session_options_with_history(compiled, history)
597 else base_options 651 else base_options
598 ) 652 )
599 session = await self._client.create_session( 653 session = await self._client.create_session(
600 session_id=derived_id, **create_options 654 session_id=derived_id, **create_options
601 ) 655 )
656 self._mark_session_created(derived_id)
602 657
603 def handle_event(event: Any) -> None: 658 def handle_event(event: Any) -> None:
604 task = asyncio.create_task(self._handle_event(conversation_id, event)) 659 task = asyncio.create_task(self._handle_event(conversation_id, event))
605 self._event_tasks.add(task) 660 self._event_tasks.add(task)
606 task.add_done_callback(self._event_tasks.discard) 661 task.add_done_callback(self._event_tasks.discard)
756 conversation_id, conversation.active, aborted=True, deleted=True 811 conversation_id, conversation.active, aborted=True, deleted=True
757 ) 812 )
758 conversation.unsubscribe() 813 conversation.unsubscribe()
759 await conversation.session.disconnect() 814 await conversation.session.disconnect()
760 await self._client.delete_session(conversation.session.session_id) 815 await self._client.delete_session(conversation.session.session_id)
816 self._remove_resume_marker(conversation.session.session_id)
761 deleted_ids.add(conversation.session.session_id) 817 deleted_ids.add(conversation.session.session_id)
762 for compiled in self._compiled.values(): 818 for compiled in self._compiled.values():
763 sdk_session_id = _derive_sdk_session_id(conversation_id, compiled) 819 sdk_session_id = _derive_sdk_session_id(conversation_id, compiled)
764 if sdk_session_id in deleted_ids: 820 if sdk_session_id in deleted_ids:
765 continue 821 continue
766 try: 822 try:
767 await self._client.delete_session(sdk_session_id) 823 await self._client.delete_session(sdk_session_id)
768 except Exception: 824 except Exception:
769 # A profile-specific persisted session may never have existed. 825 # A profile-specific persisted session may never have existed.
770 pass 826 pass
827 self._remove_resume_marker(sdk_session_id)
771 await self._send( 828 await self._send(
772 "turn.done", request_id, conversation_id, action="conversation.delete" 829 "turn.done", request_id, conversation_id, action="conversation.delete"
773 ) 830 )
774 except Exception as error: 831 except Exception as error:
775 await self._finish_with_error(request_id, conversation_id, error) 832 await self._finish_with_error(request_id, conversation_id, error)