agentic-python-coder 3.4.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.
@@ -220,13 +220,13 @@ except ImportError:
220
220
  self._execute_raw(startup_code)
221
221
 
222
222
  def _cleanup_on_error(self):
223
- """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
+ """
224
228
  try:
225
- if hasattr(self, "kc") and self.kc:
226
- self.kc.stop_channels()
227
- if hasattr(self, "km") and self.km:
228
- if self.km.is_alive():
229
- self.km.shutdown_kernel(now=True)
229
+ self.shutdown()
230
230
  except Exception:
231
231
  pass # Best effort cleanup
232
232
 
@@ -627,6 +627,33 @@ def shutdown_all_kernels():
627
627
  pass # Best effort
628
628
 
629
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
+
630
657
  # =============================================================================
631
658
  # Backward Compatibility API
632
659
  # =============================================================================
@@ -21,6 +21,7 @@ from agentic_python_coder.kernel import (
21
21
  execute_in_kernel,
22
22
  interrupt_kernel_by_id,
23
23
  kernel_exists,
24
+ kill_all_kernel_process_groups,
24
25
  list_kernels,
25
26
  restart_kernel,
26
27
  shutdown_all_kernels,
@@ -36,6 +37,7 @@ server = Server("ipython_mcp")
36
37
  MAX_OUTPUT = 100 * 1024 # 100KB truncation limit
37
38
  MAX_TIMEOUT = 300 # Maximum allowed timeout in seconds
38
39
  DEFAULT_TIMEOUT = 30 # Default timeout in seconds
40
+ STATUS_PROBE_TIMEOUT = 10 # Timeout for python_status introspection probes
39
41
  KERNEL_TIMEOUT_BUFFER = 5 # Extra seconds for kernel deadline vs asyncio timeout
40
42
 
41
43
  # Async lock for session operations (default kernel management)
@@ -346,7 +348,9 @@ versions
346
348
  % packages
347
349
  )
348
350
  try:
349
- result = await execute_with_timeout(version_code, 30, kernel_id)
351
+ result = await execute_with_timeout(
352
+ version_code, DEFAULT_TIMEOUT, kernel_id
353
+ )
350
354
  if result.get("result"):
351
355
  versions = ast.literal_eval(result["result"])
352
356
  if isinstance(versions, dict):
@@ -430,8 +434,11 @@ versions
430
434
  except SyntaxError as e:
431
435
  payload = {
432
436
  "ok": False,
433
- "error": f"Syntax error at line {e.lineno}: {e.msg}",
437
+ "error": f"Syntax error at line {e.lineno or '?'}: {e.msg}",
434
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}"}
435
442
  return [TextContent(type="text", text=json.dumps(payload))]
436
443
 
437
444
  elif name == "python_status":
@@ -455,7 +462,7 @@ versions
455
462
  # Get Python version
456
463
  try:
457
464
  result = await execute_with_timeout(
458
- "import sys; sys.version", 10, kernel_id
465
+ "import sys; sys.version", STATUS_PROBE_TIMEOUT, kernel_id
459
466
  )
460
467
  if result.get("result"):
461
468
  status["python_version"] = result["result"].strip("'\"")
@@ -469,7 +476,9 @@ import importlib.metadata
469
476
  [f"{d.metadata['Name']} {d.version}" for d in importlib.metadata.distributions()
470
477
  if d.metadata['Name'] not in ('pip', 'setuptools', 'wheel', 'ipykernel', 'jupyter-client')][:20]
471
478
  """
472
- result = await execute_with_timeout(pkg_code, 10, kernel_id)
479
+ result = await execute_with_timeout(
480
+ pkg_code, STATUS_PROBE_TIMEOUT, kernel_id
481
+ )
473
482
  if result.get("result"):
474
483
  status["packages"] = ast.literal_eval(result["result"])
475
484
  except Exception:
@@ -481,7 +490,9 @@ import importlib.metadata
481
490
  [name for name in dir() if not name.startswith('_')
482
491
  and name not in ('In', 'Out', 'get_ipython', 'exit', 'quit', 'open')]
483
492
  """
484
- result = await execute_with_timeout(var_code, 10, kernel_id)
493
+ result = await execute_with_timeout(
494
+ var_code, STATUS_PROBE_TIMEOUT, kernel_id
495
+ )
485
496
  if result.get("result"):
486
497
  status["variables"] = ast.literal_eval(result["result"])
487
498
  except Exception:
@@ -567,16 +578,25 @@ async def run_server():
567
578
  # Hard-exit fallback if kernel cleanup hangs after a termination signal
568
579
  _TERMINATION_CLEANUP_TIMEOUT = 10.0
569
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
+
570
585
 
571
586
  def _handle_termination(signum, frame):
572
587
  """Shut down all kernels on SIGTERM/SIGINT, then exit.
573
588
 
574
- The cleanup runs in a thread (not in signal-handler context) so it can
575
- safely take the kernel-registry locks; a timer hard-exits if it hangs.
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.
576
592
  """
593
+ if _terminating.is_set():
594
+ return
595
+ _terminating.set()
577
596
 
578
597
  def _cleanup_and_exit():
579
598
  try:
599
+ kill_all_kernel_process_groups()
580
600
  shutdown_all_kernels()
581
601
  finally:
582
602
  os._exit(128 + signum)
@@ -594,8 +614,11 @@ def main():
594
614
  try:
595
615
  asyncio.run(run_server())
596
616
  finally:
597
- # stdin EOF / transport close / normal exit: deterministic cleanup
598
- # (atexit also runs this, but only on clean interpreter shutdown)
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()
599
622
  shutdown_all_kernels()
600
623
 
601
624
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-python-coder
3
- Version: 3.4.0
3
+ Version: 3.4.1
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
@@ -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=JFN3gtZp3S-bGbQf2B-iQAE3AF8Q1TyQg4wrsx4Fdok,22261
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=7Qs5kJ9_W238vIywN5OIMG01vuPzfQ1cy1xFR8C7nGM,21054
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.4.0.dist-info/METADATA,sha256=A0vE8oBRtMdgx3kwnaVwZSzhCgTi-VXBRCxcJotjXY0,12872
45
- agentic_python_coder-3.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
46
- agentic_python_coder-3.4.0.dist-info/entry_points.txt,sha256=XIW3nZzL4kJ-Igohilj_mTvBcCyHBouDq2jmGzDILrk,107
47
- agentic_python_coder-3.4.0.dist-info/licenses/LICENSE,sha256=R9-M_V-SPdN71IphOVzM7kCNpUYs_hwj7rtroTwDhs0,11362
48
- agentic_python_coder-3.4.0.dist-info/RECORD,,
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,,