Mercurial
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) |