solidworks-mcp-python 1.5.0__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.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1110 @@
1
+ """Checkpoint execution service for the Prefab CAD assistant dashboard.
2
+
3
+ Responsible for planning and executing individual checkpoint steps against the
4
+ SolidWorks adapter.
5
+
6
+ Design note:
7
+ ``_run_checkpoint_tools`` uses a handler map to dispatch tool names to adapter
8
+ calls, allowing new tool bindings without growing a long conditional chain.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from textwrap import dedent
17
+ from typing import Any
18
+
19
+ from ...adapters import create_adapter
20
+ from ...agents.history_db import (
21
+ insert_tool_call_record,
22
+ list_plan_checkpoints,
23
+ update_plan_checkpoint,
24
+ upsert_design_session,
25
+ )
26
+ from ...config import load_config
27
+ from ._utils import (
28
+ DEFAULT_API_ORIGIN,
29
+ DEFAULT_PREVIEW_ORIENTATION,
30
+ DEFAULT_SESSION_ID,
31
+ DEFAULT_SOURCE_MODE,
32
+ DEFAULT_USER_GOAL,
33
+ ensure_preview_dir,
34
+ merge_metadata,
35
+ parse_json_blob,
36
+ )
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Internal helpers
41
+ # ---------------------------------------------------------------------------
42
+ def _planned_tools(planned: dict[str, Any]) -> list[str]:
43
+ """Return the list of tool names from a planned-action dict.
44
+
45
+ Args:
46
+ planned: Parsed planned-action JSON dict.
47
+
48
+ Returns:
49
+ List of tool name strings.
50
+ """
51
+ tools = planned.get("tools", [])
52
+ return [str(t) for t in tools] if isinstance(tools, list) else []
53
+
54
+
55
+ def _checkpoint_script_dir(session_id: str) -> Path:
56
+ return Path(".solidworks_mcp") / "checkpoint_scripts" / session_id
57
+
58
+
59
+ def _checkpoint_script_path(session_id: str, checkpoint_index: int) -> Path:
60
+ stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f")
61
+ script_name = f"checkpoint_{checkpoint_index:03d}_{stamp}.py"
62
+ return _checkpoint_script_dir(session_id) / script_name
63
+
64
+
65
+ def _planned_tool_payloads(planned: dict[str, Any], tool: str) -> list[dict[str, Any]]:
66
+ """Return ordered payload dicts for a tool from planned action JSON.
67
+
68
+ Supports either flat payloads (top-level keys) or nested execution-style
69
+ payloads stored under ``tool`` and ``tool#N`` keys.
70
+ """
71
+ payloads: list[dict[str, Any]] = []
72
+ direct = planned.get(tool)
73
+ if isinstance(direct, dict):
74
+ payloads.append(dict(direct))
75
+
76
+ suffixed: list[tuple[str, dict[str, Any]]] = []
77
+ for key, value in planned.items():
78
+ if not isinstance(key, str) or not isinstance(value, dict):
79
+ continue
80
+ if key.startswith(f"{tool}#"):
81
+ suffixed.append((key, dict(value)))
82
+
83
+ def _suffix_order(item: tuple[str, dict[str, Any]]) -> tuple[int, str]:
84
+ key = item[0]
85
+ suffix = key.split("#", 1)[1] if "#" in key else ""
86
+ return (int(suffix) if suffix.isdigit() else 10_000, key)
87
+
88
+ for _, payload in sorted(suffixed, key=_suffix_order):
89
+ payloads.append(payload)
90
+
91
+ return payloads
92
+
93
+
94
+ def _should_open_empty_part(
95
+ session_meta: dict[str, Any], planned: dict[str, Any]
96
+ ) -> bool:
97
+ """Return whether checkpoint execution should open a blank part first."""
98
+ workflow_mode = str(session_meta.get("workflow_mode") or "").strip().lower()
99
+ if workflow_mode != "new_design":
100
+ return False
101
+
102
+ if str(session_meta.get("active_model_path") or "").strip():
103
+ return False
104
+
105
+ if bool(session_meta.get("new_design_part_opened")):
106
+ return False
107
+
108
+ tools = _planned_tools(planned)
109
+ return not any(
110
+ tool in {"create_part", "create_assembly", "open_model"} for tool in tools
111
+ )
112
+
113
+
114
+ async def _open_empty_part_before_checkpoint(
115
+ *,
116
+ session_id: str,
117
+ session_row: dict[str, Any],
118
+ db_path: Path | None,
119
+ api_origin: str,
120
+ ) -> dict[str, Any]:
121
+ """Create a blank part document and refresh preview before checkpoint execution."""
122
+ from .preview_service import refresh_preview # noqa: PLC0415
123
+
124
+ config = load_config()
125
+ adapter = await create_adapter(config)
126
+ part_name = f"{session_id}_new_part"
127
+ try:
128
+ await adapter.connect()
129
+ create_result = await adapter.create_part(name=part_name)
130
+ if not getattr(create_result, "is_success", False):
131
+ raise RuntimeError(create_result.error or "Failed to open blank part")
132
+
133
+ merge_metadata(
134
+ session_id,
135
+ db_path=db_path,
136
+ workflow_mode="new_design",
137
+ new_design_part_opened=True,
138
+ new_design_part_name=part_name,
139
+ active_model_status="Opened a new empty part document for checkpoint execution.",
140
+ active_model_type="Part",
141
+ active_model_configuration="Default",
142
+ preview_status="Refreshing preview for new empty part...",
143
+ latest_message="Opened a new empty part document for checkpoint execution.",
144
+ latest_error_text="",
145
+ remediation_hint="",
146
+ )
147
+ insert_tool_call_record(
148
+ session_id=session_id,
149
+ tool_name="ui.open_empty_part",
150
+ input_json=json.dumps(
151
+ {
152
+ "session_id": session_id,
153
+ "part_name": part_name,
154
+ "user_goal": session_row.get("user_goal") or DEFAULT_USER_GOAL,
155
+ },
156
+ ensure_ascii=True,
157
+ ),
158
+ output_json=json.dumps(
159
+ {"part_name": part_name, "status": "success"},
160
+ ensure_ascii=True,
161
+ ),
162
+ success=True,
163
+ db_path=db_path,
164
+ )
165
+
166
+ try:
167
+ await refresh_preview(
168
+ session_id,
169
+ orientation=DEFAULT_PREVIEW_ORIENTATION,
170
+ db_path=db_path,
171
+ preview_dir=ensure_preview_dir(),
172
+ api_origin=api_origin,
173
+ adapter_override=adapter,
174
+ active_model_path_override="",
175
+ reopen_active_model=False,
176
+ )
177
+ except Exception as preview_exc:
178
+ merge_metadata(
179
+ session_id,
180
+ db_path=db_path,
181
+ latest_message="Opened a new empty part document; preview refresh is still pending.",
182
+ preview_status="Opened a new empty part document; preview refresh is still pending.",
183
+ latest_error_text=str(preview_exc),
184
+ remediation_hint="Retry preview refresh after SolidWorks finishes opening the new part.",
185
+ )
186
+ finally:
187
+ try:
188
+ await adapter.disconnect()
189
+ except Exception:
190
+ pass
191
+
192
+ return {"part_name": part_name, "status": "success"}
193
+
194
+
195
+ def _render_checkpoint_script(
196
+ planned: dict[str, Any],
197
+ *,
198
+ session_id: str,
199
+ checkpoint_index: int,
200
+ checkpoint_title: str,
201
+ ) -> str:
202
+ """Render a strict script for checkpoint execution.
203
+
204
+ The generated script has explicit breakpoint comments and does not use default
205
+ geometry fallbacks. Missing parameters raise errors so users can repair the plan.
206
+ """
207
+ planned_json = json.dumps(planned, ensure_ascii=True)
208
+ return dedent(
209
+ f"""
210
+ from __future__ import annotations
211
+
212
+ import asyncio
213
+ import json
214
+ import os
215
+ import pdb
216
+ import traceback
217
+ from typing import Any
218
+
219
+ from solidworks_mcp.adapters import create_adapter
220
+ from solidworks_mcp.adapters.base import ExtrusionParameters
221
+ from solidworks_mcp.config import load_config
222
+
223
+ SESSION_ID = {session_id!r}
224
+ CHECKPOINT_INDEX = {checkpoint_index}
225
+ CHECKPOINT_TITLE = {checkpoint_title!r}
226
+ PLANNED: dict[str, Any] = json.loads({planned_json!r})
227
+ DEBUG_PAUSE = os.getenv("SOLIDWORKS_UI_CHECKPOINT_DEBUG_PAUSE", "0").strip().lower() in {"1", "true", "yes", "on"}
228
+
229
+
230
+ def require_key(key: str) -> Any:
231
+ if key not in PLANNED:
232
+ raise ValueError(f"Missing required planned key: {{key}}")
233
+ return PLANNED[key]
234
+
235
+
236
+ def require_float(key: str) -> float:
237
+ value = require_key(key)
238
+ try:
239
+ return float(value)
240
+ except (TypeError, ValueError) as exc:
241
+ raise ValueError(f"Invalid numeric value for {{key}}: {{value!r}}") from exc
242
+
243
+
244
+ def require_first_float(*keys: str) -> float:
245
+ for key in keys:
246
+ if key in PLANNED:
247
+ try:
248
+ return float(PLANNED[key])
249
+ except (TypeError, ValueError) as exc:
250
+ raise ValueError(
251
+ f"Invalid numeric value for {{key}}: {{PLANNED[key]!r}}"
252
+ ) from exc
253
+ raise ValueError(f"Missing required planned key(s): {{', '.join(keys)}}")
254
+
255
+
256
+ def require_vec(key: str, size: int) -> list[float]:
257
+ raw = require_key(key)
258
+ if not isinstance(raw, list) or len(raw) != size:
259
+ raise ValueError(
260
+ f"{{key}} must be a list with {{size}} numeric values; got {{raw!r}}"
261
+ )
262
+ try:
263
+ return [float(v) for v in raw]
264
+ except (TypeError, ValueError) as exc:
265
+ raise ValueError(f"{{key}} must contain numeric values; got {{raw!r}}") from exc
266
+
267
+
268
+ def require_result(result: Any, label: str) -> Any:
269
+ if result is None:
270
+ raise RuntimeError(f"{{label}} returned None")
271
+ if not getattr(result, "is_success", False):
272
+ raise RuntimeError(f"{{label}} failed: {{getattr(result, 'error', 'unknown error')}}")
273
+ return result
274
+
275
+
276
+ def pause_point(label: str) -> None:
277
+ print(f"EDIT POINT: {{label}}")
278
+ if DEBUG_PAUSE:
279
+ pdb.set_trace()
280
+
281
+
282
+ def payloads_for_tool(tool: str) -> list[dict[str, Any]]:
283
+ payloads: list[dict[str, Any]] = []
284
+ direct = PLANNED.get(tool)
285
+ if isinstance(direct, dict):
286
+ payloads.append(dict(direct))
287
+
288
+ suffixed: list[tuple[str, dict[str, Any]]] = []
289
+ for key, value in PLANNED.items():
290
+ if not isinstance(key, str) or not isinstance(value, dict):
291
+ continue
292
+ if key.startswith(f"{{tool}}#"):
293
+ suffixed.append((key, dict(value)))
294
+
295
+ def suffix_order(item: tuple[str, dict[str, Any]]) -> tuple[int, str]:
296
+ key = item[0]
297
+ suffix = key.split("#", 1)[1] if "#" in key else ""
298
+ return (int(suffix) if suffix.isdigit() else 10_000, key)
299
+
300
+ for _, payload in sorted(suffixed, key=suffix_order):
301
+ payloads.append(payload)
302
+
303
+ return payloads
304
+
305
+
306
+ def key_from(payload: dict[str, Any], key: str) -> Any:
307
+ if key in payload:
308
+ return payload[key]
309
+ return require_key(key)
310
+
311
+
312
+ def vec_from(payload: dict[str, Any], key: str, size: int) -> list[float]:
313
+ raw = key_from(payload, key)
314
+ if not isinstance(raw, list) or len(raw) != size:
315
+ raise ValueError(
316
+ f"{{key}} must be a list with {{size}} numeric values; got {{raw!r}}"
317
+ )
318
+ try:
319
+ return [float(v) for v in raw]
320
+ except (TypeError, ValueError) as exc:
321
+ raise ValueError(f"{{key}} must contain numeric values; got {{raw!r}}") from exc
322
+
323
+
324
+ def first_float_from(payload: dict[str, Any], *keys: str) -> float:
325
+ for key in keys:
326
+ if key in payload:
327
+ try:
328
+ return float(payload[key])
329
+ except (TypeError, ValueError) as exc:
330
+ raise ValueError(f"Invalid numeric value for {{key}}: {{payload[key]!r}}") from exc
331
+ return require_first_float(*keys)
332
+
333
+
334
+ async def run_checkpoint() -> dict[str, Any]:
335
+ tools = PLANNED.get("tools", [])
336
+ if not isinstance(tools, list) or not tools:
337
+ raise ValueError("planned.tools must be a non-empty list")
338
+
339
+ tool_runs: list[dict[str, str]] = []
340
+ failed_tools: list[str] = []
341
+
342
+ config = load_config()
343
+ adapter = await create_adapter(config)
344
+
345
+ await adapter.connect()
346
+ try:
347
+ for idx, tool in enumerate([str(t) for t in tools], start=1):
348
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} step {{idx}}/{{len(tools)}} before {{tool}}")
349
+ print(f"CHECKPOINT {{CHECKPOINT_INDEX}} STEP {{idx}}/{{len(tools)}}: {{tool}}")
350
+ try:
351
+ if tool == "create_part":
352
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
353
+ part_name = str(key_from(payload, "part_name"))
354
+ require_result(await adapter.create_part(name=part_name), "create_part")
355
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created part '{{part_name}}'"}})
356
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_part")
357
+
358
+ elif tool == "create_assembly":
359
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
360
+ assembly_name = str(key_from(payload, "assembly_name"))
361
+ require_result(await adapter.create_assembly(name=assembly_name), "create_assembly")
362
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created assembly '{{assembly_name}}'"}})
363
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_assembly")
364
+
365
+ elif tool == "open_model":
366
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
367
+ model_path = str(key_from(payload, "model_path"))
368
+ require_result(await adapter.open_model(model_path), "open_model")
369
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Opened model '{{model_path}}'"}})
370
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after open_model")
371
+
372
+ elif tool == "create_sketch":
373
+ payloads = payloads_for_tool(tool) or [{{}}]
374
+ for payload in payloads:
375
+ sketch_plane = str(key_from(payload, "sketch_plane"))
376
+ require_result(await adapter.create_sketch(sketch_plane), "create_sketch")
377
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created sketch on '{{sketch_plane}}'"}})
378
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_sketch")
379
+
380
+ elif tool == "exit_sketch":
381
+ require_result(await adapter.exit_sketch(), "exit_sketch")
382
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Exited sketch"}})
383
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after exit_sketch")
384
+
385
+ elif tool == "add_line":
386
+ payloads = payloads_for_tool(tool) or [{{}}]
387
+ for payload in payloads:
388
+ if "line_mm" in payload or "line_mm" in PLANNED:
389
+ x1, y1, x2, y2 = vec_from(payload, "line_mm", 4)
390
+ else:
391
+ sx, sy = vec_from(payload, "line_start_mm", 2)
392
+ ex, ey = vec_from(payload, "line_end_mm", 2)
393
+ x1, y1, x2, y2 = sx, sy, ex, ey
394
+ require_result(await adapter.add_line(x1, y1, x2, y2), "add_line")
395
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Added line"}})
396
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_line")
397
+
398
+ elif tool == "add_rectangle":
399
+ payloads = payloads_for_tool(tool) or [{{}}]
400
+ for payload in payloads:
401
+ x, y, width, height = vec_from(payload, "rectangle_mm", 4)
402
+ require_result(await adapter.add_rectangle(x, y, width, height), "add_rectangle")
403
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Added rectangle"}})
404
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_rectangle")
405
+
406
+ elif tool == "add_circle":
407
+ payloads = payloads_for_tool(tool) or [{{}}]
408
+ for payload in payloads:
409
+ cx, cy = vec_from(payload, "circle_center_mm", 2)
410
+ radius = first_float_from(payload, "circle_radius_mm")
411
+ require_result(await adapter.add_circle(cx, cy, radius), "add_circle")
412
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Added circle"}})
413
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_circle")
414
+
415
+ elif tool == "add_centerline":
416
+ payloads = payloads_for_tool(tool) or [{{}}]
417
+ for payload in payloads:
418
+ x1, y1, x2, y2 = vec_from(payload, "centerline_mm", 4)
419
+ require_result(await adapter.add_centerline(x1, y1, x2, y2), "add_centerline")
420
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Added centerline"}})
421
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_centerline")
422
+
423
+ elif tool == "add_arc":
424
+ payloads = payloads_for_tool(tool) or [{{}}]
425
+ for payload in payloads:
426
+ cx, cy = vec_from(payload, "arc_center_mm", 2)
427
+ sx, sy = vec_from(payload, "arc_start_mm", 2)
428
+ ex, ey = vec_from(payload, "arc_end_mm", 2)
429
+ require_result(await adapter.add_arc(cx, cy, sx, sy, ex, ey), "add_arc")
430
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Added arc"}})
431
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_arc")
432
+
433
+ elif tool == "create_extrusion":
434
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
435
+ depth = first_float_from(payload, "depth_mm", "depth")
436
+ params = ExtrusionParameters(
437
+ depth=depth,
438
+ thin_feature=bool(payload.get("thin_feature", PLANNED.get("thin_feature", False))),
439
+ thin_thickness=(
440
+ float(payload["thin_thickness_mm"])
441
+ if "thin_thickness_mm" in payload
442
+ else (
443
+ float(PLANNED["thin_thickness_mm"])
444
+ if "thin_thickness_mm" in PLANNED
445
+ else float(payload.get("thin_thickness", PLANNED.get("thin_thickness", 0.0)))
446
+ )
447
+ ),
448
+ both_directions=bool(payload.get("both_directions", PLANNED.get("both_directions", False))),
449
+ )
450
+ require_result(await adapter.create_extrusion(params), "create_extrusion")
451
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created extrusion depth={{depth}}mm"}})
452
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_extrusion")
453
+
454
+ elif tool == "create_cut_extrude":
455
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
456
+ depth = first_float_from(payload, "depth_mm", "depth")
457
+ params = ExtrusionParameters(depth=depth)
458
+ require_result(await adapter.create_cut_extrude(params), "create_cut_extrude")
459
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut-extrude depth={{depth}}mm"}})
460
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_cut_extrude")
461
+
462
+ elif tool == "create_cut":
463
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
464
+ depth = first_float_from(payload, "depth_mm", "depth")
465
+ sketch_name = str(payload.get("sketch_name", PLANNED.get("sketch_name", "")))
466
+ if sketch_name:
467
+ require_result(await adapter.create_cut(sketch_name, depth), "create_cut")
468
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut from {{sketch_name}} depth={{depth}}mm"}})
469
+ else:
470
+ params = ExtrusionParameters(depth=depth)
471
+ require_result(await adapter.create_cut_extrude(params), "create_cut")
472
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Created cut-extrude depth={{depth}}mm"}})
473
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after create_cut")
474
+
475
+ elif tool == "add_fillet":
476
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
477
+ radius = first_float_from(payload, "radius_mm", "radius")
478
+ raw_edges = payload.get("edge_names", PLANNED.get("edge_names", []))
479
+ edge_names = [str(edge) for edge in raw_edges] if isinstance(raw_edges, list) else []
480
+ require_result(await adapter.add_fillet(radius, edge_names), "add_fillet")
481
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Added fillet radius={{radius}}mm"}})
482
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after add_fillet")
483
+
484
+ elif tool == "check_sketch_fully_defined":
485
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
486
+ sketch_name = payload.get("sketch_name", PLANNED.get("sketch_name"))
487
+ require_result(
488
+ await adapter.check_sketch_fully_defined(
489
+ str(sketch_name) if sketch_name else None
490
+ ),
491
+ "check_sketch_fully_defined",
492
+ )
493
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Checked sketch definition"}})
494
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after check_sketch_fully_defined")
495
+
496
+ elif tool == "save_file":
497
+ payload = (payloads_for_tool(tool) or [{{}}])[0]
498
+ file_path = str(key_from(payload, "file_path"))
499
+ require_result(await adapter.save_file(file_path), "save_file")
500
+ tool_runs.append({{"tool": tool, "status": "success", "message": f"Saved file '{{file_path}}'"}})
501
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after save_file")
502
+
503
+ elif tool == "get_model_info":
504
+ require_result(await adapter.get_model_info(), "get_model_info")
505
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Retrieved model info"}})
506
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after get_model_info")
507
+
508
+ elif tool == "list_features":
509
+ require_result(
510
+ await adapter.list_features(include_suppressed=True),
511
+ "list_features",
512
+ )
513
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Listed features"}})
514
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after list_features")
515
+
516
+ elif tool == "get_mass_properties":
517
+ require_result(await adapter.get_mass_properties(), "get_mass_properties")
518
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Retrieved mass properties"}})
519
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after get_mass_properties")
520
+
521
+ elif tool == "analyze_geometry":
522
+ require_result(await adapter.get_model_info(), "analyze_geometry.get_model_info")
523
+ require_result(
524
+ await adapter.list_features(include_suppressed=True),
525
+ "analyze_geometry.list_features",
526
+ )
527
+ require_result(
528
+ await adapter.get_mass_properties(),
529
+ "analyze_geometry.get_mass_properties",
530
+ )
531
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Geometry analysis completed"}})
532
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after analyze_geometry")
533
+
534
+ elif tool == "export_image":
535
+ payloads = payloads_for_tool(tool)
536
+ payload = payloads[0] if payloads else PLANNED.get("export_image")
537
+ if payload is None:
538
+ payload = {{
539
+ "file_path": require_key("file_path"),
540
+ "format_type": require_key("format_type"),
541
+ }}
542
+ if "width" in PLANNED:
543
+ payload["width"] = int(PLANNED["width"])
544
+ if "height" in PLANNED:
545
+ payload["height"] = int(PLANNED["height"])
546
+ if "view_orientation" in PLANNED:
547
+ payload["view_orientation"] = str(PLANNED["view_orientation"])
548
+ if not isinstance(payload, dict):
549
+ raise ValueError("export_image must be an object payload")
550
+ require_result(await adapter.export_image(payload), "export_image")
551
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Exported image"}})
552
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after export_image")
553
+
554
+ elif tool == "check_interference":
555
+ payload = require_key("check_interference")
556
+ if not isinstance(payload, dict):
557
+ raise ValueError("check_interference must be an object payload")
558
+ require_result(await adapter.check_interference(payload), "check_interference")
559
+ tool_runs.append({{"tool": tool, "status": "success", "message": "Checked interference"}})
560
+ pause_point(f"checkpoint {{CHECKPOINT_INDEX}} after check_interference")
561
+
562
+ else:
563
+ raise ValueError(f"Unsupported tool '{{tool}}' in strict script mode")
564
+
565
+ except Exception as step_exc:
566
+ failed_tools.append(tool)
567
+ tool_runs.append({{"tool": tool, "status": "error", "message": str(step_exc)}})
568
+ break
569
+
570
+ finally:
571
+ try:
572
+ await adapter.disconnect()
573
+ except Exception:
574
+ pass
575
+
576
+ return {{
577
+ "tool_runs": tool_runs,
578
+ "failed_tools": failed_tools,
579
+ }}
580
+
581
+
582
+ def main() -> int:
583
+ try:
584
+ summary = asyncio.run(run_checkpoint())
585
+ print("CHECKPOINT_SCRIPT_RESULT::" + json.dumps(summary, ensure_ascii=True))
586
+ return 0 if not summary.get("failed_tools") else 1
587
+ except Exception as exc:
588
+ summary = {{
589
+ "tool_runs": [
590
+ {{"tool": "checkpoint.script", "status": "error", "message": str(exc)}},
591
+ {{"tool": "checkpoint.script", "status": "error", "message": traceback.format_exc()}},
592
+ ],
593
+ "failed_tools": ["checkpoint.script"],
594
+ }}
595
+ print("CHECKPOINT_SCRIPT_RESULT::" + json.dumps(summary, ensure_ascii=True))
596
+ return 1
597
+
598
+
599
+ if __name__ == "__main__":
600
+ raise SystemExit(main())
601
+ """
602
+ )
603
+
604
+
605
+ # ---------------------------------------------------------------------------
606
+ # Direct-execution helpers
607
+ # ---------------------------------------------------------------------------
608
+
609
+
610
+ def _pf(planned: dict[str, Any], *keys: str, default: float = 0.0) -> float:
611
+ """Return the first float-convertible value found under any of *keys*."""
612
+ for key in keys:
613
+ if key in planned:
614
+ try:
615
+ return float(planned[key])
616
+ except (TypeError, ValueError):
617
+ pass
618
+ return default
619
+
620
+
621
+ def _pv(
622
+ planned: dict[str, Any],
623
+ *keys: str,
624
+ size: int = 4,
625
+ default: list[float] | None = None,
626
+ ) -> list[float]:
627
+ """Return a float list of *size* from the first matching key in planned."""
628
+ for key in keys:
629
+ val = planned.get(key)
630
+ if isinstance(val, list) and len(val) == size:
631
+ try:
632
+ return [float(v) for v in val]
633
+ except (TypeError, ValueError):
634
+ pass
635
+ return default if default is not None else [0.0] * size
636
+
637
+
638
+ async def _execute_tool(
639
+ adapter: Any,
640
+ planned: dict[str, Any],
641
+ tool: str,
642
+ ) -> dict[str, Any] | None:
643
+ """Dispatch *tool* to the adapter using parameters from *planned*.
644
+
645
+ Returns a tool_run dict on success, raises on failure, or returns None for
646
+ unknown tools (caller treats None as mocked).
647
+ Uses sensible defaults for any missing geometry parameters.
648
+ """
649
+ from ...adapters.base import ExtrusionParameters # noqa: PLC0415
650
+
651
+ tool_payloads = _planned_tool_payloads(planned, tool)
652
+ ctx = dict(planned)
653
+
654
+ def _ok(result: Any, label: str) -> None:
655
+ if not getattr(result, "is_success", True):
656
+ raise RuntimeError(getattr(result, "error", f"{label} failed"))
657
+
658
+ if tool == "create_part":
659
+ name = str(ctx.get("part_name", "untitled_part"))
660
+ _ok(await adapter.create_part(name=name), "create_part")
661
+ return {"tool": tool, "status": "success", "message": f"Created part '{name}'"}
662
+
663
+ if tool == "create_assembly":
664
+ name = str(ctx.get("assembly_name", "untitled_assembly"))
665
+ _ok(await adapter.create_assembly(name=name), "create_assembly")
666
+ return {
667
+ "tool": tool,
668
+ "status": "success",
669
+ "message": f"Created assembly '{name}'",
670
+ }
671
+
672
+ if tool == "open_model":
673
+ path = str(ctx.get("model_path", ""))
674
+ if not path:
675
+ raise ValueError("open_model requires model_path")
676
+ _ok(await adapter.open_model(path), "open_model")
677
+ return {"tool": tool, "status": "success", "message": f"Opened '{path}'"}
678
+
679
+ if tool == "create_sketch":
680
+ payloads = tool_payloads or [{}]
681
+ plane = "Front"
682
+ for payload in payloads:
683
+ plane = str({**ctx, **payload}.get("sketch_plane", "Front"))
684
+ _ok(await adapter.create_sketch(plane), "create_sketch")
685
+ return {
686
+ "tool": tool,
687
+ "status": "success",
688
+ "message": f"Created sketch on '{plane}'",
689
+ }
690
+
691
+ if tool == "exit_sketch":
692
+ _ok(await adapter.exit_sketch(), "exit_sketch")
693
+ return {"tool": tool, "status": "success", "message": "Exited sketch"}
694
+
695
+ if tool == "add_line":
696
+ payloads = tool_payloads or [{}]
697
+ for payload in payloads:
698
+ merged = {**ctx, **payload}
699
+ if "line_mm" in merged:
700
+ x1, y1, x2, y2 = _pv(merged, "line_mm", size=4, default=[0, 0, 10, 10])
701
+ elif "line_start_mm" in merged:
702
+ sx, sy = _pv(merged, "line_start_mm", size=2, default=[0, 0])
703
+ ex, ey = _pv(merged, "line_end_mm", size=2, default=[10, 10])
704
+ x1, y1, x2, y2 = sx, sy, ex, ey
705
+ else:
706
+ x1, y1, x2, y2 = 0.0, 0.0, 10.0, 10.0
707
+ _ok(await adapter.add_line(x1, y1, x2, y2), "add_line")
708
+ return {"tool": tool, "status": "success", "message": "Added line"}
709
+
710
+ if tool == "add_rectangle":
711
+ payloads = tool_payloads or [{}]
712
+ for payload in payloads:
713
+ merged = {**ctx, **payload}
714
+ x, y, w, h = _pv(merged, "rectangle_mm", size=4, default=[0, 0, 50, 30])
715
+ _ok(await adapter.add_rectangle(x, y, w, h), "add_rectangle")
716
+ return {"tool": tool, "status": "success", "message": "Added rectangle"}
717
+
718
+ if tool == "add_circle":
719
+ payloads = tool_payloads or [{}]
720
+ for payload in payloads:
721
+ merged = {**ctx, **payload}
722
+ cx, cy = _pv(merged, "circle_center_mm", size=2, default=[0, 0])
723
+ radius = _pf(merged, "circle_radius_mm", default=5.0)
724
+ _ok(await adapter.add_circle(cx, cy, radius), "add_circle")
725
+ return {"tool": tool, "status": "success", "message": "Added circle"}
726
+
727
+ if tool == "add_centerline":
728
+ payloads = tool_payloads or [{}]
729
+ for payload in payloads:
730
+ merged = {**ctx, **payload}
731
+ x1, y1, x2, y2 = _pv(merged, "centerline_mm", size=4, default=[0, 0, 0, 50])
732
+ _ok(await adapter.add_centerline(x1, y1, x2, y2), "add_centerline")
733
+ return {"tool": tool, "status": "success", "message": "Added centerline"}
734
+
735
+ if tool == "add_arc":
736
+ payloads = tool_payloads or [{}]
737
+ for payload in payloads:
738
+ merged = {**ctx, **payload}
739
+ cx, cy = _pv(merged, "arc_center_mm", size=2, default=[0, 0])
740
+ sx, sy = _pv(merged, "arc_start_mm", size=2, default=[10, 0])
741
+ ex, ey = _pv(merged, "arc_end_mm", size=2, default=[0, 10])
742
+ _ok(await adapter.add_arc(cx, cy, sx, sy, ex, ey), "add_arc")
743
+ return {"tool": tool, "status": "success", "message": "Added arc"}
744
+
745
+ if tool == "create_extrusion":
746
+ payload = (tool_payloads or [{}])[0]
747
+ merged = {**ctx, **payload}
748
+ depth = _pf(merged, "depth_mm", "depth", default=10.0)
749
+ params = ExtrusionParameters(
750
+ depth=depth,
751
+ thin_feature=bool(merged.get("thin_feature", False)),
752
+ thin_thickness=float(
753
+ merged.get("thin_thickness_mm", merged.get("thin_thickness", 0.0))
754
+ ),
755
+ both_directions=bool(merged.get("both_directions", False)),
756
+ )
757
+ _ok(await adapter.create_extrusion(params), "create_extrusion")
758
+ return {
759
+ "tool": tool,
760
+ "status": "success",
761
+ "message": f"Extrusion depth={depth}mm",
762
+ }
763
+
764
+ if tool == "create_cut_extrude":
765
+ payload = (tool_payloads or [{}])[0]
766
+ merged = {**ctx, **payload}
767
+ depth = _pf(merged, "depth_mm", "depth", default=10.0)
768
+ _ok(
769
+ await adapter.create_cut_extrude(ExtrusionParameters(depth=depth)),
770
+ "create_cut_extrude",
771
+ )
772
+ return {
773
+ "tool": tool,
774
+ "status": "success",
775
+ "message": f"Cut-extrude depth={depth}mm",
776
+ }
777
+
778
+ if tool == "create_cut":
779
+ payload = (tool_payloads or [{}])[0]
780
+ merged = {**ctx, **payload}
781
+ depth = _pf(merged, "depth_mm", "depth", default=10.0)
782
+ sketch_name = str(merged.get("sketch_name", ""))
783
+ if sketch_name:
784
+ _ok(await adapter.create_cut(sketch_name, depth), "create_cut")
785
+ return {
786
+ "tool": tool,
787
+ "status": "success",
788
+ "message": f"Cut from {sketch_name} depth={depth}mm",
789
+ }
790
+ _ok(
791
+ await adapter.create_cut_extrude(ExtrusionParameters(depth=depth)),
792
+ "create_cut",
793
+ )
794
+ return {
795
+ "tool": tool,
796
+ "status": "success",
797
+ "message": f"Cut-extrude depth={depth}mm",
798
+ }
799
+
800
+ if tool == "add_fillet":
801
+ payload = (tool_payloads or [{}])[0]
802
+ merged = {**ctx, **payload}
803
+ radius = _pf(merged, "radius_mm", "radius", default=1.0)
804
+ raw = merged.get("edge_names", [])
805
+ edge_names = [str(e) for e in raw] if isinstance(raw, list) else []
806
+ _ok(await adapter.add_fillet(radius, edge_names), "add_fillet")
807
+ return {
808
+ "tool": tool,
809
+ "status": "success",
810
+ "message": f"Fillet radius={radius}mm",
811
+ }
812
+
813
+ if tool == "check_sketch_fully_defined":
814
+ payload = (tool_payloads or [{}])[0]
815
+ check_sketch_name: str | None = {**ctx, **payload}.get("sketch_name")
816
+ _ok(
817
+ await adapter.check_sketch_fully_defined(
818
+ str(check_sketch_name) if check_sketch_name else None
819
+ ),
820
+ "check_sketch_fully_defined",
821
+ )
822
+ return {
823
+ "tool": tool,
824
+ "status": "success",
825
+ "message": "Checked sketch definition",
826
+ }
827
+
828
+ if tool == "save_file":
829
+ payload = (tool_payloads or [{}])[0]
830
+ file_path = str({**ctx, **payload}.get("file_path", ""))
831
+ if not file_path:
832
+ raise ValueError("save_file requires file_path")
833
+ _ok(await adapter.save_file(file_path), "save_file")
834
+ return {"tool": tool, "status": "success", "message": f"Saved '{file_path}'"}
835
+
836
+ if tool == "get_model_info":
837
+ _ok(await adapter.get_model_info(), "get_model_info")
838
+ return {"tool": tool, "status": "success", "message": "Retrieved model info"}
839
+
840
+ if tool == "list_features":
841
+ _ok(await adapter.list_features(include_suppressed=True), "list_features")
842
+ return {"tool": tool, "status": "success", "message": "Listed features"}
843
+
844
+ if tool == "get_mass_properties":
845
+ _ok(await adapter.get_mass_properties(), "get_mass_properties")
846
+ return {
847
+ "tool": tool,
848
+ "status": "success",
849
+ "message": "Retrieved mass properties",
850
+ }
851
+
852
+ if tool == "analyze_geometry":
853
+ _ok(await adapter.get_model_info(), "get_model_info")
854
+ _ok(await adapter.list_features(include_suppressed=True), "list_features")
855
+ _ok(await adapter.get_mass_properties(), "get_mass_properties")
856
+ return {
857
+ "tool": tool,
858
+ "status": "success",
859
+ "message": "Geometry analysis completed",
860
+ }
861
+
862
+ if tool == "export_image":
863
+ payloads = tool_payloads
864
+ payload = payloads[0] if payloads else ctx.get("export_image") or {}
865
+ if not isinstance(payload, dict):
866
+ raise ValueError("export_image must be an object payload")
867
+ _ok(await adapter.export_image(payload), "export_image")
868
+ return {"tool": tool, "status": "success", "message": "Exported image"}
869
+
870
+ if tool == "check_interference":
871
+ payloads = tool_payloads
872
+ payload = payloads[0] if payloads else ctx.get("check_interference") or {}
873
+ if not isinstance(payload, dict):
874
+ raise ValueError("check_interference must be an object payload")
875
+ _ok(await adapter.check_interference(payload), "check_interference")
876
+ return {
877
+ "tool": tool,
878
+ "status": "success",
879
+ "message": "Checked assembly interference",
880
+ }
881
+
882
+ return None # Unknown tool → caller mocks it
883
+
884
+
885
+ async def _run_checkpoint_tools(
886
+ planned: dict[str, Any],
887
+ *,
888
+ session_id: str = DEFAULT_SESSION_ID,
889
+ checkpoint_index: int = 0,
890
+ checkpoint_title: str = "",
891
+ repair_context: dict[str, Any] | None = None,
892
+ ) -> dict[str, Any]:
893
+ """Execute checkpoint tools directly via the adapter with mocked-tool handling.
894
+
895
+ Generates the checkpoint script as an audit artifact but executes tools
896
+ in-process so that test adapters (monkeypatched create_adapter) work.
897
+ Unrecognized tool names are recorded as mocked
898
+ and do not block checkpoint success.
899
+ """
900
+ tools = _planned_tools(planned)
901
+ script_dir = _checkpoint_script_dir(session_id)
902
+ script_dir.mkdir(parents=True, exist_ok=True)
903
+ script_path = _checkpoint_script_path(session_id, checkpoint_index)
904
+ script_text = _render_checkpoint_script(
905
+ planned,
906
+ session_id=session_id,
907
+ checkpoint_index=checkpoint_index,
908
+ checkpoint_title=checkpoint_title,
909
+ )
910
+ script_path.write_text(script_text, encoding="utf-8")
911
+
912
+ tool_runs: list[dict[str, Any]] = []
913
+ failed_tools: list[str] = []
914
+ mocked_tools: list[str] = []
915
+
916
+ config = load_config()
917
+ adapter = await create_adapter(config)
918
+ try:
919
+ await adapter.connect()
920
+ except Exception as adapter_error:
921
+ return {
922
+ "tool_runs": [
923
+ {
924
+ "tool": "checkpoint.execute",
925
+ "status": "error",
926
+ "message": f"Adapter connection failed: {adapter_error}",
927
+ }
928
+ ],
929
+ "failed_tools": ["checkpoint.execute"],
930
+ "mocked_tools": [],
931
+ "script_path": str(script_path),
932
+ "script_text": script_text,
933
+ "stdout_text": "",
934
+ "stderr_text": str(adapter_error),
935
+ "validation_failures": [],
936
+ }
937
+
938
+ try:
939
+ for tool in tools:
940
+ try:
941
+ run = await _execute_tool(adapter, planned, tool)
942
+ except Exception as exc:
943
+ failed_tools.append(tool)
944
+ tool_runs.append({"tool": tool, "status": "error", "message": str(exc)})
945
+ break
946
+
947
+ if run is None:
948
+ mocked_tools.append(tool)
949
+ tool_runs.append(
950
+ {
951
+ "tool": tool,
952
+ "status": "mocked",
953
+ "message": f"Unknown tool '{tool}' — mocked",
954
+ }
955
+ )
956
+ else:
957
+ tool_runs.append(run)
958
+ finally:
959
+ try:
960
+ await adapter.disconnect()
961
+ except Exception:
962
+ pass
963
+
964
+ return {
965
+ "tool_runs": tool_runs,
966
+ "failed_tools": failed_tools,
967
+ "mocked_tools": mocked_tools,
968
+ "script_path": str(script_path),
969
+ "script_text": script_text,
970
+ "stdout_text": "",
971
+ "stderr_text": "",
972
+ "validation_failures": [],
973
+ }
974
+
975
+
976
+ # ---------------------------------------------------------------------------
977
+ # Public API
978
+ # ---------------------------------------------------------------------------
979
+ async def execute_next_checkpoint(
980
+ session_id: str = DEFAULT_SESSION_ID,
981
+ *,
982
+ db_path: Path | None = None,
983
+ ) -> dict[str, Any]:
984
+ """Find and execute the next un-executed checkpoint in the session plan.
985
+
986
+ Args:
987
+ session_id: Dashboard session identifier.
988
+ db_path: Optional SQLite path override.
989
+
990
+ Returns:
991
+ Full dashboard state payload.
992
+ """
993
+ from .session_service import ( # noqa: PLC0415
994
+ build_dashboard_state,
995
+ ensure_dashboard_session,
996
+ )
997
+
998
+ session_row = ensure_dashboard_session(session_id, db_path=db_path)
999
+ checkpoints = list_plan_checkpoints(session_id, db_path=db_path)
1000
+ target = next((row for row in checkpoints if not row["executed"]), None)
1001
+ if target is None:
1002
+ merge_metadata(
1003
+ session_id,
1004
+ db_path=db_path,
1005
+ latest_message="All checkpoints have already been executed.",
1006
+ )
1007
+ return build_dashboard_state(session_id, db_path=db_path)
1008
+
1009
+ planned = parse_json_blob(target["planned_action_json"])
1010
+ session_meta = parse_json_blob(session_row.get("metadata_json"))
1011
+
1012
+ if _should_open_empty_part(session_meta, planned):
1013
+ await _open_empty_part_before_checkpoint(
1014
+ session_id=session_id,
1015
+ session_row=session_row,
1016
+ db_path=db_path,
1017
+ api_origin=DEFAULT_API_ORIGIN,
1018
+ )
1019
+
1020
+ run_summary = await _run_checkpoint_tools(
1021
+ planned,
1022
+ session_id=session_id,
1023
+ checkpoint_index=int(target["checkpoint_index"]),
1024
+ checkpoint_title=str(target["title"]),
1025
+ repair_context={
1026
+ "active_sketch_plane": session_meta.get("active_sketch_plane"),
1027
+ "last_sketch_name": session_meta.get("last_sketch_name"),
1028
+ "user_goal": session_row.get("user_goal") or DEFAULT_USER_GOAL,
1029
+ },
1030
+ )
1031
+ failed_tools = run_summary["failed_tools"]
1032
+ tool_runs = run_summary["tool_runs"]
1033
+ mocked_tools = run_summary.get("mocked_tools", [])
1034
+ executed = not failed_tools
1035
+
1036
+ if failed_tools:
1037
+ message = (
1038
+ f"Checkpoint {target['checkpoint_index']} failed on tools: "
1039
+ f"{', '.join(failed_tools)}."
1040
+ )
1041
+ elif mocked_tools and not any(r.get("status") == "success" for r in tool_runs):
1042
+ message = (
1043
+ f"Checkpoint {target['checkpoint_index']} executed "
1044
+ f"(tools MOCKED: {', '.join(mocked_tools)})."
1045
+ )
1046
+ else:
1047
+ message = (
1048
+ f"Executed checkpoint {target['checkpoint_index']}: {target['title']}."
1049
+ )
1050
+
1051
+ result_json = json.dumps(
1052
+ {
1053
+ "status": "success" if executed else "error",
1054
+ "message": message,
1055
+ "tools": _planned_tools(planned),
1056
+ "tool_runs": tool_runs,
1057
+ "failed_tools": failed_tools,
1058
+ "script_path": run_summary.get("script_path", ""),
1059
+ "script_text": run_summary.get("script_text", ""),
1060
+ "stdout_tail": str(run_summary.get("stdout_text", ""))[-4000:],
1061
+ "stderr_tail": str(run_summary.get("stderr_text", ""))[-4000:],
1062
+ "validation_failures": run_summary.get("validation_failures", []),
1063
+ },
1064
+ ensure_ascii=True,
1065
+ )
1066
+ update_plan_checkpoint(
1067
+ int(target["id"]),
1068
+ approved_by_user=True,
1069
+ executed=executed,
1070
+ result_json=result_json,
1071
+ db_path=db_path,
1072
+ )
1073
+
1074
+ for tool_run in tool_runs:
1075
+ insert_tool_call_record(
1076
+ session_id=session_id,
1077
+ checkpoint_id=int(target["id"]),
1078
+ tool_name=tool_run["tool"],
1079
+ input_json=json.dumps(planned, ensure_ascii=True),
1080
+ output_json=json.dumps(tool_run, ensure_ascii=True),
1081
+ success=tool_run["status"] == "success",
1082
+ db_path=db_path,
1083
+ )
1084
+
1085
+ upsert_design_session(
1086
+ session_id=session_id,
1087
+ user_goal=session_row.get("user_goal") or DEFAULT_USER_GOAL,
1088
+ source_mode=session_row.get("source_mode") or DEFAULT_SOURCE_MODE,
1089
+ accepted_family=session_row.get("accepted_family"),
1090
+ status="executing" if executed else "error",
1091
+ current_checkpoint_index=(
1092
+ target["checkpoint_index"]
1093
+ if executed
1094
+ else session_row.get("current_checkpoint_index") or 0
1095
+ ),
1096
+ metadata_json=session_row.get("metadata_json"),
1097
+ db_path=db_path,
1098
+ )
1099
+ merge_metadata(
1100
+ session_id,
1101
+ db_path=db_path,
1102
+ latest_message=message,
1103
+ latest_error_text=(message if failed_tools else ""),
1104
+ remediation_hint=(
1105
+ "Open the generated checkpoint script, repair planned parameters, then rerun this checkpoint."
1106
+ if failed_tools
1107
+ else ""
1108
+ ),
1109
+ )
1110
+ return build_dashboard_state(session_id, db_path=db_path)