agentic-python-coder 3.3.0__py3-none-any.whl → 3.4.1__py3-none-any.whl
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/kernel.py +58 -7
- agentic_python_coder/mcp_server.py +101 -5
- {agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/METADATA +1 -1
- {agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/RECORD +7 -7
- {agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/WHEEL +0 -0
- {agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/entry_points.txt +0 -0
- {agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/licenses/LICENSE +0 -0
agentic_python_coder/kernel.py
CHANGED
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import shutil
|
|
13
|
+
import signal
|
|
13
14
|
import threading
|
|
14
15
|
import time
|
|
15
16
|
import uuid
|
|
@@ -219,13 +220,13 @@ except ImportError:
|
|
|
219
220
|
self._execute_raw(startup_code)
|
|
220
221
|
|
|
221
222
|
def _cleanup_on_error(self):
|
|
222
|
-
"""Clean up resources if initialization fails.
|
|
223
|
+
"""Clean up resources if initialization fails.
|
|
224
|
+
|
|
225
|
+
Routes through shutdown() so a partially-started kernel tree gets
|
|
226
|
+
the same defensive process-group kill as a healthy one.
|
|
227
|
+
"""
|
|
223
228
|
try:
|
|
224
|
-
|
|
225
|
-
self.kc.stop_channels()
|
|
226
|
-
if hasattr(self, "km") and self.km:
|
|
227
|
-
if self.km.is_alive():
|
|
228
|
-
self.km.shutdown_kernel(now=True)
|
|
229
|
+
self.shutdown()
|
|
229
230
|
except Exception:
|
|
230
231
|
pass # Best effort cleanup
|
|
231
232
|
|
|
@@ -311,7 +312,21 @@ except ImportError:
|
|
|
311
312
|
return self._execute_raw(code, deadline_timeout)
|
|
312
313
|
|
|
313
314
|
def shutdown(self):
|
|
314
|
-
"""Shutdown the kernel and clean up.
|
|
315
|
+
"""Shutdown the kernel and clean up.
|
|
316
|
+
|
|
317
|
+
Kernels run in their own process group (jupyter_client launches with
|
|
318
|
+
start_new_session=True). After the regular shutdown, defensively kill
|
|
319
|
+
the whole group: uv-wrapped kernels are a tree (uv parent + python
|
|
320
|
+
child) and a surviving member would be orphaned.
|
|
321
|
+
"""
|
|
322
|
+
# Capture the kernel's process group before shutdown clears it
|
|
323
|
+
pgid = None
|
|
324
|
+
try:
|
|
325
|
+
if hasattr(self, "km") and self.km:
|
|
326
|
+
pgid = getattr(getattr(self.km, "provisioner", None), "pgid", None)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
315
330
|
try:
|
|
316
331
|
if hasattr(self, "kc") and self.kc:
|
|
317
332
|
try:
|
|
@@ -330,6 +345,15 @@ except ImportError:
|
|
|
330
345
|
# Ignore errors during shutdown
|
|
331
346
|
pass
|
|
332
347
|
|
|
348
|
+
# Defensive: ensure no process-tree member survived. Never signal our
|
|
349
|
+
# own group (pgid could be bogus if start_new_session did not apply).
|
|
350
|
+
if pgid and pgid > 0:
|
|
351
|
+
try:
|
|
352
|
+
if pgid != os.getpgid(0):
|
|
353
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
354
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
355
|
+
pass
|
|
356
|
+
|
|
333
357
|
|
|
334
358
|
# =============================================================================
|
|
335
359
|
# Core Registry Functions
|
|
@@ -603,6 +627,33 @@ def shutdown_all_kernels():
|
|
|
603
627
|
pass # Best effort
|
|
604
628
|
|
|
605
629
|
|
|
630
|
+
def kill_all_kernel_process_groups():
|
|
631
|
+
"""Hard-kill (SIGKILL) every registered kernel's process group.
|
|
632
|
+
|
|
633
|
+
Deliberately lock-free: this is the termination path, where a busy
|
|
634
|
+
kernel's _exec_lock may be held indefinitely by an in-flight execution
|
|
635
|
+
and the graceful shutdown_all_kernels() would block behind it. Snapshot
|
|
636
|
+
the registry and signal each group directly. Never signals our own
|
|
637
|
+
process group. Kernels mid-creation (registry slot still None) cannot
|
|
638
|
+
be reached here — that sub-second window is a known limitation.
|
|
639
|
+
"""
|
|
640
|
+
try:
|
|
641
|
+
own_pgid = os.getpgid(0)
|
|
642
|
+
except OSError:
|
|
643
|
+
own_pgid = None
|
|
644
|
+
|
|
645
|
+
for state in list(_kernels.values()):
|
|
646
|
+
if state is None:
|
|
647
|
+
continue
|
|
648
|
+
try:
|
|
649
|
+
km = state.kernel.km
|
|
650
|
+
pgid = getattr(getattr(km, "provisioner", None), "pgid", None)
|
|
651
|
+
if pgid and pgid > 0 and pgid != own_pgid:
|
|
652
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
653
|
+
except (ProcessLookupError, PermissionError, OSError, AttributeError):
|
|
654
|
+
pass # Best effort
|
|
655
|
+
|
|
656
|
+
|
|
606
657
|
# =============================================================================
|
|
607
658
|
# Backward Compatibility API
|
|
608
659
|
# =============================================================================
|
|
@@ -7,6 +7,9 @@ import ast
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import threading
|
|
10
13
|
|
|
11
14
|
from mcp.server import Server
|
|
12
15
|
from mcp.server.stdio import stdio_server
|
|
@@ -18,8 +21,10 @@ from agentic_python_coder.kernel import (
|
|
|
18
21
|
execute_in_kernel,
|
|
19
22
|
interrupt_kernel_by_id,
|
|
20
23
|
kernel_exists,
|
|
24
|
+
kill_all_kernel_process_groups,
|
|
21
25
|
list_kernels,
|
|
22
26
|
restart_kernel,
|
|
27
|
+
shutdown_all_kernels,
|
|
23
28
|
)
|
|
24
29
|
|
|
25
30
|
# Configure logging
|
|
@@ -32,6 +37,7 @@ server = Server("ipython_mcp")
|
|
|
32
37
|
MAX_OUTPUT = 100 * 1024 # 100KB truncation limit
|
|
33
38
|
MAX_TIMEOUT = 300 # Maximum allowed timeout in seconds
|
|
34
39
|
DEFAULT_TIMEOUT = 30 # Default timeout in seconds
|
|
40
|
+
STATUS_PROBE_TIMEOUT = 10 # Timeout for python_status introspection probes
|
|
35
41
|
KERNEL_TIMEOUT_BUFFER = 5 # Extra seconds for kernel deadline vs asyncio timeout
|
|
36
42
|
|
|
37
43
|
# Async lock for session operations (default kernel management)
|
|
@@ -240,6 +246,30 @@ No side effects - safe to call anytime.""",
|
|
|
240
246
|
},
|
|
241
247
|
},
|
|
242
248
|
),
|
|
249
|
+
Tool(
|
|
250
|
+
name="submit_code",
|
|
251
|
+
description="""Submit your final, verified, self-contained Python program.
|
|
252
|
+
|
|
253
|
+
Call this ONCE at the end of a solve, only after you have executed the program
|
|
254
|
+
via python_exec and your verification passed. The submission must be a
|
|
255
|
+
standalone program: all imports included, no reliance on session state.
|
|
256
|
+
|
|
257
|
+
The code is syntax-checked with compile():
|
|
258
|
+
- {"ok": true} if it parses
|
|
259
|
+
- {"ok": false, "error": "<detail>"} on a syntax error — fix and resubmit
|
|
260
|
+
|
|
261
|
+
The code is NOT executed and NOT written to disk; the client persists it.""",
|
|
262
|
+
inputSchema={
|
|
263
|
+
"type": "object",
|
|
264
|
+
"properties": {
|
|
265
|
+
"code": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"description": "Complete standalone Python program (final answer).",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
"required": ["code"],
|
|
271
|
+
},
|
|
272
|
+
),
|
|
243
273
|
Tool(
|
|
244
274
|
name="python_interrupt",
|
|
245
275
|
description="""Interrupt running code in the session.
|
|
@@ -318,7 +348,9 @@ versions
|
|
|
318
348
|
% packages
|
|
319
349
|
)
|
|
320
350
|
try:
|
|
321
|
-
result = await execute_with_timeout(
|
|
351
|
+
result = await execute_with_timeout(
|
|
352
|
+
version_code, DEFAULT_TIMEOUT, kernel_id
|
|
353
|
+
)
|
|
322
354
|
if result.get("result"):
|
|
323
355
|
versions = ast.literal_eval(result["result"])
|
|
324
356
|
if isinstance(versions, dict):
|
|
@@ -391,6 +423,24 @@ versions
|
|
|
391
423
|
result = await execute_with_timeout(code, timeout, kernel_id)
|
|
392
424
|
return [TextContent(type="text", text=json.dumps(result))]
|
|
393
425
|
|
|
426
|
+
elif name == "submit_code":
|
|
427
|
+
code = arguments.get("code", "")
|
|
428
|
+
if not code.strip():
|
|
429
|
+
payload = {"ok": False, "error": "Empty submission"}
|
|
430
|
+
else:
|
|
431
|
+
try:
|
|
432
|
+
compile(code, "<submission>", "exec")
|
|
433
|
+
payload = {"ok": True}
|
|
434
|
+
except SyntaxError as e:
|
|
435
|
+
payload = {
|
|
436
|
+
"ok": False,
|
|
437
|
+
"error": f"Syntax error at line {e.lineno or '?'}: {e.msg}",
|
|
438
|
+
}
|
|
439
|
+
except ValueError as e:
|
|
440
|
+
# compile() raises ValueError e.g. for null bytes in source
|
|
441
|
+
payload = {"ok": False, "error": f"Invalid source: {e}"}
|
|
442
|
+
return [TextContent(type="text", text=json.dumps(payload))]
|
|
443
|
+
|
|
394
444
|
elif name == "python_status":
|
|
395
445
|
kernel_id = get_kernel_id(arguments)
|
|
396
446
|
is_active = kernel_exists(kernel_id)
|
|
@@ -412,7 +462,7 @@ versions
|
|
|
412
462
|
# Get Python version
|
|
413
463
|
try:
|
|
414
464
|
result = await execute_with_timeout(
|
|
415
|
-
"import sys; sys.version",
|
|
465
|
+
"import sys; sys.version", STATUS_PROBE_TIMEOUT, kernel_id
|
|
416
466
|
)
|
|
417
467
|
if result.get("result"):
|
|
418
468
|
status["python_version"] = result["result"].strip("'\"")
|
|
@@ -426,7 +476,9 @@ import importlib.metadata
|
|
|
426
476
|
[f"{d.metadata['Name']} {d.version}" for d in importlib.metadata.distributions()
|
|
427
477
|
if d.metadata['Name'] not in ('pip', 'setuptools', 'wheel', 'ipykernel', 'jupyter-client')][:20]
|
|
428
478
|
"""
|
|
429
|
-
result = await execute_with_timeout(
|
|
479
|
+
result = await execute_with_timeout(
|
|
480
|
+
pkg_code, STATUS_PROBE_TIMEOUT, kernel_id
|
|
481
|
+
)
|
|
430
482
|
if result.get("result"):
|
|
431
483
|
status["packages"] = ast.literal_eval(result["result"])
|
|
432
484
|
except Exception:
|
|
@@ -438,7 +490,9 @@ import importlib.metadata
|
|
|
438
490
|
[name for name in dir() if not name.startswith('_')
|
|
439
491
|
and name not in ('In', 'Out', 'get_ipython', 'exit', 'quit', 'open')]
|
|
440
492
|
"""
|
|
441
|
-
result = await execute_with_timeout(
|
|
493
|
+
result = await execute_with_timeout(
|
|
494
|
+
var_code, STATUS_PROBE_TIMEOUT, kernel_id
|
|
495
|
+
)
|
|
442
496
|
if result.get("result"):
|
|
443
497
|
status["variables"] = ast.literal_eval(result["result"])
|
|
444
498
|
except Exception:
|
|
@@ -521,9 +575,51 @@ async def run_server():
|
|
|
521
575
|
await server.run(read, write, server.create_initialization_options())
|
|
522
576
|
|
|
523
577
|
|
|
578
|
+
# Hard-exit fallback if kernel cleanup hangs after a termination signal
|
|
579
|
+
_TERMINATION_CLEANUP_TIMEOUT = 10.0
|
|
580
|
+
|
|
581
|
+
# Set on the first termination signal; later signals are ignored instead of
|
|
582
|
+
# stacking additional timers and cleanup threads
|
|
583
|
+
_terminating = threading.Event()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _handle_termination(signum, frame):
|
|
587
|
+
"""Shut down all kernels on SIGTERM/SIGINT, then exit.
|
|
588
|
+
|
|
589
|
+
Kernel process groups are hard-killed first (lock-free, cannot block on
|
|
590
|
+
a busy kernel's exec lock); the graceful registry cleanup then runs in a
|
|
591
|
+
thread, with a timer that hard-exits if it hangs.
|
|
592
|
+
"""
|
|
593
|
+
if _terminating.is_set():
|
|
594
|
+
return
|
|
595
|
+
_terminating.set()
|
|
596
|
+
|
|
597
|
+
def _cleanup_and_exit():
|
|
598
|
+
try:
|
|
599
|
+
kill_all_kernel_process_groups()
|
|
600
|
+
shutdown_all_kernels()
|
|
601
|
+
finally:
|
|
602
|
+
os._exit(128 + signum)
|
|
603
|
+
|
|
604
|
+
threading.Timer(
|
|
605
|
+
_TERMINATION_CLEANUP_TIMEOUT, lambda: os._exit(128 + signum)
|
|
606
|
+
).start()
|
|
607
|
+
threading.Thread(target=_cleanup_and_exit, daemon=True).start()
|
|
608
|
+
|
|
609
|
+
|
|
524
610
|
def main():
|
|
525
611
|
"""Entry point for coder-mcp command."""
|
|
526
|
-
|
|
612
|
+
signal.signal(signal.SIGTERM, _handle_termination)
|
|
613
|
+
signal.signal(signal.SIGINT, _handle_termination)
|
|
614
|
+
try:
|
|
615
|
+
asyncio.run(run_server())
|
|
616
|
+
finally:
|
|
617
|
+
# stdin EOF / transport close / normal exit: the client is gone, so
|
|
618
|
+
# hard-kill kernel groups first (immune to a busy kernel's exec
|
|
619
|
+
# lock), then clean up the registry (atexit also runs the graceful
|
|
620
|
+
# path, but only on clean interpreter shutdown)
|
|
621
|
+
kill_all_kernel_process_groups()
|
|
622
|
+
shutdown_all_kernels()
|
|
527
623
|
|
|
528
624
|
|
|
529
625
|
if __name__ == "__main__":
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
agentic_python_coder/__init__.py,sha256=e6o-k__u5xNIiQAkZbHtbZC2-NwpeOdi4MdQtAqJBjE,2112
|
|
2
2
|
agentic_python_coder/agent.py,sha256=gzDFf4n7J1JdWawYPzWQFVwcHZ2rsRHUzVZVcGYjQWo,14451
|
|
3
3
|
agentic_python_coder/cli.py,sha256=HS9nlqsiWZAvg0Hc8ExoVT8eEcy2t3Bux6UGukERWqQ,14663
|
|
4
|
-
agentic_python_coder/kernel.py,sha256=
|
|
4
|
+
agentic_python_coder/kernel.py,sha256=Q-yLb8Il54kZv41qev2uzASFlr6bGjQ7ztBy7yD48GM,23273
|
|
5
5
|
agentic_python_coder/llm.py,sha256=zjsk4-Kdx92hFSxywPGIkLv8zflWnE2cMEcpeXu6ttw,6998
|
|
6
|
-
agentic_python_coder/mcp_server.py,sha256=
|
|
6
|
+
agentic_python_coder/mcp_server.py,sha256=Osdl8UnjkupXbPLiYCugiapGDJsryZfVai0o-jepuqw,22047
|
|
7
7
|
agentic_python_coder/project_md.py,sha256=4xkQHlWb4cDPEkvegw1YwcfaTJ3aFbrl0CYcC_xxABY,3249
|
|
8
8
|
agentic_python_coder/runner.py,sha256=Kbw1ceYJ4MVfUKgSIkiuBhMb8ZtbZuJwbBeTUsvPRts,13045
|
|
9
9
|
agentic_python_coder/tools.py,sha256=tjhQbsF3n3bFIegnEOtE8czjzbtMSXy6B9gYeb-YAJ4,12088
|
|
@@ -41,8 +41,8 @@ agentic_python_coder/models/sonnet46.json,sha256=Ops1FpBoyx9PBGRhIXG8u-YGBgIui9D
|
|
|
41
41
|
agentic_python_coder/models/sonnet5.json,sha256=kf6DIkQlcGVZ40IzRv3GXFtNncZF_gKqQeGzZAxE_KM,87
|
|
42
42
|
agentic_python_coder/prompts/system.md,sha256=hREDVAhJXwCBAgJ3AE9JD6j-JRHREUPlkRSvThOvM3A,3807
|
|
43
43
|
agentic_python_coder/prompts/system_todo.md,sha256=TuNeeUNS9KqyF62Da6t2mlUCYhKsgzwGe5ic_vFPDqw,5054
|
|
44
|
-
agentic_python_coder-3.
|
|
45
|
-
agentic_python_coder-3.
|
|
46
|
-
agentic_python_coder-3.
|
|
47
|
-
agentic_python_coder-3.
|
|
48
|
-
agentic_python_coder-3.
|
|
44
|
+
agentic_python_coder-3.4.1.dist-info/METADATA,sha256=C9UTWTVtnf7-0Bgiu44LMEd16zA00T3UxkS92MVV_Ac,12872
|
|
45
|
+
agentic_python_coder-3.4.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
46
|
+
agentic_python_coder-3.4.1.dist-info/entry_points.txt,sha256=XIW3nZzL4kJ-Igohilj_mTvBcCyHBouDq2jmGzDILrk,107
|
|
47
|
+
agentic_python_coder-3.4.1.dist-info/licenses/LICENSE,sha256=R9-M_V-SPdN71IphOVzM7kCNpUYs_hwj7rtroTwDhs0,11362
|
|
48
|
+
agentic_python_coder-3.4.1.dist-info/RECORD,,
|
|
File without changes
|
{agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agentic_python_coder-3.3.0.dist-info → agentic_python_coder-3.4.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|