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,1508 @@
1
+ """SQLModel persistence for agent runs and tool-error cataloging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel
10
+ from sqlalchemy import inspect as sa_inspect
11
+ from sqlalchemy import text as sa_text
12
+ from sqlalchemy.pool import NullPool
13
+ from sqlmodel import Field, Session, SQLModel, create_engine, select
14
+
15
+ DEFAULT_DB_PATH = Path(".solidworks_mcp") / "agent_memory.sqlite3"
16
+
17
+
18
+ class ErrorRecord(BaseModel):
19
+ """A normalized error record from an MCP call or planning step.
20
+
21
+ Attributes:
22
+ error_message (str): The error message value.
23
+ error_type (str): The error type value.
24
+ remediation (str): The remediation value.
25
+ root_cause (str): The root cause value.
26
+ source (str): The source value.
27
+ tool_name (str): The tool name value.
28
+ """
29
+
30
+ source: str
31
+ tool_name: str
32
+ error_type: str
33
+ error_message: str
34
+ root_cause: str
35
+ remediation: str
36
+
37
+
38
+ def _utc_now_iso() -> str:
39
+ """Build internal utc now iso.
40
+
41
+ Returns:
42
+ str: The resulting text value.
43
+ """
44
+
45
+ return datetime.now(UTC).isoformat()
46
+
47
+
48
+ class AgentRun(SQLModel, table=True):
49
+ """One recorded agent run.
50
+
51
+ Attributes:
52
+ agent_name (str): The agent name value.
53
+ created_at (str): The created at value.
54
+ id (int | None): The id value.
55
+ model_name (str | None): The model name value.
56
+ output_json (str | None): The output json value.
57
+ prompt (str): The prompt value.
58
+ run_id (str): The run id value.
59
+ status (str): The status value.
60
+ """
61
+
62
+ id: int | None = Field(default=None, primary_key=True)
63
+ run_id: str
64
+ agent_name: str
65
+ prompt: str
66
+ model_name: str | None = None
67
+ status: str
68
+ output_json: str | None = None
69
+ created_at: str
70
+
71
+
72
+ class ToolEvent(SQLModel, table=True):
73
+ """One tool lifecycle event linked to a run.
74
+
75
+ Attributes:
76
+ created_at (str): The created at value.
77
+ id (int | None): The id value.
78
+ payload_json (str | None): The payload json value.
79
+ phase (str): The phase value.
80
+ run_id (str): The run id value.
81
+ tool_name (str): The tool name value.
82
+ """
83
+
84
+ id: int | None = Field(default=None, primary_key=True)
85
+ run_id: str
86
+ tool_name: str
87
+ phase: str
88
+ payload_json: str | None = None
89
+ created_at: str
90
+
91
+
92
+ class ErrorCatalog(SQLModel, table=True):
93
+ """Persisted error records for recovery recommendations.
94
+
95
+ Attributes:
96
+ created_at (str): The created at value.
97
+ error_message (str): The error message value.
98
+ error_type (str): The error type value.
99
+ id (int | None): The id value.
100
+ remediation (str): The remediation value.
101
+ root_cause (str): The root cause value.
102
+ run_id (str | None): The run id value.
103
+ source (str): The source value.
104
+ tool_name (str): The tool name value.
105
+ """
106
+
107
+ id: int | None = Field(default=None, primary_key=True)
108
+ run_id: str | None = None
109
+ source: str
110
+ tool_name: str
111
+ error_type: str
112
+ error_message: str
113
+ root_cause: str
114
+ remediation: str
115
+ created_at: str
116
+
117
+
118
+ class ConversationEvent(SQLModel, table=True):
119
+ """One message or system event in a conversation, linked to a run context.
120
+
121
+ Attributes:
122
+ content_snippet (str): The content snippet value.
123
+ conversation_id (str): The conversation id value.
124
+ created_at (str): The created at value.
125
+ event_type (str): The event type value.
126
+ id (int | None): The id value.
127
+ metadata_json (str | None): The metadata json value.
128
+ role (str | None): The role value.
129
+ run_id (str | None): The run id value.
130
+ """
131
+
132
+ id: int | None = Field(default=None, primary_key=True)
133
+ conversation_id: str
134
+ run_id: str | None = None
135
+ event_type: str # "user_message", "assistant_message", "system_event", "tool_call"
136
+ role: str | None = None # "user", "assistant", "system"
137
+ content_snippet: str # truncated for privacy; full content may be elsewhere
138
+ metadata_json: str | None = None # tool_name, phase, status, etc.
139
+ created_at: str
140
+
141
+
142
+ class DesignSession(SQLModel, table=True):
143
+ """Persistent interactive design session metadata.
144
+
145
+ Attributes:
146
+ accepted_family (str | None): The accepted family value.
147
+ created_at (str): The created at value.
148
+ current_checkpoint_index (int): The current checkpoint index value.
149
+ id (int | None): The id value.
150
+ metadata_json (str | None): The metadata json value.
151
+ session_id (str): The session id value.
152
+ source_mode (str): The source mode value.
153
+ status (str): The status value.
154
+ updated_at (str): The updated at value.
155
+ user_goal (str): The user goal value.
156
+ """
157
+
158
+ id: int | None = Field(default=None, primary_key=True)
159
+ session_id: str
160
+ user_goal: str
161
+ source_mode: str = "model" # model/image/text
162
+ accepted_family: str | None = None
163
+ status: str = "active"
164
+ current_checkpoint_index: int = 0
165
+ metadata_json: str | None = None
166
+ created_at: str
167
+ updated_at: str
168
+
169
+
170
+ class PlanCheckpoint(SQLModel, table=True):
171
+ """One approval boundary in an interactive design session.
172
+
173
+ Attributes:
174
+ approved_by_user (bool): The approved by user value.
175
+ checkpoint_index (int): The checkpoint index value.
176
+ created_at (str): The created at value.
177
+ executed (bool): The executed value.
178
+ id (int | None): The id value.
179
+ planned_action_json (str): The planned action json value.
180
+ result_json (str | None): The result json value.
181
+ rollback_snapshot_id (int | None): The rollback snapshot id value.
182
+ session_id (str): The session id value.
183
+ title (str): The title value.
184
+ updated_at (str): The updated at value.
185
+ """
186
+
187
+ id: int | None = Field(default=None, primary_key=True)
188
+ session_id: str
189
+ checkpoint_index: int
190
+ title: str
191
+ planned_action_json: str
192
+ approved_by_user: bool = False
193
+ executed: bool = False
194
+ result_json: str | None = None
195
+ rollback_snapshot_id: int | None = None
196
+ created_at: str
197
+ updated_at: str
198
+
199
+
200
+ class ToolCallRecord(SQLModel, table=True):
201
+ """Execution log for tool calls scoped to session/checkpoint.
202
+
203
+ Attributes:
204
+ checkpoint_id (int | None): The checkpoint id value.
205
+ created_at (str): The created at value.
206
+ id (int | None): The id value.
207
+ input_json (str | None): The input json value.
208
+ latency_ms (float | None): The latency ms value.
209
+ output_json (str | None): The output json value.
210
+ run_id (str | None): The run id value.
211
+ session_id (str): The session id value.
212
+ status (str | None): Lifecycle status — None (active), 'reverted'.
213
+ success (bool): The success value.
214
+ tool_name (str): The tool name value.
215
+ """
216
+
217
+ id: int | None = Field(default=None, primary_key=True)
218
+ session_id: str
219
+ checkpoint_id: int | None = None
220
+ run_id: str | None = None
221
+ tool_name: str
222
+ input_json: str | None = None
223
+ output_json: str | None = None
224
+ success: bool = True
225
+ latency_ms: float | None = None
226
+ status: str | None = None
227
+ # Rendered SolidWorks-as-Code Python for this call, computed at write time
228
+ # (issue #28). None for rows written before the column existed — session
229
+ # export re-renders those on demand.
230
+ script_line: str | None = None
231
+ created_at: str
232
+
233
+
234
+ class EvidenceLink(SQLModel, table=True):
235
+ """Evidence references used to justify planning decisions.
236
+
237
+ Attributes:
238
+ checkpoint_id (int | None): The checkpoint id value.
239
+ created_at (str): The created at value.
240
+ id (int | None): The id value.
241
+ payload_json (str | None): The payload json value.
242
+ rationale (str | None): The rationale value.
243
+ relevance_score (float | None): The relevance score value.
244
+ session_id (str): The session id value.
245
+ source_id (str): The source id value.
246
+ source_type (str): The source type value.
247
+ """
248
+
249
+ id: int | None = Field(default=None, primary_key=True)
250
+ session_id: str
251
+ checkpoint_id: int | None = None
252
+ source_type: str
253
+ source_id: str
254
+ relevance_score: float | None = None
255
+ rationale: str | None = None
256
+ payload_json: str | None = None
257
+ created_at: str
258
+
259
+
260
+ class ModelStateSnapshot(SQLModel, table=True):
261
+ """Rollback/diff snapshot of model state at a checkpoint.
262
+
263
+ Attributes:
264
+ checkpoint_id (int | None): The checkpoint id value.
265
+ created_at (str): The created at value.
266
+ feature_tree_json (str | None): The feature tree json value.
267
+ id (int | None): The id value.
268
+ mass_properties_json (str | None): The mass properties json value.
269
+ model_path (str | None): The model path value.
270
+ screenshot_path (str | None): The screenshot path value.
271
+ session_id (str): The session id value.
272
+ state_fingerprint (str | None): The state fingerprint value.
273
+ """
274
+
275
+ id: int | None = Field(default=None, primary_key=True)
276
+ session_id: str
277
+ checkpoint_id: int | None = None
278
+ model_path: str | None = None
279
+ feature_tree_json: str | None = None
280
+ mass_properties_json: str | None = None
281
+ screenshot_path: str | None = None
282
+ state_fingerprint: str | None = None
283
+ created_at: str
284
+
285
+
286
+ class SoCCheckpoint(SQLModel, table=True):
287
+ """Named save point in a SolidWorks-as-Code session.
288
+
289
+ A checkpoint ties together a human label, the saved .sldprt file that
290
+ represents the model state at that point, and the range of ToolCallRecord
291
+ rows that produced it. The exporter uses this to emit structured comment
292
+ blocks; soc_rewind uses the file_path to restore the model; soc_pickup
293
+ uses snapshot_id to diff against the current feature tree.
294
+
295
+ Attributes:
296
+ file_path (str): Path to the .sldprt saved at this checkpoint.
297
+ first_record_id (int | None): First ToolCallRecord.id in this block.
298
+ id (int | None): Primary key.
299
+ label (str): Short human name for this checkpoint.
300
+ last_record_id (int | None): Last ToolCallRecord.id in this block.
301
+ session_id (str): Owning SoC session.
302
+ snapshot_id (int | None): FK to ModelStateSnapshot (feature tree, mass props).
303
+ created_at (str): ISO timestamp.
304
+ """
305
+
306
+ id: int | None = Field(default=None, primary_key=True)
307
+ session_id: str
308
+ label: str
309
+ file_path: str
310
+ first_record_id: int | None = None
311
+ last_record_id: int | None = None
312
+ snapshot_id: int | None = None
313
+ created_at: str
314
+
315
+
316
+ class SketchGraphSnapshot(SQLModel, table=True):
317
+ """Lightweight relational sketch graph storage (Section F).
318
+
319
+ Attributes:
320
+ created_at (str): The created at value.
321
+ edges_json (str): The edges json value.
322
+ graph_format (str): The graph format value.
323
+ id (int | None): The id value.
324
+ metadata_json (str | None): The metadata json value.
325
+ model_path (str | None): The model path value.
326
+ nodes_json (str): The nodes json value.
327
+ session_id (str): The session id value.
328
+ """
329
+
330
+ id: int | None = Field(default=None, primary_key=True)
331
+ session_id: str
332
+ model_path: str | None = None
333
+ graph_format: str = "json"
334
+ nodes_json: str
335
+ edges_json: str
336
+ metadata_json: str | None = None
337
+ created_at: str
338
+
339
+
340
+ def _build_engine(db_path: Path | None = None) -> Any:
341
+ """Build a local SQLite engine from the configured path.
342
+
343
+ Args:
344
+ db_path (Path | None): The db path value. Defaults to None.
345
+
346
+ Returns:
347
+ Any: SQLAlchemy engine instance for the resolved SQLite path.
348
+ """
349
+ resolved = db_path or DEFAULT_DB_PATH
350
+ resolved.parent.mkdir(parents=True, exist_ok=True)
351
+ # NullPool closes sqlite connections immediately after each session/transaction,
352
+ # which prevents a long-lived pool from surfacing ResourceWarning noise at process exit.
353
+ return create_engine(f"sqlite:///{resolved}", echo=False, poolclass=NullPool)
354
+
355
+
356
+ def init_db(db_path: Path | None = None) -> Path:
357
+ """Create SQLModel tables used by the lightweight agent memory system.
358
+
359
+ Args:
360
+ db_path (Path | None): The db path value. Defaults to None.
361
+
362
+ Returns:
363
+ Path: The result produced by the operation.
364
+ """
365
+ resolved = db_path or DEFAULT_DB_PATH
366
+ engine = _build_engine(resolved)
367
+ try:
368
+ SQLModel.metadata.create_all(engine)
369
+ _ensure_columns(engine)
370
+ finally:
371
+ engine.dispose()
372
+ return resolved
373
+
374
+
375
+ # Columns added to existing tables after their first release. This repo has no
376
+ # migration framework (issue #28): create_all() makes new tables but never
377
+ # alters an existing one, so a pre-existing local DB would raise
378
+ # "no such column" on the next insert. Checked and patched on every init_db().
379
+ _ADDED_COLUMNS: dict[str, dict[str, str]] = {
380
+ "toolcallrecord": {"script_line": "TEXT"},
381
+ }
382
+
383
+
384
+ def _ensure_columns(engine: Any) -> None:
385
+ """Add any missing post-release columns to already-existing tables."""
386
+ inspector = sa_inspect(engine)
387
+ for table, columns in _ADDED_COLUMNS.items():
388
+ present = {col["name"] for col in inspector.get_columns(table)}
389
+ missing = {
390
+ name: sql_type for name, sql_type in columns.items() if name not in present
391
+ }
392
+ if not missing:
393
+ continue
394
+ with engine.begin() as conn:
395
+ for name, sql_type in missing.items():
396
+ conn.execute(
397
+ sa_text(f"ALTER TABLE {table} ADD COLUMN {name} {sql_type}")
398
+ )
399
+
400
+
401
+ def insert_run(
402
+ *,
403
+ run_id: str,
404
+ agent_name: str,
405
+ prompt: str,
406
+ status: str,
407
+ output_json: str | None,
408
+ model_name: str | None,
409
+ db_path: Path | None = None,
410
+ ) -> None:
411
+ """Record one prompt run and optionally the validated output payload.
412
+
413
+ Args:
414
+ run_id (str): The run id value.
415
+ agent_name (str): The agent name value.
416
+ prompt (str): The prompt value.
417
+ status (str): The status value.
418
+ output_json (str | None): The output json value.
419
+ model_name (str | None): Embedding model name to use.
420
+ db_path (Path | None): The db path value. Defaults to None.
421
+
422
+ Returns:
423
+ None: None.
424
+ """
425
+ resolved = init_db(db_path)
426
+ engine = _build_engine(resolved)
427
+ with Session(engine) as session:
428
+ session.add(
429
+ AgentRun(
430
+ run_id=run_id,
431
+ agent_name=agent_name,
432
+ prompt=prompt,
433
+ model_name=model_name,
434
+ status=status,
435
+ output_json=output_json,
436
+ created_at=_utc_now_iso(),
437
+ )
438
+ )
439
+ session.commit()
440
+
441
+
442
+ def insert_tool_event(
443
+ *,
444
+ run_id: str,
445
+ tool_name: str,
446
+ phase: str,
447
+ payload_json: str | None,
448
+ db_path: Path | None = None,
449
+ ) -> None:
450
+ """Store lifecycle events around MCP tool usage to aid troubleshooting.
451
+
452
+ Args:
453
+ run_id (str): The run id value.
454
+ tool_name (str): The tool name value.
455
+ phase (str): The phase value.
456
+ payload_json (str | None): The payload json value.
457
+ db_path (Path | None): The db path value. Defaults to None.
458
+
459
+ Returns:
460
+ None: None.
461
+ """
462
+ resolved = init_db(db_path)
463
+ engine = _build_engine(resolved)
464
+ with Session(engine) as session:
465
+ session.add(
466
+ ToolEvent(
467
+ run_id=run_id,
468
+ tool_name=tool_name,
469
+ phase=phase,
470
+ payload_json=payload_json,
471
+ created_at=_utc_now_iso(),
472
+ )
473
+ )
474
+ session.commit()
475
+
476
+
477
+ def insert_error(
478
+ record: ErrorRecord, run_id: str | None = None, db_path: Path | None = None
479
+ ) -> None:
480
+ """Persist an error with normalized root cause and remediation guidance.
481
+
482
+ Args:
483
+ record (ErrorRecord): The record value.
484
+ run_id (str | None): The run id value. Defaults to None.
485
+ db_path (Path | None): The db path value. Defaults to None.
486
+
487
+ Returns:
488
+ None: None.
489
+ """
490
+ resolved = init_db(db_path)
491
+ engine = _build_engine(resolved)
492
+ with Session(engine) as session:
493
+ session.add(
494
+ ErrorCatalog(
495
+ run_id=run_id,
496
+ source=record.source,
497
+ tool_name=record.tool_name,
498
+ error_type=record.error_type,
499
+ error_message=record.error_message,
500
+ root_cause=record.root_cause,
501
+ remediation=record.remediation,
502
+ created_at=_utc_now_iso(),
503
+ )
504
+ )
505
+ session.commit()
506
+
507
+
508
+ def find_recent_errors(
509
+ limit: int = 20, db_path: Path | None = None
510
+ ) -> list[dict[str, Any]]:
511
+ """Return recent errors so agents can avoid repeated failing states.
512
+
513
+ Args:
514
+ limit (int): The limit value. Defaults to 20.
515
+ db_path (Path | None): The db path value. Defaults to None.
516
+
517
+ Returns:
518
+ list[dict[str, Any]]: A list containing the resulting items.
519
+ """
520
+ resolved = init_db(db_path)
521
+ engine = _build_engine(resolved)
522
+ with Session(engine) as session:
523
+ rows = session.exec(
524
+ select(ErrorCatalog).order_by(ErrorCatalog.id.desc()).limit(limit) # type: ignore[union-attr]
525
+ ).all()
526
+
527
+ return [
528
+ {
529
+ "run_id": row.run_id,
530
+ "source": row.source,
531
+ "tool_name": row.tool_name,
532
+ "error_type": row.error_type,
533
+ "error_message": row.error_message,
534
+ "root_cause": row.root_cause,
535
+ "remediation": row.remediation,
536
+ "created_at": row.created_at,
537
+ }
538
+ for row in rows
539
+ ]
540
+
541
+
542
+ def insert_conversation_event(
543
+ *,
544
+ conversation_id: str,
545
+ event_type: str,
546
+ content_snippet: str,
547
+ role: str | None = None,
548
+ run_id: str | None = None,
549
+ metadata_json: str | None = None,
550
+ db_path: Path | None = None,
551
+ ) -> None:
552
+ """Record a conversation event (message, system event, or tool call) linked to a run.
553
+
554
+ Args:
555
+ conversation_id (str): The conversation id value.
556
+ event_type (str): The event type value.
557
+ content_snippet (str): The content snippet value.
558
+ role (str | None): The role value. Defaults to None.
559
+ run_id (str | None): The run id value. Defaults to None.
560
+ metadata_json (str | None): The metadata json value. Defaults to None.
561
+ db_path (Path | None): The db path value. Defaults to None.
562
+
563
+ Returns:
564
+ None: None.
565
+ """
566
+ resolved = init_db(db_path)
567
+ engine = _build_engine(resolved)
568
+ with Session(engine) as session:
569
+ session.add(
570
+ ConversationEvent(
571
+ conversation_id=conversation_id,
572
+ run_id=run_id,
573
+ event_type=event_type,
574
+ role=role,
575
+ content_snippet=content_snippet,
576
+ metadata_json=metadata_json,
577
+ created_at=_utc_now_iso(),
578
+ )
579
+ )
580
+ session.commit()
581
+
582
+
583
+ def find_conversation_events(
584
+ conversation_id: str, db_path: Path | None = None
585
+ ) -> list[dict[str, Any]]:
586
+ """Retrieve all events for a conversation, ordered by creation time.
587
+
588
+ Args:
589
+ conversation_id (str): The conversation id value.
590
+ db_path (Path | None): The db path value. Defaults to None.
591
+
592
+ Returns:
593
+ list[dict[str, Any]]: A list containing the resulting items.
594
+ """
595
+ resolved = init_db(db_path)
596
+ engine = _build_engine(resolved)
597
+ with Session(engine) as session:
598
+ rows = session.exec(
599
+ select(ConversationEvent)
600
+ .where(ConversationEvent.conversation_id == conversation_id)
601
+ .order_by(ConversationEvent.id.asc()) # type: ignore[union-attr]
602
+ ).all()
603
+
604
+ return [
605
+ {
606
+ "id": row.id,
607
+ "conversation_id": row.conversation_id,
608
+ "run_id": row.run_id,
609
+ "event_type": row.event_type,
610
+ "role": row.role,
611
+ "content_snippet": row.content_snippet,
612
+ "metadata_json": row.metadata_json,
613
+ "created_at": row.created_at,
614
+ }
615
+ for row in rows
616
+ ]
617
+
618
+
619
+ def find_run_timeline(run_id: str, db_path: Path | None = None) -> dict[str, Any]:
620
+ """Reconstruct a complete timeline for one run, joining runs, tool events, and conversation events.
621
+
622
+ Args:
623
+ run_id (str): The run id value.
624
+ db_path (Path | None): The db path value. Defaults to None.
625
+
626
+ Returns:
627
+ dict[str, Any]: A dictionary containing the resulting values.
628
+ """
629
+ resolved = init_db(db_path)
630
+ engine = _build_engine(resolved)
631
+
632
+ timeline: dict[str, Any] = {
633
+ "run_id": run_id,
634
+ "run_info": None,
635
+ "events": [],
636
+ }
637
+
638
+ with Session(engine) as session:
639
+ run_row = session.exec(
640
+ select(AgentRun).where(AgentRun.run_id == run_id)
641
+ ).first()
642
+
643
+ if run_row:
644
+ timeline["run_info"] = {
645
+ "agent_name": run_row.agent_name,
646
+ "prompt_preview": run_row.prompt[:200] if run_row.prompt else None,
647
+ "model_name": run_row.model_name,
648
+ "status": run_row.status,
649
+ "created_at": run_row.created_at,
650
+ }
651
+
652
+ tool_events = session.exec(
653
+ select(ToolEvent)
654
+ .where(ToolEvent.run_id == run_id)
655
+ .order_by(ToolEvent.id.asc()) # type: ignore[union-attr]
656
+ ).all()
657
+
658
+ convo_events = session.exec(
659
+ select(ConversationEvent)
660
+ .where(ConversationEvent.run_id == run_id)
661
+ .order_by(ConversationEvent.id.asc()) # type: ignore[union-attr]
662
+ ).all()
663
+
664
+ events = []
665
+ for evt in tool_events:
666
+ events.append(
667
+ {
668
+ "timestamp": evt.created_at,
669
+ "event_type": "tool",
670
+ "tool_name": evt.tool_name,
671
+ "phase": evt.phase,
672
+ "payload_preview": evt.payload_json[:100]
673
+ if evt.payload_json
674
+ else None,
675
+ }
676
+ )
677
+
678
+ for conv_evt in convo_events:
679
+ events.append(
680
+ {
681
+ "timestamp": conv_evt.created_at,
682
+ "event_type": "message",
683
+ "role": conv_evt.role,
684
+ "content_preview": conv_evt.content_snippet[:100],
685
+ "metadata": conv_evt.metadata_json,
686
+ }
687
+ )
688
+
689
+ events.sort(key=lambda e: e["timestamp"] or "")
690
+ timeline["events"] = events
691
+
692
+ return timeline
693
+
694
+
695
+ def upsert_design_session(
696
+ *,
697
+ session_id: str,
698
+ user_goal: str,
699
+ source_mode: str = "model",
700
+ accepted_family: str | None = None,
701
+ status: str = "active",
702
+ current_checkpoint_index: int = 0,
703
+ metadata_json: str | None = None,
704
+ db_path: Path | None = None,
705
+ ) -> None:
706
+ """Create or update one interactive design session row.
707
+
708
+ Args:
709
+ session_id (str): The session id value.
710
+ user_goal (str): The user goal value.
711
+ source_mode (str): The source mode value. Defaults to "model".
712
+ accepted_family (str | None): The accepted family value. Defaults to None.
713
+ status (str): The status value. Defaults to "active".
714
+ current_checkpoint_index (int): The current checkpoint index value. Defaults to 0.
715
+ metadata_json (str | None): The metadata json value. Defaults to None.
716
+ db_path (Path | None): The db path value. Defaults to None.
717
+
718
+ Returns:
719
+ None: None.
720
+ """
721
+ resolved = init_db(db_path)
722
+ engine = _build_engine(resolved)
723
+ now = _utc_now_iso()
724
+ with Session(engine) as session:
725
+ row = session.exec(
726
+ select(DesignSession).where(DesignSession.session_id == session_id)
727
+ ).first()
728
+ if row is None:
729
+ session.add(
730
+ DesignSession(
731
+ session_id=session_id,
732
+ user_goal=user_goal,
733
+ source_mode=source_mode,
734
+ accepted_family=accepted_family,
735
+ status=status,
736
+ current_checkpoint_index=current_checkpoint_index,
737
+ metadata_json=metadata_json,
738
+ created_at=now,
739
+ updated_at=now,
740
+ )
741
+ )
742
+ else:
743
+ row.user_goal = user_goal
744
+ row.source_mode = source_mode
745
+ row.accepted_family = accepted_family
746
+ row.status = status
747
+ row.current_checkpoint_index = current_checkpoint_index
748
+ row.metadata_json = metadata_json
749
+ row.updated_at = now
750
+ session.add(row)
751
+ session.commit()
752
+
753
+
754
+ def get_design_session(
755
+ session_id: str, db_path: Path | None = None
756
+ ) -> dict[str, Any] | None:
757
+ """Return one session row as a dictionary.
758
+
759
+ Args:
760
+ session_id (str): The session id value.
761
+ db_path (Path | None): The db path value. Defaults to None.
762
+
763
+ Returns:
764
+ dict[str, Any] | None: A dictionary containing the resulting values.
765
+ """
766
+ resolved = init_db(db_path)
767
+ engine = _build_engine(resolved)
768
+ with Session(engine) as session:
769
+ row = session.exec(
770
+ select(DesignSession).where(DesignSession.session_id == session_id)
771
+ ).first()
772
+
773
+ if row is None:
774
+ return None
775
+ return {
776
+ "session_id": row.session_id,
777
+ "user_goal": row.user_goal,
778
+ "source_mode": row.source_mode,
779
+ "accepted_family": row.accepted_family,
780
+ "status": row.status,
781
+ "current_checkpoint_index": row.current_checkpoint_index,
782
+ "metadata_json": row.metadata_json,
783
+ "created_at": row.created_at,
784
+ "updated_at": row.updated_at,
785
+ }
786
+
787
+
788
+ def insert_plan_checkpoint(
789
+ *,
790
+ session_id: str,
791
+ checkpoint_index: int,
792
+ title: str,
793
+ planned_action_json: str,
794
+ approved_by_user: bool = False,
795
+ executed: bool = False,
796
+ result_json: str | None = None,
797
+ rollback_snapshot_id: int | None = None,
798
+ db_path: Path | None = None,
799
+ ) -> int:
800
+ """Insert a new checkpoint and return its ID.
801
+
802
+ Args:
803
+ session_id (str): The session id value.
804
+ checkpoint_index (int): The checkpoint index value.
805
+ title (str): The title value.
806
+ planned_action_json (str): The planned action json value.
807
+ approved_by_user (bool): The approved by user value. Defaults to False.
808
+ executed (bool): The executed value. Defaults to False.
809
+ result_json (str | None): The result json value. Defaults to None.
810
+ rollback_snapshot_id (int | None): The rollback snapshot id value. Defaults to None.
811
+ db_path (Path | None): The db path value. Defaults to None.
812
+
813
+ Returns:
814
+ int: The computed numeric result.
815
+ """
816
+ resolved = init_db(db_path)
817
+ engine = _build_engine(resolved)
818
+ now = _utc_now_iso()
819
+ with Session(engine) as session:
820
+ row = PlanCheckpoint(
821
+ session_id=session_id,
822
+ checkpoint_index=checkpoint_index,
823
+ title=title,
824
+ planned_action_json=planned_action_json,
825
+ approved_by_user=approved_by_user,
826
+ executed=executed,
827
+ result_json=result_json,
828
+ rollback_snapshot_id=rollback_snapshot_id,
829
+ created_at=now,
830
+ updated_at=now,
831
+ )
832
+ session.add(row)
833
+ session.commit()
834
+ session.refresh(row)
835
+ return int(row.id or 0)
836
+
837
+
838
+ def update_plan_checkpoint(
839
+ checkpoint_id: int,
840
+ *,
841
+ approved_by_user: bool | None = None,
842
+ executed: bool | None = None,
843
+ result_json: str | None = None,
844
+ rollback_snapshot_id: int | None = None,
845
+ db_path: Path | None = None,
846
+ ) -> None:
847
+ """Patch checkpoint approval/execution fields.
848
+
849
+ Args:
850
+ checkpoint_id (int): The checkpoint id value.
851
+ approved_by_user (bool | None): The approved by user value. Defaults to None.
852
+ executed (bool | None): The executed value. Defaults to None.
853
+ result_json (str | None): The result json value. Defaults to None.
854
+ rollback_snapshot_id (int | None): The rollback snapshot id value. Defaults to None.
855
+ db_path (Path | None): The db path value. Defaults to None.
856
+
857
+ Returns:
858
+ None: None.
859
+ """
860
+ resolved = init_db(db_path)
861
+ engine = _build_engine(resolved)
862
+ with Session(engine) as session:
863
+ row = session.exec(
864
+ select(PlanCheckpoint).where(PlanCheckpoint.id == checkpoint_id)
865
+ ).first()
866
+ if row is None:
867
+ return
868
+ if approved_by_user is not None:
869
+ row.approved_by_user = approved_by_user
870
+ if executed is not None:
871
+ row.executed = executed
872
+ if result_json is not None:
873
+ row.result_json = result_json
874
+ if rollback_snapshot_id is not None:
875
+ row.rollback_snapshot_id = rollback_snapshot_id
876
+ row.updated_at = _utc_now_iso()
877
+ session.add(row)
878
+ session.commit()
879
+
880
+
881
+ def update_plan_checkpoint_planned_action(
882
+ checkpoint_id: int,
883
+ *,
884
+ planned_action_json: str,
885
+ executed: bool | None = None,
886
+ result_json: str | None = None,
887
+ db_path: Path | None = None,
888
+ ) -> None:
889
+ """Update checkpoint planned-action JSON and optional execution status.
890
+
891
+ Args:
892
+ checkpoint_id: Target checkpoint row ID.
893
+ planned_action_json: New planned action payload encoded as JSON string.
894
+ executed: Optional executed flag override.
895
+ result_json: Optional result payload override.
896
+ db_path: Optional SQLite path override.
897
+ """
898
+ resolved = init_db(db_path)
899
+ engine = _build_engine(resolved)
900
+ with Session(engine) as session:
901
+ row = session.exec(
902
+ select(PlanCheckpoint).where(PlanCheckpoint.id == checkpoint_id)
903
+ ).first()
904
+ if row is None:
905
+ return
906
+
907
+ row.planned_action_json = planned_action_json
908
+ if executed is not None:
909
+ row.executed = executed
910
+ if result_json is not None:
911
+ row.result_json = result_json
912
+ row.updated_at = _utc_now_iso()
913
+
914
+ session.add(row)
915
+ session.commit()
916
+
917
+
918
+ def list_plan_checkpoints(
919
+ session_id: str,
920
+ db_path: Path | None = None,
921
+ ) -> list[dict[str, Any]]:
922
+ """List all checkpoints for a session.
923
+
924
+ Args:
925
+ session_id (str): The session id value.
926
+ db_path (Path | None): The db path value. Defaults to None.
927
+
928
+ Returns:
929
+ list[dict[str, Any]]: A list containing the resulting items.
930
+ """
931
+ resolved = init_db(db_path)
932
+ engine = _build_engine(resolved)
933
+ with Session(engine) as session:
934
+ rows = session.exec(
935
+ select(PlanCheckpoint)
936
+ .where(PlanCheckpoint.session_id == session_id)
937
+ .order_by(PlanCheckpoint.checkpoint_index.asc()) # type: ignore[attr-defined]
938
+ ).all()
939
+
940
+ return [
941
+ {
942
+ "id": row.id,
943
+ "session_id": row.session_id,
944
+ "checkpoint_index": row.checkpoint_index,
945
+ "title": row.title,
946
+ "planned_action_json": row.planned_action_json,
947
+ "approved_by_user": row.approved_by_user,
948
+ "executed": row.executed,
949
+ "result_json": row.result_json,
950
+ "rollback_snapshot_id": row.rollback_snapshot_id,
951
+ "created_at": row.created_at,
952
+ "updated_at": row.updated_at,
953
+ }
954
+ for row in rows
955
+ ]
956
+
957
+
958
+ def replace_plan_checkpoints(
959
+ *,
960
+ session_id: str,
961
+ checkpoints: list[dict[str, Any]],
962
+ db_path: Path | None = None,
963
+ ) -> None:
964
+ """Replace all session checkpoints with a new ordered checkpoint list.
965
+
966
+ Args:
967
+ session_id (str): The session id value.
968
+ checkpoints (list[dict[str, Any]]): Replacement checkpoint records.
969
+ db_path (Path | None): The db path value. Defaults to None.
970
+
971
+ Returns:
972
+ None: None.
973
+ """
974
+ resolved = init_db(db_path)
975
+ engine = _build_engine(resolved)
976
+ now = _utc_now_iso()
977
+
978
+ with Session(engine) as session:
979
+ existing_rows = session.exec(
980
+ select(PlanCheckpoint).where(PlanCheckpoint.session_id == session_id)
981
+ ).all()
982
+ for existing in existing_rows:
983
+ session.delete(existing)
984
+
985
+ for index, item in enumerate(checkpoints, start=1):
986
+ planned_action_json = str(item.get("planned_action_json") or "{}")
987
+ title = str(item.get("title") or f"Checkpoint {index}")
988
+ approved_by_user = bool(item.get("approved_by_user", index == 1))
989
+ row = PlanCheckpoint(
990
+ session_id=session_id,
991
+ checkpoint_index=int(item.get("checkpoint_index") or index),
992
+ title=title,
993
+ planned_action_json=planned_action_json,
994
+ approved_by_user=approved_by_user,
995
+ executed=False,
996
+ result_json=None,
997
+ rollback_snapshot_id=None,
998
+ created_at=now,
999
+ updated_at=now,
1000
+ )
1001
+ session.add(row)
1002
+
1003
+ session.commit()
1004
+
1005
+
1006
+ def insert_tool_call_record(
1007
+ *,
1008
+ session_id: str,
1009
+ tool_name: str,
1010
+ checkpoint_id: int | None = None,
1011
+ run_id: str | None = None,
1012
+ input_json: str | None = None,
1013
+ output_json: str | None = None,
1014
+ success: bool = True,
1015
+ latency_ms: float | None = None,
1016
+ script_line: str | None = None,
1017
+ db_path: Path | None = None,
1018
+ ) -> None:
1019
+ """Insert one tool call execution record.
1020
+
1021
+ Args:
1022
+ session_id (str): The session id value.
1023
+ tool_name (str): The tool name value.
1024
+ checkpoint_id (int | None): The checkpoint id value. Defaults to None.
1025
+ run_id (str | None): The run id value. Defaults to None.
1026
+ input_json (str | None): The input json value. Defaults to None.
1027
+ output_json (str | None): The output json value. Defaults to None.
1028
+ success (bool): The success value. Defaults to True.
1029
+ latency_ms (float | None): The latency ms value. Defaults to None.
1030
+ script_line (str | None): Rendered SolidWorks-as-Code Python for this
1031
+ call (issue #28). Computed from the tool name and payloads when
1032
+ not supplied. Defaults to None.
1033
+ db_path (Path | None): The db path value. Defaults to None.
1034
+
1035
+ Returns:
1036
+ None: None.
1037
+ """
1038
+ if script_line is None:
1039
+ # Render at write time so every row is self-describing. Import here to
1040
+ # avoid a module-level cycle (soc_exporter imports from history_db).
1041
+ try:
1042
+ from .soc_exporter import render_single
1043
+
1044
+ script_line = render_single(tool_name, input_json, output_json) or None
1045
+ except Exception: # noqa: BLE001 - never let rendering block a log write
1046
+ script_line = None
1047
+
1048
+ resolved = init_db(db_path)
1049
+ engine = _build_engine(resolved)
1050
+ with Session(engine) as session:
1051
+ session.add(
1052
+ ToolCallRecord(
1053
+ session_id=session_id,
1054
+ checkpoint_id=checkpoint_id,
1055
+ run_id=run_id,
1056
+ tool_name=tool_name,
1057
+ input_json=input_json,
1058
+ output_json=output_json,
1059
+ success=success,
1060
+ latency_ms=latency_ms,
1061
+ script_line=script_line,
1062
+ created_at=_utc_now_iso(),
1063
+ )
1064
+ )
1065
+ session.commit()
1066
+
1067
+
1068
+ def list_tool_call_records(
1069
+ session_id: str,
1070
+ checkpoint_id: int | None = None,
1071
+ db_path: Path | None = None,
1072
+ include_reverted: bool = False,
1073
+ ) -> list[dict[str, Any]]:
1074
+ """List tool call records for a session and optional checkpoint.
1075
+
1076
+ Args:
1077
+ session_id (str): The session id value.
1078
+ checkpoint_id (int | None): The checkpoint id value. Defaults to None.
1079
+ db_path (Path | None): The db path value. Defaults to None.
1080
+ include_reverted (bool): Include records with status='reverted'. Defaults to False.
1081
+
1082
+ Returns:
1083
+ list[dict[str, Any]]: A list containing the resulting items.
1084
+ """
1085
+ resolved = init_db(db_path)
1086
+ engine = _build_engine(resolved)
1087
+ with Session(engine) as session:
1088
+ query = select(ToolCallRecord).where(ToolCallRecord.session_id == session_id)
1089
+ if checkpoint_id is not None:
1090
+ query = query.where(ToolCallRecord.checkpoint_id == checkpoint_id)
1091
+ if not include_reverted:
1092
+ query = query.where(
1093
+ (ToolCallRecord.status == None) | (ToolCallRecord.status != "reverted") # noqa: E711
1094
+ )
1095
+ rows = session.exec(query.order_by(ToolCallRecord.id.asc())).all() # type: ignore[union-attr]
1096
+
1097
+ return [
1098
+ {
1099
+ "id": row.id,
1100
+ "session_id": row.session_id,
1101
+ "checkpoint_id": row.checkpoint_id,
1102
+ "run_id": row.run_id,
1103
+ "tool_name": row.tool_name,
1104
+ "input_json": row.input_json,
1105
+ "output_json": row.output_json,
1106
+ "success": row.success,
1107
+ "latency_ms": row.latency_ms,
1108
+ "status": row.status,
1109
+ "created_at": row.created_at,
1110
+ }
1111
+ for row in rows
1112
+ ]
1113
+
1114
+
1115
+ def revert_tool_call_records(
1116
+ session_id: str,
1117
+ from_record_id: int,
1118
+ db_path: Path | None = None,
1119
+ ) -> int:
1120
+ """Mark ToolCallRecords as reverted from from_record_id onward.
1121
+
1122
+ Sets status='reverted' on all records for the session with id >= from_record_id.
1123
+ Reverted records are excluded from list_tool_call_records() by default and from
1124
+ the soc_exporter output, so re-generating the script omits the rolled-back work.
1125
+
1126
+ Args:
1127
+ session_id (str): The SoC session whose records should be reverted.
1128
+ from_record_id (int): First record ID to mark as reverted (inclusive).
1129
+ db_path (Path | None): Override default SQLite DB path.
1130
+
1131
+ Returns:
1132
+ int: Number of records marked reverted.
1133
+ """
1134
+ resolved = init_db(db_path)
1135
+ engine = _build_engine(resolved)
1136
+ with Session(engine) as db_session:
1137
+ rows = db_session.exec(
1138
+ select(ToolCallRecord)
1139
+ .where(ToolCallRecord.session_id == session_id)
1140
+ .where(ToolCallRecord.id >= from_record_id) # type: ignore[operator]
1141
+ ).all()
1142
+ for row in rows:
1143
+ row.status = "reverted"
1144
+ db_session.commit()
1145
+ return len(rows)
1146
+
1147
+
1148
+ def insert_evidence_link(
1149
+ *,
1150
+ session_id: str,
1151
+ source_type: str,
1152
+ source_id: str,
1153
+ checkpoint_id: int | None = None,
1154
+ relevance_score: float | None = None,
1155
+ rationale: str | None = None,
1156
+ payload_json: str | None = None,
1157
+ db_path: Path | None = None,
1158
+ ) -> None:
1159
+ """Insert one evidence row used by planning/classification.
1160
+
1161
+ Args:
1162
+ session_id (str): The session id value.
1163
+ source_type (str): The source type value.
1164
+ source_id (str): The source id value.
1165
+ checkpoint_id (int | None): The checkpoint id value. Defaults to None.
1166
+ relevance_score (float | None): The relevance score value. Defaults to None.
1167
+ rationale (str | None): The rationale value. Defaults to None.
1168
+ payload_json (str | None): The payload json value. Defaults to None.
1169
+ db_path (Path | None): The db path value. Defaults to None.
1170
+
1171
+ Returns:
1172
+ None: None.
1173
+ """
1174
+ resolved = init_db(db_path)
1175
+ engine = _build_engine(resolved)
1176
+ with Session(engine) as session:
1177
+ session.add(
1178
+ EvidenceLink(
1179
+ session_id=session_id,
1180
+ checkpoint_id=checkpoint_id,
1181
+ source_type=source_type,
1182
+ source_id=source_id,
1183
+ relevance_score=relevance_score,
1184
+ rationale=rationale,
1185
+ payload_json=payload_json,
1186
+ created_at=_utc_now_iso(),
1187
+ )
1188
+ )
1189
+ session.commit()
1190
+
1191
+
1192
+ def list_evidence_links(
1193
+ session_id: str,
1194
+ checkpoint_id: int | None = None,
1195
+ db_path: Path | None = None,
1196
+ ) -> list[dict[str, Any]]:
1197
+ """List evidence rows for a session and optional checkpoint.
1198
+
1199
+ Args:
1200
+ session_id (str): The session id value.
1201
+ checkpoint_id (int | None): The checkpoint id value. Defaults to None.
1202
+ db_path (Path | None): The db path value. Defaults to None.
1203
+
1204
+ Returns:
1205
+ list[dict[str, Any]]: A list containing the resulting items.
1206
+ """
1207
+ resolved = init_db(db_path)
1208
+ engine = _build_engine(resolved)
1209
+ with Session(engine) as session:
1210
+ query = select(EvidenceLink).where(EvidenceLink.session_id == session_id)
1211
+ if checkpoint_id is not None:
1212
+ query = query.where(EvidenceLink.checkpoint_id == checkpoint_id)
1213
+ rows = session.exec(query.order_by(EvidenceLink.id.asc())).all() # type: ignore[union-attr]
1214
+
1215
+ return [
1216
+ {
1217
+ "id": row.id,
1218
+ "session_id": row.session_id,
1219
+ "checkpoint_id": row.checkpoint_id,
1220
+ "source_type": row.source_type,
1221
+ "source_id": row.source_id,
1222
+ "relevance_score": row.relevance_score,
1223
+ "rationale": row.rationale,
1224
+ "payload_json": row.payload_json,
1225
+ "created_at": row.created_at,
1226
+ }
1227
+ for row in rows
1228
+ ]
1229
+
1230
+
1231
+ def insert_model_state_snapshot(
1232
+ *,
1233
+ session_id: str,
1234
+ checkpoint_id: int | None = None,
1235
+ model_path: str | None = None,
1236
+ feature_tree_json: str | None = None,
1237
+ mass_properties_json: str | None = None,
1238
+ screenshot_path: str | None = None,
1239
+ state_fingerprint: str | None = None,
1240
+ db_path: Path | None = None,
1241
+ ) -> int:
1242
+ """Insert model snapshot row and return snapshot ID for rollback tracking.
1243
+
1244
+ Args:
1245
+ session_id (str): The session id value.
1246
+ checkpoint_id (int | None): The checkpoint id value. Defaults to None.
1247
+ model_path (str | None): The model path value. Defaults to None.
1248
+ feature_tree_json (str | None): The feature tree json value. Defaults to None.
1249
+ mass_properties_json (str | None): The mass properties json value. Defaults to None.
1250
+ screenshot_path (str | None): The screenshot path value. Defaults to None.
1251
+ state_fingerprint (str | None): The state fingerprint value. Defaults to None.
1252
+ db_path (Path | None): The db path value. Defaults to None.
1253
+
1254
+ Returns:
1255
+ int: The computed numeric result.
1256
+ """
1257
+ resolved = init_db(db_path)
1258
+ engine = _build_engine(resolved)
1259
+ with Session(engine) as session:
1260
+ row = ModelStateSnapshot(
1261
+ session_id=session_id,
1262
+ checkpoint_id=checkpoint_id,
1263
+ model_path=model_path,
1264
+ feature_tree_json=feature_tree_json,
1265
+ mass_properties_json=mass_properties_json,
1266
+ screenshot_path=screenshot_path,
1267
+ state_fingerprint=state_fingerprint,
1268
+ created_at=_utc_now_iso(),
1269
+ )
1270
+ session.add(row)
1271
+ session.commit()
1272
+ session.refresh(row)
1273
+ return int(row.id or 0)
1274
+
1275
+
1276
+ def list_model_state_snapshots(
1277
+ session_id: str,
1278
+ db_path: Path | None = None,
1279
+ ) -> list[dict[str, Any]]:
1280
+ """List snapshots for a session newest first for diff/rollback flows.
1281
+
1282
+ Args:
1283
+ session_id (str): The session id value.
1284
+ db_path (Path | None): The db path value. Defaults to None.
1285
+
1286
+ Returns:
1287
+ list[dict[str, Any]]: A list containing the resulting items.
1288
+ """
1289
+ resolved = init_db(db_path)
1290
+ engine = _build_engine(resolved)
1291
+ with Session(engine) as session:
1292
+ rows = session.exec(
1293
+ select(ModelStateSnapshot)
1294
+ .where(ModelStateSnapshot.session_id == session_id)
1295
+ .order_by(ModelStateSnapshot.id.desc()) # type: ignore[union-attr]
1296
+ ).all()
1297
+
1298
+ return [
1299
+ {
1300
+ "id": row.id,
1301
+ "session_id": row.session_id,
1302
+ "checkpoint_id": row.checkpoint_id,
1303
+ "model_path": row.model_path,
1304
+ "feature_tree_json": row.feature_tree_json,
1305
+ "mass_properties_json": row.mass_properties_json,
1306
+ "screenshot_path": row.screenshot_path,
1307
+ "state_fingerprint": row.state_fingerprint,
1308
+ "created_at": row.created_at,
1309
+ }
1310
+ for row in rows
1311
+ ]
1312
+
1313
+
1314
+ def create_soc_checkpoint(
1315
+ *,
1316
+ session_id: str,
1317
+ label: str,
1318
+ file_path: str,
1319
+ first_record_id: int | None = None,
1320
+ last_record_id: int | None = None,
1321
+ snapshot_id: int | None = None,
1322
+ db_path: Path | None = None,
1323
+ ) -> int:
1324
+ """Insert a SoCCheckpoint and return its id.
1325
+
1326
+ Args:
1327
+ session_id (str): Owning SoC session.
1328
+ label (str): Short human name (e.g. "base-extrude").
1329
+ file_path (str): Path to the .sldprt saved at this checkpoint.
1330
+ first_record_id (int | None): First ToolCallRecord.id in this block.
1331
+ last_record_id (int | None): Last ToolCallRecord.id in this block.
1332
+ snapshot_id (int | None): FK to ModelStateSnapshot.
1333
+ db_path (Path | None): Override default SQLite path.
1334
+
1335
+ Returns:
1336
+ int: The new SoCCheckpoint.id.
1337
+ """
1338
+ resolved = init_db(db_path)
1339
+ engine = _build_engine(resolved)
1340
+ with Session(engine) as session:
1341
+ row = SoCCheckpoint(
1342
+ session_id=session_id,
1343
+ label=label,
1344
+ file_path=file_path,
1345
+ first_record_id=first_record_id,
1346
+ last_record_id=last_record_id,
1347
+ snapshot_id=snapshot_id,
1348
+ created_at=_utc_now_iso(),
1349
+ )
1350
+ session.add(row)
1351
+ session.commit()
1352
+ session.refresh(row)
1353
+ return int(row.id) # type: ignore[arg-type]
1354
+
1355
+
1356
+ def list_soc_checkpoints(
1357
+ session_id: str,
1358
+ db_path: Path | None = None,
1359
+ ) -> list[dict[str, Any]]:
1360
+ """List SoCCheckpoints for a session in creation order.
1361
+
1362
+ Args:
1363
+ session_id (str): Owning SoC session.
1364
+ db_path (Path | None): Override default SQLite path.
1365
+
1366
+ Returns:
1367
+ list[dict[str, Any]]: Ordered list of checkpoint dicts.
1368
+ """
1369
+ resolved = init_db(db_path)
1370
+ engine = _build_engine(resolved)
1371
+ with Session(engine) as session:
1372
+ rows = session.exec(
1373
+ select(SoCCheckpoint)
1374
+ .where(SoCCheckpoint.session_id == session_id)
1375
+ .order_by(SoCCheckpoint.id.asc()) # type: ignore[union-attr]
1376
+ ).all()
1377
+ return [
1378
+ {
1379
+ "id": row.id,
1380
+ "session_id": row.session_id,
1381
+ "label": row.label,
1382
+ "file_path": row.file_path,
1383
+ "first_record_id": row.first_record_id,
1384
+ "last_record_id": row.last_record_id,
1385
+ "snapshot_id": row.snapshot_id,
1386
+ "created_at": row.created_at,
1387
+ }
1388
+ for row in rows
1389
+ ]
1390
+
1391
+
1392
+ def get_soc_checkpoint(
1393
+ session_id: str,
1394
+ label: str,
1395
+ db_path: Path | None = None,
1396
+ ) -> dict[str, Any] | None:
1397
+ """Fetch a single SoCCheckpoint by session + label.
1398
+
1399
+ Args:
1400
+ session_id (str): Owning SoC session.
1401
+ label (str): Checkpoint label to look up.
1402
+ db_path (Path | None): Override default SQLite path.
1403
+
1404
+ Returns:
1405
+ dict[str, Any] | None: Checkpoint dict or None if not found.
1406
+ """
1407
+ resolved = init_db(db_path)
1408
+ engine = _build_engine(resolved)
1409
+ with Session(engine) as session:
1410
+ row = session.exec(
1411
+ select(SoCCheckpoint)
1412
+ .where(SoCCheckpoint.session_id == session_id)
1413
+ .where(SoCCheckpoint.label == label)
1414
+ .order_by(SoCCheckpoint.id.desc()) # type: ignore[union-attr]
1415
+ ).first()
1416
+ if row is None:
1417
+ return None
1418
+ return {
1419
+ "id": row.id,
1420
+ "session_id": row.session_id,
1421
+ "label": row.label,
1422
+ "file_path": row.file_path,
1423
+ "first_record_id": row.first_record_id,
1424
+ "last_record_id": row.last_record_id,
1425
+ "snapshot_id": row.snapshot_id,
1426
+ "created_at": row.created_at,
1427
+ }
1428
+
1429
+
1430
+ def insert_sketch_graph_snapshot(
1431
+ *,
1432
+ session_id: str,
1433
+ nodes_json: str,
1434
+ edges_json: str,
1435
+ model_path: str | None = None,
1436
+ graph_format: str = "json",
1437
+ metadata_json: str | None = None,
1438
+ db_path: Path | None = None,
1439
+ ) -> None:
1440
+ """Store lightweight sketch graph snapshots in SQLite (Section F).
1441
+
1442
+ Args:
1443
+ session_id (str): The session id value.
1444
+ nodes_json (str): The nodes json value.
1445
+ edges_json (str): The edges json value.
1446
+ model_path (str | None): The model path value. Defaults to None.
1447
+ graph_format (str): The graph format value. Defaults to "json".
1448
+ metadata_json (str | None): The metadata json value. Defaults to None.
1449
+ db_path (Path | None): The db path value. Defaults to None.
1450
+
1451
+ Returns:
1452
+ None: None.
1453
+ """
1454
+ resolved = init_db(db_path)
1455
+ engine = _build_engine(resolved)
1456
+ with Session(engine) as session:
1457
+ session.add(
1458
+ SketchGraphSnapshot(
1459
+ session_id=session_id,
1460
+ model_path=model_path,
1461
+ graph_format=graph_format,
1462
+ nodes_json=nodes_json,
1463
+ edges_json=edges_json,
1464
+ metadata_json=metadata_json,
1465
+ created_at=_utc_now_iso(),
1466
+ )
1467
+ )
1468
+ session.commit()
1469
+
1470
+
1471
+ def list_sketch_graph_snapshots(
1472
+ session_id: str,
1473
+ model_path: str | None = None,
1474
+ db_path: Path | None = None,
1475
+ ) -> list[dict[str, Any]]:
1476
+ """List sketch graph snapshots for a session.
1477
+
1478
+ Args:
1479
+ session_id (str): The session id value.
1480
+ model_path (str | None): The model path value. Defaults to None.
1481
+ db_path (Path | None): The db path value. Defaults to None.
1482
+
1483
+ Returns:
1484
+ list[dict[str, Any]]: A list containing the resulting items.
1485
+ """
1486
+ resolved = init_db(db_path)
1487
+ engine = _build_engine(resolved)
1488
+ with Session(engine) as session:
1489
+ query = select(SketchGraphSnapshot).where(
1490
+ SketchGraphSnapshot.session_id == session_id
1491
+ )
1492
+ if model_path is not None:
1493
+ query = query.where(SketchGraphSnapshot.model_path == model_path)
1494
+ rows = session.exec(query.order_by(SketchGraphSnapshot.id.desc())).all() # type: ignore[union-attr]
1495
+
1496
+ return [
1497
+ {
1498
+ "id": row.id,
1499
+ "session_id": row.session_id,
1500
+ "model_path": row.model_path,
1501
+ "graph_format": row.graph_format,
1502
+ "nodes_json": row.nodes_json,
1503
+ "edges_json": row.edges_json,
1504
+ "metadata_json": row.metadata_json,
1505
+ "created_at": row.created_at,
1506
+ }
1507
+ for row in rows
1508
+ ]