agentic-python-coder 2.2.1__tar.gz → 2.3.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 (68) hide show
  1. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/PKG-INFO +31 -15
  2. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/README.md +18 -3
  3. agentic_python_coder-2.3.0/coder/src/agentic_python_coder/__init__.py +74 -0
  4. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/agent.py +26 -46
  5. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/cli.py +17 -7
  6. agentic_python_coder-2.3.0/coder/src/agentic_python_coder/kernel.py +634 -0
  7. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/llm.py +4 -1
  8. agentic_python_coder-2.3.0/coder/src/agentic_python_coder/mcp_server.py +526 -0
  9. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/tests/test_library_api.py +7 -7
  10. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/tests/test_mcp_server.py +109 -20
  11. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/pyproject.toml +13 -12
  12. agentic_python_coder-2.2.1/coder/src/agentic_python_coder/__init__.py +0 -39
  13. agentic_python_coder-2.2.1/coder/src/agentic_python_coder/kernel.py +0 -343
  14. agentic_python_coder-2.2.1/coder/src/agentic_python_coder/mcp_server.py +0 -485
  15. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/.gitignore +0 -0
  16. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/LICENSE +0 -0
  17. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/.gitignore +0 -0
  18. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/prompts/system.md +0 -0
  19. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/prompts/system_todo.md +0 -0
  20. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
  21. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
  22. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/clingo.md +0 -0
  23. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/bird_reasoning.md +0 -0
  24. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/diagnosis.md +0 -0
  25. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/simple_coloring.md +0 -0
  26. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/stable_marriage.md +0 -0
  27. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/sudoku_mini.md +0 -0
  28. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
  29. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -0
  30. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/magic_square.md +0 -0
  31. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/n_queens.md +0 -0
  32. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
  33. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
  34. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/email_extraction.md +0 -0
  35. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/phone_validation.md +0 -0
  36. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/url_parsing.md +0 -0
  37. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/deepseek31.json +0 -0
  38. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/gemini25.json +0 -0
  39. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/gpt5.json +0 -0
  40. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/grok41.json +0 -0
  41. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/opus45.json +0 -0
  42. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/qwen3.json +0 -0
  43. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/models/sonnet45.json +0 -0
  44. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/project_md.py +0 -0
  45. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/runner.py +0 -0
  46. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/src/agentic_python_coder/tools.py +0 -0
  47. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/tests/test_kernel.py +0 -0
  48. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/tests/test_todo_flag_integration.py +0 -0
  49. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/coder/tests/test_todo_tool_availability.py +0 -0
  50. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/README.md +0 -0
  51. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/clingo.md +0 -0
  52. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/sample_tasks/bird_reasoning.md +0 -0
  53. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/sample_tasks/diagnosis.md +0 -0
  54. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/sample_tasks/simple_coloring.md +0 -0
  55. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/sample_tasks/stable_marriage.md +0 -0
  56. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/clingo/sample_tasks/sudoku_mini.md +0 -0
  57. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/cpmpy/README.md +0 -0
  58. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/cpmpy/cpmpy.md +0 -0
  59. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/cpmpy/sample_problems/magic_square.md +0 -0
  60. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/cpmpy/sample_problems/n_queens.md +0 -0
  61. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/README.md +0 -0
  62. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/regex.md +0 -0
  63. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/email_extraction.md +0 -0
  64. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/phone_validation.md +0 -0
  65. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/test_email/email_extractor.py +0 -0
  66. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/test_email/extracted_emails.txt +0 -0
  67. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/test_email/text.txt +0 -0
  68. {agentic_python_coder-2.2.1 → agentic_python_coder-2.3.0}/examples/regex/sample_tasks/url_parsing.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-python-coder
3
- Version: 2.2.1
3
+ Version: 2.3.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
@@ -14,25 +14,26 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
15
  Classifier: Topic :: Software Development :: Code Generators
16
16
  Requires-Python: <3.14,>=3.13
17
- Requires-Dist: ipykernel>=6.30.1
18
- Requires-Dist: jupyter-client>=8.6.3
19
- Requires-Dist: langchain-anthropic>=1.2.0
20
- Requires-Dist: langchain-core>=1.1.0
21
- Requires-Dist: langchain-experimental>=0.4.0
22
- Requires-Dist: langchain-openai>=1.1.0
23
- Requires-Dist: langgraph>=1.0.4
17
+ Requires-Dist: ipykernel>=7.0.0
18
+ Requires-Dist: jupyter-client>=8.7.0
19
+ Requires-Dist: langchain-anthropic>=1.3.0
20
+ Requires-Dist: langchain-core>=1.2.0
21
+ Requires-Dist: langchain-experimental>=0.4.1
22
+ Requires-Dist: langchain-openai>=1.1.6
23
+ Requires-Dist: langchain>=1.2.0
24
+ Requires-Dist: langgraph>=1.0.5
24
25
  Requires-Dist: mcp>=1.0.0
25
26
  Requires-Dist: python-dotenv>=1.2.1
26
27
  Requires-Dist: pyyaml>=6.0.3
27
28
  Requires-Dist: rich>=14.2.0
28
29
  Provides-Extra: dev
29
- Requires-Dist: mypy>=1.19.0; extra == 'dev'
30
- Requires-Dist: ruff>=0.14.7; extra == 'dev'
30
+ Requires-Dist: mypy>=1.19.1; extra == 'dev'
31
+ Requires-Dist: ruff>=0.14.10; extra == 'dev'
31
32
  Provides-Extra: test
32
- Requires-Dist: pytest-asyncio>=1.2.0; extra == 'test'
33
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'test'
33
34
  Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
34
35
  Requires-Dist: pytest-watch>=4.2.0; extra == 'test'
35
- Requires-Dist: pytest>=9.0.1; extra == 'test'
36
+ Requires-Dist: pytest>=9.0.2; extra == 'test'
36
37
  Description-Content-Type: text/markdown
37
38
 
38
39
  # Agentic Python Coder
@@ -291,17 +292,32 @@ Add to your MCP settings (e.g., `~/.claude/claude_desktop_config.json` or projec
291
292
  | Tool | Description |
292
293
  |------|-------------|
293
294
  | `python_exec` | Execute Python code. Auto-starts session if needed. Default 30s timeout. |
294
- | `python_reset` | Clear session state. Optionally install packages (e.g., `packages=["numpy", "pandas"]`). |
295
- | `python_status` | Check if session is active, Python version, installed packages, defined variables. |
295
+ | `python_reset` | Create new kernel (no `kernel_id`) OR reset existing kernel (with `kernel_id`). Optionally install packages. |
296
+ | `python_status` | Check session state: active flag, all active kernel IDs, Python version, packages, variables. |
296
297
  | `python_interrupt` | Send interrupt signal to stop long-running code. Session state is preserved. |
297
298
 
299
+ ### Multi-Agent Workflow
300
+
301
+ For parallel agents, each agent gets its own kernel:
302
+
303
+ ```
304
+ Agent A Agent B
305
+ ──────── ────────
306
+ python_reset() → kernel_id="aaa" python_reset() → kernel_id="bbb"
307
+ python_exec(kernel_id="aaa", ...) python_exec(kernel_id="bbb", ...)
308
+ python_exec(kernel_id="aaa", ...) python_exec(kernel_id="bbb", ...)
309
+ ```
310
+
311
+ Simple single-agent use: just call `python_exec()` — the default kernel auto-starts.
312
+
298
313
  ### Features
299
314
 
300
315
  - **Persistent state**: Variables, imports, and definitions persist across executions
301
- - **Auto-start**: Session starts automatically on first `python_exec`
316
+ - **Auto-start**: Default session starts automatically on first `python_exec`
302
317
  - **Package installation**: Use `python_reset` with `packages` parameter to install dependencies
303
318
  - **Timeout handling**: Long-running code times out gracefully (session preserved)
304
319
  - **Interrupt support**: Stop runaway code without losing session state
320
+ - **Multi-kernel**: Each `python_reset()` creates an isolated kernel for parallel agents
305
321
 
306
322
  ### Usage Tips
307
323
 
@@ -254,17 +254,32 @@ Add to your MCP settings (e.g., `~/.claude/claude_desktop_config.json` or projec
254
254
  | Tool | Description |
255
255
  |------|-------------|
256
256
  | `python_exec` | Execute Python code. Auto-starts session if needed. Default 30s timeout. |
257
- | `python_reset` | Clear session state. Optionally install packages (e.g., `packages=["numpy", "pandas"]`). |
258
- | `python_status` | Check if session is active, Python version, installed packages, defined variables. |
257
+ | `python_reset` | Create new kernel (no `kernel_id`) OR reset existing kernel (with `kernel_id`). Optionally install packages. |
258
+ | `python_status` | Check session state: active flag, all active kernel IDs, Python version, packages, variables. |
259
259
  | `python_interrupt` | Send interrupt signal to stop long-running code. Session state is preserved. |
260
260
 
261
+ ### Multi-Agent Workflow
262
+
263
+ For parallel agents, each agent gets its own kernel:
264
+
265
+ ```
266
+ Agent A Agent B
267
+ ──────── ────────
268
+ python_reset() → kernel_id="aaa" python_reset() → kernel_id="bbb"
269
+ python_exec(kernel_id="aaa", ...) python_exec(kernel_id="bbb", ...)
270
+ python_exec(kernel_id="aaa", ...) python_exec(kernel_id="bbb", ...)
271
+ ```
272
+
273
+ Simple single-agent use: just call `python_exec()` — the default kernel auto-starts.
274
+
261
275
  ### Features
262
276
 
263
277
  - **Persistent state**: Variables, imports, and definitions persist across executions
264
- - **Auto-start**: Session starts automatically on first `python_exec`
278
+ - **Auto-start**: Default session starts automatically on first `python_exec`
265
279
  - **Package installation**: Use `python_reset` with `packages` parameter to install dependencies
266
280
  - **Timeout handling**: Long-running code times out gracefully (session preserved)
267
281
  - **Interrupt support**: Stop runaway code without losing session state
282
+ - **Multi-kernel**: Each `python_reset()` creates an isolated kernel for parallel agents
268
283
 
269
284
  ### Usage Tips
270
285
 
@@ -0,0 +1,74 @@
1
+ """Python Coding Agent - A minimal coding assistant using LangGraph and OpenRouter."""
2
+
3
+ __version__ = "2.3.0"
4
+
5
+ # High-level API (recommended for most users)
6
+ from agentic_python_coder.runner import solve_task
7
+
8
+ # Lower-level API (for custom workflows)
9
+ from agentic_python_coder.agent import (
10
+ create_coding_agent,
11
+ run_agent,
12
+ get_final_response,
13
+ DEFAULT_STEP_LIMIT,
14
+ )
15
+
16
+ # LLM utilities
17
+ from agentic_python_coder.llm import (
18
+ get_openrouter_llm,
19
+ load_model_config,
20
+ list_available_models,
21
+ DEFAULT_MODEL,
22
+ )
23
+
24
+ # Kernel management (multi-kernel API)
25
+ from agentic_python_coder.kernel import (
26
+ # Core functions
27
+ create_kernel,
28
+ execute_in_kernel,
29
+ shutdown_kernel_by_id,
30
+ interrupt_kernel_by_id,
31
+ restart_kernel,
32
+ # Query functions
33
+ list_kernels,
34
+ kernel_exists,
35
+ get_kernel_info,
36
+ shutdown_all_kernels,
37
+ # Backward compat
38
+ get_kernel,
39
+ shutdown_kernel,
40
+ # Constants
41
+ DEFAULT_KERNEL_ID,
42
+ MAX_KERNELS,
43
+ )
44
+
45
+ __all__ = [
46
+ # Version
47
+ "__version__",
48
+ # High-level
49
+ "solve_task",
50
+ # Low-level agent
51
+ "create_coding_agent",
52
+ "run_agent",
53
+ "get_final_response",
54
+ "DEFAULT_STEP_LIMIT",
55
+ # LLM
56
+ "get_openrouter_llm",
57
+ "load_model_config",
58
+ "list_available_models",
59
+ "DEFAULT_MODEL",
60
+ # Kernel management
61
+ "create_kernel",
62
+ "execute_in_kernel",
63
+ "shutdown_kernel_by_id",
64
+ "interrupt_kernel_by_id",
65
+ "restart_kernel",
66
+ "list_kernels",
67
+ "kernel_exists",
68
+ "get_kernel_info",
69
+ "shutdown_all_kernels",
70
+ "get_kernel",
71
+ "shutdown_kernel",
72
+ "DEFAULT_KERNEL_ID",
73
+ "MAX_KERNELS",
74
+ ]
@@ -1,8 +1,11 @@
1
1
  """ReAct agent for Python coding tasks."""
2
2
 
3
+ import json
4
+ import os
5
+ import time
3
6
  from typing import Dict, Any, List, Optional
4
7
  from pathlib import Path
5
- from langgraph.prebuilt import create_react_agent
8
+ from langchain.agents import create_agent
6
9
  from langgraph.checkpoint.memory import InMemorySaver
7
10
 
8
11
  from agentic_python_coder.llm import get_openrouter_llm
@@ -45,7 +48,7 @@ def create_coding_agent(
45
48
  working_directory: Directory for file operations
46
49
  system_prompt: System prompt as string (takes precedence over path)
47
50
  system_prompt_path: Path to system prompt file (used if system_prompt not provided)
48
- model: Optional model name (defaults to claude-sonnet-4.5)
51
+ model: Optional model name (uses configured default if not specified)
49
52
  project_prompt: Optional project-specific prompt
50
53
  with_packages: Optional list of packages for dynamic mode
51
54
  task_content: Task description/content
@@ -66,8 +69,6 @@ def create_coding_agent(
66
69
 
67
70
  # Store packages for kernel initialization
68
71
  if with_packages is not None:
69
- import os
70
-
71
72
  os.environ["CODER_WITH_PACKAGES"] = ",".join(with_packages)
72
73
 
73
74
  # Get LLM instance
@@ -108,15 +109,13 @@ def create_coding_agent(
108
109
 
109
110
  # Create the agent with memory
110
111
  checkpointer = InMemorySaver()
111
- agent = create_react_agent(
112
- llm, tools, prompt=combined_prompt, checkpointer=checkpointer
112
+ agent = create_agent(
113
+ llm, tools, system_prompt=combined_prompt, checkpointer=checkpointer
113
114
  )
114
115
 
115
116
  # Store metadata for run_agent to use
116
117
  agent._coder_metadata = {
117
118
  "working_directory": working_directory,
118
- "with_packages": with_packages,
119
- "task_basename": task_basename,
120
119
  }
121
120
 
122
121
  return agent
@@ -126,24 +125,21 @@ def _print_tool_progress(tool_name: str, args: dict):
126
125
  """Print progress info for a tool call."""
127
126
  if tool_name == "python_exec" and "code" in args:
128
127
  code = args["code"]
128
+ code_stripped = code.strip()
129
129
  if "def " in code:
130
- func_match = code.split("def ")[1].split("(")[0] if "def " in code else ""
130
+ func_match = code.split("def ")[1].split("(")[0]
131
131
  print(f" {tool_name}: defining function {func_match}()")
132
132
  elif "class " in code:
133
- class_match = (
134
- code.split("class ")[1].split("(")[0].split(":")[0]
135
- if "class " in code
136
- else ""
137
- )
133
+ class_match = code.split("class ")[1].split("(")[0].split(":")[0]
138
134
  print(f" {tool_name}: defining class {class_match}")
139
- elif "import " in code and len(code.strip().split("\n")) == 1:
140
- print(f" {tool_name}: {code.strip()}")
141
- elif "=" in code and len(code.strip().split("\n")) == 1:
135
+ elif "import " in code and len(code_stripped.split("\n")) == 1:
136
+ print(f" {tool_name}: {code_stripped}")
137
+ elif "=" in code and len(code_stripped.split("\n")) == 1:
142
138
  var_name = code.split("=")[0].strip()
143
139
  print(f" {tool_name}: assigning variable {var_name}")
144
- elif code.strip().startswith("print("):
140
+ elif code_stripped.startswith("print("):
145
141
  print(
146
- f" {tool_name}: {code.strip()[:50]}{'...' if len(code.strip()) > 50 else ''}"
142
+ f" {tool_name}: {code_stripped[:50]}{'...' if len(code_stripped) > 50 else ''}"
147
143
  )
148
144
  elif "read_csv" in code or "read_excel" in code or "read_json" in code:
149
145
  print(f" {tool_name}: loading data file")
@@ -170,32 +166,23 @@ def _print_tool_progress(tool_name: str, args: dict):
170
166
  print(f"\n {tool_name}:")
171
167
  todos = args["todos"]
172
168
  for todo in todos:
173
- status_symbol = (
174
- ""
175
- if todo["status"] == "completed"
176
- else "☐"
177
- if todo["status"] == "pending"
178
- else "▶"
179
- )
180
- print(f" {status_symbol} {todo['content']}")
169
+ status = todo.get("status", "")
170
+ content = todo.get("content", "")
171
+ status_symbol = "" if status == "completed" else "☐" if status == "pending" else "▶"
172
+ print(f" {status_symbol} {content}")
181
173
  else:
182
- arg_str = str(args)[:30]
183
- if len(str(args)) > 30:
184
- arg_str += "..."
185
- print(f" {tool_name}: {arg_str}")
174
+ args_str = str(args)
175
+ arg_display = args_str[:30] + "..." if len(args_str) > 30 else args_str
176
+ print(f" {tool_name}: {arg_display}")
186
177
 
187
178
 
188
- def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
179
+ def _process_tool_calls(msg, stats: dict, quiet: bool):
189
180
  """Process tool calls from a message, updating stats and optionally printing."""
190
181
  if hasattr(msg, "tool_calls") and msg.tool_calls:
191
182
  for tool_call in msg.tool_calls:
192
183
  tool_name = tool_call.get("name") or tool_call.get("function", {}).get(
193
184
  "name"
194
185
  )
195
- tool_id = tool_call.get("id")
196
- if tool_id:
197
- current_tools[tool_id] = tool_name
198
-
199
186
  if tool_name:
200
187
  stats["tool_usage"][tool_name] = (
201
188
  stats["tool_usage"].get(tool_name, 0) + 1
@@ -213,9 +200,6 @@ def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
213
200
  for tool_call in tool_calls:
214
201
  function = tool_call.get("function", {})
215
202
  tool_name = function.get("name")
216
- tool_id = tool_call.get("id")
217
- if tool_id and tool_name:
218
- current_tools[tool_id] = tool_name
219
203
 
220
204
  if tool_name:
221
205
  stats["tool_usage"][tool_name] = (
@@ -225,11 +209,9 @@ def _process_tool_calls(msg, current_tools: dict, stats: dict, quiet: bool):
225
209
  if not quiet and tool_name:
226
210
  args_str = function.get("arguments", "{}")
227
211
  try:
228
- import json
229
-
230
212
  args = json.loads(args_str)
231
213
  _print_tool_progress(tool_name, args)
232
- except Exception:
214
+ except json.JSONDecodeError:
233
215
  print(f" {tool_name}")
234
216
 
235
217
 
@@ -243,6 +225,7 @@ def _update_token_stats(msg, stats: dict):
243
225
  "completion_tokens", 0
244
226
  )
245
227
  stats["token_consumption"]["total_tokens"] += usage.get("total_tokens", 0)
228
+ return # Avoid double-counting if both metadata sources exist
246
229
 
247
230
  if hasattr(msg, "usage_metadata") and msg.usage_metadata:
248
231
  stats["token_consumption"]["input_tokens"] += msg.usage_metadata.get(
@@ -285,7 +268,6 @@ def run_agent(
285
268
  config = {"configurable": {"thread_id": thread_id}, "recursion_limit": limit}
286
269
 
287
270
  messages = []
288
- current_tools = {}
289
271
 
290
272
  # Initialize statistics
291
273
  stats = {
@@ -294,8 +276,6 @@ def run_agent(
294
276
  "execution_time_seconds": 0,
295
277
  }
296
278
 
297
- import time
298
-
299
279
  start_time = time.time()
300
280
 
301
281
  # Stream the agent's work
@@ -313,7 +293,7 @@ def run_agent(
313
293
  messages.append(msg)
314
294
 
315
295
  # Process tool calls (always update stats, optionally print)
316
- _process_tool_calls(msg, current_tools, stats, quiet)
296
+ _process_tool_calls(msg, stats, quiet)
317
297
 
318
298
  # Update token statistics
319
299
  _update_token_stats(msg, stats)
@@ -23,7 +23,11 @@ from agentic_python_coder.project_md import (
23
23
  check_packages_available,
24
24
  create_project_prompt,
25
25
  )
26
- from agentic_python_coder.llm import DEFAULT_MODEL, list_available_models, load_model_config
26
+ from agentic_python_coder.llm import (
27
+ DEFAULT_MODEL,
28
+ list_available_models,
29
+ load_model_config,
30
+ )
27
31
  from agentic_python_coder import __version__
28
32
 
29
33
 
@@ -88,7 +92,8 @@ def parse_args():
88
92
  )
89
93
 
90
94
  parser.add_argument(
91
- "--version", "-V",
95
+ "--version",
96
+ "-V",
92
97
  action="version",
93
98
  version=f"%(prog)s {__version__}",
94
99
  )
@@ -106,7 +111,9 @@ def parse_args():
106
111
  help="Path to task file (creates {basename}_code.py and {basename}.jsonl)",
107
112
  )
108
113
 
109
- parser.add_argument("--model", help=f"Model name or JSON file (default: {DEFAULT_MODEL})")
114
+ parser.add_argument(
115
+ "--model", help=f"Model name or JSON file (default: {DEFAULT_MODEL})"
116
+ )
110
117
 
111
118
  parser.add_argument(
112
119
  "--interactive", "-i", action="store_true", help="Interactive mode"
@@ -145,7 +152,8 @@ def parse_args():
145
152
  )
146
153
 
147
154
  parser.add_argument(
148
- "--quiet", "-q",
155
+ "--quiet",
156
+ "-q",
149
157
  action="store_true",
150
158
  help="Suppress console output during execution",
151
159
  )
@@ -176,13 +184,13 @@ def copy_resource_dir(source_path, dest_path: Path):
176
184
  dest_item = dest_path / item.name
177
185
  if item.is_file():
178
186
  # Skip __pycache__ and .pyc files
179
- if item.name.endswith('.pyc') or item.name == '__pycache__':
187
+ if item.name.endswith(".pyc") or item.name == "__pycache__":
180
188
  continue
181
189
  # Read and write file content
182
190
  content = item.read_bytes()
183
191
  dest_item.write_bytes(content)
184
192
  elif item.is_dir():
185
- if item.name == '__pycache__':
193
+ if item.name == "__pycache__":
186
194
  continue
187
195
  copy_resource_dir(item, dest_item)
188
196
 
@@ -216,7 +224,9 @@ def init_examples(template: str = "all"):
216
224
 
217
225
  print(f"\nExamples initialized in: {output_dir.absolute()}")
218
226
  print("\nUsage:")
219
- print(f" coder --with cpmpy --project {output_dir}/cpmpy/cpmpy.md --task {output_dir}/cpmpy/sample_tasks/n_queens.md")
227
+ print(
228
+ f" coder --with cpmpy --project {output_dir}/cpmpy/cpmpy.md --task {output_dir}/cpmpy/sample_tasks/n_queens.md"
229
+ )
220
230
 
221
231
 
222
232
  def validate_packages(packages):