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.
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/PKG-INFO +4 -2
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/kernel.py +204 -44
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/mcp_server.py +145 -94
- agentic_python_coder-3.5.0/coder/src/agentic_python_coder/runner.py +235 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/pyproject.toml +5 -2
- agentic_python_coder-3.4.4/coder/src/agentic_python_coder/runner.py +0 -334
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/.gitignore +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/LICENSE +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/README.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/.gitignore +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/prompts/system.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/prompts/system_todo.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/__init__.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/agent.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/cli.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/clingo.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -0
- {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
- {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
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
- {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
- {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
- {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
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/llm.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/deepseek31.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini25.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini31.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini3flash.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini4argon.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt52.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56sol.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56terra.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6astra.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6sol.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/grok47.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus45.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus48.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus55.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/qwen3.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet45.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet46.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet5.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet55.json +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/project_md.py +0 -0
- {agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/tools.py +0 -0
- {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.
|
|
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,>=
|
|
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
|
{agentic_python_coder-3.4.4 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/kernel.py
RENAMED
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
builtins.input = _blocked_input
|
|
220
|
-
|
|
221
|
-
# Set matplotlib to non-interactive backend if available
|
|
222
|
-
try:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
except ImportError:
|
|
226
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
363
|
+
error = f"{content['ename']}: {content['evalue']}"
|
|
283
364
|
# Include traceback for debugging
|
|
284
365
|
if content.get("traceback"):
|
|
285
|
-
|
|
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.
|
|
457
|
-
|
|
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.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
663
|
+
km.interrupt_kernel()
|
|
515
664
|
time.sleep(0.5) # Grace period
|
|
516
665
|
|
|
517
|
-
if not state.kernel.
|
|
518
|
-
|
|
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:
|