Per-WS-connection agent state, bridging the worker thread ↔ asyncio loop.
| 95 | |
| 96 | @dataclass |
| 97 | class _AgentSession: |
| 98 | """Per-WS-connection agent state, bridging the worker thread ↔ asyncio loop.""" |
| 99 | |
| 100 | session_id: str |
| 101 | cwd: str |
| 102 | config: AgentServerConfig |
| 103 | loop: asyncio.AbstractEventLoop |
| 104 | out_queue: asyncio.Queue[dict | None] |
| 105 | |
| 106 | # Built lazily/eagerly at spawn; see ``_build_runtime``. |
| 107 | provider: Any = None |
| 108 | provider_name: str = "" |
| 109 | tool_registry: Any = None |
| 110 | tool_context: Any = None |
| 111 | session: Any = None |
| 112 | system_prompt: Any = "You are a helpful assistant." |
| 113 | _base_system_prompt: Any = None # system prompt before the /plan section is composed in |
| 114 | _language: Any = None # preferred response language (the original's LanguagePicker, §6) |
| 115 | _thinking: Any = None # extended-thinking override (ThinkingToggle); None = model default |
| 116 | init_error: str | None = None |
| 117 | _session_name: str | None = None # user-set label (/rename) shown in /resume |
| 118 | _mcp_runtime: Any = None # McpRuntime (connected MCP servers) when configured |
| 119 | _effort: str | None = None # /effort reasoning level, injected via extra_body when set |
| 120 | _knowledge: Any = None # KnowledgeGraph (lazy-loaded), populated at each turn end |
| 121 | _knowledge_enabled: bool = True # the original's knowledgeGraphEnabled (default on) |
| 122 | _knowledge_semantic: bool = False # opt-in model-based extraction (vs heuristic) |
| 123 | _bgtasks: Any = None # BackgroundTasks registry (lazy), the original's Ctrl+B runs |
| 124 | |
| 125 | # Worker + cross-thread coordination. |
| 126 | _inbox: _queue.Queue = field(default_factory=_queue.Queue) |
| 127 | _worker: threading.Thread | None = None |
| 128 | _stop: threading.Event = field(default_factory=threading.Event) |
| 129 | _lock: threading.Lock = field(default_factory=threading.Lock) |
| 130 | _pending: dict[str, _Pending] = field(default_factory=dict) |
| 131 | _current_abort: AbortController | None = None |
| 132 | |
| 133 | # ─── outbound helpers (worker thread → main loop) ────────────────────── |
| 134 | |
| 135 | def _emit(self, msg: dict) -> None: |
| 136 | """Thread-safe enqueue of one outbound SDK message. |
| 137 | |
| 138 | Every message is passed through ``_json_safe`` so a stray |
| 139 | non-serializable value can never make the server's ``json.dumps`` in |
| 140 | the WS pump raise and silently kill the outbound stream. |
| 141 | """ |
| 142 | try: |
| 143 | self.loop.call_soon_threadsafe(self.out_queue.put_nowait, _json_safe(msg)) |
| 144 | except RuntimeError: |
| 145 | # Loop closed (server shutting down) — drop. |
| 146 | pass |
| 147 | |
| 148 | def _close_stream(self) -> None: |
| 149 | try: |
| 150 | self.loop.call_soon_threadsafe(self.out_queue.put_nowait, None) |
| 151 | except RuntimeError: |
| 152 | pass |
| 153 | |
| 154 | # ─── init ────────────────────────────────────────────────────────────── |