agentic-python-coder 3.4.3__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.3 → agentic_python_coder-3.5.0}/PKG-INFO +4 -2
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/kernel.py +209 -45
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/mcp_server.py +167 -81
- agentic_python_coder-3.5.0/coder/src/agentic_python_coder/runner.py +235 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/pyproject.toml +9 -2
- agentic_python_coder-3.4.3/coder/src/agentic_python_coder/runner.py +0 -334
- agentic_python_coder-3.4.3/coder/tests/test_kernel.py +0 -212
- agentic_python_coder-3.4.3/coder/tests/test_library_api.py +0 -384
- agentic_python_coder-3.4.3/coder/tests/test_llm_config.py +0 -90
- agentic_python_coder-3.4.3/coder/tests/test_mcp_hardening.py +0 -275
- agentic_python_coder-3.4.3/coder/tests/test_mcp_server.py +0 -429
- agentic_python_coder-3.4.3/coder/tests/test_react_loop.py +0 -306
- agentic_python_coder-3.4.3/coder/tests/test_todo_flag_integration.py +0 -131
- agentic_python_coder-3.4.3/coder/tests/test_todo_tool_availability.py +0 -58
- agentic_python_coder-3.4.3/coder/tests/test_tool_descriptions.py +0 -74
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/.gitignore +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/LICENSE +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/.gitignore +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/prompts/system.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/prompts/system_todo.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/__init__.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/agent.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/cli.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/clingo.md +0 -0
- {agentic_python_coder-3.4.3 → 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.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/diagnosis.md +0 -0
- {agentic_python_coder-3.4.3 → 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.3 → 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.3 → 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.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -0
- {agentic_python_coder-3.4.3 → 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.3 → 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.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
- {agentic_python_coder-3.4.3 → 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.3 → 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.3 → 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.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/llm.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/deepseek31.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini25.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini31.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini3flash.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gemini4argon.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt52.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56sol.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt56terra.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6astra.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/gpt6sol.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/grok47.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus45.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus48.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/opus55.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/qwen3.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet45.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet46.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet5.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/models/sonnet55.json +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/project_md.py +0 -0
- {agentic_python_coder-3.4.3 → agentic_python_coder-3.5.0}/coder/src/agentic_python_coder/tools.py +0 -0
- {agentic_python_coder-3.4.3 → 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<2
|
|
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.3 → 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
|
|
|
@@ -157,7 +194,11 @@ class PythonKernel:
|
|
|
157
194
|
self.with_packages = with_packages or []
|
|
158
195
|
|
|
159
196
|
# Set kernel's working directory
|
|
160
|
-
|
|
197
|
+
# Send the kernel's fd 1 to our stderr: inherited stdout is the
|
|
198
|
+
# JSON-RPC pipe under ipython_mcp, and native code (e.g. solver
|
|
199
|
+
# progress output) writing to fd 1 would corrupt the protocol.
|
|
200
|
+
# print() is unaffected (ipykernel routes it over iopub).
|
|
201
|
+
kernel_kwargs = {"stdout": 2}
|
|
161
202
|
if cwd:
|
|
162
203
|
kernel_kwargs["cwd"] = cwd
|
|
163
204
|
|
|
@@ -166,11 +207,17 @@ class PythonKernel:
|
|
|
166
207
|
if with_packages is not None:
|
|
167
208
|
logger.info(f"Starting UV kernel with packages: {with_packages}")
|
|
168
209
|
self.km = UVKernelManager(
|
|
169
|
-
with_packages=with_packages,
|
|
210
|
+
with_packages=with_packages,
|
|
211
|
+
cwd=cwd,
|
|
212
|
+
kernel_name="python3",
|
|
213
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
170
214
|
)
|
|
171
215
|
else:
|
|
172
216
|
logger.info("Starting standard kernel (no dynamic packages)")
|
|
173
|
-
self.km = KernelManager(
|
|
217
|
+
self.km = KernelManager(
|
|
218
|
+
kernel_name="python3",
|
|
219
|
+
kernel_spec_manager=_native_kernel_spec_manager(),
|
|
220
|
+
)
|
|
174
221
|
|
|
175
222
|
# Start the kernel
|
|
176
223
|
logger.debug(f"Starting kernel in directory: {cwd}")
|
|
@@ -196,30 +243,35 @@ class PythonKernel:
|
|
|
196
243
|
|
|
197
244
|
def _inject_startup_code(self):
|
|
198
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)
|
|
199
248
|
startup_code = """
|
|
200
|
-
|
|
201
|
-
import warnings
|
|
202
|
-
import builtins
|
|
203
|
-
from IPython.core.interactiveshell import InteractiveShell
|
|
204
|
-
|
|
205
|
-
# Configure IPython to display last expression value
|
|
206
|
-
InteractiveShell.ast_node_interactivity = 'last_expr'
|
|
207
|
-
|
|
208
|
-
# Suppress pkg_resources deprecation warning from CPMpy
|
|
209
|
-
warnings.filterwarnings('ignore', message='pkg_resources is deprecated', category=UserWarning)
|
|
210
|
-
warnings.filterwarnings('ignore', message='.*pkg_resources.*', category=DeprecationWarning)
|
|
211
|
-
|
|
212
|
-
# Block interactive input() - not supported in MCP/automated sessions
|
|
213
|
-
def _blocked_input(*args, **kwargs):
|
|
214
|
-
|
|
215
|
-
builtins.input = _blocked_input
|
|
216
|
-
|
|
217
|
-
# Set matplotlib to non-interactive backend if available
|
|
218
|
-
try:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
except ImportError:
|
|
222
|
-
|
|
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
|
|
223
275
|
"""
|
|
224
276
|
self._execute_raw(startup_code)
|
|
225
277
|
|
|
@@ -234,20 +286,53 @@ except ImportError:
|
|
|
234
286
|
except Exception:
|
|
235
287
|
pass # Best effort cleanup
|
|
236
288
|
|
|
237
|
-
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]:
|
|
238
301
|
"""Execute code without locking (internal use only).
|
|
239
302
|
|
|
240
303
|
Args:
|
|
241
304
|
code: Python code to execute
|
|
242
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
|
|
243
308
|
|
|
244
309
|
Returns:
|
|
245
310
|
Dict with stdout, stderr, result, and error fields
|
|
246
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
|
+
|
|
247
332
|
# Send the execution request (silent=False to get execute_result)
|
|
248
333
|
# allow_stdin=False prevents kernel from requesting input
|
|
249
334
|
msg_id = self.kc.execute(
|
|
250
|
-
code, silent=False, store_history=
|
|
335
|
+
code, silent=False, store_history=store_history, allow_stdin=False
|
|
251
336
|
)
|
|
252
337
|
|
|
253
338
|
# Collect output
|
|
@@ -275,17 +360,23 @@ except ImportError:
|
|
|
275
360
|
elif msg_type == "execute_result":
|
|
276
361
|
output["result"] = content["data"].get("text/plain", "")
|
|
277
362
|
elif msg_type == "error":
|
|
278
|
-
|
|
363
|
+
error = f"{content['ename']}: {content['evalue']}"
|
|
279
364
|
# Include traceback for debugging
|
|
280
365
|
if content.get("traceback"):
|
|
281
|
-
|
|
366
|
+
error += "\n" + "\n".join(content["traceback"])
|
|
367
|
+
output["error"] = _ANSI_RE.sub("", error)
|
|
282
368
|
elif msg_type == "status" and content["execution_state"] == "idle":
|
|
283
369
|
completed = True
|
|
284
370
|
break # Only exit on idle status
|
|
285
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
|
|
286
377
|
continue # Keep waiting, don't break
|
|
287
378
|
|
|
288
|
-
if not completed:
|
|
379
|
+
if not completed and interrupt_on_timeout:
|
|
289
380
|
# Deadline hit while the kernel was still running: report a
|
|
290
381
|
# timeout instead of silently returning partial output as success,
|
|
291
382
|
# and interrupt so the kernel doesn't block the next execution
|
|
@@ -300,7 +391,7 @@ except ImportError:
|
|
|
300
391
|
except Exception:
|
|
301
392
|
pass # Best effort
|
|
302
393
|
|
|
303
|
-
return output
|
|
394
|
+
return output, completed
|
|
304
395
|
|
|
305
396
|
def execute(self, code: str, deadline_timeout: int = 60) -> Dict[str, str]:
|
|
306
397
|
"""Execute code with thread-safe locking.
|
|
@@ -342,6 +433,10 @@ except ImportError:
|
|
|
342
433
|
try:
|
|
343
434
|
if self.km.is_alive():
|
|
344
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()
|
|
345
440
|
except Exception:
|
|
346
441
|
pass
|
|
347
442
|
self.km = None # Clear reference
|
|
@@ -364,6 +459,25 @@ except ImportError:
|
|
|
364
459
|
# =============================================================================
|
|
365
460
|
|
|
366
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
|
+
|
|
367
481
|
def create_kernel(
|
|
368
482
|
kernel_id: Optional[str] = None,
|
|
369
483
|
with_packages: Optional[List[str]] = None,
|
|
@@ -414,6 +528,7 @@ def create_kernel(
|
|
|
414
528
|
)
|
|
415
529
|
with _kernels_lock:
|
|
416
530
|
_kernels[kernel_id] = state
|
|
531
|
+
_crashed.pop(kernel_id, None)
|
|
417
532
|
logger.info(f"Created kernel {kernel_id}")
|
|
418
533
|
return kernel_id
|
|
419
534
|
except Exception:
|
|
@@ -449,9 +564,8 @@ def execute_in_kernel(
|
|
|
449
564
|
raise KeyError(f"Kernel {kernel_id} is being created")
|
|
450
565
|
|
|
451
566
|
# Check if alive before execution
|
|
452
|
-
if not state.kernel.
|
|
453
|
-
|
|
454
|
-
_kernels.pop(kernel_id, None)
|
|
567
|
+
if not state.kernel._alive():
|
|
568
|
+
_retire_dead_kernel(kernel_id, state)
|
|
455
569
|
raise RuntimeError(f"Kernel {kernel_id} is dead")
|
|
456
570
|
|
|
457
571
|
# Execute (uses per-kernel lock internally)
|
|
@@ -460,17 +574,54 @@ def execute_in_kernel(
|
|
|
460
574
|
# Update last_used
|
|
461
575
|
state.last_used = time.time()
|
|
462
576
|
|
|
463
|
-
# Check if crashed during execution
|
|
464
|
-
if not state.kernel.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
)
|
|
468
584
|
|
|
469
585
|
# Always include kernel_id in result
|
|
470
586
|
result["kernel_id"] = kernel_id
|
|
471
587
|
return result
|
|
472
588
|
|
|
473
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
|
+
|
|
474
625
|
def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
475
626
|
"""Shutdown specific kernel.
|
|
476
627
|
|
|
@@ -479,6 +630,7 @@ def shutdown_kernel_by_id(kernel_id: str) -> bool:
|
|
|
479
630
|
"""
|
|
480
631
|
with _kernels_lock:
|
|
481
632
|
state = _kernels.pop(kernel_id, None)
|
|
633
|
+
_crashed.pop(kernel_id, None) # an explicit shutdown forgets a crash
|
|
482
634
|
|
|
483
635
|
if state is None:
|
|
484
636
|
return False
|
|
@@ -504,15 +656,15 @@ def interrupt_kernel_by_id(kernel_id: str) -> bool:
|
|
|
504
656
|
return False
|
|
505
657
|
state = _kernels[kernel_id]
|
|
506
658
|
|
|
507
|
-
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():
|
|
508
661
|
return False
|
|
509
662
|
|
|
510
|
-
|
|
663
|
+
km.interrupt_kernel()
|
|
511
664
|
time.sleep(0.5) # Grace period
|
|
512
665
|
|
|
513
|
-
if not state.kernel.
|
|
514
|
-
|
|
515
|
-
_kernels.pop(kernel_id, None)
|
|
666
|
+
if not state.kernel._alive():
|
|
667
|
+
_retire_dead_kernel(kernel_id, state)
|
|
516
668
|
raise RuntimeError(f"Kernel {kernel_id} died from interrupt")
|
|
517
669
|
|
|
518
670
|
return True
|
|
@@ -528,10 +680,21 @@ def restart_kernel(
|
|
|
528
680
|
kernel_id: Kernel to restart.
|
|
529
681
|
with_packages: New packages (None = keep existing).
|
|
530
682
|
|
|
683
|
+
A kernel that died (and was dropped from the registry) is restarted
|
|
684
|
+
with the packages and cwd it had.
|
|
685
|
+
|
|
531
686
|
Raises:
|
|
532
687
|
KeyError: If kernel not found.
|
|
533
688
|
RuntimeError: If new kernel fails to start.
|
|
534
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
|
+
|
|
535
698
|
with _kernels_lock:
|
|
536
699
|
if kernel_id not in _kernels:
|
|
537
700
|
raise KeyError(f"Unknown kernel: {kernel_id}")
|
|
@@ -623,6 +786,7 @@ def shutdown_all_kernels():
|
|
|
623
786
|
"""Shutdown all kernels. Called by atexit."""
|
|
624
787
|
with _kernels_lock:
|
|
625
788
|
kernel_ids = list(_kernels.keys())
|
|
789
|
+
_crashed.clear()
|
|
626
790
|
|
|
627
791
|
for kid in kernel_ids:
|
|
628
792
|
try:
|