mrmd-python 0.3.8__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maxime Rivest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrmd-python
3
- Version: 0.3.8
3
+ Version: 0.4.0
4
4
  Summary: Python runtime server implementing the MRMD Runtime Protocol (MRP)
5
5
  Author: mrmd contributors
6
6
  License: MIT
7
+ License-File: LICENSE
7
8
  Keywords: ipython,mrmd,mrp,notebook,runtime
8
9
  Classifier: Development Status :: 3 - Alpha
9
10
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mrmd-python"
3
- version = "0.3.8"
3
+ version = "0.4.0"
4
4
  description = "Python runtime server implementing the MRMD Runtime Protocol (MRP)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -66,6 +66,11 @@ Using with uvx (no install needed):
66
66
  default=0,
67
67
  help="Port to bind to (0 = auto-assign)",
68
68
  )
69
+ parser.add_argument(
70
+ "--host",
71
+ default="localhost",
72
+ help="Host to bind to (default: localhost, use 0.0.0.0 for containers)",
73
+ )
69
74
  parser.add_argument(
70
75
  "--venv",
71
76
  default=None,
@@ -196,7 +201,7 @@ Using with uvx (no install needed):
196
201
  print(f" cwd: {cwd}")
197
202
  print(f" port: {args.port or 'auto'}")
198
203
  print(f"\nPress Ctrl+C to stop\n")
199
- run_daemon(args.id, args.port, venv, cwd, args.assets_dir)
204
+ run_daemon(args.id, args.port, venv, cwd, args.assets_dir, host=args.host)
200
205
  else:
201
206
  # Spawn daemon (non-blocking)
202
207
  print(f"Starting mrmd-python daemon...")
@@ -144,7 +144,6 @@ class DaemonRuntimeClient:
144
144
  data = {
145
145
  "code": code,
146
146
  "storeHistory": store_history,
147
- "session": "default", # Daemon uses default session
148
147
  }
149
148
  if exec_id:
150
149
  data["execId"] = exec_id
@@ -170,7 +169,6 @@ class DaemonRuntimeClient:
170
169
  data = {
171
170
  "code": code,
172
171
  "storeHistory": store_history,
173
- "session": "default",
174
172
  }
175
173
  if exec_id:
176
174
  data["execId"] = exec_id
@@ -225,7 +223,6 @@ class DaemonRuntimeClient:
225
223
  result = self._post("/complete", {
226
224
  "code": code,
227
225
  "cursor": cursor_pos,
228
- "session": "default",
229
226
  })
230
227
 
231
228
  matches = []
@@ -252,7 +249,6 @@ class DaemonRuntimeClient:
252
249
  "code": code,
253
250
  "cursor": cursor_pos,
254
251
  "detail": detail,
255
- "session": "default",
256
252
  })
257
253
 
258
254
  return InspectResult(
@@ -273,7 +269,6 @@ class DaemonRuntimeClient:
273
269
  result = self._post("/hover", {
274
270
  "code": code,
275
271
  "cursor": cursor_pos,
276
- "session": "default",
277
272
  })
278
273
 
279
274
  return HoverResult(
@@ -286,7 +281,7 @@ class DaemonRuntimeClient:
286
281
 
287
282
  def get_variables(self) -> VariablesResult:
288
283
  """Get user variables."""
289
- result = self._post("/variables", {"session": "default"})
284
+ result = self._post("/variables", {})
290
285
 
291
286
  variables = []
292
287
  for v in result.get("variables", []):
@@ -310,7 +305,6 @@ class DaemonRuntimeClient:
310
305
  def get_variable_detail(self, name: str, path: Optional[list[str]] = None) -> VariableDetail:
311
306
  """Get detailed info about a variable."""
312
307
  result = self._post(f"/variables/{name}", {
313
- "session": "default",
314
308
  "path": path,
315
309
  })
316
310
 
@@ -343,7 +337,6 @@ class DaemonRuntimeClient:
343
337
  """Check if code is a complete statement."""
344
338
  result = self._post("/is_complete", {
345
339
  "code": code,
346
- "session": "default",
347
340
  })
348
341
 
349
342
  return IsCompleteResult(
@@ -355,7 +348,6 @@ class DaemonRuntimeClient:
355
348
  """Format code using black."""
356
349
  result = self._post("/format", {
357
350
  "code": code,
358
- "session": "default",
359
351
  })
360
352
 
361
353
  return result.get("formatted", code), result.get("changed", False)
@@ -363,8 +355,7 @@ class DaemonRuntimeClient:
363
355
  def reset(self):
364
356
  """Reset the runtime namespace."""
365
357
  self._ensure_connected()
366
- # Use the session reset endpoint
367
- self._client.post("/sessions/default/reset")
358
+ self._client.post("/reset")
368
359
 
369
360
  def get_info(self) -> dict:
370
361
  """Get info about the daemon runtime."""
@@ -408,7 +399,7 @@ class DaemonRuntimeClient:
408
399
  Returns True if request was sent successfully.
409
400
  """
410
401
  try:
411
- result = self._post("/mrp/v1/interrupt", {"session": "default"})
402
+ result = self._post("/interrupt", {})
412
403
  return result.get("interrupted", False)
413
404
  except Exception:
414
405
  return False
@@ -226,6 +226,7 @@ def run_daemon(
226
226
  venv: str,
227
227
  cwd: Optional[str] = None,
228
228
  assets_dir: Optional[str] = None,
229
+ host: str = "localhost",
229
230
  ):
230
231
  """
231
232
  Run the daemon runtime server.
@@ -299,7 +300,7 @@ def run_daemon(
299
300
  # Run uvicorn (this blocks)
300
301
  uvicorn.run(
301
302
  app,
302
- host="localhost",
303
+ host=host,
303
304
  port=port,
304
305
  log_level="info",
305
306
  access_log=False,
@@ -7,7 +7,7 @@ The server exposes endpoints at /mrp/v1/* for:
7
7
  - Code execution (sync and streaming)
8
8
  - Completions, hover, and inspect
9
9
  - Variable inspection
10
- - Session management
10
+ - Runtime reset
11
11
  - Asset serving (for matplotlib figures, HTML output, etc.)
12
12
 
13
13
  Usage:
@@ -37,15 +37,12 @@ from starlette.middleware.cors import CORSMiddleware
37
37
  from sse_starlette.sse import EventSourceResponse
38
38
 
39
39
  from .runtime_client import DaemonRuntimeClient
40
- from .runtime_daemon import list_runtimes, is_runtime_alive, kill_runtime
41
40
  from .worker import IPythonWorker
42
- # All sessions use independent daemon runtimes for:
43
- # - GPU memory isolation (critical for vLLM)
44
- # - Process survival (daemon persists if orchestrator dies)
45
- # - Registry in ~/.mrmd/runtimes/ for discovery
41
+ # Runtime workers can run either:
42
+ # - locally in-process (daemon_mode=True), or
43
+ # - in an external daemon process via DaemonRuntimeClient.
46
44
  #
47
- # Exception: When running IN a daemon (daemon_mode=True), we use the local
48
- # IPythonWorker to avoid recursive daemon spawning.
45
+ # Runtime model is single-namespace: one server process == one REPL runtime.
49
46
  from .types import (
50
47
  Capabilities,
51
48
  CapabilityFeatures,
@@ -68,17 +65,12 @@ def _get_current_venv() -> str | None:
68
65
  return os.environ.get("VIRTUAL_ENV")
69
66
 
70
67
 
71
- class SessionManager:
72
- """Manages multiple Python sessions.
68
+ class RuntimeManager:
69
+ """Manages a single Python runtime process.
73
70
 
74
- Two modes of operation:
75
- 1. daemon_mode=True: Used when running INSIDE a daemon process.
76
- Uses IPythonWorker directly for local execution.
77
- This is a single, independent Python runtime.
78
-
79
- 2. daemon_mode=False (default): Used by orchestrators.
80
- Each session spawns an independent daemon process via DaemonRuntimeClient.
81
- Daemons survive if orchestrator dies, variables persist, GPU memory isolated.
71
+ Runtime model:
72
+ - one server process = one REPL namespace
73
+ - no per-request or per-document MRP sessions
82
74
  """
83
75
 
84
76
  def __init__(
@@ -92,75 +84,53 @@ class SessionManager:
92
84
  self.assets_dir = assets_dir
93
85
  self.default_venv = venv
94
86
  self.daemon_mode = daemon_mode
95
- self.sessions: dict[str, dict] = {}
96
- # In daemon_mode: workers dict holds IPythonWorker instances
97
- # In orchestrator mode: workers dict holds DaemonRuntimeClient instances
98
- self.workers: dict[str, IPythonWorker | DaemonRuntimeClient] = {}
87
+ self.runtime: dict | None = None
88
+ self.worker: IPythonWorker | DaemonRuntimeClient | None = None
99
89
  self._pending_inputs: dict[str, asyncio.Future] = {}
100
90
  self._lock = threading.Lock()
101
91
 
102
- def get_or_create_session(
92
+ def get_or_create_runtime(
103
93
  self,
104
- session_id: str,
105
94
  venv: str | None = None,
106
95
  cwd: str | None = None,
107
96
  ) -> tuple[IPythonWorker | DaemonRuntimeClient, dict]:
108
- """
109
- Get or create a session.
110
-
111
- Behavior depends on daemon_mode:
112
- - daemon_mode=True: Creates local IPythonWorker (for use inside daemon)
113
- - daemon_mode=False: Creates DaemonRuntimeClient (spawns independent daemon)
114
-
115
- Args:
116
- session_id: Unique session identifier
117
- venv: Path to virtual environment. If not provided, auto-detects.
118
- cwd: Working directory override for this session.
119
-
120
- Returns:
121
- Tuple of (worker, session_info)
122
- """
123
- print(f"[MRPServer.get_or_create_session] session_id={session_id}", flush=True)
124
- print(f"[MRPServer.get_or_create_session] venv param={venv}", flush=True)
125
- print(f"[MRPServer.get_or_create_session] self.default_venv={self.default_venv}", flush=True)
126
- print(f"[MRPServer.get_or_create_session] _get_current_venv()={_get_current_venv()}", flush=True)
127
- print(f"[MRPServer.get_or_create_session] sys.prefix={sys.prefix}", flush=True)
97
+ """Get or create the single runtime worker."""
98
+ print(f"[MRPServer.get_or_create_runtime] venv param={venv}", flush=True)
99
+ print(f"[MRPServer.get_or_create_runtime] self.default_venv={self.default_venv}", flush=True)
100
+ print(f"[MRPServer.get_or_create_runtime] _get_current_venv()={_get_current_venv()}", flush=True)
101
+ print(f"[MRPServer.get_or_create_runtime] sys.prefix={sys.prefix}", flush=True)
128
102
 
129
103
  effective_venv = venv or self.default_venv or _get_current_venv() or sys.prefix
130
104
  effective_cwd = cwd or self.cwd
131
105
 
132
- print(f"[MRPServer.get_or_create_session] effective_venv={effective_venv}", flush=True)
133
- print(f"[MRPServer.get_or_create_session] daemon_mode={self.daemon_mode}", flush=True)
106
+ print(f"[MRPServer.get_or_create_runtime] effective_venv={effective_venv}", flush=True)
107
+ print(f"[MRPServer.get_or_create_runtime] daemon_mode={self.daemon_mode}", flush=True)
134
108
 
135
109
  with self._lock:
136
- if session_id not in self.sessions:
137
- print(f"[MRPServer.get_or_create_session] Creating NEW session", flush=True)
110
+ if self.runtime is None or self.worker is None:
111
+ print("[MRPServer.get_or_create_runtime] Creating runtime", flush=True)
138
112
  if self.daemon_mode:
139
- # DAEMON MODE: Use local IPythonWorker
140
- # This is the actual execution engine inside the daemon
141
- print(f"[MRPServer.get_or_create_session] Creating IPythonWorker with venv={effective_venv}", flush=True)
142
113
  worker = IPythonWorker(
143
114
  cwd=effective_cwd,
144
115
  assets_dir=self.assets_dir,
145
116
  venv=effective_venv,
146
117
  )
147
- self.sessions[session_id] = {
148
- "id": session_id,
118
+ runtime = {
119
+ "id": "runtime",
149
120
  "language": "python",
150
121
  "created": datetime.now(timezone.utc).isoformat(),
151
122
  "lastActivity": datetime.now(timezone.utc).isoformat(),
152
123
  "executionCount": 0,
153
124
  "variableCount": 0,
154
- "workerType": "local", # Local IPython worker
125
+ "workerType": "local",
155
126
  "environment": {
156
127
  "cwd": effective_cwd,
157
128
  "virtualenv": effective_venv,
158
129
  },
159
130
  }
160
131
  else:
161
- # ORCHESTRATOR MODE: Spawn independent daemon via HTTP client
162
132
  client = DaemonRuntimeClient(
163
- runtime_id=session_id,
133
+ runtime_id="runtime",
164
134
  venv=effective_venv,
165
135
  cwd=effective_cwd,
166
136
  assets_dir=self.assets_dir,
@@ -168,14 +138,14 @@ class SessionManager:
168
138
  )
169
139
  daemon_info = client.get_info()
170
140
  worker = client
171
- self.sessions[session_id] = {
172
- "id": session_id,
141
+ runtime = {
142
+ "id": "runtime",
173
143
  "language": "python",
174
144
  "created": datetime.now(timezone.utc).isoformat(),
175
145
  "lastActivity": datetime.now(timezone.utc).isoformat(),
176
146
  "executionCount": 0,
177
147
  "variableCount": 0,
178
- "workerType": "daemon", # Independent daemon process
148
+ "workerType": "daemon",
179
149
  "daemon": {
180
150
  "pid": daemon_info.get("pid"),
181
151
  "port": daemon_info.get("port"),
@@ -187,51 +157,40 @@ class SessionManager:
187
157
  },
188
158
  }
189
159
 
190
- self.workers[session_id] = worker
191
- else:
192
- print(f"[MRPServer.get_or_create_session] Using EXISTING session", flush=True)
193
- existing_worker = self.workers.get(session_id)
194
- if existing_worker and hasattr(existing_worker, 'venv'):
195
- print(f"[MRPServer.get_or_create_session] Existing worker venv={existing_worker.venv}", flush=True)
196
-
197
- session = self.sessions[session_id]
198
- session["lastActivity"] = datetime.now(timezone.utc).isoformat()
199
- return self.workers[session_id], session
160
+ self.worker = worker
161
+ self.runtime = runtime
200
162
 
201
- def get_session(self, session_id: str) -> dict | None:
202
- """Get session info."""
203
- return self.sessions.get(session_id)
163
+ self.runtime["lastActivity"] = datetime.now(timezone.utc).isoformat()
164
+ return self.worker, self.runtime
204
165
 
205
- def list_sessions(self) -> list[dict]:
206
- """List all sessions."""
207
- return list(self.sessions.values())
166
+ def get_runtime(self) -> dict | None:
167
+ """Get runtime info if initialized."""
168
+ return self.runtime
208
169
 
209
- def destroy_session(self, session_id: str) -> bool:
210
- """
211
- Destroy a session and release all resources.
212
-
213
- In daemon mode: Resets the local IPythonWorker
214
- In orchestrator mode: Kills the daemon process, releasing GPU/VRAM
215
- """
216
- with self._lock:
217
- if session_id in self.sessions:
218
- if session_id in self.workers:
219
- worker = self.workers[session_id]
220
- if hasattr(worker, 'shutdown'):
221
- worker.shutdown() # DaemonRuntimeClient - kills daemon
222
- elif hasattr(worker, 'reset'):
223
- worker.reset() # IPythonWorker - just reset
224
- del self.workers[session_id]
225
-
226
- del self.sessions[session_id]
227
- return True
170
+ def reset_runtime(self) -> bool:
171
+ """Reset runtime state (clear namespace)."""
172
+ if not self.worker or not self.runtime:
228
173
  return False
229
-
230
- def get_worker_info(self, session_id: str) -> dict | None:
231
- """Get info about the worker for a session."""
232
- if session_id not in self.workers:
233
- return None
234
- return self.workers[session_id].get_info()
174
+ if hasattr(self.worker, 'reset'):
175
+ self.worker.reset()
176
+ self.runtime["executionCount"] = 0
177
+ self.runtime["variableCount"] = 0
178
+ self.runtime["lastActivity"] = datetime.now(timezone.utc).isoformat()
179
+ return True
180
+
181
+ def shutdown(self) -> bool:
182
+ """Shutdown runtime worker and clear metadata."""
183
+ with self._lock:
184
+ if self.worker is None:
185
+ return False
186
+ worker = self.worker
187
+ if hasattr(worker, 'shutdown'):
188
+ worker.shutdown()
189
+ elif hasattr(worker, 'reset'):
190
+ worker.reset()
191
+ self.worker = None
192
+ self.runtime = None
193
+ return True
235
194
 
236
195
  def register_pending_input(self, exec_id: str, loop: asyncio.AbstractEventLoop) -> asyncio.Future:
237
196
  """Register that an execution is waiting for input."""
@@ -249,15 +208,10 @@ class SessionManager:
249
208
  return False
250
209
 
251
210
  def cancel_pending_input(self, exec_id: str) -> bool:
252
- """Cancel a pending input request.
253
-
254
- This is called when the user dismisses the input field (e.g., cancels
255
- execution, navigates away) to unblock the waiting worker thread.
256
- """
211
+ """Cancel a pending input request."""
257
212
  if exec_id in self._pending_inputs:
258
213
  future = self._pending_inputs.pop(exec_id)
259
214
  if not future.done():
260
- # Set exception to unblock the waiting worker
261
215
  future.get_loop().call_soon_threadsafe(
262
216
  future.set_exception,
263
217
  InputCancelledError("Input cancelled by user")
@@ -280,7 +234,7 @@ class MRPServer:
280
234
  self.assets_dir = assets_dir or os.path.join(self.cwd, ".mrmd-assets")
281
235
  self.venv = venv
282
236
  self.daemon_mode = daemon_mode
283
- self.session_manager = SessionManager(
237
+ self.runtime_manager = RuntimeManager(
284
238
  cwd=self.cwd,
285
239
  assets_dir=self.assets_dir,
286
240
  venv=venv,
@@ -307,8 +261,6 @@ class MRPServer:
307
261
  format=True,
308
262
  assets=True,
309
263
  ),
310
- defaultSession="default",
311
- maxSessions=10,
312
264
  environment=Environment(
313
265
  cwd=self.cwd,
314
266
  executable=sys.executable,
@@ -325,74 +277,22 @@ class MRPServer:
325
277
  caps = self.get_capabilities()
326
278
  return JSONResponse(_dataclass_to_dict(caps))
327
279
 
328
- async def handle_list_sessions(self, request: Request) -> JSONResponse:
329
- """GET /sessions"""
330
- sessions = self.session_manager.list_sessions()
331
- return JSONResponse({"sessions": sessions})
332
-
333
- async def handle_create_session(self, request: Request) -> JSONResponse:
334
- """POST /sessions
335
-
336
- Create a new session with optional environment configuration.
337
-
338
- Request body:
339
- id: Optional session ID (generated if not provided)
340
- language: Language (default: python)
341
- environment:
342
- cwd: Working directory
343
- virtualenv: Path to venv (enables subprocess isolation)
344
- executable: Python executable (ignored, derived from venv)
345
- env: Environment variables (not yet implemented)
346
- dependencies: Package dependencies (not yet implemented)
347
- """
348
- body = await request.json()
349
- session_id = body.get("id", str(uuid.uuid4())[:8])
350
-
351
- # Extract environment config
352
- env_config = body.get("environment", {})
353
- venv = env_config.get("virtualenv")
354
- cwd = env_config.get("cwd")
355
-
356
- worker, session = self.session_manager.get_or_create_session(
357
- session_id,
358
- venv=venv,
359
- cwd=cwd,
360
- )
361
- return JSONResponse(session)
362
-
363
- async def handle_get_session(self, request: Request) -> JSONResponse:
364
- """GET /sessions/{id}"""
365
- session_id = request.path_params["id"]
366
- session = self.session_manager.get_session(session_id)
367
- if not session:
368
- return JSONResponse({"error": "Session not found"}, status_code=404)
369
- return JSONResponse(session)
370
-
371
- async def handle_delete_session(self, request: Request) -> JSONResponse:
372
- """DELETE /sessions/{id}"""
373
- session_id = request.path_params["id"]
374
- if self.session_manager.destroy_session(session_id):
375
- return JSONResponse({"success": True})
376
- return JSONResponse({"error": "Session not found"}, status_code=404)
377
-
378
- async def handle_reset_session(self, request: Request) -> JSONResponse:
379
- """POST /sessions/{id}/reset"""
380
- session_id = request.path_params["id"]
381
- worker, session = self.session_manager.get_or_create_session(session_id)
280
+ async def handle_reset_runtime(self, request: Request) -> JSONResponse:
281
+ """POST /reset"""
282
+ worker, runtime = self.runtime_manager.get_or_create_runtime()
382
283
  worker.reset()
383
- session["executionCount"] = 0
384
- session["variableCount"] = 0
284
+ runtime["executionCount"] = 0
285
+ runtime["variableCount"] = 0
385
286
  return JSONResponse({"success": True})
386
287
 
387
288
  async def handle_execute(self, request: Request) -> JSONResponse:
388
289
  """POST /execute"""
389
290
  body = await request.json()
390
291
  code = body.get("code", "")
391
- session_id = body.get("session", "default")
392
292
  store_history = body.get("storeHistory", True)
393
293
  exec_id = body.get("execId", str(uuid.uuid4())[:8])
394
294
 
395
- worker, session = self.session_manager.get_or_create_session(session_id)
295
+ worker, runtime = self.runtime_manager.get_or_create_runtime()
396
296
 
397
297
  # Run in thread pool to not block
398
298
  loop = asyncio.get_event_loop()
@@ -400,8 +300,8 @@ class MRPServer:
400
300
  None, lambda: worker.execute(code, store_history, exec_id)
401
301
  )
402
302
 
403
- session["executionCount"] = result.executionCount
404
- session["variableCount"] = len(worker.get_variables().variables)
303
+ runtime["executionCount"] = result.executionCount
304
+ runtime["variableCount"] = len(worker.get_variables().variables)
405
305
 
406
306
  return JSONResponse(_dataclass_to_dict(result))
407
307
 
@@ -410,12 +310,11 @@ class MRPServer:
410
310
  print(f"[DEBUG] handle_execute_stream called", flush=True)
411
311
  body = await request.json()
412
312
  code = body.get("code", "")
413
- session_id = body.get("session", "default")
414
313
  store_history = body.get("storeHistory", True)
415
314
  exec_id = body.get("execId", str(uuid.uuid4())[:8])
416
- print(f"[DEBUG] session_id={session_id}, code length={len(code)}, exec_id={exec_id}", flush=True)
315
+ print(f"[DEBUG] code length={len(code)}, exec_id={exec_id}", flush=True)
417
316
 
418
- worker, session = self.session_manager.get_or_create_session(session_id)
317
+ worker, runtime = self.runtime_manager.get_or_create_runtime()
419
318
  print(f"[DEBUG] Got worker, starting event generator", flush=True)
420
319
 
421
320
  async def event_generator():
@@ -468,7 +367,7 @@ class MRPServer:
468
367
  )
469
368
 
470
369
  # Register that we're waiting for input and get a future
471
- future = self.session_manager.register_pending_input(exec_id, loop)
370
+ future = self.runtime_manager.register_pending_input(exec_id, loop)
472
371
 
473
372
  # Wait for the input (blocking - we're in a worker thread)
474
373
  # Use run_coroutine_threadsafe to wait on the future from this thread
@@ -529,8 +428,8 @@ class MRPServer:
529
428
 
530
429
  result = result_holder[0]
531
430
  if result:
532
- session["executionCount"] = result.executionCount
533
- session["variableCount"] = len(worker.get_variables().variables)
431
+ runtime["executionCount"] = result.executionCount
432
+ runtime["variableCount"] = len(worker.get_variables().variables)
534
433
 
535
434
  if result.success:
536
435
  yield {
@@ -553,7 +452,7 @@ class MRPServer:
553
452
  exec_id = body.get("exec_id", "")
554
453
  text = body.get("text", "")
555
454
 
556
- if self.session_manager.provide_input(exec_id, text):
455
+ if self.runtime_manager.provide_input(exec_id, text):
557
456
  return JSONResponse({"accepted": True})
558
457
  return JSONResponse({"accepted": False, "error": "No pending input request"})
559
458
 
@@ -566,27 +465,17 @@ class MRPServer:
566
465
  body = await request.json()
567
466
  exec_id = body.get("exec_id", "")
568
467
 
569
- if self.session_manager.cancel_pending_input(exec_id):
468
+ if self.runtime_manager.cancel_pending_input(exec_id):
570
469
  return JSONResponse({"cancelled": True})
571
470
  return JSONResponse({"cancelled": False, "error": "No pending input request"})
572
471
 
573
472
  async def handle_interrupt(self, request: Request) -> JSONResponse:
574
473
  """POST /interrupt
575
474
 
576
- Interrupt currently running code in a session.
475
+ Interrupt currently running code in the runtime.
577
476
  Sends SIGINT to subprocess workers or sets interrupt flag for local workers.
578
477
  """
579
- body = await request.json()
580
- session_id = body.get("session", "default")
581
-
582
- # Get the worker for this session (don't create if doesn't exist)
583
- session = self.session_manager.get_session(session_id)
584
- if not session:
585
- return JSONResponse({"interrupted": False, "error": "Session not found"})
586
-
587
- worker = self.session_manager.workers.get(session_id)
588
- if not worker:
589
- return JSONResponse({"interrupted": False, "error": "Worker not found"})
478
+ worker, _ = self.runtime_manager.get_or_create_runtime()
590
479
 
591
480
  # Call interrupt on the worker
592
481
  try:
@@ -600,9 +489,8 @@ class MRPServer:
600
489
  body = await request.json()
601
490
  code = body.get("code", "")
602
491
  cursor = body.get("cursor", len(code))
603
- session_id = body.get("session", "default")
604
492
 
605
- worker, _ = self.session_manager.get_or_create_session(session_id)
493
+ worker, _ = self.runtime_manager.get_or_create_runtime()
606
494
 
607
495
  loop = asyncio.get_event_loop()
608
496
  result = await loop.run_in_executor(
@@ -616,10 +504,9 @@ class MRPServer:
616
504
  body = await request.json()
617
505
  code = body.get("code", "")
618
506
  cursor = body.get("cursor", len(code))
619
- session_id = body.get("session", "default")
620
507
  detail = body.get("detail", 1)
621
508
 
622
- worker, _ = self.session_manager.get_or_create_session(session_id)
509
+ worker, _ = self.runtime_manager.get_or_create_runtime()
623
510
 
624
511
  loop = asyncio.get_event_loop()
625
512
  result = await loop.run_in_executor(
@@ -633,9 +520,8 @@ class MRPServer:
633
520
  body = await request.json()
634
521
  code = body.get("code", "")
635
522
  cursor = body.get("cursor", len(code))
636
- session_id = body.get("session", "default")
637
523
 
638
- worker, _ = self.session_manager.get_or_create_session(session_id)
524
+ worker, _ = self.runtime_manager.get_or_create_runtime()
639
525
 
640
526
  loop = asyncio.get_event_loop()
641
527
  result = await loop.run_in_executor(
@@ -647,9 +533,8 @@ class MRPServer:
647
533
  async def handle_variables(self, request: Request) -> JSONResponse:
648
534
  """POST /variables"""
649
535
  body = await request.json()
650
- session_id = body.get("session", "default")
651
536
 
652
- worker, _ = self.session_manager.get_or_create_session(session_id)
537
+ worker, _ = self.runtime_manager.get_or_create_runtime()
653
538
 
654
539
  loop = asyncio.get_event_loop()
655
540
  result = await loop.run_in_executor(None, worker.get_variables)
@@ -660,10 +545,9 @@ class MRPServer:
660
545
  """POST /variables/{name}"""
661
546
  name = request.path_params["name"]
662
547
  body = await request.json()
663
- session_id = body.get("session", "default")
664
548
  path = body.get("path")
665
549
 
666
- worker, _ = self.session_manager.get_or_create_session(session_id)
550
+ worker, _ = self.runtime_manager.get_or_create_runtime()
667
551
 
668
552
  loop = asyncio.get_event_loop()
669
553
  result = await loop.run_in_executor(
@@ -676,9 +560,8 @@ class MRPServer:
676
560
  """POST /is_complete"""
677
561
  body = await request.json()
678
562
  code = body.get("code", "")
679
- session_id = body.get("session", "default")
680
563
 
681
- worker, _ = self.session_manager.get_or_create_session(session_id)
564
+ worker, _ = self.runtime_manager.get_or_create_runtime()
682
565
 
683
566
  loop = asyncio.get_event_loop()
684
567
  result = await loop.run_in_executor(
@@ -691,9 +574,8 @@ class MRPServer:
691
574
  """POST /format"""
692
575
  body = await request.json()
693
576
  code = body.get("code", "")
694
- session_id = body.get("session", "default")
695
577
 
696
- worker, _ = self.session_manager.get_or_create_session(session_id)
578
+ worker, _ = self.runtime_manager.get_or_create_runtime()
697
579
 
698
580
  loop = asyncio.get_event_loop()
699
581
  formatted, changed = await loop.run_in_executor(
@@ -728,11 +610,7 @@ class MRPServer:
728
610
  """Create all routes."""
729
611
  return [
730
612
  Route("/mrp/v1/capabilities", self.handle_capabilities, methods=["GET"]),
731
- Route("/mrp/v1/sessions", self.handle_list_sessions, methods=["GET"]),
732
- Route("/mrp/v1/sessions", self.handle_create_session, methods=["POST"]),
733
- Route("/mrp/v1/sessions/{id}", self.handle_get_session, methods=["GET"]),
734
- Route("/mrp/v1/sessions/{id}", self.handle_delete_session, methods=["DELETE"]),
735
- Route("/mrp/v1/sessions/{id}/reset", self.handle_reset_session, methods=["POST"]),
613
+ Route("/mrp/v1/reset", self.handle_reset_runtime, methods=["POST"]),
736
614
  Route("/mrp/v1/execute", self.handle_execute, methods=["POST"]),
737
615
  Route("/mrp/v1/execute/stream", self.handle_execute_stream, methods=["POST"]),
738
616
  Route("/mrp/v1/input", self.handle_input, methods=["POST"]),
@@ -778,7 +656,7 @@ def create_app(
778
656
  assets_dir: Directory for saving assets (plots, etc.)
779
657
  venv: Path to virtual environment to use for code execution.
780
658
  daemon_mode: If True, use local IPythonWorker (for daemon process).
781
- If False, spawn independent daemon processes for each session.
659
+ If False, use an external daemon-backed runtime worker.
782
660
  """
783
661
  server = MRPServer(cwd=cwd, assets_dir=assets_dir, venv=venv, daemon_mode=daemon_mode)
784
662
 
@@ -42,27 +42,10 @@ class Capabilities:
42
42
  version: str = ""
43
43
  languages: list[str] = field(default_factory=lambda: ["python", "py", "python3"])
44
44
  features: CapabilityFeatures = field(default_factory=CapabilityFeatures)
45
- defaultSession: str = "default"
46
- maxSessions: int = 10
47
45
  environment: Environment = field(default_factory=Environment)
48
46
  lspFallback: str | None = None
49
47
 
50
48
 
51
- # =============================================================================
52
- # Sessions
53
- # =============================================================================
54
-
55
-
56
- @dataclass
57
- class Session:
58
- id: str
59
- language: str = "python"
60
- created: str = ""
61
- lastActivity: str = ""
62
- executionCount: int = 0
63
- variableCount: int = 0
64
-
65
-
66
49
  # =============================================================================
67
50
  # Execution
68
51
  # =============================================================================
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.10"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.11'",
@@ -285,7 +285,7 @@ wheels = [
285
285
 
286
286
  [[package]]
287
287
  name = "mrmd-python"
288
- version = "0.3.6"
288
+ version = "0.3.8"
289
289
  source = { editable = "." }
290
290
  dependencies = [
291
291
  { name = "black" },
File without changes
File without changes