diff mrjunejune/inference/mock_sidecar.py @ 261:b401627fc49e

Add JRPG mock flows and interactive previews Add scripted mock SSE commands, custom event forwarding, animated chat turns, full-height message navigation, and a cyberpunk resume dossier. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <mrjunejune@users.noreply.github.com>
date Wed, 05 Aug 2026 20:38:32 -0700
parents
children 056790c4fb0d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/inference/mock_sidecar.py	Wed Aug 05 20:38:32 2026 -0700
@@ -0,0 +1,526 @@
+from __future__ import annotations
+
+import argparse
+import asyncio
+import json
+import os
+import pathlib
+import re
+import sys
+from dataclasses import dataclass
+from typing import Any, Awaitable, Callable
+
+
+JsonObject = dict[str, Any]
+Emit = Callable[[JsonObject], Awaitable[None]]
+COMMAND_PATTERN = re.compile(r"^![a-z][a-z0-9_-]*$")
+EVENT_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
+RESERVED_EVENT_TYPES = {"bridge.closed", "ready", "turn.accepted", "turn.done"}
+RESERVED_EVENT_FIELDS = {
+    "conversation_id",
+    "mock",
+    "mock_command",
+    "request_id",
+}
+MAX_DELAY_MS = 60_000
+
+
+@dataclass(frozen=True)
+class MockEvent:
+    event_type: str
+    delay_ms: int | None
+    payload: JsonObject
+
+
+@dataclass(frozen=True)
+class MockCommand:
+    name: str
+    description: str
+    events: tuple[MockEvent, ...]
+    failed: bool
+
+
+@dataclass(frozen=True)
+class MockConfig:
+    delay_ms: int
+    fallback: str
+    commands: dict[str, MockCommand]
+
+    def select(self, prompt: str) -> MockCommand:
+        first_token = prompt.strip().split(maxsplit=1)[0].lower()
+        return self.commands.get(first_token, self.commands[self.fallback])
+
+
+@dataclass
+class MockTurn:
+    request_id: str
+    task: asyncio.Task[None]
+
+
+def _require_non_negative_integer(value: Any, description: str) -> int:
+    if not isinstance(value, int) or isinstance(value, bool) or value < 0:
+        raise ValueError(f"{description} must be a non-negative integer")
+    if value > MAX_DELAY_MS:
+        raise ValueError(f"{description} must not exceed {MAX_DELAY_MS}")
+    return value
+
+
+def _validate_event(command_name: str, index: int, raw: Any) -> MockEvent:
+    if not isinstance(raw, dict):
+        raise ValueError(f"{command_name} event {index} must be an object")
+    event_type = raw.get("type")
+    if not isinstance(event_type, str) or not EVENT_PATTERN.fullmatch(event_type):
+        raise ValueError(
+            f"{command_name} event {index} requires a safe event type"
+        )
+    if event_type in RESERVED_EVENT_TYPES:
+        raise ValueError(
+            f"{command_name} cannot script lifecycle event {event_type}"
+        )
+    reserved = set(raw) & RESERVED_EVENT_FIELDS
+    if reserved:
+        raise ValueError(
+            f"{command_name} event {index} uses reserved fields: "
+            f"{', '.join(sorted(reserved))}"
+        )
+    delay_ms = raw.get("delay_ms")
+    if delay_ms is not None:
+        delay_ms = _require_non_negative_integer(
+            delay_ms,
+            f"{command_name} event {index} delay_ms",
+        )
+    payload = {
+        key: value
+        for key, value in raw.items()
+        if key not in {"delay_ms", "type"}
+    }
+    if event_type == "assistant.delta":
+        if not isinstance(payload.get("delta"), str) or not payload["delta"]:
+            raise ValueError(
+                f"{command_name} assistant.delta requires non-empty delta"
+            )
+    elif event_type == "assistant.completed":
+        if not isinstance(payload.get("content"), str):
+            raise ValueError(
+                f"{command_name} assistant.completed requires content"
+            )
+    elif event_type == "assistant.usage":
+        usage = payload.get("usage")
+        if not isinstance(usage, dict):
+            raise ValueError(f"{command_name} assistant.usage requires usage")
+        for key in ("input_tokens", "output_tokens"):
+            _require_non_negative_integer(
+                usage.get(key),
+                f"{command_name} assistant.usage {key}",
+            )
+    elif event_type == "turn.error":
+        error = payload.get("error")
+        if not isinstance(error, dict):
+            raise ValueError(f"{command_name} turn.error requires error")
+        if (
+            not isinstance(error.get("code"), str)
+            or not error["code"]
+            or not isinstance(error.get("message"), str)
+            or not error["message"]
+        ):
+            raise ValueError(
+                f"{command_name} turn.error requires error code and message"
+            )
+    return MockEvent(event_type, delay_ms, payload)
+
+
+def _validate_command(name: str, raw: Any) -> MockCommand:
+    if not COMMAND_PATTERN.fullmatch(name):
+        raise ValueError(f"invalid mock command: {name}")
+    if not isinstance(raw, dict):
+        raise ValueError(f"{name} must contain an object")
+    unknown = set(raw) - {"description", "events"}
+    if unknown:
+        raise ValueError(
+            f"{name} has unknown keys: {', '.join(sorted(unknown))}"
+        )
+    description = raw.get("description", "")
+    if not isinstance(description, str):
+        raise ValueError(f"{name} description must be a string")
+    raw_events = raw.get("events")
+    if not isinstance(raw_events, list) or not raw_events:
+        raise ValueError(f"{name} events must be a non-empty array")
+    if len(raw_events) > 256:
+        raise ValueError(f"{name} has too many events")
+    events = tuple(
+        _validate_event(name, index, event)
+        for index, event in enumerate(raw_events)
+    )
+    errors = [event for event in events if event.event_type == "turn.error"]
+    completed = [
+        event for event in events if event.event_type == "assistant.completed"
+    ]
+    if errors:
+        if len(errors) != 1 or errors[0] is not events[-1]:
+            raise ValueError(f"{name} turn.error must be the final scripted event")
+        if completed:
+            raise ValueError(f"{name} cannot complete and fail the same turn")
+    elif len(completed) != 1:
+        raise ValueError(f"{name} requires one assistant.completed event")
+
+    deltas = "".join(
+        event.payload["delta"]
+        for event in events
+        if event.event_type == "assistant.delta"
+    )
+    if completed:
+        completed_index = events.index(completed[0])
+        if any(
+            event.event_type == "assistant.delta"
+            for event in events[completed_index + 1:]
+        ):
+            raise ValueError(
+                f"{name} assistant.completed must follow all deltas"
+            )
+    if completed and deltas and deltas != completed[0].payload["content"]:
+        raise ValueError(f"{name} deltas must join to completed content")
+    return MockCommand(name, description, events, failed=bool(errors))
+
+
+def load_mock_config(path: str | os.PathLike[str]) -> MockConfig:
+    with open(path, encoding="utf-8") as config_file:
+        payload = json.load(config_file)
+    if not isinstance(payload, dict):
+        raise ValueError("mock response file must contain a JSON object")
+    unknown = set(payload) - {"commands", "delay_ms", "fallback"}
+    if unknown:
+        raise ValueError(f"unknown mock config keys: {', '.join(sorted(unknown))}")
+    delay_ms = _require_non_negative_integer(
+        payload.get("delay_ms", 55),
+        "delay_ms",
+    )
+    fallback = payload.get("fallback")
+    if not isinstance(fallback, str) or not COMMAND_PATTERN.fullmatch(fallback):
+        raise ValueError("fallback must be a !command")
+    raw_commands = payload.get("commands")
+    if not isinstance(raw_commands, dict) or not raw_commands:
+        raise ValueError("commands must be a non-empty object")
+    commands = {
+        name.lower(): _validate_command(name.lower(), raw)
+        for name, raw in raw_commands.items()
+    }
+    if fallback.lower() not in commands:
+        raise ValueError(f"fallback command is not defined: {fallback}")
+    return MockConfig(delay_ms, fallback.lower(), commands)
+
+
+class MockSidecar:
+    def __init__(self, emit: Emit, config: MockConfig) -> None:
+        self._emit = emit
+        self._config = config
+        self._active: dict[str, MockTurn] = {}
+        self.shutting_down = False
+
+    async def announce_ready(self) -> None:
+        await self._send("ready", None, None, status="ok", mock=True)
+
+    async def dispatch(self, command: JsonObject) -> None:
+        command_name = command.get("command")
+        request_id = command.get("request_id")
+        conversation_id = command.get("conversation_id")
+        if not isinstance(request_id, str) or not request_id:
+            await self._fail(
+                request_id if isinstance(request_id, str) else None,
+                conversation_id if isinstance(conversation_id, str) else None,
+                "invalid_request",
+                "request_id is required",
+            )
+            return
+        if command_name == "shutdown":
+            await self._shutdown(request_id, conversation_id)
+            return
+        if command_name == "health":
+            await self._send(
+                "ready",
+                request_id,
+                conversation_id if isinstance(conversation_id, str) else None,
+                status="ok",
+                mock=True,
+            )
+            return
+        if not isinstance(conversation_id, str) or not conversation_id:
+            await self._fail(
+                request_id,
+                None,
+                "invalid_request",
+                "conversation_id is required",
+            )
+            return
+        if command_name == "turn.start":
+            prompt = command.get("prompt")
+            if not isinstance(prompt, str) or not prompt:
+                await self._fail(
+                    request_id,
+                    conversation_id,
+                    "invalid_request",
+                    "prompt is required",
+                )
+                return
+            await self._start_turn(request_id, conversation_id, prompt)
+        elif command_name == "turn.abort":
+            await self._abort_turn(request_id, conversation_id)
+        elif command_name == "conversation.delete":
+            await self._delete_conversation(request_id, conversation_id)
+        else:
+            await self._fail(
+                request_id,
+                conversation_id,
+                "unknown_command",
+                f"unsupported command: {command_name!r}",
+            )
+
+    async def wait_for_idle(self) -> None:
+        tasks = [turn.task for turn in self._active.values()]
+        if tasks:
+            await asyncio.gather(*tasks, return_exceptions=True)
+
+    async def _start_turn(
+        self,
+        request_id: str,
+        conversation_id: str,
+        prompt: str,
+    ) -> None:
+        if conversation_id in self._active:
+            await self._fail(
+                request_id,
+                conversation_id,
+                "turn_in_progress",
+                "the conversation already has an active turn",
+            )
+            return
+        scripted_command = self._config.select(prompt)
+        await self._send(
+            "turn.accepted",
+            request_id,
+            conversation_id,
+            mock=True,
+            mock_command=scripted_command.name,
+        )
+        task = asyncio.create_task(
+            self._stream_turn(request_id, conversation_id, scripted_command)
+        )
+        self._active[conversation_id] = MockTurn(request_id, task)
+
+    async def _stream_turn(
+        self,
+        request_id: str,
+        conversation_id: str,
+        scripted_command: MockCommand,
+    ) -> None:
+        try:
+            for event in scripted_command.events:
+                delay = (
+                    self._config.delay_ms
+                    if event.delay_ms is None
+                    else event.delay_ms
+                )
+                await asyncio.sleep(delay / 1000)
+                await self._send(
+                    event.event_type,
+                    request_id,
+                    conversation_id,
+                    **event.payload,
+                    mock=True,
+                    mock_command=scripted_command.name,
+                )
+            await self._send(
+                "turn.done",
+                request_id,
+                conversation_id,
+                failed=scripted_command.failed,
+                mock=True,
+                mock_command=scripted_command.name,
+            )
+        except asyncio.CancelledError:
+            await self._send(
+                "turn.done",
+                request_id,
+                conversation_id,
+                aborted=True,
+                mock=True,
+                mock_command=scripted_command.name,
+            )
+        except Exception as error:
+            await self._send(
+                "turn.error",
+                request_id,
+                conversation_id,
+                error={
+                    "code": "mock_script_failed",
+                    "message": str(error),
+                },
+                mock_command=scripted_command.name,
+            )
+            await self._send(
+                "turn.done",
+                request_id,
+                conversation_id,
+                failed=True,
+                mock=True,
+                mock_command=scripted_command.name,
+            )
+        finally:
+            active = self._active.get(conversation_id)
+            if active is not None and active.request_id == request_id:
+                self._active.pop(conversation_id, None)
+
+    async def _abort_turn(
+        self,
+        request_id: str,
+        conversation_id: str,
+    ) -> None:
+        active = self._active.get(conversation_id)
+        if active is None:
+            await self._fail(
+                request_id,
+                conversation_id,
+                "no_active_turn",
+                "no active turn to abort",
+            )
+            return
+        await self._send(
+            "turn.accepted",
+            request_id,
+            conversation_id,
+            action="abort",
+            target_request_id=active.request_id,
+            mock=True,
+        )
+        active.task.cancel()
+        await asyncio.gather(active.task, return_exceptions=True)
+        await self._send(
+            "turn.done",
+            request_id,
+            conversation_id,
+            action="abort",
+            target_request_id=active.request_id,
+            mock=True,
+        )
+
+    async def _delete_conversation(
+        self,
+        request_id: str,
+        conversation_id: str,
+    ) -> None:
+        active = self._active.get(conversation_id)
+        if active is not None:
+            active.task.cancel()
+            await asyncio.gather(active.task, return_exceptions=True)
+        await self._send(
+            "turn.done",
+            request_id,
+            conversation_id,
+            action="conversation.delete",
+            mock=True,
+        )
+
+    async def _shutdown(
+        self,
+        request_id: str,
+        conversation_id: Any,
+    ) -> None:
+        self.shutting_down = True
+        tasks = [turn.task for turn in self._active.values()]
+        for task in tasks:
+            task.cancel()
+        if tasks:
+            await asyncio.gather(*tasks, return_exceptions=True)
+        await self._send(
+            "turn.done",
+            request_id,
+            conversation_id if isinstance(conversation_id, str) else None,
+            action="shutdown",
+            mock=True,
+        )
+
+    async def _fail(
+        self,
+        request_id: str | None,
+        conversation_id: str | None,
+        code: str,
+        message: str,
+    ) -> None:
+        await self._send(
+            "turn.error",
+            request_id,
+            conversation_id,
+            error={"code": code, "message": message},
+        )
+        await self._send(
+            "turn.done",
+            request_id,
+            conversation_id,
+            failed=True,
+            mock=True,
+        )
+
+    async def _send(
+        self,
+        event_type: str,
+        request_id: str | None,
+        conversation_id: str | None,
+        **fields: Any,
+    ) -> None:
+        await self._emit(
+            {
+                "type": event_type,
+                "request_id": request_id,
+                "conversation_id": conversation_id,
+                **fields,
+            }
+        )
+
+
+async def run(config: MockConfig) -> None:
+    async def emit(payload: JsonObject) -> None:
+        sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
+        sys.stdout.flush()
+
+    sidecar = MockSidecar(emit, config)
+    await sidecar.announce_ready()
+    while not sidecar.shutting_down:
+        line = await asyncio.to_thread(sys.stdin.readline)
+        if not line:
+            break
+        try:
+            command = json.loads(line)
+            if not isinstance(command, dict):
+                raise ValueError("command must be a JSON object")
+        except (json.JSONDecodeError, ValueError) as error:
+            await sidecar._fail(None, None, "invalid_json", str(error))
+            continue
+        await sidecar.dispatch(command)
+    await sidecar.wait_for_idle()
+
+
+def main() -> None:
+    packaged_responses = pathlib.Path(__file__).with_name("mock_responses.json")
+    parser = argparse.ArgumentParser(description="Scripted JRPG mock sidecar")
+    parser.add_argument("copilot_cli", nargs="?")
+    parser.add_argument("--responses", default=str(packaged_responses))
+    args = parser.parse_args()
+    responses_path = os.environ.get("MRJUNEJUNE_MOCK_RESPONSES") or args.responses
+    config = load_mock_config(responses_path)
+    delay_override = os.environ.get("MRJUNEJUNE_MOCK_DELAY_MS")
+    if delay_override is not None:
+        try:
+            delay_ms = int(delay_override)
+        except ValueError as error:
+            raise ValueError(
+                "MRJUNEJUNE_MOCK_DELAY_MS must be a non-negative integer"
+            ) from error
+        delay_ms = _require_non_negative_integer(
+            delay_ms,
+            "MRJUNEJUNE_MOCK_DELAY_MS",
+        )
+        config = MockConfig(delay_ms, config.fallback, config.commands)
+    asyncio.run(run(config))
+
+
+if __name__ == "__main__":
+    main()