pi-revit 0.4.0 → 0.5.0

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 (119) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +465 -430
  3. package/README.md +604 -548
  4. package/bin/pi-revit.js +9 -9
  5. package/docs/architecture.md +271 -0
  6. package/docs/evaluation.md +434 -0
  7. package/docs/invariants.json +147 -0
  8. package/extensions/pi-revit/completion-monitor.ts +55 -0
  9. package/extensions/pi-revit/contracts.ts +146 -0
  10. package/extensions/pi-revit/discovery.ts +93 -0
  11. package/extensions/pi-revit/index.ts +342 -255
  12. package/extensions/pi-revit/instance-router.ts +86 -86
  13. package/extensions/pi-revit/platform-prompt.ts +40 -0
  14. package/extensions/pi-revit/scope-monitor.ts +114 -0
  15. package/extensions/pi-revit/script-library.ts +144 -144
  16. package/extensions/pi-revit/tool-catalog.ts +113 -14
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +8 -2
  20. package/scripts/build.ps1 +9 -9
  21. package/scripts/check-sdk.ps1 +66 -66
  22. package/scripts/check-tool-documentation.mjs +287 -0
  23. package/scripts/deploy.ps1 +16 -16
  24. package/scripts/generate-contracts.mjs +80 -0
  25. package/scripts/lib/platform.mjs +226 -0
  26. package/scripts/test-extension.mjs +15 -0
  27. package/skills/pi-revit/SKILL.md +30 -218
  28. package/skills/pi-revit/contracts.generated.json +3524 -0
  29. package/skills/pi-revit/references/execution-rules.md +41 -0
  30. package/skills/pi-revit/references/model-audit-export.md +38 -27
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +37 -26
  33. package/skills/pi-revit/references/tool-index.md +89 -0
  34. package/skills/pi-revit/references/tools/capture_view.md +62 -0
  35. package/skills/pi-revit/references/tools/change_element_types.md +65 -0
  36. package/skills/pi-revit/references/tools/create_tags.md +85 -0
  37. package/skills/pi-revit/references/tools/delete_elements.md +66 -0
  38. package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
  39. package/skills/pi-revit/references/tools/export_documents.md +75 -0
  40. package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
  41. package/skills/pi-revit/references/tools/get_element_details.md +66 -0
  42. package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
  43. package/skills/pi-revit/references/tools/get_element_types.md +67 -0
  44. package/skills/pi-revit/references/tools/get_elements.md +87 -0
  45. package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
  46. package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
  47. package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
  48. package/skills/pi-revit/references/tools/get_model_health.md +53 -0
  49. package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
  50. package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
  51. package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
  52. package/skills/pi-revit/references/tools/get_schedules.md +71 -0
  53. package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
  54. package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
  55. package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
  56. package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
  57. package/skills/pi-revit/references/tools/manage_selection.md +66 -0
  58. package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
  59. package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
  60. package/skills/pi-revit/references/tools/manage_views.md +95 -0
  61. package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
  62. package/skills/pi-revit/references/tools/open_view.md +59 -0
  63. package/skills/pi-revit/references/tools/ping.md +41 -0
  64. package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
  65. package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
  66. package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
  67. package/skills/pi-revit/references/tools/set_parameters.md +75 -0
  68. package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
  69. package/skills/pi-revit/references/tools/transform_elements.md +79 -0
  70. package/skills/pi-revit/references/visual-verification.md +36 -0
  71. package/skills/pi-revit/tool-manifest.json +338 -0
  72. package/src/Revit/BridgeServer.cs +93 -87
  73. package/src/Revit/OperationStore.cs +178 -178
  74. package/src/Revit/ToolRegistry.cs +88 -57
  75. package/src/Revit/Tools/CaptureView.cs +10 -2
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -95
  79. package/src/Revit/Tools/DeleteElements.cs +53 -44
  80. package/src/Revit/Tools/DocumentGuard.cs +74 -64
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -27
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
  85. package/src/Revit/Tools/ExportDocuments.cs +129 -121
  86. package/src/Revit/Tools/GetElementDetails.cs +37 -41
  87. package/src/Revit/Tools/GetElementRelationships.cs +82 -76
  88. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  89. package/src/Revit/Tools/GetElements.cs +75 -86
  90. package/src/Revit/Tools/GetLinkedElements.cs +89 -82
  91. package/src/Revit/Tools/GetLinkedModels.cs +73 -66
  92. package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
  93. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  94. package/src/Revit/Tools/GetModelOverview.cs +185 -158
  95. package/src/Revit/Tools/GetScheduleFields.cs +44 -37
  96. package/src/Revit/Tools/GetSchedules.cs +96 -89
  97. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  98. package/src/Revit/Tools/InheritedState.cs +144 -0
  99. package/src/Revit/Tools/ManageElementSets.cs +114 -106
  100. package/src/Revit/Tools/ManageSchedules.cs +174 -164
  101. package/src/Revit/Tools/ManageSelection.cs +45 -37
  102. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
  103. package/src/Revit/Tools/ManageSheets.cs +72 -63
  104. package/src/Revit/Tools/ManageViews.cs +115 -100
  105. package/src/Revit/Tools/MeasureGeometry.cs +60 -54
  106. package/src/Revit/Tools/ModelChanges.cs +154 -0
  107. package/src/Revit/Tools/ModelEditBatch.cs +105 -102
  108. package/src/Revit/Tools/ModelEditInputs.cs +49 -49
  109. package/src/Revit/Tools/OpenView.cs +9 -2
  110. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  111. package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
  112. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  113. package/src/Revit/Tools/SetParameters.cs +60 -79
  114. package/src/Revit/Tools/SpatialBounds.cs +30 -30
  115. package/src/Revit/Tools/SummarizeElements.cs +94 -87
  116. package/src/Revit/Tools/ToolContract.cs +48 -0
  117. package/src/Revit/Tools/ToolSupport.cs +5 -1
  118. package/src/Revit/Tools/TransformElements.cs +72 -57
  119. package/workspace/AGENTS.md +26 -20
@@ -0,0 +1,338 @@
1
+ {
2
+ "schema_version": 2,
3
+ "description": "Hand-maintained documentation index: one summary and group per public tool, plus guidance resources. Executable contracts (schemas, effects, keywords, limits, verification) are owned by code and generated into contracts.generated.json.",
4
+ "documentation_revision": "2026-09-29.1",
5
+ "groups": [
6
+ {
7
+ "id": "connection",
8
+ "title": "Connection, discovery, and local utilities"
9
+ },
10
+ {
11
+ "id": "inspection",
12
+ "title": "Model inspection and review"
13
+ },
14
+ {
15
+ "id": "spatial",
16
+ "title": "Links, coordinates, and spatial analysis"
17
+ },
18
+ {
19
+ "id": "editing",
20
+ "title": "Selection and editing"
21
+ },
22
+ {
23
+ "id": "drawings",
24
+ "title": "Drawings, schedules, and delivery"
25
+ },
26
+ {
27
+ "id": "api",
28
+ "title": "API and custom execution"
29
+ }
30
+ ],
31
+ "tools": [
32
+ {
33
+ "name": "capture_view",
34
+ "source": "bridge",
35
+ "group": "drawings",
36
+ "summary": "Capture a view as an image for visual verification."
37
+ },
38
+ {
39
+ "name": "change_element_types",
40
+ "source": "bridge",
41
+ "group": "editing",
42
+ "summary": "Change element types with partial or atomic outcomes."
43
+ },
44
+ {
45
+ "name": "create_tags",
46
+ "source": "bridge",
47
+ "group": "drawings",
48
+ "summary": "Create host element, room, space and area tags."
49
+ },
50
+ {
51
+ "name": "delete_elements",
52
+ "source": "bridge",
53
+ "group": "editing",
54
+ "summary": "Preview or perform deletion including dependent elements."
55
+ },
56
+ {
57
+ "name": "execute_csharp",
58
+ "source": "bridge",
59
+ "group": "api",
60
+ "summary": "Execute custom synchronous C# in Revit."
61
+ },
62
+ {
63
+ "name": "export_documents",
64
+ "source": "bridge",
65
+ "group": "drawings",
66
+ "summary": "Export sheets, views or the model to PDF, DWG, PNG or IFC."
67
+ },
68
+ {
69
+ "name": "find_revit_tools",
70
+ "source": "native",
71
+ "group": "connection",
72
+ "summary": "Discover tools, activate capabilities, or find packaged documentation."
73
+ },
74
+ {
75
+ "name": "get_element_details",
76
+ "source": "bridge",
77
+ "group": "inspection",
78
+ "summary": "Inspect element parameters, geometry bounds and identity."
79
+ },
80
+ {
81
+ "name": "get_element_relationships",
82
+ "source": "bridge",
83
+ "group": "inspection",
84
+ "summary": "Inspect hosts, joined elements, members and dependents."
85
+ },
86
+ {
87
+ "name": "get_element_types",
88
+ "source": "bridge",
89
+ "group": "inspection",
90
+ "summary": "Discover available element types and family symbols."
91
+ },
92
+ {
93
+ "name": "get_elements",
94
+ "source": "bridge",
95
+ "group": "inspection",
96
+ "summary": "Query, count and filter elements and project parameter values."
97
+ },
98
+ {
99
+ "name": "get_linked_elements",
100
+ "source": "bridge",
101
+ "group": "spatial",
102
+ "summary": "Query elements inside a selected linked document."
103
+ },
104
+ {
105
+ "name": "get_linked_models",
106
+ "source": "bridge",
107
+ "group": "spatial",
108
+ "summary": "Discover loaded and unloaded Revit link instances."
109
+ },
110
+ {
111
+ "name": "get_model_coordinates",
112
+ "source": "bridge",
113
+ "group": "spatial",
114
+ "summary": "Read base points, project locations and shared-coordinate mappings."
115
+ },
116
+ {
117
+ "name": "get_model_health",
118
+ "source": "bridge",
119
+ "group": "inspection",
120
+ "summary": "Inspect model warnings and health information."
121
+ },
122
+ {
123
+ "name": "get_model_overview",
124
+ "source": "bridge",
125
+ "group": "inspection",
126
+ "summary": "Inspect project identity, units, levels and category counts."
127
+ },
128
+ {
129
+ "name": "get_revit_operation",
130
+ "source": "native",
131
+ "group": "connection",
132
+ "summary": "Inspect an original operation receipt after uncertain results."
133
+ },
134
+ {
135
+ "name": "get_schedule_fields",
136
+ "source": "bridge",
137
+ "group": "drawings",
138
+ "summary": "Discover eligible parameter and field-type pairs."
139
+ },
140
+ {
141
+ "name": "get_schedules",
142
+ "source": "bridge",
143
+ "group": "drawings",
144
+ "summary": "Inspect schedule fields, sorting, filters and table cells."
145
+ },
146
+ {
147
+ "name": "manage_element_sets",
148
+ "source": "bridge",
149
+ "group": "inspection",
150
+ "summary": "Retain query membership and reread current member values."
151
+ },
152
+ {
153
+ "name": "manage_revit_instances",
154
+ "source": "native",
155
+ "group": "connection",
156
+ "summary": "List and select local Revit bridge sessions."
157
+ },
158
+ {
159
+ "name": "manage_revit_scripts",
160
+ "source": "native",
161
+ "group": "connection",
162
+ "summary": "Save, inspect and run exact versions of reusable scripts."
163
+ },
164
+ {
165
+ "name": "manage_schedules",
166
+ "source": "bridge",
167
+ "group": "drawings",
168
+ "summary": "Create and configure schedules, fields, filters and sorting."
169
+ },
170
+ {
171
+ "name": "manage_selection",
172
+ "source": "bridge",
173
+ "group": "editing",
174
+ "summary": "Read or change selection, zoom and temporary isolation."
175
+ },
176
+ {
177
+ "name": "manage_sheet_placements",
178
+ "source": "bridge",
179
+ "group": "drawings",
180
+ "summary": "List, place and move viewports and schedules on sheets."
181
+ },
182
+ {
183
+ "name": "manage_sheets",
184
+ "source": "bridge",
185
+ "group": "drawings",
186
+ "summary": "Create, rename and renumber sheets."
187
+ },
188
+ {
189
+ "name": "manage_views",
190
+ "source": "bridge",
191
+ "group": "drawings",
192
+ "summary": "Create, duplicate and update plans, sections and 3D views."
193
+ },
194
+ {
195
+ "name": "measure_geometry",
196
+ "source": "bridge",
197
+ "group": "spatial",
198
+ "summary": "Measure point distance or approximate element bounding-box gaps."
199
+ },
200
+ {
201
+ "name": "open_view",
202
+ "source": "bridge",
203
+ "group": "editing",
204
+ "summary": "Activate a Revit view or sheet."
205
+ },
206
+ {
207
+ "name": "ping",
208
+ "source": "native",
209
+ "group": "connection",
210
+ "summary": "Check Revit bridge availability and versions."
211
+ },
212
+ {
213
+ "name": "query_spatial_elements",
214
+ "source": "bridge",
215
+ "group": "spatial",
216
+ "summary": "Query elements by bounding-box intersection or containment."
217
+ },
218
+ {
219
+ "name": "read_revit_result",
220
+ "source": "native",
221
+ "group": "connection",
222
+ "summary": "Read saved large results in bounded fragments."
223
+ },
224
+ {
225
+ "name": "search_api_docs",
226
+ "source": "bridge",
227
+ "group": "api",
228
+ "summary": "Search installed Revit API classes, methods, signatures and enums."
229
+ },
230
+ {
231
+ "name": "set_parameters",
232
+ "source": "bridge",
233
+ "group": "editing",
234
+ "summary": "Edit or preview parameter values with optional atomic rollback."
235
+ },
236
+ {
237
+ "name": "summarize_elements",
238
+ "source": "bridge",
239
+ "group": "inspection",
240
+ "summary": "Count a whole element query grouped by type, category, level or parameter."
241
+ },
242
+ {
243
+ "name": "transform_elements",
244
+ "source": "bridge",
245
+ "group": "editing",
246
+ "summary": "Move, copy or rotate elements with preview rollback."
247
+ }
248
+ ],
249
+ "guidance": [
250
+ {
251
+ "name": "execution-rules",
252
+ "kind": "reference",
253
+ "path": "references/execution-rules.md",
254
+ "summary": "Shared rules for model targeting, document identity, parameters, units and transactions.",
255
+ "keywords": [
256
+ "identity",
257
+ "document id",
258
+ "expected_document_id",
259
+ "units",
260
+ "parameters",
261
+ "transactions",
262
+ "preview",
263
+ "atomic",
264
+ "localized names"
265
+ ]
266
+ },
267
+ {
268
+ "name": "operation-recovery",
269
+ "kind": "reference",
270
+ "path": "references/operation-recovery.md",
271
+ "summary": "Recover after timeouts, cancellations and uncertain outcomes using operation receipts.",
272
+ "keywords": [
273
+ "timeout",
274
+ "retry",
275
+ "receipt",
276
+ "uncertain",
277
+ "failed",
278
+ "bridge unavailable",
279
+ "no document"
280
+ ]
281
+ },
282
+ {
283
+ "name": "visual-verification",
284
+ "kind": "reference",
285
+ "path": "references/visual-verification.md",
286
+ "summary": "Capture and inspect visible results and exported deliverables, then stop when the request is verified.",
287
+ "keywords": [
288
+ "verify",
289
+ "visual check",
290
+ "drawing check",
291
+ "image",
292
+ "overlap",
293
+ "layout",
294
+ "deliverable"
295
+ ]
296
+ },
297
+ {
298
+ "name": "tool-index",
299
+ "kind": "reference",
300
+ "path": "references/tool-index.md",
301
+ "summary": "Every public tool grouped by task, with manual links.",
302
+ "keywords": [
303
+ "tools",
304
+ "capabilities",
305
+ "tool list",
306
+ "manuals"
307
+ ]
308
+ },
309
+ {
310
+ "name": "room-documentation",
311
+ "kind": "workflow",
312
+ "path": "references/room-documentation.md",
313
+ "summary": "Room plans, sections, tags, schedules and sheets as one documentation workflow.",
314
+ "keywords": [
315
+ "rooms",
316
+ "room plan",
317
+ "room tags",
318
+ "room schedule",
319
+ "room sheets",
320
+ "room documentation"
321
+ ]
322
+ },
323
+ {
324
+ "name": "model-audit-export",
325
+ "kind": "workflow",
326
+ "path": "references/model-audit-export.md",
327
+ "summary": "Multi-step model audit, data review and requested exports.",
328
+ "keywords": [
329
+ "audit",
330
+ "review",
331
+ "model quality",
332
+ "export",
333
+ "report",
334
+ "data review"
335
+ ]
336
+ }
337
+ ]
338
+ }
@@ -39,8 +39,8 @@ namespace RevitBridge
39
39
  private readonly CommandQueue _queue;
40
40
  private readonly ToolRegistry _registry;
41
41
  private readonly string _revitVersion;
42
- private readonly Func<bool> _hasOpenDocument;
43
- private readonly OperationStore _operations = new();
42
+ private readonly Func<bool> _hasOpenDocument;
43
+ private readonly OperationStore _operations = new();
44
44
 
45
45
  /// <summary>How often the bridge re-checks that bridge.json still points somewhere
46
46
  /// live (see EnsureBridgeInfo). Cheap: one small file read per tick.</summary>
@@ -118,23 +118,23 @@ namespace RevitBridge
118
118
 
119
119
  public static string BridgeInfoPath() => Path.Combine(BridgeInfoDirectory(), "bridge.json");
120
120
 
121
- private void WriteBridgeInfo()
122
- {
123
- Directory.CreateDirectory(BridgeInfoDirectory());
124
- string json = JsonSerializer.Serialize(new
121
+ private void WriteBridgeInfo()
122
+ {
123
+ Directory.CreateDirectory(BridgeInfoDirectory());
124
+ string json = JsonSerializer.Serialize(new
125
125
  {
126
126
  baseUrl = $"http://127.0.0.1:{_port}",
127
127
  token = _token,
128
128
  pid = Environment.ProcessId,
129
- revitVersion = _revitVersion,
130
- bridgeId = _operations.Generation,
131
- supportsOperationTracking = true,
129
+ revitVersion = _revitVersion,
130
+ bridgeId = _operations.Generation,
131
+ supportsOperationTracking = true,
132
132
  startedAtUtc = DateTime.UtcNow.ToString("o"),
133
- }, new JsonSerializerOptions { WriteIndented = true });
134
- string instances = Path.Combine(BridgeInfoDirectory(), "instances");
135
- Directory.CreateDirectory(instances);
136
- File.WriteAllText(Path.Combine(instances, _operations.Generation + ".json"), json);
137
- File.WriteAllText(BridgeInfoPath(), json);
133
+ }, new JsonSerializerOptions { WriteIndented = true });
134
+ string instances = Path.Combine(BridgeInfoDirectory(), "instances");
135
+ Directory.CreateDirectory(instances);
136
+ File.WriteAllText(Path.Combine(instances, _operations.Generation + ".json"), json);
137
+ File.WriteAllText(BridgeInfoPath(), json);
138
138
  }
139
139
 
140
140
  /// <summary>
@@ -193,9 +193,9 @@ namespace RevitBridge
193
193
  }
194
194
  }
195
195
 
196
- private void DeleteBridgeInfoIfOwned()
197
- {
198
- try { File.Delete(Path.Combine(BridgeInfoDirectory(), "instances", _operations.Generation + ".json")); } catch { }
196
+ private void DeleteBridgeInfoIfOwned()
197
+ {
198
+ try { File.Delete(Path.Combine(BridgeInfoDirectory(), "instances", _operations.Generation + ".json")); } catch { }
199
199
  try
200
200
  {
201
201
  string path = BridgeInfoPath();
@@ -345,10 +345,10 @@ namespace RevitBridge
345
345
  private async Task<(int Status, object Response)> RouteAsync(string method, string path, Dictionary<string, string> query, string body, CancellationToken token)
346
346
  {
347
347
  if (method == "GET" && path == "/ping")
348
- return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion, bridgeId = _operations.Generation, supportsOperationTracking = true });
349
-
350
- if (method == "GET" && path.StartsWith("/operations/", StringComparison.Ordinal))
351
- return (200, _operations.Status(WebUtility.UrlDecode(path["/operations/".Length..])));
348
+ return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion, bridgeId = _operations.Generation, supportsOperationTracking = true });
349
+
350
+ if (method == "GET" && path.StartsWith("/operations/", StringComparison.Ordinal))
351
+ return (200, _operations.Status(WebUtility.UrlDecode(path["/operations/".Length..])));
352
352
 
353
353
  if (method == "GET" && path == "/tools")
354
354
  {
@@ -402,81 +402,87 @@ namespace RevitBridge
402
402
  return (400, new { error = true, message = $"Invalid JSON tool arguments: {ex.Message}" });
403
403
  }
404
404
 
405
- int timeoutMs = ResolveTimeoutMs(query);
406
-
407
- if (query.TryGetValue("operation_id", out string? operationId))
408
- {
409
- try
410
- {
411
- var (entry, isNew) = _operations.Reserve(operationId, name, args, TimeSpan.FromMilliseconds(timeoutMs));
412
- var completion = _operations.Wait(entry);
413
- if (isNew) _ = CompleteTrackedOperationAsync(entry, tool, args, timeoutMs);
414
- var reply = await completion;
415
- return (reply.Status, reply.Response);
416
- }
417
- catch (ArgumentException error) { return (409, new { error = true, message = error.Message, operation_id = operationId }); }
418
- catch (InvalidOperationException error) { return (503, new { error = true, message = error.Message, operation_id = operationId }); }
419
- }
420
- var result = await RunToolAsync(tool, args, timeoutMs, null);
421
- return (result.Status, result.Response);
422
- }
423
-
424
- private async Task CompleteTrackedOperationAsync(OperationStore.Entry entry, ITool tool, JsonElement args, int timeoutMs)
425
- {
426
- var reply = await RunToolAsync(tool, args, timeoutMs, entry);
427
- _operations.Complete(entry, new OperationStore.Reply(reply.Status, reply.Response, reply.Outcome));
428
- }
429
-
430
- private async Task<(int Status, object Response, string? Outcome)> RunToolAsync(ITool tool, JsonElement args, int timeoutMs, OperationStore.Entry? operation)
431
- {
432
- string name = tool.Name;
433
- // Cached receipts remain accessible even when the active document closes.
434
- if (tool.RequiresDocument && !_hasOpenDocument())
435
- return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." }, null);
436
-
437
- void StartOperation()
438
- {
439
- if (operation != null && !_operations.TryStart(operation)) throw new TimeoutException("Operation expired before execution; no tool action was performed.");
440
- }
441
-
442
- try
405
+ int timeoutMs = ResolveTimeoutMs(query);
406
+
407
+ if (query.TryGetValue("operation_id", out string? operationId))
408
+ {
409
+ try
410
+ {
411
+ var (entry, isNew) = _operations.Reserve(operationId, name, args, TimeSpan.FromMilliseconds(timeoutMs));
412
+ var completion = _operations.Wait(entry);
413
+ if (isNew) _ = CompleteTrackedOperationAsync(entry, tool, args, timeoutMs);
414
+ var reply = await completion;
415
+ return (reply.Status, reply.Response);
416
+ }
417
+ catch (ArgumentException error) { return (409, new { error = true, message = error.Message, operation_id = operationId }); }
418
+ catch (InvalidOperationException error) { return (503, new { error = true, message = error.Message, operation_id = operationId }); }
419
+ }
420
+ var result = await RunToolAsync(tool, args, timeoutMs, null);
421
+ return (result.Status, result.Response);
422
+ }
423
+
424
+ private async Task CompleteTrackedOperationAsync(OperationStore.Entry entry, ITool tool, JsonElement args, int timeoutMs)
425
+ {
426
+ var reply = await RunToolAsync(tool, args, timeoutMs, entry);
427
+ _operations.Complete(entry, new OperationStore.Reply(reply.Status, reply.Response, reply.Outcome));
428
+ }
429
+
430
+ private async Task<(int Status, object Response, string? Outcome)> RunToolAsync(ITool tool, JsonElement args, int timeoutMs, OperationStore.Entry? operation)
431
+ {
432
+ string name = tool.Name;
433
+ // Cached receipts remain accessible even when the active document closes.
434
+ if (tool.RequiresDocument && !_hasOpenDocument())
435
+ return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." }, null);
436
+
437
+ void StartOperation()
438
+ {
439
+ if (operation != null && !_operations.TryStart(operation)) throw new TimeoutException("Operation expired before execution; no tool action was performed.");
440
+ }
441
+
442
+ try
443
443
  {
444
444
  object? output = tool.RequiresDocument
445
445
  ? await _queue.RunAsync(uiApp =>
446
- {
447
- StartOperation();
448
- var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
449
- RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name, tool.Write);
450
- return tool.Execute(args, new ToolContext(document, uiApp));
446
+ {
447
+ StartOperation();
448
+ var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
449
+ RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name, tool.Write);
450
+ // A tool declared for other document kinds is refused before it runs (inv:document-kind-declared).
451
+ RevitBridge.Tools.DocumentGuard.CheckKind(tool, document);
452
+ // Every tool that can change the model reports what it changed (model_changes),
453
+ // without tool-specific code, so future tools inherit it.
454
+ using var changes = RevitBridge.Tools.ModelChangeRecorder.For(tool, uiApp.Application, document);
455
+ var result = tool.Execute(args, new ToolContext(document, uiApp));
456
+ return changes == null ? result : changes.Attach(result);
451
457
  }, TimeSpan.FromMilliseconds(timeoutMs))
452
458
  // RequiresDocument = false tools never touch the Revit API, so they
453
459
  // run right here on the server task instead of the CommandQueue.
454
- : ExecuteWithoutDocument();
455
-
456
- try { return (200, BuildToolResponse(name, output), null); }
457
- catch (Exception error)
458
- {
459
- return (500, new { error = true, toolName = name, result_unavailable = true,
460
- message = "The tool finished, but its response could not be constructed. Model or other effects may already have occurred; inspect them before retrying. " + error.Message }, "result_unavailable");
461
- }
462
-
463
- object? ExecuteWithoutDocument()
464
- {
465
- StartOperation();
466
- return tool.Execute(args, new ToolContext(null, null));
467
- }
460
+ : ExecuteWithoutDocument();
461
+
462
+ try { return (200, BuildToolResponse(name, output), null); }
463
+ catch (Exception error)
464
+ {
465
+ return (500, new { error = true, toolName = name, result_unavailable = true,
466
+ message = "The tool finished, but its response could not be constructed. Model or other effects may already have occurred; inspect them before retrying. " + error.Message }, "result_unavailable");
467
+ }
468
+
469
+ object? ExecuteWithoutDocument()
470
+ {
471
+ StartOperation();
472
+ return tool.Execute(args, new ToolContext(null, null));
473
+ }
468
474
  }
469
475
  catch (NoActiveDocumentException ex)
470
476
  {
471
- return (409, new { error = true, hasActiveDocument = false, message = ex.Message }, null);
477
+ return (409, new { error = true, hasActiveDocument = false, message = ex.Message }, null);
472
478
  }
473
479
  catch (ArgumentException ex)
474
480
  {
475
- return (400, new { error = true, toolName = name, message = ex.Message }, null);
481
+ return (400, new { error = true, toolName = name, message = ex.Message }, null);
476
482
  }
477
483
  catch (Exception ex)
478
484
  {
479
- return (500, new { error = true, toolName = name, message = ex.Message }, null);
485
+ return (500, new { error = true, toolName = name, message = ex.Message }, null);
480
486
  }
481
487
  }
482
488
 
@@ -486,9 +492,9 @@ namespace RevitBridge
486
492
  ? Math.Clamp(value, 1_000, 600_000)
487
493
  : 30_000;
488
494
 
489
- /// <summary>Complete bounded JSON for model context; the full payload always
490
- /// remains in details for the extension's saved-result retrieval. ToolOutput
491
- /// compact text is a display summary, not a substitute for requested data.</summary>
495
+ /// <summary>Complete bounded JSON for model context; the full payload always
496
+ /// remains in details for the extension's saved-result retrieval. ToolOutput
497
+ /// compact text is a display summary, not a substitute for requested data.</summary>
492
498
  private static object BuildToolResponse(string toolName, object? output)
493
499
  {
494
500
  object? payload = output;
@@ -499,11 +505,11 @@ namespace RevitBridge
499
505
  compact = toolOutput.CompactText;
500
506
  }
501
507
 
502
- string text = JsonSerializer.Serialize(payload);
508
+ string text = JsonSerializer.Serialize(payload);
503
509
  bool truncated = text.Length > MaxContentChars;
504
510
  if (truncated)
505
511
  {
506
- text = $"Result exceeds the {MaxContentChars}-character inline limit. The complete value is in details.payload; the Pi extension saves it locally and provides read_revit_result for bounded retrieval.";
512
+ text = $"Result exceeds the {MaxContentChars}-character inline limit. The complete value is in details.payload; the Pi extension saves it locally and provides read_revit_result for bounded retrieval.";
507
513
  }
508
514
 
509
515
  return new
@@ -511,7 +517,7 @@ namespace RevitBridge
511
517
  success = true,
512
518
  toolName,
513
519
  content = new[] { new { type = "text", text } },
514
- details = new { payload, summary = compact, contentTruncated = truncated },
520
+ details = new { payload, summary = compact, contentTruncated = truncated },
515
521
  isError = false,
516
522
  };
517
523
  }