agentic-python-coder 3.4.4__tar.gz → 3.5.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.
Files changed (55) hide show
  1. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/PKG-INFO +4 -2
  2. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/kernel.py +204 -44
  3. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/mcp_server.py +145 -94
  4. agentic_python_coder-3.5.0/coder/src/agentic_python_coder/runner.py +235 -0
  5. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/pyproject.toml +5 -2
  6. agentic_python_coder-3.4.4/coder/src/agentic_python_coder/runner.py +0 -334
  7. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/.gitignore +0 -0
  8. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/LICENSE +0 -0
  9. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/README.md +0 -0
  10. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/.gitignore +0 -0
  11. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/prompts/system.md +0 -0
  12. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/prompts/system_todo.md +0 -0
  13. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/__init__.py +0 -0
  14. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/agent.py +0 -0
  15. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/cli.py +0 -0
  16. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
  17. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
  18. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/clingo.md +0 -0
  19. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/bird_reasoning.md +0 -0
  20. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/diagnosis.md +0 -0
  21. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/simple_coloring.md +0 -0
  22. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/stable_marriage.md +0 -0
  23. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/sudoku_mini.md +0 -0
  24. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
  25. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -0
  26. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/magic_square.md +0 -0
  27. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/n_queens.md +0 -0
  28. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
  29. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
  30. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/email_extraction.md +0 -0
  31. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/phone_validation.md +0 -0
  32. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/url_parsing.md +0 -0
  33. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/llm.py +0 -0
  34. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/deepseek31.json +0 -0
  35. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini25.json +0 -0
  36. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini31.json +0 -0
  37. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini3flash.json +0 -0
  38. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini4argon.json +0 -0
  39. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt52.json +0 -0
  40. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56sol.json +0 -0
  41. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56terra.json +0 -0
  42. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6astra.json +0 -0
  43. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6sol.json +0 -0
  44. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/grok47.json +0 -0
  45. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus45.json +0 -0
  46. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus48.json +0 -0
  47. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus55.json +0 -0
  48. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/qwen3.json +0 -0
  49. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet45.json +0 -0
  50. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet46.json +0 -0
  51. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet5.json +0 -0
  52. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet55.json +0 -0
  53. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/project_md.py +0 -0
  54. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/tools.py +0 -0
  55. {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/examples/clingo/clingo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-python-coder
3
- Version: 3.4.4
3
+ Version: 3.5.0
4
4
  Summary: A lightweight Python coding agent that writes, executes, and iterates on code through natural language instructions
5
5
  Author: Stefan Szeider
6
6
  License: Apache-2.0
@@ -17,9 +17,11 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
17
  Classifier: Topic :: Software Development :: Code Generators
18
18
  Requires-Python: >=3.13
19
19
  Requires-Dist: ipykernel>=7.3.0
20
+ Requires-Dist: jsonschema>=4.20.0
20
21
  Requires-Dist: jupyter-client>=8.9.1
21
- Requires-Dist: mcp<3,>=1.28.1
22
+ Requires-Dist: mcp<3,>=2
22
23
  Requires-Dist: openai>=2.45.0
24
+ Requires-Dist: packaging>=22
23
25
  Requires-Dist: python-dotenv>=1.2.2
24
26
  Requires-Dist: pyyaml>=6.0.3
25
27
  Requires-Dist: rich>=15.0.0
@@ -21,6 +21,7 @@ from queue import Empty
21
21
  from typing import Any, Dict, List, Optional
22
22
 
23
23
  from jupyter_client import KernelManager
24
+ from jupyter_client.kernelspec import KernelSpecManager
24
25
 
25
26
  log_level = os.environ.get("CODER_LOG_LEVEL", "WARNING").upper()
26
27
  logging.basicConfig(
@@ -45,14 +46,50 @@ class KernelState:
45
46
  last_used: float = field(default_factory=time.time)
46
47
 
47
48
 
49
+ def _dead_output() -> Dict[str, Any]:
50
+ return {
51
+ "stdout": "",
52
+ "stderr": "",
53
+ "result": None,
54
+ "error": "Kernel died during execution",
55
+ }
56
+
57
+
48
58
  # Kernel registry and global lock
49
59
  _kernels: Dict[str, Optional[KernelState]] = {}
50
60
  _kernels_lock = threading.Lock()
51
61
 
62
+
63
+ @dataclass
64
+ class _CrashRecord:
65
+ """Restart configuration of a kernel whose process died."""
66
+
67
+ packages: List[str]
68
+ cwd: Optional[str]
69
+
70
+
71
+ # Kernels whose process died, by ID, so that restart_kernel() can bring them
72
+ # back with the same packages and cwd. Guarded by _kernels_lock.
73
+ _crashed: Dict[str, _CrashRecord] = {}
74
+
75
+ # ANSI escape sequences (IPython colours its tracebacks)
76
+ _ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
77
+
52
78
  # Suppress Jupyter warnings
53
79
  warnings.filterwarnings("ignore", category=DeprecationWarning)
54
80
 
55
81
 
82
+ def _native_kernel_spec_manager() -> KernelSpecManager:
83
+ """Kernelspec manager that only knows ipykernel's built-in "python3" spec.
84
+
85
+ With the default search path, a user or system kernelspec named
86
+ "python3" (Anaconda, Jupyter installs) would replace the interpreter,
87
+ environment and launch arguments. The built-in spec launches
88
+ ``sys.executable -m ipykernel_launcher -f {connection_file}``.
89
+ """
90
+ return KernelSpecManager(kernel_dirs=[])
91
+
92
+
56
93
  class UVKernelManager(KernelManager):
57
94
  """KernelManager that can wrap kernel launch with UV for dynamic packages."""
58
95
 
@@ -170,11 +207,17 @@ class PythonKernel:
170
207
  if with_packages is not None:
171
208
  logger.info(f"Starting UV kernel with packages: {with_packages}")
172
209
  self.km = UVKernelManager(
173
- with_packages=with_packages, cwd=cwd, kernel_name="python3"
210
+ with_packages=with_packages,
211
+ cwd=cwd,
212
+ kernel_name="python3",
213
+ kernel_spec_manager=_native_kernel_spec_manager(),
174
214
  )
175
215
  else:
176
216
  logger.info("Starting standard kernel (no dynamic packages)")
177
- self.km = KernelManager(kernel_name="python3")
217
+ self.km = KernelManager(
218
+ kernel_name="python3",
219
+ kernel_spec_manager=_native_kernel_spec_manager(),
220
+ )
178
221
 
179
222
  # Start the kernel
180
223
  logger.debug(f"Starting kernel in directory: {cwd}")
@@ -200,30 +243,35 @@ class PythonKernel:
200
243
 
201
244
  def _inject_startup_code(self):
202
245
  """Inject safety and configuration code: block input(), set matplotlib, etc."""
246
+ # Runs inside a function so its imports do not leak into the user
247
+ # namespace (code relying on them would fail when run standalone)
203
248
  startup_code = """
204
- import sys
205
- import warnings
206
- import builtins
207
- from IPython.core.interactiveshell import InteractiveShell
208
-
209
- # Configure IPython to display last expression value
210
- InteractiveShell.ast_node_interactivity = 'last_expr'
211
-
212
- # Suppress pkg_resources deprecation warning from CPMpy
213
- warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
214
- warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
215
-
216
- # Block interactive input() - not supported in MCP/automated sessions
217
- def _blocked_input(*args, **kwargs):
218
- raise RuntimeError("Interactive input() not supported")
219
- builtins.input = _blocked_input
220
-
221
- # Set matplotlib to non-interactive backend if available
222
- try:
223
- import matplotlib
224
- matplotlib.use('Agg')
225
- except ImportError:
226
- pass
249
+ def __coder_startup():
250
+ import warnings
251
+ import builtins
252
+ from IPython.core.interactiveshell import InteractiveShell
253
+
254
+ # Configure IPython to display last expression value
255
+ InteractiveShell.ast_node_interactivity = 'last_expr'
256
+
257
+ # Suppress pkg_resources deprecation warning from CPMpy
258
+ warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
259
+ warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
260
+
261
+ # Block interactive input() - not supported in MCP/automated sessions
262
+ def _blocked_input(*args, **kwargs):
263
+ raise RuntimeError("Interactive input() not supported")
264
+ builtins.input = _blocked_input
265
+
266
+ # Set matplotlib to non-interactive backend if available
267
+ try:
268
+ import matplotlib
269
+ matplotlib.use('Agg')
270
+ except ImportError:
271
+ pass
272
+
273
+ __coder_startup()
274
+ del __coder_startup
227
275
  """
228
276
  self._execute_raw(startup_code)
229
277
 
@@ -238,20 +286,53 @@ except ImportError:
238
286
  except Exception:
239
287
  pass # Best effort cleanup
240
288
 
241
- def _execute_raw(self, code: str, deadline_timeout: int = 60) -> Dict[str, str]:
289
+ def _alive(self) -> bool:
290
+ """True if the kernel process runs (False once shut down/retired)."""
291
+ km = self.km if hasattr(self, "km") else None
292
+ return km is not None and km.is_alive()
293
+
294
+ def _execute_raw(
295
+ self,
296
+ code: str,
297
+ deadline_timeout: int = 60,
298
+ store_history: bool = True,
299
+ interrupt_on_timeout: bool = True,
300
+ ) -> Dict[str, str]:
242
301
  """Execute code without locking (internal use only).
243
302
 
244
303
  Args:
245
304
  code: Python code to execute
246
305
  deadline_timeout: Hard deadline in seconds for entire execution
306
+ store_history: Record the cell in IPython history
307
+ interrupt_on_timeout: Interrupt the kernel when the deadline hits
247
308
 
248
309
  Returns:
249
310
  Dict with stdout, stderr, result, and error fields
250
311
  """
312
+ return self._execute_collect(
313
+ code, deadline_timeout, store_history, interrupt_on_timeout
314
+ )[0]
315
+
316
+ def _execute_collect(
317
+ self,
318
+ code: str,
319
+ deadline_timeout: int = 60,
320
+ store_history: bool = True,
321
+ interrupt_on_timeout: bool = True,
322
+ ) -> tuple:
323
+ """Like _execute_raw, but returns (output, completed).
324
+
325
+ completed is False when the kernel died or the deadline hit before
326
+ the kernel reported idle.
327
+ """
328
+ if self.kc is None or not self._alive():
329
+ # Shut down or retired by another caller while we waited
330
+ return _dead_output(), False
331
+
251
332
  # Send the execution request (silent=False to get execute_result)
252
333
  # allow_stdin=False prevents kernel from requesting input
253
334
  msg_id = self.kc.execute(
254
- code, silent=False, store_history=True, allow_stdin=False
335
+ code, silent=False, store_history=store_history, allow_stdin=False
255
336
  )
256
337
 
257
338
  # Collect output
@@ -279,17 +360,23 @@ except ImportError:
279
360
  elif msg_type == "execute_result":
280
361
  output["result"] = content["data"].get("text/plain", "")
281
362
  elif msg_type == "error":
282
- output["error"] = f"{content['ename']}: {content['evalue']}"
363
+ error = f"{content['ename']}: {content['evalue']}"
283
364
  # Include traceback for debugging
284
365
  if content.get("traceback"):
285
- output["error"] += "\n" + "\n".join(content["traceback"])
366
+ error += "\n" + "\n".join(content["traceback"])
367
+ output["error"] = _ANSI_RE.sub("", error)
286
368
  elif msg_type == "status" and content["execution_state"] == "idle":
287
369
  completed = True
288
370
  break # Only exit on idle status
289
371
  except Empty:
372
+ # No idle status will ever come from a dead kernel: report
373
+ # the crash now instead of waiting for the deadline
374
+ if not self._alive():
375
+ output["error"] = "Kernel died during execution"
376
+ return output, False
290
377
  continue # Keep waiting, don't break
291
378
 
292
- if not completed:
379
+ if not completed and interrupt_on_timeout:
293
380
  # Deadline hit while the kernel was still running: report a
294
381
  # timeout instead of silently returning partial output as success,
295
382
  # and interrupt so the kernel doesn't block the next execution
@@ -304,7 +391,7 @@ except ImportError:
304
391
  except Exception:
305
392
  pass # Best effort
306
393
 
307
- return output
394
+ return output, completed
308
395
 
309
396
  def execute(self, code: str, deadline_timeout: int = 60) -> Dict[str, str]:
310
397
  """Execute code with thread-safe locking.
@@ -346,6 +433,10 @@ except ImportError:
346
433
  try:
347
434
  if self.km.is_alive():
348
435
  self.km.shutdown_kernel(now=True)
436
+ else:
437
+ # Dead kernel: still release the control socket,
438
+ # zmq context and connection file
439
+ self.km.cleanup_resources()
349
440
  except Exception:
350
441
  pass
351
442
  self.km = None # Clear reference
@@ -368,6 +459,25 @@ except ImportError:
368
459
  # =============================================================================
369
460
 
370
461
 
462
+ def _retire_dead_kernel(kernel_id: str, state: KernelState) -> None:
463
+ """Drop a dead kernel from the registry, keeping its config for restart.
464
+
465
+ Releases the dead kernel's client channels and manager resources.
466
+ Callers must not hold the kernel's exec lock.
467
+ """
468
+ with _kernels_lock:
469
+ # Record only if this call removed this exact state: a delayed
470
+ # caller must not resurrect an ID that was reset or shut down since
471
+ if _kernels.get(kernel_id) is state:
472
+ _kernels.pop(kernel_id)
473
+ _crashed[kernel_id] = _CrashRecord(list(state.packages), state.cwd)
474
+ try:
475
+ with state.kernel._exec_lock:
476
+ state.kernel.shutdown()
477
+ except Exception:
478
+ pass # Best effort
479
+
480
+
371
481
  def create_kernel(
372
482
  kernel_id: Optional[str] = None,
373
483
  with_packages: Optional[List[str]] = None,
@@ -418,6 +528,7 @@ def create_kernel(
418
528
  )
419
529
  with _kernels_lock:
420
530
  _kernels[kernel_id] = state
531
+ _crashed.pop(kernel_id, None)
421
532
  logger.info(f"Created kernel {kernel_id}")
422
533
  return kernel_id
423
534
  except Exception:
@@ -453,9 +564,8 @@ def execute_in_kernel(
453
564
  raise KeyError(f"Kernel {kernel_id} is being created")
454
565
 
455
566
  # Check if alive before execution
456
- if not state.kernel.km.is_alive():
457
- with _kernels_lock:
458
- _kernels.pop(kernel_id, None)
567
+ if not state.kernel._alive():
568
+ _retire_dead_kernel(kernel_id, state)
459
569
  raise RuntimeError(f"Kernel {kernel_id} is dead")
460
570
 
461
571
  # Execute (uses per-kernel lock internally)
@@ -464,17 +574,54 @@ def execute_in_kernel(
464
574
  # Update last_used
465
575
  state.last_used = time.time()
466
576
 
467
- # Check if crashed during execution
468
- if not state.kernel.km.is_alive():
469
- with _kernels_lock:
470
- _kernels.pop(kernel_id, None)
471
- result["error"] = f"Kernel {kernel_id} crashed during execution"
577
+ # Check if crashed during execution (or retired by another caller)
578
+ if not state.kernel._alive():
579
+ _retire_dead_kernel(kernel_id, state)
580
+ result["error"] = (
581
+ f"Kernel {kernel_id} crashed during execution."
582
+ " Call python_reset to restart it."
583
+ )
472
584
 
473
585
  # Always include kernel_id in result
474
586
  result["kernel_id"] = kernel_id
475
587
  return result
476
588
 
477
589
 
590
+ def probe_kernel(
591
+ kernel_id: str, codes: List[str], timeout: int
592
+ ) -> Optional[List[Dict[str, Any]]]:
593
+ """Run introspection snippets in an idle kernel (for python_status).
594
+
595
+ Never waits behind or interrupts running user code: returns None when
596
+ the kernel is unknown, dead or busy; a dead kernel is retired. Snippets
597
+ are not stored in the IPython history. If a snippet does not complete,
598
+ the remaining ones are not sent and the results so far are returned.
599
+ """
600
+ with _kernels_lock:
601
+ state = _kernels.get(kernel_id)
602
+ if state is None:
603
+ return None
604
+ kernel = state.kernel
605
+ if not kernel._exec_lock.acquire(blocking=False):
606
+ return None # busy
607
+ results: List[Dict[str, Any]] = []
608
+ try:
609
+ if kernel._alive():
610
+ for code in codes:
611
+ output, completed = kernel._execute_collect(
612
+ code, timeout, store_history=False, interrupt_on_timeout=False
613
+ )
614
+ if not completed:
615
+ break
616
+ results.append(output)
617
+ finally:
618
+ kernel._exec_lock.release()
619
+ if not kernel._alive():
620
+ _retire_dead_kernel(kernel_id, state)
621
+ return None
622
+ return results
623
+
624
+
478
625
  def shutdown_kernel_by_id(kernel_id: str) -> bool:
479
626
  """Shutdown specific kernel.
480
627
 
@@ -483,6 +630,7 @@ def shutdown_kernel_by_id(kernel_id: str) -> bool:
483
630
  """
484
631
  with _kernels_lock:
485
632
  state = _kernels.pop(kernel_id, None)
633
+ _crashed.pop(kernel_id, None) # an explicit shutdown forgets a crash
486
634
 
487
635
  if state is None:
488
636
  return False
@@ -508,15 +656,15 @@ def interrupt_kernel_by_id(kernel_id: str) -> bool:
508
656
  return False
509
657
  state = _kernels[kernel_id]
510
658
 
511
- if state is None or not state.kernel.km.is_alive():
659
+ km = state.kernel.km if state is not None else None
660
+ if km is None or not km.is_alive():
512
661
  return False
513
662
 
514
- state.kernel.km.interrupt_kernel()
663
+ km.interrupt_kernel()
515
664
  time.sleep(0.5) # Grace period
516
665
 
517
- if not state.kernel.km.is_alive():
518
- with _kernels_lock:
519
- _kernels.pop(kernel_id, None)
666
+ if not state.kernel._alive():
667
+ _retire_dead_kernel(kernel_id, state)
520
668
  raise RuntimeError(f"Kernel {kernel_id} died from interrupt")
521
669
 
522
670
  return True
@@ -532,10 +680,21 @@ def restart_kernel(
532
680
  kernel_id: Kernel to restart.
533
681
  with_packages: New packages (None = keep existing).
534
682
 
683
+ A kernel that died (and was dropped from the registry) is restarted
684
+ with the packages and cwd it had.
685
+
535
686
  Raises:
536
687
  KeyError: If kernel not found.
537
688
  RuntimeError: If new kernel fails to start.
538
689
  """
690
+ with _kernels_lock:
691
+ crashed = None if kernel_id in _kernels else _crashed.get(kernel_id)
692
+ if crashed is not None:
693
+ packages = with_packages if with_packages is not None else crashed.packages
694
+ create_kernel(kernel_id, packages, crashed.cwd)
695
+ logger.info(f"Restarted crashed kernel {kernel_id}")
696
+ return
697
+
539
698
  with _kernels_lock:
540
699
  if kernel_id not in _kernels:
541
700
  raise KeyError(f"Unknown kernel: {kernel_id}")
@@ -627,6 +786,7 @@ def shutdown_all_kernels():
627
786
  """Shutdown all kernels. Called by atexit."""
628
787
  with _kernels_lock:
629
788
  kernel_ids = list(_kernels.keys())
789
+ _crashed.clear()
630
790
 
631
791
  for kid in kernel_ids:
632
792
  try: