agentic-python-coder 3.2.0__tar.gz → 3.4.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 (54) hide show
  1. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/.gitignore +0 -4
  2. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/PKG-INFO +54 -12
  3. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/README.md +42 -0
  4. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/__init__.py +6 -1
  5. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/agent.py +52 -14
  6. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/cli.py +19 -4
  7. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/clingo.md +64 -1
  8. agentic_python_coder-3.4.0/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +100 -0
  9. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/kernel.py +42 -1
  10. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/llm.py +19 -5
  11. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/mcp_server.py +79 -7
  12. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/deepseek31.json +0 -1
  13. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/gemini25.json +0 -1
  14. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/gemini31.json +0 -1
  15. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/gemini3pro.json +0 -1
  16. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/gpt52.json +0 -1
  17. agentic_python_coder-3.4.0/coder/src/agentic_python_coder/models/gpt56sol.json +5 -0
  18. agentic_python_coder-3.4.0/coder/src/agentic_python_coder/models/gpt56terra.json +5 -0
  19. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/grok41.json +0 -1
  20. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/opus45.json +1 -2
  21. agentic_python_coder-3.4.0/coder/src/agentic_python_coder/models/opus48.json +5 -0
  22. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/qwen3.json +0 -1
  23. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/sonnet45.json +1 -2
  24. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/sonnet46.json +1 -2
  25. agentic_python_coder-3.4.0/coder/src/agentic_python_coder/models/sonnet5.json +5 -0
  26. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/project_md.py +6 -2
  27. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/runner.py +8 -1
  28. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/tools.py +28 -7
  29. agentic_python_coder-3.4.0/examples/clingo/clingo.md +5 -0
  30. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/pyproject.toml +16 -12
  31. agentic_python_coder-3.2.0/MEM/MEM_000.md +0 -114
  32. agentic_python_coder-3.2.0/MEM/MEM_001_frameworkless_react_agent.md +0 -110
  33. agentic_python_coder-3.2.0/MEM/MEM_002_tool_description_quality.md +0 -107
  34. agentic_python_coder-3.2.0/coder/src/agentic_python_coder/examples/cpmpy/cpmpy.md +0 -57
  35. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/LICENSE +0 -0
  36. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/.gitignore +0 -0
  37. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/prompts/system.md +0 -0
  38. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/prompts/system_todo.md +0 -0
  39. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/__init__.py +0 -0
  40. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/README.md +0 -0
  41. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/bird_reasoning.md +0 -0
  42. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/diagnosis.md +0 -0
  43. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/simple_coloring.md +0 -0
  44. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/stable_marriage.md +0 -0
  45. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/clingo/sample_tasks/sudoku_mini.md +0 -0
  46. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/cpmpy/README.md +0 -0
  47. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/magic_square.md +0 -0
  48. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/cpmpy/sample_tasks/n_queens.md +0 -0
  49. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/regex/README.md +0 -0
  50. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/regex/regex.md +0 -0
  51. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/email_extraction.md +0 -0
  52. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/phone_validation.md +0 -0
  53. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/examples/regex/sample_tasks/url_parsing.md +0 -0
  54. {agentic_python_coder-3.2.0 → agentic_python_coder-3.4.0}/coder/src/agentic_python_coder/models/gemini3flash.json +0 -0
@@ -154,7 +154,6 @@ uv.lock
154
154
  # Project specific
155
155
  tests/
156
156
  coder-examples/
157
- CLAUDE-archive.md
158
157
  EXPERTISE/
159
158
  PROCESS_NOTES.md
160
159
  conversation_log.json
@@ -162,8 +161,6 @@ coder_output.log
162
161
  task.md
163
162
  solution.py
164
163
  *.tmp
165
- CLAUDE.md
166
- **/CLAUDE.md
167
164
  TRASH/
168
165
  LOCAL/
169
166
  ASP/
@@ -172,7 +169,6 @@ CPMPY/
172
169
  PAPER/
173
170
  PAPER-ASP/
174
171
  ZEBRA/
175
- .mcp.json
176
172
 
177
173
  # Test files and folders (root level only)
178
174
  /test-*/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-python-coder
3
- Version: 3.2.0
3
+ Version: 3.4.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,21 +14,21 @@ 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>=7.2.0
18
- Requires-Dist: jupyter-client>=8.8.0
19
- Requires-Dist: mcp>=1.26.0
20
- Requires-Dist: openai>=2.21.0
21
- Requires-Dist: python-dotenv>=1.2.1
17
+ Requires-Dist: ipykernel>=7.3.0
18
+ Requires-Dist: jupyter-client>=8.9.1
19
+ Requires-Dist: mcp>=1.28.1
20
+ Requires-Dist: openai>=2.45.0
21
+ Requires-Dist: python-dotenv>=1.2.2
22
22
  Requires-Dist: pyyaml>=6.0.3
23
- Requires-Dist: rich>=14.3.2
23
+ Requires-Dist: rich>=15.0.0
24
24
  Provides-Extra: dev
25
- Requires-Dist: mypy>=1.19.1; extra == 'dev'
26
- Requires-Dist: ruff>=0.14.14; extra == 'dev'
25
+ Requires-Dist: mypy>=2.2.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.15.21; extra == 'dev'
27
27
  Provides-Extra: test
28
- Requires-Dist: pytest-asyncio>=1.3.0; extra == 'test'
29
- Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
28
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == 'test'
29
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'test'
30
30
  Requires-Dist: pytest-watch>=4.2.0; extra == 'test'
31
- Requires-Dist: pytest>=9.0.2; extra == 'test'
31
+ Requires-Dist: pytest>=9.1.1; extra == 'test'
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  # Agentic Python Coder
@@ -147,19 +147,41 @@ coder -i
147
147
  ```bash
148
148
  # Built-in models (versioned names)
149
149
  coder --model sonnet45 "task" # Claude Sonnet 4.5 (default)
150
+ coder --model sonnet46 "task" # Claude Sonnet 4.6
151
+ coder --model sonnet5 "task" # Claude Sonnet 5
150
152
  coder --model opus45 "task" # Claude Opus 4.5
153
+ coder --model opus48 "task" # Claude Opus 4.8
151
154
  coder --model deepseek31 "task" # DeepSeek v3.1
152
155
  coder --model grok41 "task" # X.AI Grok 4.1
153
156
  coder --model qwen3 "task" # Qwen3 Coder
154
157
  coder --model gemini25 "task" # Gemini Pro 2.5
155
158
  coder --model gemini3pro "task" # Gemini 3 Pro Preview
159
+ coder --model gemini31 "task" # Gemini 3.1 Pro Preview
156
160
  coder --model gemini3flash "task" # Gemini 3 Flash Preview (fast, low-cost)
157
161
  coder --model gpt52 "task" # GPT-5.2
162
+ coder --model gpt56sol "task" # GPT-5.6 Sol
163
+ coder --model gpt56terra "task" # GPT-5.6 Terra
158
164
 
159
165
  # Custom model (JSON file)
160
166
  coder --model ./mymodel.json "task"
161
167
  ```
162
168
 
169
+ A custom model JSON can also point at any OpenAI-compatible endpoint
170
+ (OpenAI directly, a local Ollama/vLLM server, etc.) instead of OpenRouter:
171
+
172
+ ```json
173
+ {
174
+ "path": "gpt-4o-mini",
175
+ "base_url": "https://api.openai.com/v1",
176
+ "api_key_env": "OPENAI_API_KEY",
177
+ "temperature": 0.0
178
+ }
179
+ ```
180
+
181
+ `base_url` overrides the OpenRouter endpoint; `api_key_env` names the
182
+ environment variable holding the key for that endpoint (default:
183
+ `OPENROUTER_API_KEY`).
184
+
163
185
  ### Project Templates
164
186
 
165
187
  Domain-specific templates improve results. Bundled examples are available on GitHub at [`coder/src/agentic_python_coder/examples/`](coder/src/agentic_python_coder/examples/). Use `--init` to copy them locally:
@@ -225,6 +247,7 @@ messages, stats, log_path = solve_task(
225
247
  save_log=True, # Save conversation log
226
248
  task_basename=None, # Base name for output files
227
249
  step_limit=None, # Max agent steps (default: 200)
250
+ llm_config=None, # Pre-built LLMConfig (overrides model/api_key)
228
251
  )
229
252
  ```
230
253
 
@@ -253,6 +276,25 @@ messages2, stats2 = run_agent(agent, "Now plot column A", quiet=True)
253
276
  print(get_final_response(messages2))
254
277
  ```
255
278
 
279
+ Instead of a model name, you can pass a pre-built `LLMConfig` wrapping any
280
+ OpenAI-compatible client:
281
+
282
+ ```python
283
+ from openai import OpenAI
284
+ from agentic_python_coder import LLMConfig, create_coding_agent
285
+
286
+ llm_config = LLMConfig(
287
+ client=OpenAI(api_key="...", base_url="https://api.openai.com/v1"),
288
+ model="gpt-4o-mini",
289
+ api_params={"temperature": 0.0},
290
+ )
291
+
292
+ agent = create_coding_agent(
293
+ working_directory="/tmp/workspace",
294
+ llm_config=llm_config,
295
+ )
296
+ ```
297
+
256
298
  #### `get_openrouter_llm()` — LLM Access
257
299
 
258
300
  ```python
@@ -114,19 +114,41 @@ coder -i
114
114
  ```bash
115
115
  # Built-in models (versioned names)
116
116
  coder --model sonnet45 "task" # Claude Sonnet 4.5 (default)
117
+ coder --model sonnet46 "task" # Claude Sonnet 4.6
118
+ coder --model sonnet5 "task" # Claude Sonnet 5
117
119
  coder --model opus45 "task" # Claude Opus 4.5
120
+ coder --model opus48 "task" # Claude Opus 4.8
118
121
  coder --model deepseek31 "task" # DeepSeek v3.1
119
122
  coder --model grok41 "task" # X.AI Grok 4.1
120
123
  coder --model qwen3 "task" # Qwen3 Coder
121
124
  coder --model gemini25 "task" # Gemini Pro 2.5
122
125
  coder --model gemini3pro "task" # Gemini 3 Pro Preview
126
+ coder --model gemini31 "task" # Gemini 3.1 Pro Preview
123
127
  coder --model gemini3flash "task" # Gemini 3 Flash Preview (fast, low-cost)
124
128
  coder --model gpt52 "task" # GPT-5.2
129
+ coder --model gpt56sol "task" # GPT-5.6 Sol
130
+ coder --model gpt56terra "task" # GPT-5.6 Terra
125
131
 
126
132
  # Custom model (JSON file)
127
133
  coder --model ./mymodel.json "task"
128
134
  ```
129
135
 
136
+ A custom model JSON can also point at any OpenAI-compatible endpoint
137
+ (OpenAI directly, a local Ollama/vLLM server, etc.) instead of OpenRouter:
138
+
139
+ ```json
140
+ {
141
+ "path": "gpt-4o-mini",
142
+ "base_url": "https://api.openai.com/v1",
143
+ "api_key_env": "OPENAI_API_KEY",
144
+ "temperature": 0.0
145
+ }
146
+ ```
147
+
148
+ `base_url` overrides the OpenRouter endpoint; `api_key_env` names the
149
+ environment variable holding the key for that endpoint (default:
150
+ `OPENROUTER_API_KEY`).
151
+
130
152
  ### Project Templates
131
153
 
132
154
  Domain-specific templates improve results. Bundled examples are available on GitHub at [`coder/src/agentic_python_coder/examples/`](coder/src/agentic_python_coder/examples/). Use `--init` to copy them locally:
@@ -192,6 +214,7 @@ messages, stats, log_path = solve_task(
192
214
  save_log=True, # Save conversation log
193
215
  task_basename=None, # Base name for output files
194
216
  step_limit=None, # Max agent steps (default: 200)
217
+ llm_config=None, # Pre-built LLMConfig (overrides model/api_key)
195
218
  )
196
219
  ```
197
220
 
@@ -220,6 +243,25 @@ messages2, stats2 = run_agent(agent, "Now plot column A", quiet=True)
220
243
  print(get_final_response(messages2))
221
244
  ```
222
245
 
246
+ Instead of a model name, you can pass a pre-built `LLMConfig` wrapping any
247
+ OpenAI-compatible client:
248
+
249
+ ```python
250
+ from openai import OpenAI
251
+ from agentic_python_coder import LLMConfig, create_coding_agent
252
+
253
+ llm_config = LLMConfig(
254
+ client=OpenAI(api_key="...", base_url="https://api.openai.com/v1"),
255
+ model="gpt-4o-mini",
256
+ api_params={"temperature": 0.0},
257
+ )
258
+
259
+ agent = create_coding_agent(
260
+ working_directory="/tmp/workspace",
261
+ llm_config=llm_config,
262
+ )
263
+ ```
264
+
223
265
  #### `get_openrouter_llm()` — LLM Access
224
266
 
225
267
  ```python
@@ -1,6 +1,11 @@
1
1
  """Python Coding Agent - A minimal coding assistant using direct OpenAI API and OpenRouter."""
2
2
 
3
- __version__ = "3.0.0"
3
+ from importlib.metadata import PackageNotFoundError, version as _package_version
4
+
5
+ try:
6
+ __version__ = _package_version("agentic-python-coder")
7
+ except PackageNotFoundError: # running from source without installation
8
+ __version__ = "0.0.0+unknown"
4
9
 
5
10
  # High-level API (recommended for most users)
6
11
  from agentic_python_coder.runner import solve_task
@@ -50,6 +50,7 @@ def create_coding_agent(
50
50
  api_key: Optional[str] = None,
51
51
  todo: bool = False,
52
52
  verbose: bool = False,
53
+ llm_config: Optional[LLMConfig] = None,
53
54
  ) -> CodingAgent:
54
55
  """Create a ReAct agent for Python coding tasks.
55
56
 
@@ -65,6 +66,8 @@ def create_coding_agent(
65
66
  api_key: Optional API key override
66
67
  todo: If True, includes todo_write tool for task tracking
67
68
  verbose: If True, print progress info (default False for library use)
69
+ llm_config: Optional pre-built LLMConfig (any OpenAI-compatible
70
+ client); takes precedence over model/api_key
68
71
 
69
72
  Returns:
70
73
  Configured CodingAgent
@@ -77,17 +80,19 @@ def create_coding_agent(
77
80
  set_task_basename(task_basename)
78
81
 
79
82
  # Store packages for kernel initialization (clear if None to avoid state leaks)
83
+ # JSON-encoded: a comma join would corrupt specs like "pkg[extra1,extra2]"
80
84
  if with_packages is not None:
81
- os.environ["CODER_WITH_PACKAGES"] = ",".join(with_packages)
85
+ os.environ["CODER_WITH_PACKAGES"] = json.dumps(with_packages)
82
86
  else:
83
87
  os.environ.pop("CODER_WITH_PACKAGES", None)
84
88
 
85
- # Get LLM config
86
- llm_config = get_openrouter_llm(
87
- model=model or DEFAULT_MODEL,
88
- api_key=api_key,
89
- verbose=verbose,
90
- )
89
+ # Get LLM config (a pre-built one takes precedence)
90
+ if llm_config is None:
91
+ llm_config = get_openrouter_llm(
92
+ model=model or DEFAULT_MODEL,
93
+ api_key=api_key,
94
+ verbose=verbose,
95
+ )
91
96
 
92
97
  # Create tool registry
93
98
  tools = create_tool_registry(todo=todo)
@@ -199,8 +204,9 @@ def run_agent(
199
204
  Returns:
200
205
  Tuple of (List of new messages from this call, Statistics dictionary)
201
206
  """
202
- # Set working directory at execution time (not creation time)
203
- # This prevents race conditions when multiple agents are created
207
+ # The working directory is a process-wide singleton; set it at execution
208
+ # time so it always matches the agent currently running (concurrent
209
+ # run_agent calls in one process are not supported)
204
210
  working_dir.set(agent.working_directory)
205
211
 
206
212
  limit = step_limit if step_limit is not None else DEFAULT_STEP_LIMIT
@@ -228,7 +234,7 @@ def run_agent(
228
234
  openai_tools = agent.tools.get_openai_tools()
229
235
 
230
236
  # ReAct loop
231
- for step_num in range(limit):
237
+ for _ in range(limit):
232
238
  # Build request kwargs
233
239
  request_kwargs: dict[str, Any] = {
234
240
  "model": agent.llm_config.model,
@@ -243,13 +249,17 @@ def run_agent(
243
249
  # Call API
244
250
  response = agent.llm_config.client.chat.completions.create(**request_kwargs)
245
251
 
246
- # Track tokens
252
+ # Track tokens (some providers return usage objects with None fields)
247
253
  if response.usage:
248
- stats["token_consumption"]["input_tokens"] += response.usage.prompt_tokens
254
+ stats["token_consumption"]["input_tokens"] += (
255
+ response.usage.prompt_tokens or 0
256
+ )
249
257
  stats["token_consumption"]["output_tokens"] += (
250
- response.usage.completion_tokens
258
+ response.usage.completion_tokens or 0
259
+ )
260
+ stats["token_consumption"]["total_tokens"] += (
261
+ response.usage.total_tokens or 0
251
262
  )
252
- stats["token_consumption"]["total_tokens"] += response.usage.total_tokens
253
263
 
254
264
  if not response.choices:
255
265
  break
@@ -324,6 +334,29 @@ def run_agent(
324
334
  }
325
335
  )
326
336
 
337
+ elif response.choices[0].finish_reason == "length":
338
+ # Response cut off at max_tokens with no tool call — don't accept
339
+ # the truncated text as a final answer; ask the model to continue
340
+ agent.messages.append(
341
+ {
342
+ "role": "assistant",
343
+ "content": assistant_message.content or "",
344
+ }
345
+ )
346
+ agent.messages.append(
347
+ {
348
+ "role": "user",
349
+ "content": (
350
+ "Your previous response was truncated at the max_tokens "
351
+ "limit. Continue, and be more concise."
352
+ ),
353
+ }
354
+ )
355
+ if not quiet:
356
+ print(
357
+ "Warning: response truncated at max_tokens; asking model to continue"
358
+ )
359
+
327
360
  else:
328
361
  # No tool calls — final answer
329
362
  agent.messages.append(
@@ -333,6 +366,11 @@ def run_agent(
333
366
  }
334
367
  )
335
368
  break
369
+ else:
370
+ # Loop exhausted without a final answer
371
+ stats["step_limit_reached"] = True
372
+ if not quiet:
373
+ print(f"Warning: step limit ({limit}) reached without a final response")
336
374
 
337
375
  stats["execution_time_seconds"] = time.time() - start_time
338
376
 
@@ -260,6 +260,9 @@ def validate_model(model):
260
260
  print(f" - {m}")
261
261
  print("\nOr provide a path to a custom model JSON file.")
262
262
  sys.exit(1)
263
+ except ValueError as e:
264
+ print(f"Error: {e}")
265
+ sys.exit(1)
263
266
 
264
267
 
265
268
  def load_project_prompt(project_path: str) -> Optional[str]:
@@ -290,12 +293,19 @@ def load_project_prompt(project_path: str) -> Optional[str]:
290
293
 
291
294
 
292
295
  def run_interactive(
293
- working_dir: Path, model: str, project_prompt: str, api_key: str, todo: bool
296
+ working_dir: Path,
297
+ model: str,
298
+ project_prompt: str,
299
+ api_key: str,
300
+ todo: bool,
301
+ with_packages=None,
294
302
  ):
295
303
  """Run the agent in interactive mode."""
296
304
  print(f"\nInteractive mode - working in: {working_dir}")
297
305
  if project_prompt:
298
306
  print("Project configuration loaded")
307
+ if with_packages:
308
+ print(f"Dynamic packages: {', '.join(with_packages)}")
299
309
  print("Type 'exit' or 'quit' to stop.\n")
300
310
 
301
311
  # Load system prompt
@@ -307,12 +317,12 @@ def run_interactive(
307
317
  system_prompt=system_prompt,
308
318
  model=model,
309
319
  project_prompt=project_prompt,
320
+ with_packages=with_packages,
310
321
  api_key=api_key,
311
322
  todo=todo,
312
323
  verbose=True,
313
324
  )
314
325
 
315
- thread_id = "interactive"
316
326
  all_messages = []
317
327
  cumulative_stats = {
318
328
  "tool_usage": {},
@@ -333,7 +343,7 @@ def run_interactive(
333
343
  continue
334
344
 
335
345
  print("\nAgent working...\n")
336
- messages, stats = run_agent(agent, user_input, thread_id, quiet=False)
346
+ messages, stats = run_agent(agent, user_input, quiet=False)
337
347
  all_messages.extend(messages)
338
348
 
339
349
  # Update cumulative stats
@@ -440,7 +450,12 @@ def main():
440
450
  try:
441
451
  if args.interactive:
442
452
  run_interactive(
443
- working_dir, args.model, project_prompt, args.api_key, args.todo
453
+ working_dir,
454
+ args.model,
455
+ project_prompt,
456
+ args.api_key,
457
+ args.todo,
458
+ with_packages=args.with_packages,
444
459
  )
445
460
  else:
446
461
  # Use solve_task for the main flow
@@ -29,9 +29,10 @@ Before completing your solution, verify:
29
29
  ☐ 2. **Model solves correctly** - At least one answer set is found (or UNSAT handled)
30
30
  ☐ 3. **Optimization applied** - If problem asks for optimal, use #minimize/#maximize
31
31
  ☐ 4. **Solution extracted** - Answer set atoms properly extracted and formatted
32
+ ☐ 4b. **No Model escapes on_model** - All solution data extracted to plain Python objects inside the callback (touching a Model after solve() returns segfaults)
32
33
  ☐ 5. **Output format correct** - JSON output with appropriate structure
33
34
  ☐ 6. **Predicates meaningful** - Clear names that reflect the problem domain
34
- ☐ 7. **Constraints verified** - All problem constraints are encoded
35
+ ☐ 7. **Constraints verified independently** - All problem constraints are encoded AND re-checked by a plain-Python verify() whose expectations are re-derived from the problem text, not from your encoding's own interpretation
35
36
  ☐ 8. **File written** - Create `solution.py` with complete working code
36
37
 
37
38
  ## Section 2: Critical Rules of Engagement
@@ -474,6 +475,38 @@ def extract_solution(model):
474
475
  return solution
475
476
  ```
476
477
 
478
+ ### CRITICAL: The Model Object Is Only Valid Inside on_model (Segfault Risk)
479
+
480
+ The `clingo.Model` object passed to your `on_model` callback is only valid DURING
481
+ that callback. After `ctl.solve()` returns, clingo frees the underlying model —
482
+ touching it later is a use-after-free that crashes the Python process with a
483
+ segmentation fault (exit code 139) and NO Python traceback.
484
+
485
+ Extract everything you need into plain Python objects (ints, strings, lists, dicts)
486
+ INSIDE the callback. Never store the Model object itself for later use.
487
+
488
+ ```python
489
+ # WRONG - segfaults after solve() returns:
490
+ saved = None
491
+ def on_model(m):
492
+ global saved
493
+ saved = m # Model outlives the callback
494
+ ctl.solve(on_model=on_model)
495
+ atoms = saved.symbols(atoms=True) # use-after-free -> SIGSEGV, no traceback
496
+
497
+ # CORRECT - extract inside the callback:
498
+ result = []
499
+ def on_model(m):
500
+ for a in m.symbols(atoms=True):
501
+ if a.match("assigned", 2):
502
+ result.append((str(a.arguments[0]), str(a.arguments[1])))
503
+ ctl.solve(on_model=on_model)
504
+ # work with the plain tuples in result
505
+ ```
506
+
507
+ This also applies to verification: snapshot the atoms into plain Python inside
508
+ `on_model`, then run your checks on the snapshot after solving.
509
+
477
510
  ## Section 4: Problem-Solving Pattern Library
478
511
 
479
512
  ### 4.1 Modeling State and Change (NEW - CRITICAL)
@@ -964,6 +997,36 @@ possible_col(C) :- col(C).
964
997
  4. **Let solver determine gaps**: Don't over-constrain non-run cells
965
998
  5. **Different is adjacent**: Cells of different colors/states CAN touch
966
999
 
1000
+ ### 4.11 "Reachable Only After" / Ordered Reachability Constraints
1001
+
1002
+ When a problem requires that some target element become reachable only after all
1003
+ other elements (or only after specific ones) under an evolving reachability process,
1004
+ do NOT model it as an existential ordering — guessing a permutation with the target
1005
+ pinned last only proves that SOME order exists, which is weaker than what such
1006
+ problems ask. The intended reading is almost always about the actual reachability
1007
+ process: everything reachable gets reached as soon as it is reachable.
1008
+
1009
+ Model the process directly with a time-indexed fixpoint and constrain first-reach
1010
+ times:
1011
+
1012
+ ```asp
1013
+ step(0..max_step).
1014
+ reached(S, 0) :- start(S).
1015
+ reached(Y, T+1) :- edge(X, Y), reached(X, T), step(T+1), unlocked(X, Y, T).
1016
+ reached(X, T+1) :- reached(X, T), step(T+1). % persistence
1017
+ first_reached(X, 0) :- reached(X, 0).
1018
+ first_reached(X, T) :- reached(X, T), not reached(X, T-1), T > 0, step(T).
1019
+
1020
+ % target must not become reachable before (or together with) any other element:
1021
+ :- first_reached(target, Tg), first_reached(X, Tx), X != target, Tx >= Tg.
1022
+ ```
1023
+
1024
+ Adapt `unlocked/3` to whatever gates progression (collected items, satisfied
1025
+ prerequisites, ...). Verify with a plain-Python simulation of the same process:
1026
+ repeatedly add everything currently reachable, one step at a time, and check the
1027
+ required ordering of first-reach steps against the problem text — do not reuse
1028
+ your encoding's ordering predicates in the verifier.
1029
+
967
1030
  ## Section 5: Debugging & Advanced Techniques
968
1031
 
969
1032
  ### 5.1 Efficient Generation: Constrain Choices Early (Anti-Pattern: Generate-and-Filter)
@@ -0,0 +1,100 @@
1
+ # CPMPY project prompt
2
+
3
+ You are solving constraint programming problems using CPMpy.
4
+
5
+ ## Core Rules
6
+
7
+ 1. Use CPMpy's constraint modeling - never write search algorithms
8
+ 2. Output ONLY valid JSON using `json.dumps()` - no other text
9
+ 3. Always `import json` if outputting JSON
10
+ 4. Check the exact output format required
11
+ 5. Test your solution manually to verify it satisfies the problem
12
+
13
+ ## Basic Template
14
+ ```python
15
+ from cpmpy import *
16
+ import json
17
+
18
+ # Variables
19
+ # Constraints
20
+ # Solve
21
+ if model.solve():
22
+ # Build result dict as specified
23
+ result = {...}
24
+ # Verify solution satisfies problem requirements
25
+ print(json.dumps(result))
26
+ else:
27
+ print(json.dumps({"error": "No solution"}))
28
+ ```
29
+
30
+ ## Essential Constraints
31
+ - `AllDifferent(vars)` - all different values
32
+ - `sum(vars) == total` - sum constraint
33
+ - `Circuit(x)` - variables x form a Hamiltonian circuit (for routing/tour problems)
34
+ - `InDomain(var, [values])` - restrict variable to a set of values. **Must be explicitly imported**: `from cpmpy import InDomain`
35
+ - Logical: `&`, `|`, `~` on constraints; `(cond).implies(other)` on constraints only
36
+ - Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=` between variables and expressions
37
+ - Element: `vars[idx]` where idx is a decision variable (direct indexing works)
38
+
39
+ ## CPMpy Pitfalls - Avoid These
40
+ - Do NOT use `.is_in()`, `.in_domain()`, or other methods that don't exist — use `InDomain(var, list)` instead
41
+ - Do NOT use `~var` on integer variables — `~` only works on boolean constraints
42
+ - Do NOT call `.implies()` on integer variables — only on boolean expressions/constraints
43
+ - If an operator/method fails, fall back to explicit OR/AND over individual equality constraints
44
+ - When in doubt, use simple comparisons and loops instead of advanced abstractions
45
+
46
+ ## Optimization
47
+ - Use `model.minimize(objective)` or `model.maximize(objective)`
48
+ - CPMpy automatically finds the OPTIMAL solution, not just first valid
49
+ - The solver continues searching until it proves optimality
50
+ - Always verify the objective value matches your expectation
51
+ - Example:
52
+ ```python
53
+ profit = sum(price[i] * x[i] for i in range(n))
54
+ model.maximize(profit)
55
+ ```
56
+
57
+ ## Output Format Rules
58
+ - Follow the output JSON format specification EXACTLY — do not invent alternative encodings
59
+ - If a field is described as nested arrays, output nested arrays (e.g., `[[1,2],[3,4]]`), never compress to single integers
60
+ - If the narrative text conflicts with the explicit "Output format" block, the Output format block wins
61
+ - If the narrative mentions optional variants or extensions of the problem ("one extension is...", "another version..."), solve the base problem as specified by the input data and the Output format block — do not output an extended variant unless the output spec explicitly requires it
62
+ - Derive every output array's dimensions from parameters stated in the problem and from the axis names in the format spec: `result[a][b]` means outer axis `a` and inner axis `b` — never swap axes or substitute a different quantity
63
+ - Check array dimensions and value ranges match the spec before saving
64
+
65
+ ## Verification Requirement (MANDATORY)
66
+
67
+ CPMpy models can easily contain subtle logic bugs. Before saving, you MUST:
68
+
69
+ 1. **Solve and extract** the solution values
70
+ 2. **Write a `verify()` function** that checks every constraint from the problem statement using plain Python loops and asserts — independent of your CPMpy model:
71
+ ```python
72
+ def verify(result):
73
+ # Check array shapes — sizes re-derived from the problem text,
74
+ # NOT from variables your model introduced
75
+ assert len(result["schedule"]) == n_weeks
76
+ # Check each constraint with plain Python
77
+ for w in range(n_weeks):
78
+ teams_this_week = [result["schedule"][w][p] for p in range(n_periods)]
79
+ assert len(set(teams_this_week)) == len(teams_this_week), "Duplicate team in week"
80
+ # ... check ALL constraints from the problem statement
81
+ print("All checks passed")
82
+ ```
83
+
84
+ The verifier must be derived from the problem text alone: re-read the problem
85
+ statement and re-derive every expected dimension, entry encoding, and constraint
86
+ from its own parameters. Never assert against quantities or interpretations that
87
+ your model introduced — `assert len(x) == my_total` proves nothing if `my_total`
88
+ encodes a misreading. If your model and your verifier share an assumption, a
89
+ wrong assumption passes silently.
90
+ 3. **Execute the verification** via python_exec and confirm it passes
91
+ 4. **For optimization**: verify optimality by re-solving with a stricter bound:
92
+ ```python
93
+ model2 = Model(model.constraints)
94
+ model2 += objective < best_value # or > for maximize
95
+ assert not model2.solve(), "Solution is not optimal"
96
+ ```
97
+
98
+ Do NOT call save_code until verification passes. If it fails, fix the model and re-verify.
99
+
100
+ That's it. Read the problem carefully, model it declaratively, verify independently, and let CPMpy find the optimal solution.